diff --git a/extension/package.json b/extension/package.json index 4cb8a996ee..ad35a0f42f 100644 --- a/extension/package.json +++ b/extension/package.json @@ -1449,7 +1449,7 @@ }, { "view": "dvc.views.welcome", - "contents": "New to the extension?\n[Show Walkthrough](command:dvc.getStarted)\n\nThe extension is currently unable to initialize.\n[Show Setup](command:dvc.showExperimentsSetup)", + "contents": "New to the extension?\n[Show Walkthrough](command:dvc.getStarted)\n\nThe extension is currently unable to initialize.\n[Show Setup](command:dvc.showDvcSetup)", "when": "true" }, { @@ -1491,7 +1491,7 @@ { "id": "dvc.installDVC", "title": "Install DVC", - "description": "This extension requires DVC to be installed.\n\n[Show Setup](command:dvc.showExperimentsSetup)\n", + "description": "This extension requires DVC to be installed.\n\n[Show Setup](command:dvc.showDvcSetup)\n", "media": { "markdown": "resources/walkthrough/install-dvc.md" }, diff --git a/extension/resources/walkthrough/install-dvc.md b/extension/resources/walkthrough/install-dvc.md index 851417e200..58d78bddb5 100644 --- a/extension/resources/walkthrough/install-dvc.md +++ b/extension/resources/walkthrough/install-dvc.md @@ -16,7 +16,7 @@ DVC icon like this in the status bar:

If you see instead the crossed circle icon, click on the icon or follow the -[Setup](command:dvc.showExperimentsSetup) wizard. +[Setup](command:dvc.showDvcSetup) wizard. > **Note**: The correct Python interpreter must be set for the current workspace > when relying on the Python extension for auto environment activation. diff --git a/extension/src/commands/external.ts b/extension/src/commands/external.ts index f92b6ddae0..2f2dbd4ed9 100644 --- a/extension/src/commands/external.ts +++ b/extension/src/commands/external.ts @@ -97,6 +97,7 @@ export enum RegisteredCommands { SETUP_SHOW = 'dvc.showSetup', SETUP_SHOW_EXPERIMENTS = 'dvc.showExperimentsSetup', + SETUP_SHOW_DVC = 'dvc.showDvcSetup', SELECT_FOCUSED_PROJECTS = 'dvc.selectFocusedProjects', ADD_STUDIO_ACCESS_TOKEN = 'dvc.addStudioAccessToken', diff --git a/extension/src/commands/util.ts b/extension/src/commands/util.ts index 5fff197667..08c97dfa0a 100644 --- a/extension/src/commands/util.ts +++ b/extension/src/commands/util.ts @@ -7,5 +7,5 @@ export const showSetupOrExecuteCommand = (setup: Setup, callback: (context: Context) => Promise) => (context: Context) => setup.shouldBeShown() - ? commands.executeCommand(RegisteredCommands.SETUP_SHOW_EXPERIMENTS) + ? commands.executeCommand(RegisteredCommands.SETUP_SHOW_DVC) : callback(context) diff --git a/extension/src/setup/register.ts b/extension/src/setup/register.ts index 7e65fbf393..f0e8468abc 100644 --- a/extension/src/setup/register.ts +++ b/extension/src/setup/register.ts @@ -44,6 +44,13 @@ const registerSetupShowCommands = ( } ) + internalCommands.registerExternalCommand( + RegisteredCommands.SETUP_SHOW_DVC, + async () => { + await setup.showSetup(SetupSection.DVC) + } + ) + internalCommands.registerExternalCommand( RegisteredCommands.SETUP_SHOW_STUDIO_CONNECT, async () => { diff --git a/extension/src/setup/webview/contract.ts b/extension/src/setup/webview/contract.ts index 6866baaff5..a3aa5df5ea 100644 --- a/extension/src/setup/webview/contract.ts +++ b/extension/src/setup/webview/contract.ts @@ -14,12 +14,14 @@ export type SetupData = { export enum SetupSection { EXPERIMENTS = 'experiments', - STUDIO = 'studio' + STUDIO = 'studio', + DVC = 'dvc' } export const DEFAULT_SECTION_COLLAPSED = { [SetupSection.EXPERIMENTS]: false, - [SetupSection.STUDIO]: false + [SetupSection.STUDIO]: false, + [SetupSection.DVC]: false } export type SectionCollapsed = typeof DEFAULT_SECTION_COLLAPSED diff --git a/extension/src/telemetry/constants.ts b/extension/src/telemetry/constants.ts index e365288daf..8b63d74e2d 100644 --- a/extension/src/telemetry/constants.ts +++ b/extension/src/telemetry/constants.ts @@ -275,6 +275,7 @@ export interface IEventNamePropertyMapping { [EventName.SETUP_SHOW]: undefined [EventName.SETUP_SHOW_EXPERIMENTS]: undefined + [EventName.SETUP_SHOW_DVC]: undefined [EventName.SELECT_FOCUSED_PROJECTS]: undefined [EventName.SETUP_SHOW_STUDIO_SETTINGS]: undefined [EventName.SETUP_SHOW_STUDIO_CONNECT]: undefined diff --git a/extension/src/test/suite/experiments/workspace.test.ts b/extension/src/test/suite/experiments/workspace.test.ts index c8505896fe..01551b29d6 100644 --- a/extension/src/test/suite/experiments/workspace.test.ts +++ b/extension/src/test/suite/experiments/workspace.test.ts @@ -975,9 +975,7 @@ suite('Workspace Experiments Test Suite', () => { await commands.executeCommand(RegisteredCommands.EXPERIMENT_SHOW) - expect(executeCommandSpy).to.have.been.calledWithMatch( - 'dvc.showExperimentsSetup' - ) + expect(executeCommandSpy).to.have.been.calledWithMatch('dvc.showDvcSetup') }) it('should not show the experiments webview if the setup should be shown', async () => { @@ -1001,7 +999,7 @@ suite('Workspace Experiments Test Suite', () => { await commands.executeCommand(RegisteredCommands.EXPERIMENT_SHOW) - expect(executeCommandSpy).not.to.be.calledWith('dvc.showExperimentsSetup') + expect(executeCommandSpy).not.to.be.calledWith('dvc.showDvcSetup') }) it('should show the experiments webview if the setup should not be shown', async () => { diff --git a/webview/src/setup/components/App.test.tsx b/webview/src/setup/components/App.test.tsx index 38ea22bbc1..3784e95214 100644 --- a/webview/src/setup/components/App.test.tsx +++ b/webview/src/setup/components/App.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen } from '@testing-library/react' +import { fireEvent, render, screen, within } from '@testing-library/react' import { MessageFromWebviewType, MessageToWebviewType @@ -22,6 +22,7 @@ const renderApp = ({ isPythonExtensionInstalled, isStudioConnected, needsGitInitialized, + needsGitCommit, projectInitialized, pythonBinPath, sectionCollapsed, @@ -38,6 +39,7 @@ const renderApp = ({ hasData, isPythonExtensionInstalled, isStudioConnected, + needsGitCommit, needsGitInitialized, projectInitialized, pythonBinPath, @@ -51,15 +53,15 @@ const renderApp = ({ } describe('App', () => { - describe('Experiments', () => { - it('should send the initialized message on first render', () => { - render() - expect(mockPostMessage).toHaveBeenCalledWith({ - type: MessageFromWebviewType.INITIALIZED - }) - expect(mockPostMessage).toHaveBeenCalledTimes(1) + it('should send the initialized message on first render', () => { + render() + expect(mockPostMessage).toHaveBeenCalledWith({ + type: MessageFromWebviewType.INITIALIZED }) + expect(mockPostMessage).toHaveBeenCalledTimes(1) + }) + describe('DVC', () => { it('should show a screen saying that DVC is incompatible if the cli version is unexpected', () => { renderApp({ canGitInitialize: false, @@ -337,7 +339,7 @@ describe('App', () => { ).not.toBeInTheDocument() }) - it('should send a message to initialize the project when clicking the Initialize Project buttons when the project is not initialized', () => { + it('should send a message to initialize the project when clicking the Initialize Project button when the project is not initialized', () => { renderApp({ canGitInitialize: false, cliCompatible: true, @@ -360,7 +362,7 @@ describe('App', () => { }) }) - it('should show a screen saying that the project contains no data if dvc is installed, the project is initialized but has no data', () => { + it('should open the experiments section when clicking the Open Experiments button when the project is initialized but has no data', () => { renderApp({ canGitInitialize: false, cliCompatible: true, @@ -375,9 +377,98 @@ describe('App', () => { shareLiveToStudio: false }) - expect( - screen.getByText('Your project contains no data') - ).toBeInTheDocument() + mockPostMessage.mockClear() + const button = screen.getAllByText('Show Experiments')[0] + fireEvent.click(button) + + expect(screen.getByText('Your project contains no data')).toBeVisible() + expect(screen.getByText('Setup Complete')).not.toBeVisible() + }) + + it('should enable the user to open the experiments webview when they have completed onboarding', () => { + renderApp({ + canGitInitialize: false, + cliCompatible: true, + hasData: true, + isPythonExtensionInstalled: true, + isStudioConnected: true, + needsGitCommit: false, + needsGitInitialized: false, + projectInitialized: true, + pythonBinPath: 'python', + sectionCollapsed: undefined, + shareLiveToStudio: false + }) + mockPostMessage.mockClear() + const button = screen.getAllByText('Show Experiments')[0] + fireEvent.click(button) + expect(mockPostMessage).toHaveBeenCalledTimes(1) + expect(mockPostMessage).toHaveBeenCalledWith({ + type: MessageFromWebviewType.OPEN_EXPERIMENTS_WEBVIEW + }) + }) + }) + + describe('Experiments', () => { + it('should show a screen saying that dvc is not setup if the project is not initalized', () => { + renderApp({ + canGitInitialize: false, + cliCompatible: true, + hasData: false, + isPythonExtensionInstalled: false, + isStudioConnected: false, + needsGitCommit: true, + needsGitInitialized: true, + projectInitialized: false, + pythonBinPath: undefined, + sectionCollapsed: undefined, + shareLiveToStudio: false + }) + + expect(screen.getByText('DVC is not setup')).toBeInTheDocument() + }) + + it('should open the dvc section when clicking the Setup DVC button on the dvc is not setup screen', () => { + renderApp({ + canGitInitialize: false, + cliCompatible: true, + hasData: false, + isPythonExtensionInstalled: false, + isStudioConnected: false, + needsGitCommit: true, + needsGitInitialized: true, + projectInitialized: false, + pythonBinPath: undefined, + sectionCollapsed: undefined, + shareLiveToStudio: false + }) + + const experimentsText = screen.getByText('DVC is not setup') + expect(experimentsText).toBeInTheDocument() + + mockPostMessage.mockClear() + const button = screen.getByText('Setup DVC') + fireEvent.click(button) + expect(screen.getByText('DVC is not initialized')).toBeVisible() + expect(experimentsText).not.toBeVisible() + }) + + 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, + hasData: false, + isPythonExtensionInstalled: false, + isStudioConnected: false, + needsGitCommit: false, + needsGitInitialized: undefined, + projectInitialized: false, + pythonBinPath: undefined, + sectionCollapsed: undefined, + shareLiveToStudio: false + }) + + expect(screen.getByText('DVC is not setup')).toBeInTheDocument() }) it('should not show a screen saying that the project contains no data if dvc is installed, the project is initialized and has data', () => { @@ -400,6 +491,62 @@ describe('App', () => { ).not.toBeInTheDocument() }) + it('should show a screen saying there needs to be a git commit if the project is initialized, dvc is installed, but has not git commit', () => { + renderApp({ + canGitInitialize: false, + cliCompatible: true, + hasData: false, + isPythonExtensionInstalled: false, + isStudioConnected: false, + needsGitCommit: true, + needsGitInitialized: false, + projectInitialized: true, + pythonBinPath: undefined, + sectionCollapsed: undefined, + shareLiveToStudio: false + }) + + expect(screen.getByText('No Git commits detected')).toBeInTheDocument() + }) + + it('should show a loading screen if the project is loading in data', () => { + renderApp({ + canGitInitialize: false, + cliCompatible: true, + hasData: undefined, + isPythonExtensionInstalled: false, + isStudioConnected: false, + needsGitCommit: false, + needsGitInitialized: undefined, + projectInitialized: true, + pythonBinPath: undefined, + sectionCollapsed: undefined, + shareLiveToStudio: false + }) + + expect(screen.getByText('Loading Project...')).toBeInTheDocument() + }) + + it('should show a screen saying that the project contains no data if dvc is installed, the project is initialized but has no data', () => { + renderApp({ + canGitInitialize: false, + cliCompatible: true, + hasData: false, + isPythonExtensionInstalled: false, + isStudioConnected: false, + needsGitCommit: false, + needsGitInitialized: undefined, + projectInitialized: true, + pythonBinPath: undefined, + sectionCollapsed: undefined, + shareLiveToStudio: false + }) + + expect( + screen.getByText('Your project contains no data') + ).toBeInTheDocument() + }) + it('should enable the user to open the experiments webview when they have completed onboarding', () => { renderApp({ canGitInitialize: false, @@ -415,7 +562,7 @@ describe('App', () => { shareLiveToStudio: false }) mockPostMessage.mockClear() - const button = screen.getByText('Show Experiments') + const button = screen.getAllByText('Show Experiments')[1] fireEvent.click(button) expect(mockPostMessage).toHaveBeenCalledTimes(1) expect(mockPostMessage).toHaveBeenCalledWith({ @@ -439,7 +586,9 @@ describe('App', () => { sectionCollapsed: undefined, shareLiveToStudio: false }) - const buttons = await screen.findAllByRole('button') + const buttons = await within( + await screen.findByTestId('setup-studio-content') + ).findAllByRole('button') expect(buttons).toHaveLength(3) }) @@ -580,36 +729,66 @@ describe('App', () => { } const experimentsText = 'Your project contains no data' const studioButtonText = 'Update Token' + const dvcText = 'Setup Complete' + + it('should render the app with other sections collapsed if the DVC section is focused', () => { + renderApp({ + ...testData, + sectionCollapsed: { + [SetupSection.EXPERIMENTS]: true, + [SetupSection.STUDIO]: true, + [SetupSection.DVC]: false + } + }) + mockPostMessage.mockClear() + const dvc = screen.getByText('DVC') + expect(dvc).toBeVisible() + expect(screen.queryByText(dvcText)).toBeVisible() + const experiments = screen.getByText('Experiments') + expect(experiments).toBeVisible() + expect(screen.getByText(experimentsText)).not.toBeVisible() + const studio = screen.getByText('Studio') + expect(studio).toBeVisible() + expect(screen.queryByText(studioButtonText)).not.toBeVisible() + }) - it('should render the app with the Studio section collapsed if the Experiments section is focused', () => { + it('should render the app with other sections collapsed if the Experiments section is focused', () => { renderApp({ ...testData, sectionCollapsed: { [SetupSection.EXPERIMENTS]: false, - [SetupSection.STUDIO]: true + [SetupSection.STUDIO]: true, + [SetupSection.DVC]: true } }) mockPostMessage.mockClear() const studio = screen.getByText('Studio') expect(studio).toBeVisible() expect(screen.queryByText(studioButtonText)).not.toBeVisible() + const dvc = screen.getByText('DVC') + expect(dvc).toBeVisible() + expect(screen.queryByText(dvcText)).not.toBeVisible() const experiments = screen.getByText('Experiments') expect(experiments).toBeVisible() expect(screen.getByText(experimentsText)).toBeVisible() }) - it('should render the app with the Experiments section collapsed if the Studio section is focused', () => { + it('should render the app with other sections collapsed if the Studio section is focused', () => { renderApp({ ...testData, sectionCollapsed: { [SetupSection.EXPERIMENTS]: true, - [SetupSection.STUDIO]: false + [SetupSection.STUDIO]: false, + [SetupSection.DVC]: true } }) mockPostMessage.mockClear() const studio = screen.getByText('Studio') expect(studio).toBeVisible() expect(screen.queryByText(studioButtonText)).toBeVisible() + const dvc = screen.getByText('DVC') + expect(dvc).toBeVisible() + expect(screen.queryByText(dvcText)).not.toBeVisible() const experiments = screen.getByText('Experiments') expect(experiments).toBeVisible() expect(screen.getByText(experimentsText)).not.toBeVisible() diff --git a/webview/src/setup/components/App.tsx b/webview/src/setup/components/App.tsx index 91d2020d44..460b7bf079 100644 --- a/webview/src/setup/components/App.tsx +++ b/webview/src/setup/components/App.tsx @@ -8,6 +8,7 @@ import { MessageToWebview } from 'dvc/src/webview/contract' import React, { useCallback, useState } from 'react' +import { Dvc } from './Dvc' import { Experiments } from './Experiments' import { Studio } from './Studio' import { SetupContainer } from './SetupContainer' @@ -84,20 +85,33 @@ export const App: React.FC = () => { return ( <> - + + + void +} + +export const Dvc: React.FC = ({ + canGitInitialize, + cliCompatible, + isPythonExtensionInstalled, + needsGitInitialized, + projectInitialized, + pythonBinPath, + setSectionCollapsed, + isExperimentsAvailable +}) => { + if (cliCompatible === false) { + return + } + + if (cliCompatible === undefined) { + return ( + + ) + } + + if (!projectInitialized) { + return ( + + ) + } + + return ( + +

Setup Complete

+ + setSectionCollapsed({ + dvc: true, + experiments: false, + studio: true + }) + } + text="Show Experiments" + /> +
+ ) +} diff --git a/webview/src/setup/components/Experiments.tsx b/webview/src/setup/components/Experiments.tsx index ef186379f2..92d62d0397 100644 --- a/webview/src/setup/components/Experiments.tsx +++ b/webview/src/setup/components/Experiments.tsx @@ -1,26 +1,49 @@ import React from 'react' -import { CliIncompatible } from './CliIncompatible' -import { CliUnavailable } from './CliUnavailable' -import { ProjectUninitialized } from './ProjectUninitialized' -import { - checkCompatibility, - initializeDvc, - initializeGit, - installDvc, - selectPythonInterpreter, - setupWorkspace, - showExperiments, - showScmPanel -} from './messages' -import { NeedsGitCommit } from './NeedsGitCommit' +import { SectionCollapsed } from 'dvc/src/setup/webview/contract' +import { showExperiments, showScmPanel } from './messages' import { NoData } from './NoData' +import { NeedsGitCommit } from './NeedsGitCommit' import { EmptyState } from '../../shared/components/emptyState/EmptyState' import { IconButton } from '../../shared/components/button/IconButton' import { Beaker } from '../../shared/components/icons' +import { Button } from '../../shared/components/button/Button' -const ProjectSetup: React.FC<{ hasData: boolean | undefined }> = ({ - hasData +export type ExperimentsProps = { + isDvcSetup: boolean + hasData: boolean | undefined + setSectionCollapsed: (sectionCollapsed: SectionCollapsed) => void + needsGitCommit: boolean +} + +export const Experiments: React.FC = ({ + isDvcSetup, + hasData, + setSectionCollapsed, + needsGitCommit }) => { + if (!isDvcSetup) { + return ( + +

DVC is not setup

+

DVC needs to be setup before you can access experiments.

+