From e8d3ed11b4543a16d50c6c0ecad64450434dbcf2 Mon Sep 17 00:00:00 2001 From: Stephanie Roy Date: Thu, 16 Feb 2023 12:08:00 -0500 Subject: [PATCH 1/2] Move the comparison table header down from under the ribbon when scrolling (#3291) * Move the comparison table header down from under the ribbon when scrolling * Change the height on resize also * Add stories * Add Chromatic delays * Make chromati delays longer * Set param delays after play --- .../comparisonTable/ComparisonTableHead.tsx | 4 +- .../comparisonTable/styles.module.scss | 1 - .../src/plots/components/ribbon/Ribbon.tsx | 33 ++++++++++++-- .../plots/components/ribbon/ribbonSlice.ts | 25 +++++++++++ webview/src/plots/store.ts | 2 + .../src/stories/ComparisonTable.stories.tsx | 2 + webview/src/stories/Plots.stories.tsx | 45 +++++++++++++++++++ 7 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 webview/src/plots/components/ribbon/ribbonSlice.ts diff --git a/webview/src/plots/components/comparisonTable/ComparisonTableHead.tsx b/webview/src/plots/components/comparisonTable/ComparisonTableHead.tsx index 5f426d3213..af0d3bd64a 100644 --- a/webview/src/plots/components/comparisonTable/ComparisonTableHead.tsx +++ b/webview/src/plots/components/comparisonTable/ComparisonTableHead.tsx @@ -27,6 +27,7 @@ export const ComparisonTableHead: React.FC = ({ const draggedId = useSelector( (state: PlotsState) => state.dragAndDrop.draggedRef?.itemId ) + const ribbonHeight = useSelector((state: PlotsState) => state.ribbon.height) const items = columns.map(({ revision, displayColor, group }) => { const isPinned = revision === pinnedColumn @@ -38,6 +39,7 @@ export const ComparisonTableHead: React.FC = ({ [styles.pinnedColumnHeader]: isPinned, [styles.draggedColumn]: draggedId === revision })} + style={{ top: ribbonHeight - 4 }} // 4 is equal to the gap in the comparison table > = ({ }) return ( - + col.revision)} diff --git a/webview/src/plots/components/comparisonTable/styles.module.scss b/webview/src/plots/components/comparisonTable/styles.module.scss index bd2f6cd264..a2b8a51968 100644 --- a/webview/src/plots/components/comparisonTable/styles.module.scss +++ b/webview/src/plots/components/comparisonTable/styles.module.scss @@ -3,7 +3,6 @@ $gap: 4px; .comparisonTableHeader { position: sticky; - top: -#{$gap}; z-index: 2; background-color: $bg-color; } diff --git a/webview/src/plots/components/ribbon/Ribbon.tsx b/webview/src/plots/components/ribbon/Ribbon.tsx index 3792c67683..f4700a550e 100644 --- a/webview/src/plots/components/ribbon/Ribbon.tsx +++ b/webview/src/plots/components/ribbon/Ribbon.tsx @@ -1,10 +1,11 @@ import cx from 'classnames' import { MessageFromWebviewType } from 'dvc/src/webview/contract' -import React from 'react' -import { useSelector } from 'react-redux' +import React, { useCallback, useEffect, useRef } from 'react' +import { useDispatch, useSelector } from 'react-redux' import { useInView } from 'react-intersection-observer' import styles from './styles.module.scss' import { RibbonBlock } from './RibbonBlock' +import { update } from './ribbonSlice' import { sendMessage } from '../../../shared/vscode' import { IconButton } from '../../../shared/components/button/IconButton' import { PlotsState } from '../../store' @@ -18,11 +19,32 @@ export const Ribbon: React.FC = () => { rootMargin: '-5px', threshold: 0.95 }) + const measurementsRef = useRef() + const dispatch = useDispatch() const revisions = useSelector( (state: PlotsState) => state.webview.selectedRevisions ) + const changeRibbonHeight = useCallback( + () => + measurementsRef.current && + dispatch(update(measurementsRef.current.getBoundingClientRect().height)), + [dispatch] + ) + + useEffect(() => { + changeRibbonHeight() + }, [revisions, changeRibbonHeight]) + + useEffect(() => { + window.addEventListener('resize', changeRibbonHeight) + + return () => { + window.removeEventListener('resize', changeRibbonHeight) + } + }, [changeRibbonHeight]) + const removeRevision = (revision: string) => { sendMessage({ payload: revision, @@ -44,7 +66,12 @@ export const Ribbon: React.FC = () => { return (
    { + if (node) { + measurementsRef.current = node + } + ref(node) + }} data-testid="ribbon" className={cx(styles.list, needsShadow && styles.withShadow)} > diff --git a/webview/src/plots/components/ribbon/ribbonSlice.ts b/webview/src/plots/components/ribbon/ribbonSlice.ts new file mode 100644 index 0000000000..4e035d5072 --- /dev/null +++ b/webview/src/plots/components/ribbon/ribbonSlice.ts @@ -0,0 +1,25 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' + +export interface RibbonState { + height: number +} + +export const ribbonInitialState: RibbonState = { + height: 50 +} + +export const ribbonSlice = createSlice({ + initialState: ribbonInitialState, + name: 'ribbon', + reducers: { + update: (_, action: PayloadAction) => { + return { + height: action.payload + } + } + } +}) + +export const { update } = ribbonSlice.actions + +export default ribbonSlice.reducer diff --git a/webview/src/plots/store.ts b/webview/src/plots/store.ts index 4ce12b234d..57f1bb6cda 100644 --- a/webview/src/plots/store.ts +++ b/webview/src/plots/store.ts @@ -3,12 +3,14 @@ import checkpointPlotsReducer from './components/checkpointPlots/checkpointPlots import comparisonTableReducer from './components/comparisonTable/comparisonTableSlice' import templatePlotsReducer from './components/templatePlots/templatePlotsSlice' import webviewReducer from './components/webviewSlice' +import ribbonReducer from './components/ribbon/ribbonSlice' import dragAndDropReducer from '../shared/components/dragDrop/dragDropSlice' export const plotsReducers = { checkpoint: checkpointPlotsReducer, comparison: comparisonTableReducer, dragAndDrop: dragAndDropReducer, + ribbon: ribbonReducer, template: templatePlotsReducer, webview: webviewReducer } diff --git a/webview/src/stories/ComparisonTable.stories.tsx b/webview/src/stories/ComparisonTable.stories.tsx index 39d764724e..9242c7bd67 100644 --- a/webview/src/stories/ComparisonTable.stories.tsx +++ b/webview/src/stories/ComparisonTable.stories.tsx @@ -14,6 +14,7 @@ import { EXPERIMENT_WORKSPACE_ID } from 'dvc/src/cli/dvc/contract' import { ComparisonTable } from '../plots/components/comparisonTable/ComparisonTable' import { WebviewWrapper } from '../shared/components/webviewWrapper/WebviewWrapper' import { update } from '../plots/components/comparisonTable/comparisonTableSlice' +import { update as ribbonUpdate } from '../plots/components/ribbon/ribbonSlice' import { plotsReducers } from '../plots/store' const MockedState: React.FC<{ @@ -22,6 +23,7 @@ const MockedState: React.FC<{ }> = ({ children, data }) => { const dispatch = useDispatch() dispatch(update(data)) + dispatch(ribbonUpdate(0)) return <>{children} } diff --git a/webview/src/stories/Plots.stories.tsx b/webview/src/stories/Plots.stories.tsx index 7d71bc1745..ded9d4dffa 100644 --- a/webview/src/stories/Plots.stories.tsx +++ b/webview/src/stories/Plots.stories.tsx @@ -242,3 +242,48 @@ SmoothTemplate.args = { } } SmoothTemplate.parameters = chromaticParameters + +export const ScrolledHeaders = Template.bind({}) +ScrolledHeaders.play = async ({ canvasElement }) => { + await new Promise(resolve => setTimeout(resolve, 1000)) + const comparisonTableHead = await within(canvasElement).findByTestId( + 'comparison-table-head' + ) + + window.scrollTo({ + top: comparisonTableHead.getBoundingClientRect().top + 30 + }) +} +ScrolledHeaders.parameters = { + chromatic: { delay: 2500 } +} + +export const ScrolledWithManyRevisions = Template.bind({}) +ScrolledWithManyRevisions.args = { + data: { + checkpoint: checkpointPlotsFixture, + comparison: comparisonPlotsFixture, + hasPlots: true, + hasUnselectedPlots: false, + sectionCollapsed: DEFAULT_SECTION_COLLAPSED, + selectedRevisions: [ + ...plotsRevisionsFixture, + ...plotsRevisionsFixture, + ...plotsRevisionsFixture + ], + template: templatePlotsFixture + } +} +ScrolledWithManyRevisions.play = async ({ canvasElement }) => { + await new Promise(resolve => setTimeout(resolve, 1000)) + const comparisonTableHead = await within(canvasElement).findByTestId( + 'comparison-table-head' + ) + + window.scrollTo({ + top: comparisonTableHead.getBoundingClientRect().top + 30 + }) +} +ScrolledWithManyRevisions.parameters = { + chromatic: { delay: 2500 } +} From 716f5693f159b63600024e6a9449371b7ef63711 Mon Sep 17 00:00:00 2001 From: Stephanie Roy Date: Thu, 16 Feb 2023 14:36:28 -0500 Subject: [PATCH 2/2] Add add configuration button in experiments table (#3281) * Add add configuration button in table * Fix lint errors * Fix tests * Add webview tests * Add tests * Apply review comment --- extension/src/experiments/index.ts | 12 ++- extension/src/experiments/webview/contract.ts | 1 + extension/src/experiments/webview/messages.ts | 27 +++++- extension/src/experiments/workspace.ts | 3 +- .../test/fixtures/expShow/base/tableData.ts | 1 + .../fixtures/expShow/dataTypes/tableData.ts | 1 + .../expShow/deeplyNested/tableData.ts | 1 + .../fixtures/expShow/survival/tableData.ts | 1 + .../src/test/suite/experiments/index.test.ts | 85 +++++++++++++++++++ .../experiments/model/filterBy/tree.test.ts | 7 ++ extension/src/test/suite/experiments/util.ts | 3 + extension/src/test/suite/plots/util.ts | 1 + extension/src/webview/contract.ts | 2 + .../src/experiments/components/App.test.tsx | 26 ++++++ .../experiments/components/Experiments.tsx | 14 +++ .../components/table/styles.module.scss | 8 +- .../components/table/tableDataSlice.ts | 1 + webview/src/stories/Table.stories.tsx | 1 + webview/src/test/sort.ts | 1 + 19 files changed, 189 insertions(+), 7 deletions(-) diff --git a/extension/src/experiments/index.ts b/extension/src/experiments/index.ts index 614396dea6..c8a2ab98f6 100644 --- a/extension/src/experiments/index.ts +++ b/extension/src/experiments/index.ts @@ -100,12 +100,15 @@ export class Experiments extends BaseRepository { private dvcLiveOnlyCleanupInitialized = false private dvcLiveOnlySignalFile: string + private readonly addStage: () => Promise + constructor( dvcRoot: string, internalCommands: InternalCommands, updatesPaused: EventEmitter, resourceLocator: ResourceLocator, workspaceState: Memento, + addStage: () => Promise, cliData?: ExperimentsData, fileSystemData?: FileSystemData ) { @@ -117,6 +120,7 @@ export class Experiments extends BaseRepository { ) this.internalCommands = internalCommands + this.addStage = addStage this.onDidChangeIsParamsFileFocused = this.paramsFileFocused.event this.onDidChangeExperiments = this.experimentsChanged.event @@ -571,7 +575,13 @@ export class Experiments extends BaseRepository { AvailableCommands.QUEUE_KILL, dvcRoot, ...ids - ) + ), + () => + this.internalCommands.executeCommand( + AvailableCommands.STAGE_LIST, + this.dvcRoot + ), + () => this.addStage() ) this.dispose.track( diff --git a/extension/src/experiments/webview/contract.ts b/extension/src/experiments/webview/contract.ts index 82aa1e01cb..3e29e47fe5 100644 --- a/extension/src/experiments/webview/contract.ts +++ b/extension/src/experiments/webview/contract.ts @@ -98,6 +98,7 @@ export type TableData = { columnWidths: Record hasCheckpoints: boolean hasColumns: boolean + hasConfig: boolean hasRunningExperiment: boolean rows: Row[] sorts: SortDefinition[] diff --git a/extension/src/experiments/webview/messages.ts b/extension/src/experiments/webview/messages.ts index ee7f847a5c..2b0b738132 100644 --- a/extension/src/experiments/webview/messages.ts +++ b/extension/src/experiments/webview/messages.ts @@ -40,6 +40,12 @@ export class WebviewMessages { ...ids: string[] ) => Promise + private readonly hasStages: () => Promise + + private hasConfig = false + + private readonly addStage: () => Promise + constructor( dvcRoot: string, experiments: ExperimentsModel, @@ -51,7 +57,9 @@ export class WebviewMessages { stopQueuedExperiments: ( dvcRoot: string, ...ids: string[] - ) => Promise + ) => Promise, + hasStages: () => Promise, + addStage: () => Promise ) { this.dvcRoot = dvcRoot this.experiments = experiments @@ -61,6 +69,13 @@ export class WebviewMessages { this.notifyChanged = notifyChanged this.selectColumns = selectColumns this.stopQueuedExperiments = stopQueuedExperiments + this.hasStages = hasStages + void this.changeHasConfig() + this.addStage = addStage + } + + public async changeHasConfig() { + this.hasConfig = !!(await this.hasStages()) } public sendWebviewMessage() { @@ -166,6 +181,10 @@ export class WebviewMessages { return this.stopExperiments(message.payload) } + case MessageFromWebviewType.ADD_CONFIGURATION: { + return this.addConfiguration() + } + default: Logger.error(`Unexpected message: ${JSON.stringify(message)}`) } @@ -183,12 +202,18 @@ export class WebviewMessages { filters: this.experiments.getFilterPaths(), hasCheckpoints: this.checkpoints.hasCheckpoints(), hasColumns: this.columns.hasNonDefaultColumns(), + hasConfig: this.hasConfig, hasRunningExperiment: this.experiments.hasRunningExperiment(), rows: this.experiments.getRowData(), sorts: this.experiments.getSorts() } } + private async addConfiguration() { + await this.addStage() + await this.changeHasConfig() + } + private async setMaxTableHeadDepth() { const newValue = await getPositiveIntegerInput( Title.SET_EXPERIMENTS_HEADER_HEIGHT, diff --git a/extension/src/experiments/workspace.ts b/extension/src/experiments/workspace.ts index 29d5396979..72493fe3cb 100644 --- a/extension/src/experiments/workspace.ts +++ b/extension/src/experiments/workspace.ts @@ -364,7 +364,8 @@ export class WorkspaceExperiments extends BaseWorkspaceWebviews< this.internalCommands, updatesPaused, resourceLocator, - this.workspaceState + this.workspaceState, + () => this.checkOrAddPipeline(dvcRoot) ) ) diff --git a/extension/src/test/fixtures/expShow/base/tableData.ts b/extension/src/test/fixtures/expShow/base/tableData.ts index 87fb900cd5..5dcc761e17 100644 --- a/extension/src/test/fixtures/expShow/base/tableData.ts +++ b/extension/src/test/fixtures/expShow/base/tableData.ts @@ -8,6 +8,7 @@ const tableDataFixture: TableData = { columns: columnsFixture, filters: [], hasCheckpoints: true, + hasConfig: true, hasRunningExperiment: true, hasColumns: true, sorts: [], diff --git a/extension/src/test/fixtures/expShow/dataTypes/tableData.ts b/extension/src/test/fixtures/expShow/dataTypes/tableData.ts index 658788a3fd..e2b626bc9c 100644 --- a/extension/src/test/fixtures/expShow/dataTypes/tableData.ts +++ b/extension/src/test/fixtures/expShow/dataTypes/tableData.ts @@ -9,6 +9,7 @@ export const data: TableData = { filteredCounts: { experiments: 0, checkpoints: 0 }, filters: [], hasCheckpoints: false, + hasConfig: true, hasRunningExperiment: false, sorts: [], columns, diff --git a/extension/src/test/fixtures/expShow/deeplyNested/tableData.ts b/extension/src/test/fixtures/expShow/deeplyNested/tableData.ts index 28f50738b1..1350c28a0f 100644 --- a/extension/src/test/fixtures/expShow/deeplyNested/tableData.ts +++ b/extension/src/test/fixtures/expShow/deeplyNested/tableData.ts @@ -12,6 +12,7 @@ const data: TableData = { 'params:params.yaml:nested1%2Enested2%2Enested3.nested4.nested5b.doubled' ], hasCheckpoints: false, + hasConfig: true, hasRunningExperiment: false, sorts: [ { diff --git a/extension/src/test/fixtures/expShow/survival/tableData.ts b/extension/src/test/fixtures/expShow/survival/tableData.ts index 7b0b2b427c..dd426296a0 100644 --- a/extension/src/test/fixtures/expShow/survival/tableData.ts +++ b/extension/src/test/fixtures/expShow/survival/tableData.ts @@ -8,6 +8,7 @@ const data: TableData = { columns: columnsFixture, filters: [], hasCheckpoints: true, + hasConfig: true, hasRunningExperiment: true, hasColumns: true, sorts: [], diff --git a/extension/src/test/suite/experiments/index.test.ts b/extension/src/test/suite/experiments/index.test.ts index cb387bdcdc..8465e99e26 100644 --- a/extension/src/test/suite/experiments/index.test.ts +++ b/extension/src/test/suite/experiments/index.test.ts @@ -76,6 +76,7 @@ import { AvailableCommands } from '../../../commands/internal' import { Setup } from '../../../setup' import * as FileSystem from '../../../fileSystem' import * as ProcessExecution from '../../../processExecution' +import { DvcReader } from '../../../cli/dvc/reader' const { openFileInEditor } = FileSystem @@ -127,6 +128,8 @@ suite('Experiments Test Suite', () => { describe('showWebview', () => { it('should be able to make the experiment webview visible', async () => { + stub(DvcReader.prototype, 'listStages').resolves('train') + const { experiments, messageSpy } = buildExperiments( disposable, expShowFixture @@ -143,6 +146,7 @@ suite('Experiments Test Suite', () => { filters: [], hasCheckpoints: true, hasColumns: true, + hasConfig: true, hasRunningExperiment: true, rows: rowsFixture, sorts: [] @@ -177,6 +181,62 @@ suite('Experiments Test Suite', () => { expect(windowSpy).not.to.have.been.called }).timeout(WEBVIEW_TEST_TIMEOUT) + + it('should set hasConfig to false if there are no stages', async () => { + stub(DvcReader.prototype, 'listStages').resolves('') + + const { experiments, messageSpy } = buildExperiments( + disposable, + expShowFixture + ) + + await experiments.showWebview() + + const expectedTableData: TableData = { + changes: workspaceChangesFixture, + columnOrder: columnsOrderFixture, + columnWidths: {}, + columns: columnsFixture, + filteredCounts: { checkpoints: 0, experiments: 0 }, + filters: [], + hasCheckpoints: true, + hasColumns: true, + hasConfig: false, + hasRunningExperiment: true, + rows: rowsFixture, + sorts: [] + } + + expect(messageSpy).to.be.calledWithExactly(expectedTableData) + }).timeout(WEBVIEW_TEST_TIMEOUT) + + it('should set hasConfig to true if there are stages', async () => { + stub(DvcReader.prototype, 'listStages').resolves('train') + + const { experiments, messageSpy } = buildExperiments( + disposable, + expShowFixture + ) + + await experiments.showWebview() + + const expectedTableData: TableData = { + changes: workspaceChangesFixture, + columnOrder: columnsOrderFixture, + columnWidths: {}, + columns: columnsFixture, + filteredCounts: { checkpoints: 0, experiments: 0 }, + filters: [], + hasCheckpoints: true, + hasColumns: true, + hasConfig: true, + hasRunningExperiment: true, + rows: rowsFixture, + sorts: [] + } + + expect(messageSpy).to.be.calledWithExactly(expectedTableData) + }).timeout(WEBVIEW_TEST_TIMEOUT) }) describe('handleMessageFromWebview', () => { @@ -193,6 +253,7 @@ suite('Experiments Test Suite', () => { experimentsModel, internalCommands, dvcExecutor, + mockCheckOrAddPipeline, messageSpy } = buildExperiments(disposable, expShowFixture) const mockExecuteCommand = stub( @@ -209,6 +270,7 @@ suite('Experiments Test Suite', () => { experiments, experimentsModel, messageSpy, + mockCheckOrAddPipeline, mockExecuteCommand } } @@ -788,6 +850,8 @@ suite('Experiments Test Suite', () => { }).timeout(WEBVIEW_TEST_TIMEOUT) it('should be able to handle a message to select columns', async () => { + stub(DvcReader.prototype, 'listStages').resolves('train') + const { columnsModel, experiments, messageSpy } = setupExperimentsAndMockCommands() @@ -826,6 +890,7 @@ suite('Experiments Test Suite', () => { filters: [], hasCheckpoints: true, hasColumns: true, + hasConfig: true, hasRunningExperiment: true, rows: rowsFixture, sorts: [] @@ -1120,6 +1185,23 @@ suite('Experiments Test Suite', () => { expect(mockProcessExists).to.be.calledWithExactly(mockPid) expect(mockStopProcesses).to.be.calledWithExactly([mockPid]) }).timeout(WEBVIEW_TEST_TIMEOUT) + + it('should handle a message to add a configuration', async () => { + stub(DvcReader.prototype, 'listStages').resolves('') + + const { experiments, mockCheckOrAddPipeline, messageSpy } = + setupExperimentsAndMockCommands() + + const webview = await experiments.showWebview() + messageSpy.resetHistory() + const mockMessageReceived = getMessageReceivedEmitter(webview) + + mockMessageReceived.fire({ + type: MessageFromWebviewType.ADD_CONFIGURATION + }) + + expect(mockCheckOrAddPipeline).to.be.calledOnce + }).timeout(WEBVIEW_TEST_TIMEOUT) }) describe('Sorting', () => { @@ -1148,6 +1230,7 @@ suite('Experiments Test Suite', () => { updatesPaused, resourceLocator, buildMockMemento(), + () => Promise.resolve(true), buildMockData(), buildMockData() ) @@ -1364,6 +1447,7 @@ suite('Experiments Test Suite', () => { {} as EventEmitter, {} as ResourceLocator, mockMemento, + () => Promise.resolve(true), buildMockData(), buildMockData() ) @@ -1541,6 +1625,7 @@ suite('Experiments Test Suite', () => { {} as EventEmitter, {} as ResourceLocator, mockMemento, + () => Promise.resolve(true), buildMockData(), buildMockData() ) diff --git a/extension/src/test/suite/experiments/model/filterBy/tree.test.ts b/extension/src/test/suite/experiments/model/filterBy/tree.test.ts index 68e79af6ab..cadcf6700f 100644 --- a/extension/src/test/suite/experiments/model/filterBy/tree.test.ts +++ b/extension/src/test/suite/experiments/model/filterBy/tree.test.ts @@ -41,6 +41,7 @@ import { FilterItem } from '../../../../../experiments/model/filterBy/tree' import { starredFilter } from '../../../../../experiments/model/filterBy/constants' +import { DvcReader } from '../../../../../cli/dvc/reader' suite('Experiments Filter By Tree Test Suite', () => { const disposable = Disposable.fn() @@ -62,6 +63,8 @@ suite('Experiments Filter By Tree Test Suite', () => { }) it('should be able to update the table data by adding and removing a filter', async () => { + stub(DvcReader.prototype, 'listStages').resolves('train') + const { experiments, messageSpy } = buildExperiments(disposable) await experiments.isReady() @@ -124,6 +127,7 @@ suite('Experiments Filter By Tree Test Suite', () => { filters: [accuracyPath], hasCheckpoints: true, hasColumns: true, + hasConfig: true, hasRunningExperiment: true, rows: filteredRows, sorts: [] @@ -155,6 +159,7 @@ suite('Experiments Filter By Tree Test Suite', () => { filters: [], hasCheckpoints: true, hasColumns: true, + hasConfig: true, hasRunningExperiment: true, rows: [workspace, main], sorts: [] @@ -457,6 +462,7 @@ suite('Experiments Filter By Tree Test Suite', () => { }) it('should be able to filter to starred experiments', async () => { + stub(DvcReader.prototype, 'listStages').resolves('train') const { experiments, messageSpy } = buildExperiments(disposable) await experiments.isReady() @@ -485,6 +491,7 @@ suite('Experiments Filter By Tree Test Suite', () => { filters: ['starred'], hasCheckpoints: true, hasColumns: true, + hasConfig: true, hasRunningExperiment: true, rows: filteredRows, sorts: [] diff --git a/extension/src/test/suite/experiments/util.ts b/extension/src/test/suite/experiments/util.ts index bd11704b26..183807845c 100644 --- a/extension/src/test/suite/experiments/util.ts +++ b/extension/src/test/suite/experiments/util.ts @@ -65,6 +65,7 @@ export const buildExperiments = ( const mockExperimentsData = buildMockData( mockUpdateExperimentsData ) + const mockCheckOrAddPipeline = stub() const experiments = disposer.track( new Experiments( @@ -73,6 +74,7 @@ export const buildExperiments = ( updatesPaused, resourceLocator, buildMockMemento(), + mockCheckOrAddPipeline, mockExperimentsData, buildMockData() ) @@ -94,6 +96,7 @@ export const buildExperiments = ( gitReader, internalCommands, messageSpy, + mockCheckOrAddPipeline, mockCheckSignalFile, mockExperimentShow, mockGetCommitMessages, diff --git a/extension/src/test/suite/plots/util.ts b/extension/src/test/suite/plots/util.ts index 4ad0f63592..e2dedd6475 100644 --- a/extension/src/test/suite/plots/util.ts +++ b/extension/src/test/suite/plots/util.ts @@ -48,6 +48,7 @@ export const buildPlots = async ( updatesPaused, resourceLocator, buildMockMemento(), + () => Promise.resolve(true), buildMockData(), buildMockData() ) diff --git a/extension/src/webview/contract.ts b/extension/src/webview/contract.ts index 6b7ddbc8ba..ab6669f665 100644 --- a/extension/src/webview/contract.ts +++ b/extension/src/webview/contract.ts @@ -12,6 +12,7 @@ export type WebviewData = TableData | PlotsData | SetupData export enum MessageFromWebviewType { INITIALIZED = 'initialized', + ADD_CONFIGURATION = 'add-configuration', APPLY_EXPERIMENT_TO_WORKSPACE = 'apply-experiment-to-workspace', ADD_STARRED_EXPERIMENT_FILTER = 'add-starred-experiment-filter', CREATE_BRANCH_FROM_EXPERIMENT = 'create-branch-from-experiment', @@ -196,6 +197,7 @@ export type MessageFromWebview = | { type: MessageFromWebviewType.SHOW_SCM_PANEL } | { type: MessageFromWebviewType.INSTALL_DVC } | { type: MessageFromWebviewType.SETUP_WORKSPACE } + | { type: MessageFromWebviewType.ADD_CONFIGURATION } export type MessageToWebview = { type: MessageToWebviewType.SET_DATA diff --git a/webview/src/experiments/components/App.test.tsx b/webview/src/experiments/components/App.test.tsx index 66a9d0b19a..a4afe2b1c2 100644 --- a/webview/src/experiments/components/App.test.tsx +++ b/webview/src/experiments/components/App.test.tsx @@ -1419,4 +1419,30 @@ describe('App', () => { expect(document.body).not.toHaveClass(styles.isColumnResizing) }) + + describe('Add configuration button', () => { + it('should show a add config button if the project has no pipeline stages', () => { + renderTable() + setTableData({ ...tableDataFixture, hasConfig: false }) + + expect(screen.getByText('Add Configuration')).toBeInTheDocument() + }) + + it('should not show a add config button if the project has pipeline stages', () => { + renderTable() + + expect(screen.queryByText('Add Configuration')).not.toBeInTheDocument() + }) + + it('should send a message to the extension to add a pipeline stage when clicking on the add config button', () => { + renderTable() + setTableData({ ...tableDataFixture, hasConfig: false }) + + fireEvent.click(screen.getByText('Add Configuration')) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: MessageFromWebviewType.ADD_CONFIGURATION + }) + }) + }) }) diff --git a/webview/src/experiments/components/Experiments.tsx b/webview/src/experiments/components/Experiments.tsx index 89bd1bfb92..77af670cf0 100644 --- a/webview/src/experiments/components/Experiments.tsx +++ b/webview/src/experiments/components/Experiments.tsx @@ -35,6 +35,8 @@ import { GetStarted } from '../../shared/components/getStarted/GetStarted' import { EmptyState } from '../../shared/components/emptyState/EmptyState' import { ExperimentsState } from '../store' import { EXPERIMENT_COLUMN_ID } from '../util/columns' +import { IconButton } from '../../shared/components/button/IconButton' +import { Add } from '../../shared/components/icons' const DEFAULT_COLUMN_WIDTH = 90 const MINIMUM_COLUMN_WIDTH = 90 @@ -131,6 +133,7 @@ export const ExperimentsTable: React.FC = () => { columnOrder: initialColumnOrder, columnWidths, hasColumns, + hasConfig, rows: data } = useSelector((state: ExperimentsState) => state.tableData) @@ -197,6 +200,17 @@ export const ExperimentsTable: React.FC = () => { return ( + {!hasConfig && ( +
    + + sendMessage({ type: MessageFromWebviewType.ADD_CONFIGURATION }) + } + text="Add Configuration" + /> +
    + )} ) } diff --git a/webview/src/experiments/components/table/styles.module.scss b/webview/src/experiments/components/table/styles.module.scss index 42a04119d9..ef9a362cb4 100644 --- a/webview/src/experiments/components/table/styles.module.scss +++ b/webview/src/experiments/components/table/styles.module.scss @@ -256,10 +256,6 @@ $bullet-size: calc(var(--design-unit) * 4px); padding-left: 1rem; } - .tableContainer { - flex: 1; - } - table { display: inline-block; border-collapse: collapse; @@ -998,3 +994,7 @@ $badge-size: 0.85rem; font-size: 0.65rem; cursor: pointer; } + +.addConfigButton { + margin: 20px auto; +} diff --git a/webview/src/experiments/components/table/tableDataSlice.ts b/webview/src/experiments/components/table/tableDataSlice.ts index cca2a285ee..6be7a26a4d 100644 --- a/webview/src/experiments/components/table/tableDataSlice.ts +++ b/webview/src/experiments/components/table/tableDataSlice.ts @@ -21,6 +21,7 @@ export const tableDataInitialState: TableDataState = { filters: [], hasCheckpoints: false, hasColumns: false, + hasConfig: false, hasData: false, hasRunningExperiment: false, rows: [], diff --git a/webview/src/stories/Table.stories.tsx b/webview/src/stories/Table.stories.tsx index ec703c2f29..7db143bb3a 100644 --- a/webview/src/stories/Table.stories.tsx +++ b/webview/src/stories/Table.stories.tsx @@ -43,6 +43,7 @@ const tableData: TableDataState = { filters: ['params:params.yaml:lr'], hasCheckpoints: true, hasColumns: true, + hasConfig: true, hasData: true, hasRunningExperiment: true, rows: addCommitDataToMainBranch(rowsFixture).map(row => ({ diff --git a/webview/src/test/sort.ts b/webview/src/test/sort.ts index 01ae23a403..53b3bec39c 100644 --- a/webview/src/test/sort.ts +++ b/webview/src/test/sort.ts @@ -48,6 +48,7 @@ export const tableData: TableData = { filters: [], hasCheckpoints: false, hasColumns: true, + hasConfig: true, hasRunningExperiment: false, rows: [ {