From 2bc96d357fef8ad41a8d5fd1c40b35df48f40369 Mon Sep 17 00:00:00 2001 From: Stephanie Roy Date: Thu, 27 Apr 2023 08:49:11 -0400 Subject: [PATCH 1/4] Make the table more compact with branches and actions (#3765) * Make the table more compact with branches and actions * Fix e2e tests * Fix story * Remove the previous commits row --- extension/src/test/e2e/extension.test.ts | 8 +-- .../table/body/PreviousCommitsRow.tsx | 22 ------ .../components/table/body/TableBody.tsx | 14 ---- .../table/body/TableContent.test.tsx | 8 +-- .../components/table/body/TableContent.tsx | 11 +-- .../body/branchDivider/BranchDivider.tsx | 4 ++ .../body/branchDivider/styles.module.scss | 7 +- .../CommitsAndBranchesNavigation.tsx | 69 ++++++++----------- .../commitsAndBranches/styles.module.scss | 4 +- .../components/table/styles.module.scss | 11 --- webview/src/stories/Table.stories.tsx | 2 +- 11 files changed, 48 insertions(+), 112 deletions(-) delete mode 100644 webview/src/experiments/components/table/body/PreviousCommitsRow.tsx diff --git a/extension/src/test/e2e/extension.test.ts b/extension/src/test/e2e/extension.test.ts index 9b2ee67b0a..06e12dcc64 100644 --- a/extension/src/test/e2e/extension.test.ts +++ b/extension/src/test/e2e/extension.test.ts @@ -51,10 +51,8 @@ describe('Experiments Table Webview', function () { const headerRows = 3 const workspaceRow = 1 const commitRows = 3 - const previousCommitRow = 1 - const actionsRow = 1 - const initialRows = - headerRows + workspaceRow + commitRows + previousCommitRow + actionsRow + const branchRow = 1 + const initialRows = headerRows + workspaceRow + commitRows + branchRow it('should load as an editor', async function () { const workbench = await browser.getWorkbench() @@ -109,7 +107,7 @@ describe('Experiments Table Webview', function () { const currentRows = await webview.row$$ - const newRow = currentRows[headerRows + workspaceRow + 1] + const newRow = currentRows[headerRows + workspaceRow + branchRow + 1] const experimentName = (await webview.getExperimentName(newRow)) as string diff --git a/webview/src/experiments/components/table/body/PreviousCommitsRow.tsx b/webview/src/experiments/components/table/body/PreviousCommitsRow.tsx deleted file mode 100644 index fb38a79b07..0000000000 --- a/webview/src/experiments/components/table/body/PreviousCommitsRow.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import cx from 'classnames' -import React from 'react' -import styles from '../styles.module.scss' - -interface PreviousCommitsRowProps { - isBranchesView?: boolean - nbColumns: number -} - -export const PreviousCommitsRow: React.FC = ({ - isBranchesView, - nbColumns -}) => ( - - - - {isBranchesView ? 'Other Branches' : 'Previous Commits'} - - - - -) diff --git a/webview/src/experiments/components/table/body/TableBody.tsx b/webview/src/experiments/components/table/body/TableBody.tsx index 15cc38a089..26bb096312 100644 --- a/webview/src/experiments/components/table/body/TableBody.tsx +++ b/webview/src/experiments/components/table/body/TableBody.tsx @@ -1,19 +1,15 @@ import React from 'react' -import { useSelector } from 'react-redux' import cx from 'classnames' import { EXPERIMENT_WORKSPACE_ID } from 'dvc/src/cli/dvc/contract' import { ExperimentGroup } from './ExperimentGroup' import { BatchSelectionProp, RowContent } from './Row' import { WorkspaceRowGroup } from './WorkspaceRowGroup' -import { PreviousCommitsRow } from './PreviousCommitsRow' import styles from '../styles.module.scss' import { InstanceProp, RowProp } from '../../../util/interfaces' -import { ExperimentsState } from '../../../store' interface TableBodyProps extends RowProp, InstanceProp, BatchSelectionProp { root: HTMLElement | null tableHeaderHeight: number - showPreviousRow?: boolean isLast?: boolean } @@ -26,7 +22,6 @@ export const TableBody: React.FC = ({ batchRowSelection, root, tableHeaderHeight, - showPreviousRow, isLast }) => { const contentProps = { @@ -37,9 +32,6 @@ export const TableBody: React.FC = ({ projectHasCheckpoints, row } - const isBranchesView = useSelector( - (state: ExperimentsState) => state.tableData.isBranchesView - ) const content = row.depth > 0 ? ( @@ -57,12 +49,6 @@ export const TableBody: React.FC = ({ ) : ( <> - {showPreviousRow && row.depth === 0 && ( - - )} 0, diff --git a/webview/src/experiments/components/table/body/TableContent.test.tsx b/webview/src/experiments/components/table/body/TableContent.test.tsx index 464d1ac270..b20ff1dd5d 100644 --- a/webview/src/experiments/components/table/body/TableContent.test.tsx +++ b/webview/src/experiments/components/table/body/TableContent.test.tsx @@ -832,13 +832,7 @@ describe('TableContent', () => { ) } - it('should not display the branches names before its rows if there is only one branch', () => { - renderTableContent() - - expect(screen.queryByTestId('branch-name')).not.toBeInTheDocument() - }) - - it('should display the branches names before its rows if there are more than one branch', () => { + it('should display the branches names before its rows', () => { const instanceRows = instance.getRowModel() const multipleBranchesInstance = { ...instance, diff --git a/webview/src/experiments/components/table/body/TableContent.tsx b/webview/src/experiments/components/table/body/TableContent.tsx index b0b7a5631b..5bd6c7d941 100644 --- a/webview/src/experiments/components/table/body/TableContent.tsx +++ b/webview/src/experiments/components/table/body/TableContent.tsx @@ -1,7 +1,6 @@ import React, { Fragment, RefObject, useCallback, useContext } from 'react' import { useSelector } from 'react-redux' import { TableBody } from './TableBody' -import { CommitsAndBranchesNavigation } from './commitsAndBranches/CommitsAndBranchesNavigation' import { BranchDivider } from './branchDivider/BranchDivider' import { RowSelectionContext } from '../RowSelectionContext' import { ExperimentsState } from '../../../store' @@ -56,9 +55,7 @@ export const TableContent: React.FC = ({ <> {branches.map((branch, branchIndex) => { const branchRows = rows.filter(row => row.original.branch === branch) - const firstPreviousCommitId = branchRows - .slice(branchIndex === 0 ? 2 : 1) - .find(row => row.depth === 0)?.id + return ( {branchRows.map((row, i) => { @@ -66,9 +63,7 @@ export const TableContent: React.FC = ({ (branchIndex === 0 && i === 1) || (branchIndex !== 0 && i === 0) return ( - {isFirstRow && branches.length > 1 && ( - {branch} - )} + {isFirstRow && {branch}} = ({ hasRunningExperiment={hasRunningExperiment} projectHasCheckpoints={hasCheckpoints} batchRowSelection={batchRowSelection} - showPreviousRow={row.id === firstPreviousCommitId} isLast={i === branchRows.length - 1} /> ) })} - ) })} diff --git a/webview/src/experiments/components/table/body/branchDivider/BranchDivider.tsx b/webview/src/experiments/components/table/body/branchDivider/BranchDivider.tsx index cc3d3deb77..d5680839b4 100644 --- a/webview/src/experiments/components/table/body/branchDivider/BranchDivider.tsx +++ b/webview/src/experiments/components/table/body/branchDivider/BranchDivider.tsx @@ -3,6 +3,7 @@ import styles from './styles.module.scss' import tablesStyles from '../../styles.module.scss' import { Icon } from '../../../../../shared/components/Icon' import { GitMerge } from '../../../../../shared/components/icons' +import { CommitsAndBranchesNavigation } from '../commitsAndBranches/CommitsAndBranchesNavigation' export const BranchDivider: React.FC = ({ children }) => ( @@ -18,6 +19,9 @@ export const BranchDivider: React.FC = ({ children }) => ( {children} + + + ) diff --git a/webview/src/experiments/components/table/body/branchDivider/styles.module.scss b/webview/src/experiments/components/table/body/branchDivider/styles.module.scss index 86024577a1..03b7270ff6 100644 --- a/webview/src/experiments/components/table/body/branchDivider/styles.module.scss +++ b/webview/src/experiments/components/table/body/branchDivider/styles.module.scss @@ -1,12 +1,17 @@ @import '../../../../../shared/variables'; .branchName { - padding: 0 20px 10px; + padding: 0 20px; display: flex; align-items: center; gap: 4px; + font-size: 0.6rem; } .icon { fill: $accent-color; } + +.branchActions { + text-align: left; +} diff --git a/webview/src/experiments/components/table/body/commitsAndBranches/CommitsAndBranchesNavigation.tsx b/webview/src/experiments/components/table/body/commitsAndBranches/CommitsAndBranchesNavigation.tsx index 0c84a113a1..91da761835 100644 --- a/webview/src/experiments/components/table/body/commitsAndBranches/CommitsAndBranchesNavigation.tsx +++ b/webview/src/experiments/components/table/body/commitsAndBranches/CommitsAndBranchesNavigation.tsx @@ -8,7 +8,6 @@ import { switchToBranchesView, switchToCommitsView } from '../../../../util/messages' -import tableStyles from '../../styles.module.scss' import { ExperimentsState } from '../../../../store' export const CommitsAndBranchesNavigation: React.FC = () => { @@ -17,46 +16,38 @@ export const CommitsAndBranchesNavigation: React.FC = () => { ) return ( - - - -
- {hasMoreCommits && ( - - )} - {isShowingMoreCommits && ( - - )} +
+ {hasMoreCommits && ( + + )} + {isShowingMoreCommits && ( + + )} - + - + - -
- - - + +
) } diff --git a/webview/src/experiments/components/table/body/commitsAndBranches/styles.module.scss b/webview/src/experiments/components/table/body/commitsAndBranches/styles.module.scss index 1e364a3233..a3abb14fbd 100644 --- a/webview/src/experiments/components/table/body/commitsAndBranches/styles.module.scss +++ b/webview/src/experiments/components/table/body/commitsAndBranches/styles.module.scss @@ -1,9 +1,7 @@ @import '../../../../../shared/variables'; .commitsAndBranchesNav { - padding: 20px 16px; - width: 100px; - overflow: visible; + text-align: left; } .commitsAndBranchesNavButton { diff --git a/webview/src/experiments/components/table/styles.module.scss b/webview/src/experiments/components/table/styles.module.scss index 63e841415a..9df1631a0c 100644 --- a/webview/src/experiments/components/table/styles.module.scss +++ b/webview/src/experiments/components/table/styles.module.scss @@ -474,17 +474,6 @@ $badge-size: 0.85rem; } } -.previousCommitsRow { - border-bottom: $row-border; - - .previousCommitsText { - font-size: 0.6rem; - padding-left: 16px; - font-weight: normal; - text-align: left; - } -} - .lastRowGroup { & > .experimentsTr:last-child { border-color: $row-bg-color; diff --git a/webview/src/stories/Table.stories.tsx b/webview/src/stories/Table.stories.tsx index 419a90ea25..dc78a04225 100644 --- a/webview/src/stories/Table.stories.tsx +++ b/webview/src/stories/Table.stories.tsx @@ -264,7 +264,7 @@ export const Scrolled: StoryFn<{ tableData: TableDataState }> = ({ Scrolled.play = async ({ canvasElement }) => { await findByText(canvasElement, '90aea7f') const rows = getAllByRole(canvasElement, 'row') - const lastRow = rows[rows.length - 1] + const lastRow = rows[rows.length - 2] const lastRowCells = within(lastRow).getAllByRole('cell') const lastCell = lastRowCells[lastRowCells.length - 1] lastCell.scrollIntoView() From 12221b184e3b8dacc01beae65fe2b2ef0d51649c Mon Sep 17 00:00:00 2001 From: Julie G <43496356+julieg18@users.noreply.github.com> Date: Thu, 27 Apr 2023 14:16:59 -0500 Subject: [PATCH 2/4] Show DVC Cli Details in DVC Setup (#3688) --- extension/src/setup/index.ts | 38 +- extension/src/setup/webview/contract.ts | 8 +- extension/src/setup/webview/messages.ts | 6 +- extension/src/test/suite/setup/index.test.ts | 22 +- .../components/table/styles.module.scss | 6 +- webview/src/setup/components/App.test.tsx | 358 ++++++++++++++++-- webview/src/setup/components/App.tsx | 20 +- .../src/setup/components/CliIncompatible.tsx | 20 - .../src/setup/components/CliUnavailable.tsx | 83 ---- .../setup/components/ProjectUninitialized.tsx | 83 ---- .../setup/components/dvc/CliIncompatible.tsx | 20 + .../setup/components/dvc/CliUnavailable.tsx | 41 ++ .../src/setup/components/{ => dvc}/Dvc.tsx | 46 ++- .../setup/components/dvc/DvcEnvCommandRow.tsx | 39 ++ .../setup/components/dvc/DvcEnvDetails.tsx | 34 ++ .../setup/components/dvc/DvcEnvInfoRow.tsx | 12 + .../setup/components/dvc/DvcUnitialized.tsx | 20 + .../setup/components/dvc/GitUnitialized.tsx | 36 ++ .../components/dvc/ProjectUninitialized.tsx | 37 ++ .../setup/components/dvc/styles.module.scss | 43 +++ webview/src/shared/mixins.scss | 9 + webview/src/stories/Setup.stories.tsx | 43 ++- 22 files changed, 746 insertions(+), 278 deletions(-) delete mode 100644 webview/src/setup/components/CliIncompatible.tsx delete mode 100644 webview/src/setup/components/CliUnavailable.tsx delete mode 100644 webview/src/setup/components/ProjectUninitialized.tsx create mode 100644 webview/src/setup/components/dvc/CliIncompatible.tsx create mode 100644 webview/src/setup/components/dvc/CliUnavailable.tsx rename webview/src/setup/components/{ => dvc}/Dvc.tsx (67%) create mode 100644 webview/src/setup/components/dvc/DvcEnvCommandRow.tsx create mode 100644 webview/src/setup/components/dvc/DvcEnvDetails.tsx create mode 100644 webview/src/setup/components/dvc/DvcEnvInfoRow.tsx create mode 100644 webview/src/setup/components/dvc/DvcUnitialized.tsx create mode 100644 webview/src/setup/components/dvc/GitUnitialized.tsx create mode 100644 webview/src/setup/components/dvc/ProjectUninitialized.tsx create mode 100644 webview/src/setup/components/dvc/styles.module.scss diff --git a/extension/src/setup/index.ts b/extension/src/setup/index.ts index 7ff15ea986..108d603d82 100644 --- a/extension/src/setup/index.ts +++ b/extension/src/setup/index.ts @@ -8,7 +8,11 @@ import { } from 'vscode' import { Disposable, Disposer } from '@hediet/std/disposable' import isEmpty from 'lodash.isempty' -import { SetupSection, SetupData as TSetupData } from './webview/contract' +import { + DvcCliDetails, + SetupSection, + SetupData as TSetupData +} from './webview/contract' import { collectSectionCollapsed } from './collect' import { WebviewMessages } from './webview/messages' import { validateTokenInput } from './inputBox' @@ -20,7 +24,6 @@ import { BaseWebview } from '../webview' import { ViewKey } from '../webview/constants' import { BaseRepository } from '../webview/repository' import { Resource } from '../resourceLocator' -import { isPythonExtensionInstalled } from '../extensions/python' import { findAbsoluteDvcRootPath, findDvcRootPaths, @@ -52,6 +55,7 @@ import { GLOBAL_WEBVIEW_DVCROOT } from '../webview/factory' import { ConfigKey, getConfigValue } from '../vscode/config' import { getValidInput } from '../vscode/inputBox' import { Title } from '../vscode/title' +import { getOptions } from '../cli/dvc/options' export type SetupWebviewWebview = BaseWebview @@ -334,10 +338,34 @@ export class Setup return this.sendDataToWebview() } + public async getDvcCliDetails(): Promise { + const dvcPath = this.config.getCliPath() + const pythonBinPath = this.config.getPythonBinPath() + const cwd = getFirstWorkspaceFolder() + + const { args, executable } = getOptions(pythonBinPath, dvcPath, cwd || '') + const commandArgs = args.length === 0 ? '' : ` ${args.join(' ')}` + const command = executable + commandArgs + + return { + command, + version: cwd ? await this.getCliVersion(cwd) : undefined + } + } + + private isDVCBeingUsedGlobally() { + const dvcPath = this.config.getCliPath() + const pythonBinPath = this.config.getPythonBinPath() + + return dvcPath || !pythonBinPath + } + private async sendDataToWebview() { const projectInitialized = this.hasRoots() const hasData = this.getHasData() + const isPythonExtensionUsed = await this.isPythonExtensionUsed() + const needsGitInitialized = !projectInitialized && !!(await this.needsGitInit()) @@ -348,11 +376,15 @@ export class Setup const pythonBinPath = await findPythonBinForInstall() + const dvcCliDetails = await this.getDvcCliDetails() + this.webviewMessages.sendWebviewMessage({ canGitInitialize, cliCompatible: this.cliCompatible, + dvcCliDetails, hasData, - isPythonExtensionInstalled: isPythonExtensionInstalled(), + isPythonExtensionUsed: + !this.isDVCBeingUsedGlobally() && isPythonExtensionUsed, isStudioConnected: this.studioIsConnected, needsGitCommit, needsGitInitialized, diff --git a/extension/src/setup/webview/contract.ts b/extension/src/setup/webview/contract.ts index a3aa5df5ea..a0b8ad14e5 100644 --- a/extension/src/setup/webview/contract.ts +++ b/extension/src/setup/webview/contract.ts @@ -1,8 +1,14 @@ +export type DvcCliDetails = { + command: string + version: string | undefined +} + export type SetupData = { canGitInitialize: boolean cliCompatible: boolean | undefined + dvcCliDetails: DvcCliDetails hasData: boolean | undefined - isPythonExtensionInstalled: boolean + isPythonExtensionUsed: boolean isStudioConnected: boolean needsGitCommit: boolean needsGitInitialized: boolean | undefined diff --git a/extension/src/setup/webview/messages.ts b/extension/src/setup/webview/messages.ts index b43d4597ee..98527b4a12 100644 --- a/extension/src/setup/webview/messages.ts +++ b/extension/src/setup/webview/messages.ts @@ -35,8 +35,9 @@ export class WebviewMessages { public sendWebviewMessage({ canGitInitialize, cliCompatible, + dvcCliDetails, hasData, - isPythonExtensionInstalled, + isPythonExtensionUsed, isStudioConnected, needsGitCommit, needsGitInitialized, @@ -48,8 +49,9 @@ export class WebviewMessages { void this.getWebview()?.show({ canGitInitialize, cliCompatible, + dvcCliDetails, hasData, - isPythonExtensionInstalled, + isPythonExtensionUsed, isStudioConnected, needsGitCommit, needsGitInitialized, diff --git a/extension/src/test/suite/setup/index.test.ts b/extension/src/test/suite/setup/index.test.ts index ca9a3a215c..5030e969cb 100644 --- a/extension/src/test/suite/setup/index.test.ts +++ b/extension/src/test/suite/setup/index.test.ts @@ -218,6 +218,7 @@ suite('Setup Test Suite', () => { setup.setCliCompatible(undefined) setup.setAvailable(false) await setup.setRoots() + stub(setup, 'getCliVersion').resolves(undefined) messageSpy.restore() const mockSendMessage = stub(BaseWebview.prototype, 'show') @@ -238,8 +239,9 @@ suite('Setup Test Suite', () => { expect(mockSendMessage).to.be.calledWithExactly({ canGitInitialize: true, cliCompatible: undefined, + dvcCliDetails: { command: 'dvc', version: undefined }, hasData: false, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: true, needsGitInitialized: true, @@ -278,8 +280,9 @@ suite('Setup Test Suite', () => { expect(mockSendMessage).to.be.calledWithExactly({ canGitInitialize: true, cliCompatible: true, + dvcCliDetails: { command: 'dvc', version: MIN_CLI_VERSION }, hasData: false, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: true, needsGitInitialized: true, @@ -324,8 +327,12 @@ suite('Setup Test Suite', () => { expect(mockSendMessage).to.be.calledWithExactly({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'dvc', + version: MIN_CLI_VERSION + }, hasData: false, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: false, @@ -370,8 +377,12 @@ suite('Setup Test Suite', () => { expect(mockSendMessage).to.be.calledWithExactly({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'dvc', + version: MIN_CLI_VERSION + }, hasData: false, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: true, needsGitInitialized: false, @@ -568,6 +579,7 @@ suite('Setup Test Suite', () => { mockRunSetup.restore() stub(config, 'isPythonExtensionUsed').returns(false) stub(config, 'getPythonBinPath').resolves(join('python')) + stub(setup, 'getDvcCliDetails').resolves(undefined) mockVersion.resetBehavior() mockVersion @@ -627,6 +639,7 @@ suite('Setup Test Suite', () => { mockExecuteCommand.restore() mockRunSetup.restore() stub(config, 'isPythonExtensionUsed').returns(true) + stub(setup, 'getDvcCliDetails').resolves(undefined) mockVersion.resetBehavior() mockVersion.rejects(new Error('no CLI here')) @@ -762,6 +775,7 @@ suite('Setup Test Suite', () => { const mockUpdate = stub() stub(workspace, 'getConfiguration').returns({ + get: stub(), update: mockUpdate } as unknown as WorkspaceConfiguration) diff --git a/webview/src/experiments/components/table/styles.module.scss b/webview/src/experiments/components/table/styles.module.scss index 9df1631a0c..ccf5961807 100644 --- a/webview/src/experiments/components/table/styles.module.scss +++ b/webview/src/experiments/components/table/styles.module.scss @@ -836,13 +836,9 @@ $badge-size: 0.85rem; // below table styles .buttonAsLink { - @extend %link; + @extend %buttonAsLink; - background: none; - border: none; - padding: 0; font-size: 0.65rem; - cursor: pointer; } .addConfigButton { diff --git a/webview/src/setup/components/App.test.tsx b/webview/src/setup/components/App.test.tsx index 0a48581f96..702ebba4b9 100644 --- a/webview/src/setup/components/App.test.tsx +++ b/webview/src/setup/components/App.test.tsx @@ -3,6 +3,7 @@ import { MessageFromWebviewType, MessageToWebviewType } from 'dvc/src/webview/contract' +import { MAX_CLI_VERSION, MIN_CLI_VERSION } from 'dvc/src/cli/dvc/contract' import '@testing-library/jest-dom/extend-expect' import React from 'react' import { SetupSection, SetupData } from 'dvc/src/setup/webview/contract' @@ -18,8 +19,9 @@ const mockPostMessage = jest.mocked(postMessage) const renderApp = ({ canGitInitialize, cliCompatible, + dvcCliDetails, hasData, - isPythonExtensionInstalled, + isPythonExtensionUsed, isStudioConnected, needsGitInitialized, needsGitCommit, @@ -36,8 +38,9 @@ const renderApp = ({ data: { canGitInitialize, cliCompatible, + dvcCliDetails, hasData, - isPythonExtensionInstalled, + isPythonExtensionUsed, isStudioConnected, needsGitCommit, needsGitInitialized, @@ -66,8 +69,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: false, + dvcCliDetails: { + command: 'dvc', + version: '1.0.0' + }, hasData: false, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: undefined, @@ -92,8 +99,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: undefined, + dvcCliDetails: { + command: 'dvc', + version: undefined + }, hasData: false, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: undefined, @@ -112,8 +123,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: undefined, + dvcCliDetails: { + command: 'dvc', + version: undefined + }, hasData: false, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: undefined, @@ -136,8 +151,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: undefined, + dvcCliDetails: { + command: `${defaultInterpreter} -m dvc`, + version: undefined + }, hasData: false, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: undefined, @@ -159,8 +178,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: undefined, + dvcCliDetails: { + command: 'python -m dvc', + version: undefined + }, hasData: false, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: undefined, @@ -170,7 +193,7 @@ describe('App', () => { shareLiveToStudio: false }) - const button = screen.getByText('Setup The Workspace') + const button = screen.getByText('Configure') fireEvent.click(button) expect(mockPostMessage).toHaveBeenCalledWith({ @@ -182,8 +205,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: undefined, + dvcCliDetails: { + command: 'python -m dvc', + version: undefined + }, hasData: false, - isPythonExtensionInstalled: true, + isPythonExtensionUsed: true, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: undefined, @@ -193,11 +220,11 @@ describe('App', () => { shareLiveToStudio: false }) - const button = screen.getByText('Select Python Interpreter') + const button = screen.getByText('Configure') fireEvent.click(button) expect(mockPostMessage).toHaveBeenCalledWith({ - type: MessageFromWebviewType.SELECT_PYTHON_INTERPRETER + type: MessageFromWebviewType.SETUP_WORKSPACE }) }) @@ -205,8 +232,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: undefined, + dvcCliDetails: { + command: 'python -m dvc', + version: undefined + }, hasData: false, - isPythonExtensionInstalled: true, + isPythonExtensionUsed: true, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: undefined, @@ -228,8 +259,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: false, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: undefined, @@ -244,12 +279,16 @@ describe('App', () => { ).not.toBeInTheDocument() }) - it('should not show a screen saying that DVC is not initialized if the project is not initialized and git is uninitialized', () => { + it('should show a screen saying that DVC is not initialized if the project is not initialized and git is uninitialized', () => { renderApp({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: false, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: true, @@ -266,8 +305,12 @@ describe('App', () => { renderApp({ canGitInitialize: true, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: false, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: true, @@ -287,8 +330,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: false, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: true, @@ -305,8 +352,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: false, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: undefined, @@ -323,8 +374,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: false, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: undefined, @@ -343,8 +398,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: false, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: undefined, @@ -366,8 +425,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: false, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: undefined, @@ -389,8 +452,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: true, - isPythonExtensionInstalled: true, + isPythonExtensionUsed: true, isStudioConnected: true, needsGitCommit: false, needsGitInitialized: false, @@ -407,6 +474,161 @@ describe('App', () => { type: MessageFromWebviewType.OPEN_EXPERIMENTS_WEBVIEW }) }) + + it('should show the user the version if dvc is installed', () => { + renderApp({ + canGitInitialize: false, + cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, + hasData: true, + isPythonExtensionUsed: true, + isStudioConnected: true, + needsGitCommit: false, + needsGitInitialized: false, + projectInitialized: true, + pythonBinPath: 'python', + sectionCollapsed: undefined, + shareLiveToStudio: false + }) + + const envDetails = screen.getByTestId('dvc-env-details') + const command = `1.0.0 (${MIN_CLI_VERSION} <= required < ${MAX_CLI_VERSION}.0.0)` + + expect(within(envDetails).getByText('Version')).toBeInTheDocument() + expect(within(envDetails).getByText(command)).toBeInTheDocument() + }) + + it('should tell the user that version is not found if dvc is not installed', () => { + renderApp({ + canGitInitialize: false, + cliCompatible: false, + dvcCliDetails: { + command: 'dvc', + version: undefined + }, + hasData: false, + isPythonExtensionUsed: false, + isStudioConnected: false, + needsGitCommit: false, + needsGitInitialized: undefined, + projectInitialized: false, + pythonBinPath: undefined, + sectionCollapsed: undefined, + shareLiveToStudio: false + }) + const envDetails = screen.getByTestId('dvc-env-details') + const command = `Not found (${MIN_CLI_VERSION} <= required < ${MAX_CLI_VERSION}.0.0)` + + expect(within(envDetails).getByText('Version')).toBeInTheDocument() + expect(within(envDetails).getByText(command)).toBeInTheDocument() + }) + + it('should show the user an example command if dvc is installed', () => { + const command = 'python -m dvc' + renderApp({ + canGitInitialize: false, + cliCompatible: true, + dvcCliDetails: { + command, + version: '1.0.0' + }, + hasData: true, + isPythonExtensionUsed: true, + isStudioConnected: true, + needsGitCommit: false, + needsGitInitialized: false, + projectInitialized: true, + pythonBinPath: 'python', + sectionCollapsed: undefined, + shareLiveToStudio: false + }) + + const envDetails = screen.getByTestId('dvc-env-details') + + expect(within(envDetails).getByText('Command')).toBeInTheDocument() + expect(within(envDetails).getByText(command)).toBeInTheDocument() + }) + + it('should show user an example command with a "Configure" button if dvc is installed without the python extension', () => { + renderApp({ + canGitInitialize: false, + cliCompatible: true, + dvcCliDetails: { + command: 'dvc', + version: '1.0.0' + }, + hasData: true, + isPythonExtensionUsed: false, + isStudioConnected: true, + needsGitCommit: false, + needsGitInitialized: false, + projectInitialized: true, + pythonBinPath: undefined, + sectionCollapsed: undefined, + shareLiveToStudio: false + }) + + const envDetails = screen.getByTestId('dvc-env-details') + + expect(within(envDetails).getByText('Command')).toBeInTheDocument() + + const configureButton = within(envDetails).getByText('Configure') + const selectButton = within(envDetails).queryByText( + 'Select Python Interpreter' + ) + + expect(configureButton).toBeInTheDocument() + expect(selectButton).not.toBeInTheDocument() + + fireEvent.click(configureButton) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: MessageFromWebviewType.SETUP_WORKSPACE + }) + }) + + it('should show user an example command with "Configure" and "Select Python Interpreter" buttons if dvc is installed with the python extension', () => { + renderApp({ + canGitInitialize: false, + cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, + hasData: true, + isPythonExtensionUsed: true, + isStudioConnected: true, + needsGitCommit: false, + needsGitInitialized: false, + projectInitialized: true, + pythonBinPath: 'python', + sectionCollapsed: undefined, + shareLiveToStudio: false + }) + + const envDetails = screen.getByTestId('dvc-env-details') + + expect(within(envDetails).getByText('Command')).toBeInTheDocument() + + const configureButton = within(envDetails).getByText('Configure') + const selectButton = within(envDetails).getByText( + 'Select Python Interpreter' + ) + + expect(configureButton).toBeInTheDocument() + expect(selectButton).toBeInTheDocument() + + mockPostMessage.mockClear() + + fireEvent.click(selectButton) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: MessageFromWebviewType.SELECT_PYTHON_INTERPRETER + }) + }) }) describe('Experiments', () => { @@ -414,8 +636,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: false, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: true, needsGitInitialized: true, @@ -432,8 +658,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: false, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: true, needsGitInitialized: true, @@ -456,9 +686,13 @@ describe('App', () => { it('should show a screen saying that dvc is not setup if the project is initalized but dvc is not installed', () => { renderApp({ canGitInitialize: false, - cliCompatible: true, + cliCompatible: false, + dvcCliDetails: { + command: 'dvc', + version: undefined + }, hasData: false, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: undefined, @@ -475,8 +709,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: true, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: undefined, @@ -495,8 +733,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: false, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: true, needsGitInitialized: false, @@ -513,8 +755,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: undefined, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: undefined, @@ -531,8 +777,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: false, - isPythonExtensionInstalled: false, + isPythonExtensionUsed: false, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: undefined, @@ -551,8 +801,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: true, - isPythonExtensionInstalled: true, + isPythonExtensionUsed: true, isStudioConnected: true, needsGitCommit: false, needsGitInitialized: false, @@ -576,8 +830,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: false, - isPythonExtensionInstalled: true, + isPythonExtensionUsed: true, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: false, @@ -596,8 +854,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: false, - isPythonExtensionInstalled: true, + isPythonExtensionUsed: true, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: false, @@ -620,8 +882,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: false, - isPythonExtensionInstalled: true, + isPythonExtensionUsed: true, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: false, @@ -644,8 +910,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: false, - isPythonExtensionInstalled: true, + isPythonExtensionUsed: true, isStudioConnected: false, needsGitCommit: false, needsGitInitialized: false, @@ -671,8 +941,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: false, - isPythonExtensionInstalled: true, + isPythonExtensionUsed: true, isStudioConnected: true, needsGitCommit: false, needsGitInitialized: false, @@ -695,8 +969,12 @@ describe('App', () => { renderApp({ canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: false, - isPythonExtensionInstalled: true, + isPythonExtensionUsed: true, isStudioConnected: true, needsGitCommit: false, needsGitInitialized: false, @@ -718,8 +996,12 @@ describe('App', () => { const testData = { canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'python -m dvc', + version: '1.0.0' + }, hasData: false, - isPythonExtensionInstalled: true, + isPythonExtensionUsed: true, isStudioConnected: true, needsGitCommit: false, needsGitInitialized: false, diff --git a/webview/src/setup/components/App.tsx b/webview/src/setup/components/App.tsx index 6140e1d4ee..1d01e31fce 100644 --- a/webview/src/setup/components/App.tsx +++ b/webview/src/setup/components/App.tsx @@ -1,14 +1,15 @@ import { DEFAULT_SECTION_COLLAPSED, SetupSection, - SetupData + SetupData, + DvcCliDetails } from 'dvc/src/setup/webview/contract' import { MessageFromWebviewType, MessageToWebview } from 'dvc/src/webview/contract' import React, { useCallback, useState } from 'react' -import { Dvc } from './Dvc' +import { Dvc } from './dvc/Dvc' import { Experiments } from './Experiments' import { Studio } from './studio/Studio' import { SetupContainer } from './SetupContainer' @@ -19,6 +20,9 @@ export const App: React.FC = () => { const [cliCompatible, setCliCompatible] = useState( undefined ) + const [dvcCliDetails, setDvcCliDetails] = useState( + undefined + ) const [projectInitialized, setProjectInitialized] = useState(false) const [needsGitInitialized, setNeedsGitInitialized] = useState< boolean | undefined @@ -30,13 +34,12 @@ export const App: React.FC = () => { const [pythonBinPath, setPythonBinPath] = useState( undefined ) - const [isPythonExtensionInstalled, setIsPythonExtensionInstalled] = + const [isPythonExtensionUsed, setisPythonExtensionUsed] = useState(false) const [hasData, setHasData] = useState(false) const [sectionCollapsed, setSectionCollapsed] = useState( DEFAULT_SECTION_COLLAPSED ) - const [isStudioConnected, setIsStudioConnected] = useState(false) const [shareLiveToStudio, setShareLiveToStudioValue] = useState(false) @@ -50,7 +53,8 @@ export const App: React.FC = () => { setCanGitInitialized(data.data.canGitInitialize) setCliCompatible(data.data.cliCompatible) setHasData(data.data.hasData) - setIsPythonExtensionInstalled(data.data.isPythonExtensionInstalled) + setDvcCliDetails(data.data.dvcCliDetails) + setisPythonExtensionUsed(data.data.isPythonExtensionUsed) setNeedsGitInitialized(data.data.needsGitInitialized) setNeedsGitCommit(data.data.needsGitCommit) setProjectInitialized(data.data.projectInitialized) @@ -65,7 +69,8 @@ export const App: React.FC = () => { setCanGitInitialized, setCliCompatible, setHasData, - setIsPythonExtensionInstalled, + setDvcCliDetails, + setisPythonExtensionUsed, setNeedsGitInitialized, setNeedsGitCommit, setProjectInitialized, @@ -96,7 +101,8 @@ export const App: React.FC = () => { void } - -export const CliIncompatible: React.FC = ({ - checkCompatibility -}) => ( - -
-

DVC is incompatible

-

The located CLI is incompatible with the extension.

-

The minimum version is {MIN_CLI_VERSION}.

-

Please update your install and try again.

-
-
-) diff --git a/webview/src/setup/components/CliUnavailable.tsx b/webview/src/setup/components/CliUnavailable.tsx deleted file mode 100644 index 880c7aa93e..0000000000 --- a/webview/src/setup/components/CliUnavailable.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react' -import { Button } from '../../shared/components/button/Button' -import { EmptyState } from '../../shared/components/emptyState/EmptyState' - -const Title: React.FC = () =>

DVC is currently unavailable

- -export type CliUnavailableProps = { - installDvc: () => void - isPythonExtensionInstalled: boolean - pythonBinPath: string | undefined - selectPythonInterpreter: () => void - setupWorkspace: () => void -} - -const OfferToInstall: React.FC<{ - children: React.ReactNode - pythonBinPath: string - installDvc: () => void -}> = ({ installDvc, pythonBinPath, children }) => ( -
-

DVC & DVCLive can be auto-installed as packages with {pythonBinPath}

-
-) - -const UpdateInterpreterOrFind: React.FC<{ - action: string - description: string - onClick: () => void -}> = ({ action, description, onClick }) => ( -
-

{description}

-
-) - -export const CliUnavailable: React.FC = ({ - installDvc, - isPythonExtensionInstalled, - pythonBinPath, - selectPythonInterpreter, - setupWorkspace -}) => { - const SetupWorkspace: React.FC<{ description: string }> = ({ - description - }) => ( - - ) - - const canInstall = !!pythonBinPath - - if (!canInstall) { - return ( - - - <p>DVC & DVCLive cannot be auto-installed as Python was not located.</p> - <SetupWorkspace description="To locate a Python Interpreter or DVC." /> - </EmptyState> - ) - } - - return ( - <EmptyState isFullScreen={false}> - <Title /> - <OfferToInstall pythonBinPath={pythonBinPath} installDvc={installDvc}> - {isPythonExtensionInstalled ? ( - <UpdateInterpreterOrFind - action="Select Python Interpreter" - description="To update the interpreter and/or locate DVC." - onClick={selectPythonInterpreter} - /> - ) : ( - <SetupWorkspace description="To update the install location or locate DVC." /> - )} - </OfferToInstall> - </EmptyState> - ) -} diff --git a/webview/src/setup/components/ProjectUninitialized.tsx b/webview/src/setup/components/ProjectUninitialized.tsx deleted file mode 100644 index 014282c67f..0000000000 --- a/webview/src/setup/components/ProjectUninitialized.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react' -import { EmptyState } from '../../shared/components/emptyState/EmptyState' -import { Button } from '../../shared/components/button/Button' - -const Header: React.FC = () => <h1>DVC is not initialized</h1> - -interface GitUninitializedProps { - canGitInitialize: boolean | undefined - initializeGit: () => void -} - -const GitIsPrerequisite: React.FC = () => ( - <p>A Git repository is a prerequisite of project initialization.</p> -) - -const GitUninitialized: React.FC<GitUninitializedProps> = ({ - canGitInitialize, - initializeGit -}) => { - if (!canGitInitialize) { - return ( - <EmptyState isFullScreen={false}> - <Header /> - <GitIsPrerequisite /> - <p> - The extension is unable to initialize a Git repository in this - workspace. - </p> - <p> - Please open a different folder which contains no Git repositories or a - single existing Git repository at the root. - </p> - </EmptyState> - ) - } - - return ( - <EmptyState isFullScreen={false}> - <Header /> - <GitIsPrerequisite /> - <Button onClick={initializeGit} text="Initialize Git" /> - </EmptyState> - ) -} - -const DvcUninitialized: React.FC<{ initializeDvc: () => void }> = ({ - initializeDvc -}) => ( - <EmptyState isFullScreen={false}> - <Header /> - <p> - The current workspace does not contain a DVC project. You can initialize a - project which will enable features powered by DVC. To learn more about how - to use DVC please read <a href="https://dvc.org/doc">our docs</a>. - </p> - <Button onClick={initializeDvc} text="Initialize Project"></Button> - </EmptyState> -) - -export interface ProjectUninitializedProps { - canGitInitialize: boolean | undefined - initializeDvc: () => void - initializeGit: () => void - needsGitInitialized: boolean | undefined -} - -export const ProjectUninitialized: React.FC<ProjectUninitializedProps> = ({ - initializeDvc, - needsGitInitialized, - canGitInitialize, - initializeGit -}) => { - if (needsGitInitialized) { - return ( - <GitUninitialized - initializeGit={initializeGit} - canGitInitialize={canGitInitialize} - /> - ) - } - - return <DvcUninitialized initializeDvc={initializeDvc} /> -} diff --git a/webview/src/setup/components/dvc/CliIncompatible.tsx b/webview/src/setup/components/dvc/CliIncompatible.tsx new file mode 100644 index 0000000000..2bb441dc9e --- /dev/null +++ b/webview/src/setup/components/dvc/CliIncompatible.tsx @@ -0,0 +1,20 @@ +import React, { PropsWithChildren } from 'react' +import { EmptyState } from '../../../shared/components/emptyState/EmptyState' +import { Button } from '../../../shared/components/button/Button' + +type CliIncompatibleProps = { + checkCompatibility: () => void +} + +export const CliIncompatible: React.FC< + PropsWithChildren<CliIncompatibleProps> +> = ({ checkCompatibility, children }) => ( + <EmptyState isFullScreen={false}> + <div> + <h1>DVC is incompatible</h1> + {children} + <p>Please update your install and try again.</p> + <Button text="Check Compatibility" onClick={checkCompatibility} /> + </div> + </EmptyState> +) diff --git a/webview/src/setup/components/dvc/CliUnavailable.tsx b/webview/src/setup/components/dvc/CliUnavailable.tsx new file mode 100644 index 0000000000..fa9b7ad303 --- /dev/null +++ b/webview/src/setup/components/dvc/CliUnavailable.tsx @@ -0,0 +1,41 @@ +import React, { PropsWithChildren } from 'react' +import styles from './styles.module.scss' +import { Button } from '../../../shared/components/button/Button' +import { EmptyState } from '../../../shared/components/emptyState/EmptyState' + +export type CliUnavailableProps = { + installDvc: () => void + pythonBinPath: string | undefined + setupWorkspace: () => void +} + +export const CliUnavailable: React.FC< + PropsWithChildren<CliUnavailableProps> +> = ({ installDvc, pythonBinPath, setupWorkspace, children }) => { + const canInstall = !!pythonBinPath + + const contents = canInstall ? ( + <> + <p> + DVC & DVCLive can be auto-installed as packages with {pythonBinPath} + </p> + <div className={styles.sideBySideButtons}> + <Button onClick={installDvc} text="Install" /> + <Button onClick={setupWorkspace} text="Configure" /> + </div> + </> + ) : ( + <> + <p>DVC & DVCLive cannot be auto-installed as Python was not located.</p> + <Button onClick={setupWorkspace} text="Configure" /> + </> + ) + + return ( + <EmptyState isFullScreen={false}> + <h1>DVC is currently unavailable</h1> + {children} + {contents} + </EmptyState> + ) +} diff --git a/webview/src/setup/components/Dvc.tsx b/webview/src/setup/components/dvc/Dvc.tsx similarity index 67% rename from webview/src/setup/components/Dvc.tsx rename to webview/src/setup/components/dvc/Dvc.tsx index 0b02b9a8d4..fd98a52f83 100644 --- a/webview/src/setup/components/Dvc.tsx +++ b/webview/src/setup/components/dvc/Dvc.tsx @@ -1,26 +1,26 @@ import React from 'react' -import { SectionCollapsed } from 'dvc/src/setup/webview/contract' +import { DvcCliDetails, SectionCollapsed } from 'dvc/src/setup/webview/contract' +import { DvcEnvDetails } from './DvcEnvDetails' import { CliIncompatible } from './CliIncompatible' -import { CliUnavailable } from './CliUnavailable' import { ProjectUninitialized } from './ProjectUninitialized' +import { CliUnavailable } from './CliUnavailable' import { checkCompatibility, initializeDvc, initializeGit, installDvc, - selectPythonInterpreter, setupWorkspace, showExperiments -} from './messages' - -import { EmptyState } from '../../shared/components/emptyState/EmptyState' -import { Beaker } from '../../shared/components/icons' -import { IconButton } from '../../shared/components/button/IconButton' +} from '../messages' +import { EmptyState } from '../../../shared/components/emptyState/EmptyState' +import { Beaker } from '../../../shared/components/icons' +import { IconButton } from '../../../shared/components/button/IconButton' export type DvcProps = { canGitInitialize: boolean | undefined cliCompatible: boolean | undefined - isPythonExtensionInstalled: boolean + dvcCliDetails: DvcCliDetails | undefined + isPythonExtensionUsed: boolean needsGitInitialized: boolean | undefined projectInitialized: boolean pythonBinPath: string | undefined @@ -31,26 +31,38 @@ export type DvcProps = { export const Dvc: React.FC<DvcProps> = ({ canGitInitialize, cliCompatible, - isPythonExtensionInstalled, + dvcCliDetails, + isPythonExtensionUsed, needsGitInitialized, projectInitialized, pythonBinPath, setSectionCollapsed, isExperimentsAvailable }) => { + const children = dvcCliDetails && ( + <DvcEnvDetails + {...dvcCliDetails} + isPythonExtensionUsed={isPythonExtensionUsed} + /> + ) + if (cliCompatible === false) { - return <CliIncompatible checkCompatibility={checkCompatibility} /> + return ( + <CliIncompatible checkCompatibility={checkCompatibility}> + {children} + </CliIncompatible> + ) } if (cliCompatible === undefined) { return ( <CliUnavailable installDvc={installDvc} - isPythonExtensionInstalled={isPythonExtensionInstalled} pythonBinPath={pythonBinPath} - selectPythonInterpreter={selectPythonInterpreter} setupWorkspace={setupWorkspace} - /> + > + {children} + </CliUnavailable> ) } @@ -61,13 +73,15 @@ export const Dvc: React.FC<DvcProps> = ({ initializeDvc={initializeDvc} initializeGit={initializeGit} needsGitInitialized={needsGitInitialized} - /> + > + {children} + </ProjectUninitialized> ) } - return ( <EmptyState isFullScreen={false}> <h1>Setup Complete</h1> + {children} <IconButton appearance="primary" icon={Beaker} diff --git a/webview/src/setup/components/dvc/DvcEnvCommandRow.tsx b/webview/src/setup/components/dvc/DvcEnvCommandRow.tsx new file mode 100644 index 0000000000..422bab99e5 --- /dev/null +++ b/webview/src/setup/components/dvc/DvcEnvCommandRow.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import { DvcEnvInfoRow } from './DvcEnvInfoRow' +import styles from './styles.module.scss' +import { selectPythonInterpreter, setupWorkspace } from '../messages' + +interface DvcEnvCommandRowProps { + command: string + isPythonExtensionUsed: boolean +} + +export const DvcEnvCommandRow: React.FC<DvcEnvCommandRowProps> = ({ + command, + isPythonExtensionUsed +}) => { + const commandText = command || 'Not found' + const commandValue = ( + <> + <span className={styles.command}>{commandText}</span> + <span className={styles.actions}> + <button className={styles.buttonAsLink} onClick={setupWorkspace}> + Configure + </button> + {isPythonExtensionUsed && ( + <> + <span className={styles.separator} /> + <button + className={styles.buttonAsLink} + onClick={selectPythonInterpreter} + > + Select Python Interpreter + </button> + </> + )} + </span> + </> + ) + + return <DvcEnvInfoRow title="Command" text={commandValue} /> +} diff --git a/webview/src/setup/components/dvc/DvcEnvDetails.tsx b/webview/src/setup/components/dvc/DvcEnvDetails.tsx new file mode 100644 index 0000000000..5622749f8b --- /dev/null +++ b/webview/src/setup/components/dvc/DvcEnvDetails.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import { DvcCliDetails } from 'dvc/src/setup/webview/contract' +import { MAX_CLI_VERSION, MIN_CLI_VERSION } from 'dvc/src/cli/dvc/contract' +import { DvcEnvInfoRow } from './DvcEnvInfoRow' +import styles from './styles.module.scss' +import { DvcEnvCommandRow } from './DvcEnvCommandRow' + +interface DvcEnvDetailsProps extends DvcCliDetails { + isPythonExtensionUsed: boolean +} + +export const DvcEnvDetails: React.FC<DvcEnvDetailsProps> = ({ + command, + version, + isPythonExtensionUsed +}) => { + const versionText = `${ + version || 'Not found' + } (${MIN_CLI_VERSION} <= required < ${MAX_CLI_VERSION}.0.0)` + + return ( + <table data-testid="dvc-env-details" className={styles.envDetails}> + <tbody> + {version && ( + <DvcEnvCommandRow + isPythonExtensionUsed={isPythonExtensionUsed} + command={command} + /> + )} + <DvcEnvInfoRow title="Version" text={versionText} /> + </tbody> + </table> + ) +} diff --git a/webview/src/setup/components/dvc/DvcEnvInfoRow.tsx b/webview/src/setup/components/dvc/DvcEnvInfoRow.tsx new file mode 100644 index 0000000000..deecc1b761 --- /dev/null +++ b/webview/src/setup/components/dvc/DvcEnvInfoRow.tsx @@ -0,0 +1,12 @@ +import React, { ReactElement } from 'react' +import styles from './styles.module.scss' + +export const DvcEnvInfoRow: React.FC<{ + title: string + text: string | ReactElement +}> = ({ title, text }) => ( + <tr> + <td className={styles.envDetailsKey}>{title}</td> + <td className={styles.envDetailsValue}>{text}</td> + </tr> +) diff --git a/webview/src/setup/components/dvc/DvcUnitialized.tsx b/webview/src/setup/components/dvc/DvcUnitialized.tsx new file mode 100644 index 0000000000..02f0fee866 --- /dev/null +++ b/webview/src/setup/components/dvc/DvcUnitialized.tsx @@ -0,0 +1,20 @@ +import React, { PropsWithChildren } from 'react' +import { EmptyState } from '../../../shared/components/emptyState/EmptyState' +import { Button } from '../../../shared/components/button/Button' + +export const DvcUninitialized: React.FC< + PropsWithChildren<{ + initializeDvc: () => void + }> +> = ({ initializeDvc, children }) => ( + <EmptyState isFullScreen={false}> + <h1>DVC is not initialized</h1> + {children} + <p> + The current workspace does not contain a DVC project. You can initialize a + project which will enable features powered by DVC. To learn more about how + to use DVC please read <a href="https://dvc.org/doc">our docs</a>. + </p> + <Button onClick={initializeDvc} text="Initialize Project"></Button> + </EmptyState> +) diff --git a/webview/src/setup/components/dvc/GitUnitialized.tsx b/webview/src/setup/components/dvc/GitUnitialized.tsx new file mode 100644 index 0000000000..50bfc7465e --- /dev/null +++ b/webview/src/setup/components/dvc/GitUnitialized.tsx @@ -0,0 +1,36 @@ +import React, { PropsWithChildren } from 'react' +import { EmptyState } from '../../../shared/components/emptyState/EmptyState' +import { Button } from '../../../shared/components/button/Button' + +interface GitUninitializedProps { + canGitInitialize: boolean | undefined + initializeGit: () => void +} + +export const GitUninitialized: React.FC< + PropsWithChildren<GitUninitializedProps> +> = ({ canGitInitialize, initializeGit, children }) => { + const conditionalContent = canGitInitialize ? ( + <Button onClick={initializeGit} text="Initialize Git" /> + ) : ( + <> + <p> + The extension is unable to initialize a Git repository in this + workspace. + </p> + <p> + Please open a different folder which contains no Git repositories or a + single existing Git repository at the root. + </p> + </> + ) + + return ( + <EmptyState isFullScreen={false}> + <h1>DVC is not initialized</h1> + {children} + <p>A Git repository is a prerequisite of project initialization.</p> + {conditionalContent} + </EmptyState> + ) +} diff --git a/webview/src/setup/components/dvc/ProjectUninitialized.tsx b/webview/src/setup/components/dvc/ProjectUninitialized.tsx new file mode 100644 index 0000000000..2af828dda3 --- /dev/null +++ b/webview/src/setup/components/dvc/ProjectUninitialized.tsx @@ -0,0 +1,37 @@ +import React, { PropsWithChildren } from 'react' +import { GitUninitialized } from './GitUnitialized' +import { DvcUninitialized } from './DvcUnitialized' + +export interface ProjectUninitializedProps { + canGitInitialize: boolean | undefined + initializeDvc: () => void + initializeGit: () => void + needsGitInitialized: boolean | undefined +} + +export const ProjectUninitialized: React.FC< + PropsWithChildren<ProjectUninitializedProps> +> = ({ + initializeDvc, + needsGitInitialized, + canGitInitialize, + initializeGit, + children +}) => { + if (needsGitInitialized) { + return ( + <GitUninitialized + initializeGit={initializeGit} + canGitInitialize={canGitInitialize} + > + {children} + </GitUninitialized> + ) + } + + return ( + <DvcUninitialized initializeDvc={initializeDvc}> + {children} + </DvcUninitialized> + ) +} diff --git a/webview/src/setup/components/dvc/styles.module.scss b/webview/src/setup/components/dvc/styles.module.scss new file mode 100644 index 0000000000..971a1c2a5c --- /dev/null +++ b/webview/src/setup/components/dvc/styles.module.scss @@ -0,0 +1,43 @@ +@import '../../../shared/variables'; +@import '../../../shared/mixins'; + +.envDetails { + margin: 0 auto; + text-align: left; + margin-bottom: 1rem; +} + +.envDetailsKey, +.envDetailsValue { + padding: 5px; +} + +.envDetailsKey { + font-weight: bold; + white-space: nowrap; + vertical-align: top; +} + +.envDetailsValue { + padding-left: 50px; + display: flex; + flex-direction: column; +} + +.separator { + margin: 0 5px; + + &::before { + content: '|'; + } +} + +.buttonAsLink { + @extend %buttonAsLink; + + font-size: inherit; +} + +.sideBySideButtons > *:not(:first-child) { + margin-left: 15px; +} diff --git a/webview/src/shared/mixins.scss b/webview/src/shared/mixins.scss index 5c6ce52440..8bb13aa32a 100644 --- a/webview/src/shared/mixins.scss +++ b/webview/src/shared/mixins.scss @@ -9,3 +9,12 @@ color: var(--vscode-textLink-activeForeground); } } + +%buttonAsLink { + @extend %link; + + background: none; + border: none; + padding: 0; + cursor: pointer; +} diff --git a/webview/src/stories/Setup.stories.tsx b/webview/src/stories/Setup.stories.tsx index ba9a0b1e72..c9fbf08672 100644 --- a/webview/src/stories/Setup.stories.tsx +++ b/webview/src/stories/Setup.stories.tsx @@ -8,8 +8,12 @@ import { App } from '../setup/components/App' const DEFAULT_DATA: SetupData = { canGitInitialize: false, cliCompatible: true, + dvcCliDetails: { + command: 'path/to/python -m dvc', + version: '1.0.0' + }, hasData: false, - isPythonExtensionInstalled: true, + isPythonExtensionUsed: true, isStudioConnected: true, needsGitCommit: false, needsGitInitialized: false, @@ -61,7 +65,6 @@ NoDataNotConnected.args = getUpdatedArgs({ }) export const CompletedConnected = Template.bind({}) - CompletedConnected.args = getUpdatedArgs({ hasData: true, isStudioConnected: true, @@ -72,24 +75,24 @@ CompletedConnected.args = getUpdatedArgs({ export const NoCLIPythonNotFound = Template.bind({}) NoCLIPythonNotFound.args = getUpdatedArgs({ cliCompatible: undefined, - isPythonExtensionInstalled: false, + dvcCliDetails: { + command: 'dvc', + version: undefined + }, + isPythonExtensionUsed: false, pythonBinPath: undefined }) -export const NoCLIPythonExtensionUsed = Template.bind({}) -NoCLIPythonExtensionUsed.args = getUpdatedArgs({ +export const NoCLIPythonFound = Template.bind({}) +NoCLIPythonFound.args = getUpdatedArgs({ cliCompatible: undefined, - isPythonExtensionInstalled: true, + dvcCliDetails: { + command: '/opt/homebrew/Caskroom/miniforge/base/bin/python -m dvc', + version: undefined + }, pythonBinPath: '/opt/homebrew/Caskroom/miniforge/base/bin/python' }) -export const NoCLIPythonExtensionNotUsed = Template.bind({}) -NoCLIPythonExtensionNotUsed.args = getUpdatedArgs({ - cliCompatible: undefined, - isPythonExtensionInstalled: false, - pythonBinPath: '.env/bin/python' -}) - export const CliFoundButNotCompatible = Template.bind({}) CliFoundButNotCompatible.args = getUpdatedArgs({ cliCompatible: false @@ -98,17 +101,25 @@ CliFoundButNotCompatible.args = getUpdatedArgs({ export const CannotInitializeGit = Template.bind({}) CannotInitializeGit.args = getUpdatedArgs({ canGitInitialize: false, - needsGitInitialized: true + needsGitInitialized: true, + projectInitialized: false }) export const CanInitializeGit = Template.bind({}) CanInitializeGit.args = getUpdatedArgs({ canGitInitialize: true, - needsGitInitialized: true + needsGitInitialized: true, + projectInitialized: false }) export const DvcUninitialized = Template.bind({}) DvcUninitialized.args = getUpdatedArgs({ canGitInitialize: undefined, - needsGitInitialized: undefined + needsGitInitialized: undefined, + projectInitialized: false +}) + +export const CliFoundManually = Template.bind({}) +CliFoundManually.args = getUpdatedArgs({ + isPythonExtensionUsed: false }) From 624a426ad9a1f169a95a1d817d16d8db3bc1ee94 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Apr 2023 19:33:45 +0000 Subject: [PATCH 3/4] chore(deps): update dependency sinon to v15.0.4 (#3772) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- extension/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/extension/package.json b/extension/package.json index 898ccb1023..3e73815e66 100644 --- a/extension/package.json +++ b/extension/package.json @@ -1693,7 +1693,7 @@ "mock-require": "3.0.3", "process-exists": "4.1.0", "shx": "0.3.4", - "sinon": "15.0.3", + "sinon": "15.0.4", "sinon-chai": "3.7.0", "ts-loader": "9.4.2", "vscode-uri": "3.0.7", diff --git a/yarn.lock b/yarn.lock index ab002b17fa..556ed1b80e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18070,10 +18070,10 @@ sinon-chai@3.7.0: resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-3.7.0.tgz#cfb7dec1c50990ed18c153f1840721cf13139783" integrity sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g== -sinon@15.0.3: - version "15.0.3" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-15.0.3.tgz#38005fcd80827177b6aa0245f82401d9ec88994b" - integrity sha512-si3geiRkeovP7Iel2O+qGL4NrO9vbMf3KsrJEi0ghP1l5aBkB5UxARea5j0FUsSqH3HLBh0dQPAyQ8fObRUqHw== +sinon@15.0.4: + version "15.0.4" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-15.0.4.tgz#bcca6fef19b14feccc96473f0d7adc81e0bc5268" + integrity sha512-uzmfN6zx3GQaria1kwgWGeKiXSSbShBbue6Dcj0SI8fiCNFbiUDqKl57WFlY5lyhxZVUKmXvzgG2pilRQCBwWg== dependencies: "@sinonjs/commons" "^3.0.0" "@sinonjs/fake-timers" "^10.0.2" From 4f4c7e5483db315408b8decaef0e6e556c29a788 Mon Sep 17 00:00:00 2001 From: Matt Seddon <37993418+mattseddon@users.noreply.github.com> Date: Fri, 28 Apr 2023 05:59:30 +1000 Subject: [PATCH 4/4] Watch all workspace folders for changes in dot folders (#3769) --- extension/src/setup/index.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/extension/src/setup/index.ts b/extension/src/setup/index.ts index 108d603d82..3fb3b13aa4 100644 --- a/extension/src/setup/index.ts +++ b/extension/src/setup/index.ts @@ -578,21 +578,17 @@ export class Setup } private watchDotFolderForChanges() { - const cwd = getFirstWorkspaceFolder() - - if (!cwd) { - return - } - const disposer = Disposable.fn() this.dotFolderWatcher = disposer this.dispose.track(this.dotFolderWatcher) - return createFileSystemWatcher( - disposable => disposer.track(disposable), - getRelativePattern(cwd, '**'), - path => this.dotFolderListener(disposer, path) - ) + for (const workspaceFolder of getWorkspaceFolders()) { + createFileSystemWatcher( + disposable => disposer.track(disposable), + getRelativePattern(workspaceFolder, '**'), + path => this.dotFolderListener(disposer, path) + ) + } } private dotFolderListener(disposer: Disposer, path: string) {