From 113766a2e76dc4c23b7bbb1cc9dda4b22031fd2b Mon Sep 17 00:00:00 2001 From: Matt Seddon Date: Tue, 16 May 2023 13:36:25 +1000 Subject: [PATCH 01/10] add Remote section to setup webview --- extension/src/cli/dvc/constants.ts | 1 + extension/src/cli/dvc/executor.ts | 8 +- extension/src/setup/collect.ts | 29 +- extension/src/setup/index.ts | 50 +- extension/src/setup/webview/contract.ts | 13 +- extension/src/setup/webview/messages.ts | 2 + extension/src/test/suite/setup/index.test.ts | 4 + extension/src/test/suite/setup/util.ts | 1 + webview/src/setup/components/App.test.tsx | 471 ++++++++---------- webview/src/setup/components/App.tsx | 23 +- webview/src/setup/components/dvc/Dvc.tsx | 1 + .../components/experiments/Experiments.tsx | 1 + .../components/remote/CliIncompatible.tsx | 30 ++ .../src/setup/components/remote/Remote.tsx | 41 ++ .../components/remote/styles.module.scss | 25 + .../components/studio/CliIncompatible.tsx | 1 + webview/src/setup/state/dvcSlice.ts | 3 +- webview/src/setup/state/remoteSlice.ts | 25 + webview/src/setup/store.ts | 4 +- .../sectionContainer/SectionContainer.tsx | 10 + webview/src/stories/Setup.stories.tsx | 6 +- 21 files changed, 473 insertions(+), 276 deletions(-) create mode 100644 webview/src/setup/components/remote/CliIncompatible.tsx create mode 100644 webview/src/setup/components/remote/Remote.tsx create mode 100644 webview/src/setup/components/remote/styles.module.scss create mode 100644 webview/src/setup/state/remoteSlice.ts diff --git a/extension/src/cli/dvc/constants.ts b/extension/src/cli/dvc/constants.ts index 17e5a7a63b..2759f0ff5a 100644 --- a/extension/src/cli/dvc/constants.ts +++ b/extension/src/cli/dvc/constants.ts @@ -30,6 +30,7 @@ export enum Command { PUSH = 'push', QUEUE = 'queue', REMOVE = 'remove', + REMOTE = 'remote', ROOT = 'root', PARAMS = 'params', METRICS = 'metrics', diff --git a/extension/src/cli/dvc/executor.ts b/extension/src/cli/dvc/executor.ts index 5bd5d88719..8136837ab5 100644 --- a/extension/src/cli/dvc/executor.ts +++ b/extension/src/cli/dvc/executor.ts @@ -7,7 +7,8 @@ import { ExperimentSubCommand, Flag, GcPreserveFlag, - QueueSubCommand + QueueSubCommand, + SubCommand } from './constants' import { addStudioAccessToken } from './options' import { CliResult, CliStarted, typeCheckCommands } from '..' @@ -35,6 +36,7 @@ export const autoRegisteredCommands = { QUEUE_KILL: 'queueKill', QUEUE_START: 'queueStart', QUEUE_STOP: 'queueStop', + REMOTE: 'remote', REMOVE: 'remove' } as const @@ -196,6 +198,10 @@ export class DvcExecutor extends DvcCli { return this.blockAndExecuteProcess(cwd, Command.REMOVE, ...args) } + public remote(cwd: string, arg: typeof SubCommand.LIST) { + return this.executeDvcProcess(cwd, Command.REMOTE, arg) + } + private executeExperimentProcess(cwd: string, ...args: Args) { return this.executeDvcProcess(cwd, Command.EXPERIMENT, ...args) } diff --git a/extension/src/setup/collect.ts b/extension/src/setup/collect.ts index 75ad20c157..ba6d52a2a2 100644 --- a/extension/src/setup/collect.ts +++ b/extension/src/setup/collect.ts @@ -1,4 +1,10 @@ -import { DEFAULT_SECTION_COLLAPSED, SetupSection } from './webview/contract' +import isEmpty from 'lodash.isempty' +import { + DEFAULT_SECTION_COLLAPSED, + RemoteList, + SetupSection +} from './webview/contract' +import { trimAndSplit } from '../util/stdout' export const collectSectionCollapsed = ( focusedSection?: SetupSection @@ -16,3 +22,24 @@ export const collectSectionCollapsed = ( return acc } + +export const collectRemoteList = async ( + dvcRoots: string[], + getRemoteList: (cwd: string) => Promise +): Promise => { + const acc: { [alias: string]: string } = {} + + for (const dvcRoot of dvcRoots) { + const remoteList = await getRemoteList(dvcRoot) + if (!remoteList) { + continue + } + const remotes = trimAndSplit(remoteList) + for (const remote of remotes) { + const [alias, storage] = remote.split(/\s+/) + acc[alias] = storage + } + } + + return isEmpty(acc) ? undefined : acc +} diff --git a/extension/src/setup/index.ts b/extension/src/setup/index.ts index 48a7621910..8dc5b5a74f 100644 --- a/extension/src/setup/index.ts +++ b/extension/src/setup/index.ts @@ -7,7 +7,7 @@ import { SetupSection, SetupData as TSetupData } from './webview/contract' -import { collectSectionCollapsed } from './collect' +import { collectRemoteList, collectSectionCollapsed } from './collect' import { WebviewMessages } from './webview/messages' import { validateTokenInput } from './inputBox' import { findPythonBinForInstall } from './autoInstall' @@ -47,7 +47,8 @@ import { Flag, ConfigKey as DvcConfigKey, DOT_DVC, - Args + Args, + SubCommand } from '../cli/dvc/constants' import { GLOBAL_WEBVIEW_DVCROOT } from '../webview/factory' import { @@ -375,14 +376,28 @@ export class Setup } } + private async getRemoteList() { + await this.config.isReady() + + if (!this.hasRoots()) { + return undefined + } + + return collectRemoteList(this.dvcRoots, (cwd: string) => + this.accessRemote(cwd, SubCommand.LIST) + ) + } + private async sendDataToWebview() { const projectInitialized = this.hasRoots() const hasData = this.getHasData() - const [isPythonExtensionUsed, dvcCliDetails] = await Promise.all([ - this.isPythonExtensionUsed(), - this.getDvcCliDetails() - ]) + const [isPythonExtensionUsed, dvcCliDetails, remoteList] = + await Promise.all([ + this.isPythonExtensionUsed(), + this.getDvcCliDetails(), + this.getRemoteList() + ]) const needsGitInitialized = !projectInitialized && !!(await this.needsGitInit()) @@ -405,6 +420,7 @@ export class Setup needsGitInitialized, projectInitialized, pythonBinPath: getBinDisplayText(pythonBinPath), + remoteList, sectionCollapsed: collectSectionCollapsed(this.focusedSection), shareLiveToStudio: getConfigValue( ExtensionConfigKey.STUDIO_SHARE_EXPERIMENTS_LIVE @@ -705,13 +721,23 @@ export class Setup ) } - private async accessConfig(cwd: string, ...args: Args) { + private accessConfig(cwd: string, ...args: Args) { + return this.accessDvc(AvailableCommands.CONFIG, cwd, ...args) + } + + private accessRemote(cwd: string, ...args: Args) { + return this.accessDvc(AvailableCommands.REMOTE, cwd, ...args) + } + + private async accessDvc( + commandId: + | typeof AvailableCommands.CONFIG + | typeof AvailableCommands.REMOTE, + cwd: string, + ...args: Args + ) { try { - return await this.internalCommands.executeCommand( - AvailableCommands.CONFIG, - cwd, - ...args - ) + return await this.internalCommands.executeCommand(commandId, cwd, ...args) } catch {} } } diff --git a/extension/src/setup/webview/contract.ts b/extension/src/setup/webview/contract.ts index f00442a13a..5620162a35 100644 --- a/extension/src/setup/webview/contract.ts +++ b/extension/src/setup/webview/contract.ts @@ -3,6 +3,8 @@ export type DvcCliDetails = { version: string | undefined } +export type RemoteList = { [alias: string]: string } | undefined + export type SetupData = { canGitInitialize: boolean cliCompatible: boolean | undefined @@ -14,20 +16,23 @@ export type SetupData = { needsGitInitialized: boolean | undefined projectInitialized: boolean pythonBinPath: string | undefined + remoteList: RemoteList sectionCollapsed: typeof DEFAULT_SECTION_COLLAPSED | undefined shareLiveToStudio: boolean } export enum SetupSection { + DVC = 'dvc', EXPERIMENTS = 'experiments', - STUDIO = 'studio', - DVC = 'dvc' + REMOTE = 'remote', + STUDIO = 'studio' } export const DEFAULT_SECTION_COLLAPSED = { + [SetupSection.DVC]: false, [SetupSection.EXPERIMENTS]: false, - [SetupSection.STUDIO]: false, - [SetupSection.DVC]: false + [SetupSection.REMOTE]: false, + [SetupSection.STUDIO]: false } export type SectionCollapsed = typeof DEFAULT_SECTION_COLLAPSED diff --git a/extension/src/setup/webview/messages.ts b/extension/src/setup/webview/messages.ts index f352057fbc..b25eb00084 100644 --- a/extension/src/setup/webview/messages.ts +++ b/extension/src/setup/webview/messages.ts @@ -40,6 +40,7 @@ export class WebviewMessages { needsGitInitialized, projectInitialized, pythonBinPath, + remoteList, sectionCollapsed, shareLiveToStudio }: SetupData) { @@ -54,6 +55,7 @@ export class WebviewMessages { needsGitInitialized, projectInitialized, pythonBinPath, + remoteList, sectionCollapsed, shareLiveToStudio }) diff --git a/extension/src/test/suite/setup/index.test.ts b/extension/src/test/suite/setup/index.test.ts index ee0fc2df89..6a9ad272dd 100644 --- a/extension/src/test/suite/setup/index.test.ts +++ b/extension/src/test/suite/setup/index.test.ts @@ -246,6 +246,7 @@ suite('Setup Test Suite', () => { needsGitInitialized: true, projectInitialized: false, pythonBinPath: undefined, + remoteList: undefined, sectionCollapsed: undefined, shareLiveToStudio: false }) @@ -287,6 +288,7 @@ suite('Setup Test Suite', () => { needsGitInitialized: true, projectInitialized: false, pythonBinPath: undefined, + remoteList: undefined, sectionCollapsed: undefined, shareLiveToStudio: false }) @@ -337,6 +339,7 @@ suite('Setup Test Suite', () => { needsGitInitialized: false, projectInitialized: false, pythonBinPath: undefined, + remoteList: undefined, sectionCollapsed: undefined, shareLiveToStudio: false }) @@ -387,6 +390,7 @@ suite('Setup Test Suite', () => { needsGitInitialized: false, projectInitialized: true, pythonBinPath: undefined, + remoteList: undefined, sectionCollapsed: undefined, shareLiveToStudio: false }) diff --git a/extension/src/test/suite/setup/util.ts b/extension/src/test/suite/setup/util.ts index f976de40ff..f2cd3ec55e 100644 --- a/extension/src/test/suite/setup/util.ts +++ b/extension/src/test/suite/setup/util.ts @@ -44,6 +44,7 @@ export const buildSetup = ( const mockEmitter = disposer.track(new EventEmitter()) stub(dvcReader, 'root').resolves(mockDvcRoot) + stub(dvcExecutor, 'remote').resolves('') const mockVersion = stub(dvcReader, 'version').resolves(MIN_CLI_VERSION) const mockGlobalVersion = stub(dvcReader, 'globalVersion').resolves( MIN_CLI_VERSION diff --git a/webview/src/setup/components/App.test.tsx b/webview/src/setup/components/App.test.tsx index 13a7d57cdf..d3207bc24e 100644 --- a/webview/src/setup/components/App.test.tsx +++ b/webview/src/setup/components/App.test.tsx @@ -37,6 +37,7 @@ const DEFAULT_DATA = { needsGitInitialized: false, projectInitialized: true, pythonBinPath: undefined, + remoteList: undefined, sectionCollapsed: undefined, shareLiveToStudio: false } @@ -115,7 +116,7 @@ describe('App', () => { }) expect(screen.getAllByText('DVC is currently unavailable')).toHaveLength( - 2 + 3 ) }) @@ -244,14 +245,6 @@ describe('App', () => { projectInitialized: false }) - expect(screen.queryByText('Initialize Git')).not.toBeInTheDocument() - }) - - it('should show a screen saying that DVC is not initialized if the project is not initialized and dvc is installed', () => { - renderApp({ - projectInitialized: false - }) - expect(screen.getByText('DVC is not initialized')).toBeInTheDocument() }) @@ -281,8 +274,9 @@ describe('App', () => { projectInitialized: false, sectionCollapsed: { [SetupSection.DVC]: false, - [SetupSection.STUDIO]: true, - [SetupSection.EXPERIMENTS]: true + [SetupSection.EXPERIMENTS]: true, + [SetupSection.REMOTE]: true, + [SetupSection.STUDIO]: true } } @@ -402,307 +396,284 @@ describe('App', () => { it('should show a passed icon if DVC CLI is compatible and project is initialized', () => { renderApp() - - const iconWrapper = screen.getAllByTestId('info-tooltip-toggle')[0] - - expect( - within(iconWrapper).getByTestId(TooltipIconType.PASSED) - ).toBeInTheDocument() }) - }) - describe('Experiments', () => { - it('should show a screen saying that dvc is not setup if the project is not initialized', () => { - renderApp({ - projectInitialized: false - }) + expect(screen.getByText('DVC is not setup')).toBeInTheDocument() + }) - 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({ + projectInitialized: false }) - it('should open the dvc section when clicking the Setup DVC button on the dvc is not setup screen', () => { - renderApp({ - projectInitialized: false - }) + const experimentsText = screen.getByText('DVC is not setup') + expect(experimentsText).toBeInTheDocument() - 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() + }) - 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 initialized but dvc is not found', () => { + renderApp({ + cliCompatible: false, + dvcCliDetails: { + command: 'dvc', + version: undefined + } }) - it('should show a screen saying that dvc is not setup if the project is initialized but dvc is not found', () => { - renderApp({ - cliCompatible: false, - dvcCliDetails: { - command: 'dvc', - version: undefined - } - }) + expect(screen.getByText('DVC is not setup')).toBeInTheDocument() + }) - 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', () => { + renderApp({ + hasData: true }) - it('should not show a screen saying that the project contains no data if dvc is installed, the project is initialized and has data', () => { - renderApp({ - hasData: true - }) + expect( + screen.queryByText('Your project contains no data') + ).not.toBeInTheDocument() + }) - expect( - screen.queryByText('Your project contains no data') - ).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({ + needsGitCommit: true }) - 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({ - needsGitCommit: true - }) + expect(screen.getByText('No Git commits detected')).toBeInTheDocument() + }) - expect(screen.getByText('No Git commits detected')).toBeInTheDocument() + it('should show a loading screen if the project is loading in data', () => { + renderApp({ + hasData: undefined }) - it('should show a loading screen if the project is loading in data', () => { - renderApp({ - hasData: undefined - }) + expect(screen.getByText('Loading Project...')).toBeInTheDocument() + }) - 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() - 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() + expect( + screen.getByText('Your project contains no data') + ).toBeInTheDocument() + }) - 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({ + hasData: true }) - - it('should enable the user to open the experiments webview when they have completed onboarding', () => { - renderApp({ - hasData: true - }) - mockPostMessage.mockClear() - fireEvent.click(screen.getByText('Show Experiments')) - expect(mockPostMessage).toHaveBeenCalledTimes(1) - expect(mockPostMessage).toHaveBeenCalledWith({ - type: MessageFromWebviewType.OPEN_EXPERIMENTS_WEBVIEW - }) + mockPostMessage.mockClear() + fireEvent.click(screen.getByText('Show Experiments')) + expect(mockPostMessage).toHaveBeenCalledTimes(1) + expect(mockPostMessage).toHaveBeenCalledWith({ + type: MessageFromWebviewType.OPEN_EXPERIMENTS_WEBVIEW }) + }) - it('should show an error icon if experiments are not setup', () => { - renderApp() + it('should show an error icon if experiments are not setup', () => { + renderApp() - const iconWrapper = screen.getAllByTestId('info-tooltip-toggle')[1] + const iconWrapper = screen.getAllByTestId('info-tooltip-toggle')[1] - expect( - within(iconWrapper).getByTestId(TooltipIconType.ERROR) - ).toBeInTheDocument() + expect( + within(iconWrapper).getByTestId(TooltipIconType.ERROR) + ).toBeInTheDocument() + }) + + it('should show an error icon if dvc is not setup', () => { + renderApp({ + cliCompatible: false }) - it('should show an error icon if dvc is not setup', () => { - renderApp({ - cliCompatible: false - }) + const iconWrapper = screen.getAllByTestId('info-tooltip-toggle')[1] - const iconWrapper = screen.getAllByTestId('info-tooltip-toggle')[1] + expect( + within(iconWrapper).getByTestId(TooltipIconType.ERROR) + ).toBeInTheDocument() + }) - expect( - within(iconWrapper).getByTestId(TooltipIconType.ERROR) - ).toBeInTheDocument() + it('should show a passed icon if experiments are setup', () => { + renderApp({ + hasData: true }) - it('should show a passed icon if experiments are setup', () => { - renderApp({ - hasData: true - }) - - const iconWrapper = screen.getAllByTestId('info-tooltip-toggle')[1] + const iconWrapper = screen.getAllByTestId('info-tooltip-toggle')[1] - expect( - within(iconWrapper).getByTestId(TooltipIconType.PASSED) - ).toBeInTheDocument() - }) + expect( + within(iconWrapper).getByTestId(TooltipIconType.PASSED) + ).toBeInTheDocument() }) +}) - describe('Studio not connected', () => { - it('should show three buttons which walk the user through saving a token', async () => { - renderApp() - const buttons = await within( - await screen.findByTestId('setup-studio-content') - ).findAllByRole('button') - expect(buttons).toHaveLength(3) - }) +describe('Studio not connected', () => { + it('should show three buttons which walk the user through saving a token', async () => { + renderApp() + const buttons = await within( + await screen.findByTestId('setup-studio-content') + ).findAllByRole('button') + expect(buttons).toHaveLength(3) + }) - it('should show a button which opens Studio', () => { - renderApp() + it('should show a button which opens Studio', () => { + renderApp() - mockPostMessage.mockClear() - const button = screen.getByText('Sign In') - fireEvent.click(button) - expect(mockPostMessage).toHaveBeenCalledTimes(1) - expect(mockPostMessage).toHaveBeenCalledWith({ - type: MessageFromWebviewType.OPEN_STUDIO - }) + mockPostMessage.mockClear() + const button = screen.getByText('Get Token') + fireEvent.click(button) + expect(mockPostMessage).toHaveBeenCalledTimes(1) + expect(mockPostMessage).toHaveBeenCalledWith({ + type: MessageFromWebviewType.OPEN_STUDIO_PROFILE }) + }) - it("should show a button which opens the user's Studio profile", () => { - renderApp() + it("should show a button which allows the user's to save their Studio access token", () => { + renderApp() - mockPostMessage.mockClear() - const button = screen.getByText('Get Token') - fireEvent.click(button) - expect(mockPostMessage).toHaveBeenCalledTimes(1) - expect(mockPostMessage).toHaveBeenCalledWith({ - type: MessageFromWebviewType.OPEN_STUDIO_PROFILE - }) + mockPostMessage.mockClear() + const button = screen.getByText('Save') + fireEvent.click(button) + expect(mockPostMessage).toHaveBeenCalledTimes(1) + expect(mockPostMessage).toHaveBeenCalledWith({ + type: MessageFromWebviewType.SAVE_STUDIO_TOKEN }) + }) - it("should show a button which allows the user's to save their Studio access token", () => { - renderApp() - - mockPostMessage.mockClear() - const button = screen.getByText('Save') - fireEvent.click(button) - expect(mockPostMessage).toHaveBeenCalledTimes(1) - expect(mockPostMessage).toHaveBeenCalledWith({ - type: MessageFromWebviewType.SAVE_STUDIO_TOKEN - }) + it('should show an error icon if dvc is not compatible', () => { + renderApp({ + cliCompatible: false }) - it('should show an error icon if dvc is not compatible', () => { - renderApp({ - cliCompatible: false - }) + const iconWrapper = screen.getAllByTestId('info-tooltip-toggle')[3] - const iconWrapper = screen.getAllByTestId('info-tooltip-toggle')[2] + expect( + within(iconWrapper).getByTestId(TooltipIconType.ERROR) + ).toBeInTheDocument() + }) - expect( - within(iconWrapper).getByTestId(TooltipIconType.ERROR) - ).toBeInTheDocument() - }) + it('should show an info icon if dvc is compatible but studio is not connected', () => { + renderApp() - it('should show an info icon if dvc is compatible but studio is not connected', () => { - renderApp() + const iconWrapper = screen.getAllByTestId('info-tooltip-toggle')[3] + + expect( + within(iconWrapper).getByTestId(TooltipIconType.INFO) + ).toBeInTheDocument() + }) +}) - const iconWrapper = screen.getAllByTestId('info-tooltip-toggle')[2] +describe('Studio connected', () => { + it('should render a checkbox which can be used to update dvc.studio.shareExperimentsLive', () => { + const shareExperimentsLive = false + renderApp({ + isStudioConnected: true + }) - expect( - within(iconWrapper).getByTestId(TooltipIconType.INFO) - ).toBeInTheDocument() + mockPostMessage.mockClear() + const checkbox = screen.getByRole('checkbox') + fireEvent.click(checkbox) + expect(mockPostMessage).toHaveBeenCalledWith({ + payload: !shareExperimentsLive, + type: MessageFromWebviewType.SET_STUDIO_SHARE_EXPERIMENTS_LIVE }) }) - describe('Studio connected', () => { - it('should render a checkbox which can be used to update dvc.studio.shareExperimentsLive', () => { - const shareExperimentsLive = false - renderApp({ - isStudioConnected: true - }) - - mockPostMessage.mockClear() - const checkbox = screen.getByRole('checkbox') - fireEvent.click(checkbox) - expect(mockPostMessage).toHaveBeenCalledWith({ - payload: !shareExperimentsLive, - type: MessageFromWebviewType.SET_STUDIO_SHARE_EXPERIMENTS_LIVE - }) + it('should enable the user to update their studio token', () => { + renderApp({ + isStudioConnected: true }) + mockPostMessage.mockClear() + const button = screen.getByText('Update Token') + fireEvent.click(button) + expect(mockPostMessage).toHaveBeenCalledWith({ + type: MessageFromWebviewType.SAVE_STUDIO_TOKEN + }) + }) - it('should enable the user to update their studio token', () => { - renderApp({ - isStudioConnected: true - }) - mockPostMessage.mockClear() - const button = screen.getByText('Update Token') - fireEvent.click(button) - expect(mockPostMessage).toHaveBeenCalledWith({ - type: MessageFromWebviewType.SAVE_STUDIO_TOKEN - }) + it('should show a passed icon if connected', () => { + renderApp({ + isStudioConnected: true }) - it('should show a passed icon if connected', () => { - renderApp({ - isStudioConnected: true - }) + const iconWrapper = screen.getAllByTestId('info-tooltip-toggle')[3] - const iconWrapper = screen.getAllByTestId('info-tooltip-toggle')[2] + expect( + within(iconWrapper).getByTestId(TooltipIconType.PASSED) + ).toBeInTheDocument() + }) +}) - expect( - within(iconWrapper).getByTestId(TooltipIconType.PASSED) - ).toBeInTheDocument() +describe('focused section', () => { + 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({ + isStudioConnected: true, + sectionCollapsed: { + [SetupSection.DVC]: false, + [SetupSection.EXPERIMENTS]: true, + [SetupSection.REMOTE]: true, + [SetupSection.STUDIO]: true + } }) + 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() }) - describe('focused section', () => { - 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 Experiments section is focused', () => { + renderApp({ + isStudioConnected: true, + sectionCollapsed: { + [SetupSection.DVC]: true, + [SetupSection.EXPERIMENTS]: false, + [SetupSection.REMOTE]: true, + [SetupSection.STUDIO]: 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 other sections collapsed if the DVC section is focused', () => { - renderApp({ - isStudioConnected: true, - 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 other sections collapsed if the Experiments section is focused', () => { - renderApp({ - isStudioConnected: true, - sectionCollapsed: { - [SetupSection.EXPERIMENTS]: false, - [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 other sections collapsed if the Studio section is focused', () => { - renderApp({ - isStudioConnected: true, - sectionCollapsed: { - [SetupSection.EXPERIMENTS]: true, - [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() + it('should render the app with other sections collapsed if the Studio section is focused', () => { + renderApp({ + isStudioConnected: true, + sectionCollapsed: { + [SetupSection.DVC]: true, + [SetupSection.EXPERIMENTS]: true, + [SetupSection.REMOTE]: true, + [SetupSection.STUDIO]: false + } }) + 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 1afb6e069e..576468f160 100644 --- a/webview/src/setup/components/App.tsx +++ b/webview/src/setup/components/App.tsx @@ -9,6 +9,7 @@ import { Dvc } from './dvc/Dvc' import { Experiments } from './experiments/Experiments' import { Studio } from './studio/Studio' import { SetupContainer } from './SetupContainer' +import { Remote } from './remote/Remote' import { useVsCodeMessaging } from '../../shared/hooks/useVsCodeMessaging' import { sendMessage } from '../../shared/vscode' import { SetupDispatch, SetupState } from '../store' @@ -25,14 +26,15 @@ import { updateProjectInitialized, updatePythonBinPath } from '../state/dvcSlice' -import { - updateIsStudioConnected, - updateShareLiveToStudio -} from '../state/studioSlice' import { updateHasData as updateExperimentsHasData, updateNeedsGitCommit } from '../state/experimentsSlice' +import { updateRemoteList } from '../state/remoteSlice' +import { + updateIsStudioConnected, + updateShareLiveToStudio +} from '../state/studioSlice' export const feedStore = ( data: MessageToWebview, @@ -80,6 +82,10 @@ export const feedStore = ( case 'shareLiveToStudio': dispatch(updateShareLiveToStudio(data.data.shareLiveToStudio)) continue + case 'remoteList': + dispatch(updateRemoteList(data.data.remoteList)) + continue + default: continue } @@ -93,6 +99,8 @@ export const App: React.FC = () => { const hasExperimentsData = useSelector( (state: SetupState) => state.experiments.hasData ) + const { remoteList } = useSelector((state: SetupState) => state.remote) + const isStudioConnected = useSelector( (state: SetupState) => state.studio.isStudioConnected ) @@ -134,6 +142,13 @@ export const App: React.FC = () => { > + + + { updateSectionCollapsed({ [SetupSection.DVC]: true, [SetupSection.EXPERIMENTS]: false, + [SetupSection.REMOTE]: false, [SetupSection.STUDIO]: false }) ) diff --git a/webview/src/setup/components/experiments/Experiments.tsx b/webview/src/setup/components/experiments/Experiments.tsx index 29b8c620a0..4f619178c2 100644 --- a/webview/src/setup/components/experiments/Experiments.tsx +++ b/webview/src/setup/components/experiments/Experiments.tsx @@ -32,6 +32,7 @@ export const Experiments: React.FC = ({ isDvcSetup }) => { updateSectionCollapsed({ [SetupSection.DVC]: false, [SetupSection.EXPERIMENTS]: true, + [SetupSection.REMOTE]: true, [SetupSection.STUDIO]: true }) ) diff --git a/webview/src/setup/components/remote/CliIncompatible.tsx b/webview/src/setup/components/remote/CliIncompatible.tsx new file mode 100644 index 0000000000..82faf4a8a6 --- /dev/null +++ b/webview/src/setup/components/remote/CliIncompatible.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import { useDispatch } from 'react-redux' +import { SetupSection } from 'dvc/src/setup/webview/contract' +import { EmptyState } from '../../../shared/components/emptyState/EmptyState' +import { Button } from '../../../shared/components/button/Button' +import { updateSectionCollapsed } from '../../state/webviewSlice' + +export const CliIncompatible: React.FC = () => { + const dispatch = useDispatch() + + return ( + +

DVC is currently unavailable

+

Locate DVC to connect to a remote

+