diff --git a/extension/src/experiments/webview/contract.ts b/extension/src/experiments/webview/contract.ts index 3dac98a247..f67d7e5193 100644 --- a/extension/src/experiments/webview/contract.ts +++ b/extension/src/experiments/webview/contract.ts @@ -43,6 +43,7 @@ export type Experiment = { starred?: boolean status?: ExperimentStatus timestamp?: string | null + branch?: string } export const isRunning = (status: ExperimentStatus | undefined): boolean => diff --git a/extension/src/test/e2e/extension.test.ts b/extension/src/test/e2e/extension.test.ts index 89cd6b75e7..9b2ee67b0a 100644 --- a/extension/src/test/e2e/extension.test.ts +++ b/extension/src/test/e2e/extension.test.ts @@ -52,7 +52,9 @@ describe('Experiments Table Webview', function () { const workspaceRow = 1 const commitRows = 3 const previousCommitRow = 1 - const initialRows = headerRows + workspaceRow + commitRows + previousCommitRow + const actionsRow = 1 + const initialRows = + headerRows + workspaceRow + commitRows + previousCommitRow + actionsRow it('should load as an editor', async function () { const workbench = await browser.getWorkbench() diff --git a/extension/src/test/fixtures/expShow/base/rows.ts b/extension/src/test/fixtures/expShow/base/rows.ts index 5c3432539f..5f7ca0b623 100644 --- a/extension/src/test/fixtures/expShow/base/rows.ts +++ b/extension/src/test/fixtures/expShow/base/rows.ts @@ -15,8 +15,9 @@ const valueWithNoChanges = (str: string) => ({ const colorsList = copyOriginalColors() -const data: Commit[] = [ +export const rowsFixtureWithBranches: Commit[] = [ { + branch: 'current', deps: { [join('data', 'data.xml')]: valueWithNoChanges( '22a1a2931c8370d3aeedd7183606fd7f' @@ -72,6 +73,7 @@ const data: Commit[] = [ starred: false }, { + branch: 'current', deps: { [join('data', 'data.xml')]: valueWithNoChanges( '22a1a2931c8370d3aeedd7183606fd7f' @@ -126,6 +128,7 @@ const data: Commit[] = [ starred: false, subRows: [ { + branch: 'current', deps: { [join('data', 'data.xml')]: valueWithNoChanges( '22a1a2931c8370d3aeedd7183606fd7f' @@ -184,6 +187,7 @@ const data: Commit[] = [ Created: '2020-12-29T15:31:52' }, { + branch: 'current', deps: { [join('data', 'data.xml')]: valueWithNoChanges( '22a1a2931c8370d3aeedd7183606fd7f' @@ -240,6 +244,7 @@ const data: Commit[] = [ Created: '2020-12-29T15:28:59' }, { + branch: 'current', deps: { [join('data', 'data.xml')]: valueWithNoChanges( '22a1a2931c8370d3aeedd7183606fd7f' @@ -297,6 +302,7 @@ const data: Commit[] = [ Created: '2020-12-29T15:27:02' }, { + branch: 'current', displayColor: undefined, id: '489fd8b', sha: '489fd8bdaa709f7330aac342e051a9431c625481', @@ -308,6 +314,7 @@ const data: Commit[] = [ status: ExperimentStatus.FAILED }, { + branch: 'current', deps: { [join('data', 'data.xml')]: valueWithNoChanges( '22a1a2931c8370d3aeedd7183606fd7f' @@ -359,6 +366,7 @@ const data: Commit[] = [ Created: '2020-12-29T15:26:36' }, { + branch: 'current', displayColor: undefined, deps: { [join('data', 'data.xml')]: valueWithNoChanges( @@ -407,6 +415,7 @@ const data: Commit[] = [ Created: '2020-12-29T15:25:27' }, { + branch: 'current', displayColor: undefined, deps: { [join('data', 'data.xml')]: valueWithNoChanges( @@ -461,4 +470,15 @@ const data: Commit[] = [ } ] -export default data +const rowsFixtureWithoutBranches = [ + ...rowsFixtureWithBranches.map(({ branch, ...row }) => + row.subRows + ? { + ...row, + subRows: row.subRows?.map(({ branch, ...subRow }) => subRow) + } + : row + ) +] + +export default rowsFixtureWithoutBranches diff --git a/extension/src/test/fixtures/expShow/base/tableData.ts b/extension/src/test/fixtures/expShow/base/tableData.ts index 39fc2f8a52..d24f76f86e 100644 --- a/extension/src/test/fixtures/expShow/base/tableData.ts +++ b/extension/src/test/fixtures/expShow/base/tableData.ts @@ -1,5 +1,5 @@ import { TableData } from '../../../../experiments/webview/contract' -import rowsFixture from './rows' +import { rowsFixtureWithBranches } from './rows' import columnsFixture from './columns' const tableDataFixture: TableData = { @@ -18,7 +18,7 @@ const tableDataFixture: TableData = { hasValidDvcYaml: true, isShowingMoreCommits: true, isBranchesView: false, - rows: rowsFixture, + rows: rowsFixtureWithBranches, selectedForPlotsCount: 2, sorts: [] } diff --git a/webview/src/experiments/components/App.tsx b/webview/src/experiments/components/App.tsx index 54a4b3c24a..269d60f1e2 100644 --- a/webview/src/experiments/components/App.tsx +++ b/webview/src/experiments/components/App.tsx @@ -91,7 +91,19 @@ export const App: React.FC> = () => { ) continue case 'rows': - dispatch(updateRows(data.data.rows)) + dispatch( + updateRows( + // Setting any branch for now just so that it isn't undefined. It does not matter the label for now as it is not shown + data.data.rows.map(row => ({ + ...row, + branch: 'current', + subRows: row.subRows?.map(subRow => ({ + ...subRow, + branch: 'current' + })) + })) + ) + ) continue case 'selectedForPlotsCount': dispatch( diff --git a/webview/src/experiments/components/table/Indicators.tsx b/webview/src/experiments/components/table/Indicators.tsx index 4432c5c79c..aa3184e75e 100644 --- a/webview/src/experiments/components/table/Indicators.tsx +++ b/webview/src/experiments/components/table/Indicators.tsx @@ -5,16 +5,19 @@ import { CellHintTooltip } from './body/CellHintTooltip' import { focusFiltersTree, focusSortsTree, - openPlotsWebview + openPlotsWebview, + selectBranches } from '../../util/messages' import { Icon } from '../../../shared/components/Icon' import { Filter, + GitMerge, GraphScatter, SortPrecedence } from '../../../shared/components/icons' import { pluralize } from '../../../util/strings' import { ExperimentsState } from '../../store' +import { featureFlag } from '../../../util/flags' export type CounterBadgeProps = { count?: number @@ -81,6 +84,10 @@ export const Indicators = () => { const selectedForPlotsCount = useSelector( (state: ExperimentsState) => state.tableData.selectedForPlotsCount ) + const branchesSelected = useSelector( + (state: ExperimentsState) => + Math.max(state.tableData.branches.length - 1, 0) // We always have one branch by default (the current one which is not selected) + ) const sortsCount = sorts?.length const filtersCount = filters?.length @@ -124,6 +131,20 @@ export const Indicators = () => { > + {featureFlag.ADD_REMOVE_BRANCHES && ( + + + + )} ) } diff --git a/webview/src/experiments/components/table/Table.test.tsx b/webview/src/experiments/components/table/Table.test.tsx index 15f870e74a..4f1b98583c 100644 --- a/webview/src/experiments/components/table/Table.test.tsx +++ b/webview/src/experiments/components/table/Table.test.tsx @@ -27,6 +27,7 @@ import { dragAndDrop, dragEnter, dragLeave } from '../../../test/dragDrop' import { DragEnterDirection } from '../../../shared/components/dragDrop/util' import { experimentsReducers } from '../../store' import { customQueries } from '../../../test/queries' +import { TableDataState } from '../../state/tableDataSlice' jest.mock('../../../shared/api') @@ -39,7 +40,8 @@ describe('Table', () => { ) => { const tableData = { ...sortingTableDataFixture, - ...partialTableData + ...partialTableData, + branches: ['current'] } return render( { id: 333 } - const tableDataWithColumnSetting: TableData = { + const tableDataWithColumnSetting: TableDataState = { ...sortingTableDataFixture, + branches: ['current'], columnWidths } render( @@ -297,8 +300,9 @@ describe('Table', () => { id: 333 } - const tableDataWithColumnSetting: TableData = { + const tableDataWithColumnSetting: TableDataState = { ...sortingTableDataFixture, + branches: ['current'], columnWidths } render( diff --git a/webview/src/experiments/components/table/Table.tsx b/webview/src/experiments/components/table/Table.tsx index 47be368356..38d408ee04 100644 --- a/webview/src/experiments/components/table/Table.tsx +++ b/webview/src/experiments/components/table/Table.tsx @@ -5,7 +5,6 @@ import styles from './styles.module.scss' import { TableHead } from './header/TableHead' import { RowSelectionContext } from './RowSelectionContext' import { Indicators } from './Indicators' -import { CommitsAndBranchesNavigation } from './commitsAndBranches/CommitsAndBranchesNavigation' import { TableContent } from './body/TableContent' import { InstanceProp } from '../../util/interfaces' @@ -54,7 +53,6 @@ export const Table: React.FC = ({ tableHeadHeight={tableHeadHeight} /> - ) diff --git a/webview/src/experiments/components/table/body/PreviousCommitsRow.tsx b/webview/src/experiments/components/table/body/PreviousCommitsRow.tsx new file mode 100644 index 0000000000..fb38a79b07 --- /dev/null +++ b/webview/src/experiments/components/table/body/PreviousCommitsRow.tsx @@ -0,0 +1,22 @@ +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 5fa846c925..15cc38a089 100644 --- a/webview/src/experiments/components/table/body/TableBody.tsx +++ b/webview/src/experiments/components/table/body/TableBody.tsx @@ -5,18 +5,19 @@ 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' -export const TableBody: React.FC< - RowProp & - InstanceProp & - BatchSelectionProp & { - root: HTMLElement | null - tableHeaderHeight: number - } -> = ({ +interface TableBodyProps extends RowProp, InstanceProp, BatchSelectionProp { + root: HTMLElement | null + tableHeaderHeight: number + showPreviousRow?: boolean + isLast?: boolean +} + +export const TableBody: React.FC = ({ row, instance, contextMenuDisabled, @@ -24,7 +25,9 @@ export const TableBody: React.FC< hasRunningExperiment, batchRowSelection, root, - tableHeaderHeight + tableHeaderHeight, + showPreviousRow, + isLast }) => { const contentProps = { batchRowSelection, @@ -54,22 +57,17 @@ export const TableBody: React.FC< ) : ( <> - {row.index === 2 && row.depth === 0 && ( - - - - {isBranchesView ? 'Other Branches' : 'Previous Commits'} - - - - + {showPreviousRow && row.depth === 0 && ( + )} 0, - [styles.expandedGroup]: row.getIsExpanded() && row.subRows.length > 0 + [styles.expandedGroup]: row.getIsExpanded() && row.subRows.length > 0, + [styles.lastRowGroup]: isLast })} > {content} diff --git a/webview/src/experiments/components/table/body/TableContent.test.tsx b/webview/src/experiments/components/table/body/TableContent.test.tsx new file mode 100644 index 0000000000..464d1ac270 --- /dev/null +++ b/webview/src/experiments/components/table/body/TableContent.test.tsx @@ -0,0 +1,863 @@ +import '@testing-library/jest-dom/extend-expect' +import { render, screen } from '@testing-library/react' +import React, { createRef } from 'react' +import { Table } from '@tanstack/react-table' +import { Experiment } from 'dvc/src/experiments/webview/contract' +import { Provider } from 'react-redux' +import { configureStore } from '@reduxjs/toolkit' +import tableData from 'dvc/src/test/fixtures/expShow/base/tableData' +import { TableContent } from './TableContent' +import { experimentsReducers } from '../../../store' + +jest.mock('../../../../shared/api') +jest.mock('./ExperimentGroup') +jest.mock('./Row') + +describe('TableContent', () => { + const mockedGetIsExpanded = jest.fn() + const mockedGetAllCells = jest.fn().mockReturnValue([1, 2, 3, 4, 5]) // Only needed as length + const instance = { + getRowModel: () => ({ + flatRows: [ + { + columnFilters: {}, + columnFiltersMeta: {}, + depth: 0, + id: '1', + index: 1, + original: { + Created: '2023-04-20T05:14:46', + branch: 'current', + commit: { + author: 'Matt Seddon', + date: '31 hours ago', + message: 'Update dependency dvc to v2.55.0 (#76)\n\n', + tags: [] + }, + deps: { + data: { + changes: false, + value: 'ab3353d' + }, + 'train.py': { + changes: false, + value: 'f431663' + } + }, + description: 'Update dependency dvc to v2.55.0 (#76)', + id: 'a9b32d1', + label: 'a9b32d1', + metrics: { + 'training/metrics.json': { + step: 14, + test: { + acc: 0.7735, + loss: 0.9596208930015564 + }, + train: { + acc: 0.7694, + loss: 0.9731049537658691 + } + } + }, + params: { + 'params.yaml': { + epochs: 15, + lr: 0.003, + weight_decay: 0 + } + }, + selected: false, + sha: 'a9b32d14966b9be1396f2211d9eb743359708a07', + starred: false, + subRows: [ + { + Created: '2023-04-21T12:04:32', + deps: { + data: { + changes: false, + value: 'ab3353d' + }, + 'train.py': { + changes: false, + value: 'f431663' + } + }, + description: '[prize-luce]', + id: 'prize-luce', + label: 'ae4100a', + metrics: { + 'training/metrics.json': { + step: 14, + test: { + acc: 0.7735, + loss: 0.9596208930015564 + }, + train: { + acc: 0.7694, + loss: 0.9731049537658691 + } + } + }, + params: { + 'params.yaml': { + epochs: 15, + lr: 0.003, + weight_decay: 0 + } + }, + selected: false, + sha: 'ae4100a4b4a972c3ceffa3062739845d944b3ddf', + starred: false + } + ] + }, + originalSubRows: [ + { + Created: '2023-04-21T12:04:32', + branch: 'current', + deps: { + data: { + changes: false, + value: 'ab3353d' + }, + 'train.py': { + changes: false, + value: 'f431663' + } + }, + description: '[prize-luce]', + id: 'prize-luce', + label: 'ae4100a', + metrics: { + 'training/metrics.json': { + step: 14, + test: { + acc: 0.7735, + loss: 0.9596208930015564 + }, + train: { + acc: 0.7694, + loss: 0.9731049537658691 + } + } + }, + params: { + 'params.yaml': { + epochs: 15, + lr: 0.003, + weight_decay: 0 + } + }, + selected: false, + sha: 'ae4100a4b4a972c3ceffa3062739845d944b3ddf', + starred: false + } + ], + subRows: [ + { + depth: 1, + id: '1.prize-luce', + index: 0, + original: { + Created: '2023-04-21T12:04:32', + branch: 'current', + deps: { + data: { + changes: false, + value: 'ab3353d' + }, + 'train.py': { + changes: false, + value: 'f431663' + } + }, + description: '[prize-luce]', + id: 'prize-luce', + label: 'ae4100a', + metrics: { + 'training/metrics.json': { + step: 14, + test: { + acc: 0.7735, + loss: 0.9596208930015564 + }, + train: { + acc: 0.7694, + loss: 0.9731049537658691 + } + } + }, + params: { + 'params.yaml': { + epochs: 15, + lr: 0.003, + weight_decay: 0 + } + }, + selected: false, + sha: 'ae4100a4b4a972c3ceffa3062739845d944b3ddf', + starred: false + }, + parentId: '1', + subRows: [] + } + ] + }, + { + depth: 0, + id: '1', + index: 1, + original: { + Created: '2023-04-20T05:14:46', + branch: 'current', + commit: { + author: 'Matt Seddon', + date: '31 hours ago', + message: 'Update dependency dvc to v2.55.0 (#76)\n\n', + tags: [] + }, + deps: { + data: { + changes: false, + value: 'ab3353d' + }, + 'train.py': { + changes: false, + value: 'f431663' + } + }, + description: 'Update dependency dvc to v2.55.0 (#76)', + id: 'a9b32d1', + label: 'a9b32d1', + metrics: { + 'training/metrics.json': { + step: 14, + test: { + acc: 0.7735, + loss: 0.9596208930015564 + }, + train: { + acc: 0.7694, + loss: 0.9731049537658691 + } + } + }, + params: { + 'params.yaml': { + epochs: 15, + lr: 0.003, + weight_decay: 0 + } + }, + selected: false, + sha: 'a9b32d14966b9be1396f2211d9eb743359708a07', + starred: false, + subRows: [ + { + Created: '2023-04-21T12:04:32', + deps: { + data: { + changes: false, + value: 'ab3353d' + }, + 'train.py': { + changes: false, + value: 'f431663' + } + }, + description: '[prize-luce]', + id: 'prize-luce', + label: 'ae4100a', + metrics: { + 'training/metrics.json': { + step: 14, + test: { + acc: 0.7735, + loss: 0.9596208930015564 + }, + train: { + acc: 0.7694, + loss: 0.9731049537658691 + } + } + }, + params: { + 'params.yaml': { + epochs: 15, + lr: 0.003, + weight_decay: 0 + } + }, + selected: false, + sha: 'ae4100a4b4a972c3ceffa3062739845d944b3ddf', + starred: false + } + ] + }, + originalSubRows: [ + { + Created: '2023-04-21T12:04:32', + branch: 'current', + deps: { + data: { + changes: false, + value: 'ab3353d' + }, + 'train.py': { + changes: false, + value: 'f431663' + } + }, + description: '[prize-luce]', + id: 'prize-luce', + label: 'ae4100a', + metrics: { + 'training/metrics.json': { + step: 14, + test: { + acc: 0.7735, + loss: 0.9596208930015564 + }, + train: { + acc: 0.7694, + loss: 0.9731049537658691 + } + } + }, + params: { + 'params.yaml': { + epochs: 15, + lr: 0.003, + weight_decay: 0 + } + }, + selected: false, + sha: 'ae4100a4b4a972c3ceffa3062739845d944b3ddf', + starred: false + } + ], + subRows: [ + { + _uniqueValuesCache: {}, + _valuesCache: {}, + columnFilters: {}, + columnFiltersMeta: {}, + depth: 1, + id: '1.prize-luce', + index: 0, + original: { + Created: '2023-04-21T12:04:32', + branch: 'current', + deps: { + data: { + changes: false, + value: 'ab3353d' + }, + 'train.py': { + changes: false, + value: 'f431663' + } + }, + description: '[prize-luce]', + id: 'prize-luce', + label: 'ae4100a', + metrics: { + 'training/metrics.json': { + step: 14, + test: { + acc: 0.7735, + loss: 0.9596208930015564 + }, + train: { + acc: 0.7694, + loss: 0.9731049537658691 + } + } + }, + params: { + 'params.yaml': { + epochs: 15, + lr: 0.003, + weight_decay: 0 + } + }, + selected: false, + sha: 'ae4100a4b4a972c3ceffa3062739845d944b3ddf', + starred: false + }, + parentId: '1', + subRows: [] + } + ] + }, + { + columnFilters: {}, + columnFiltersMeta: {}, + depth: 1, + id: '1.prize-luce', + index: 0, + original: { + Created: '2023-04-21T12:04:32', + branch: 'current', + deps: { + data: { + changes: false, + value: 'ab3353d' + }, + 'train.py': { + changes: false, + value: 'f431663' + } + }, + description: '[prize-luce]', + id: 'prize-luce', + label: 'ae4100a', + metrics: { + 'training/metrics.json': { + step: 14, + test: { + acc: 0.7735, + loss: 0.9596208930015564 + }, + train: { + acc: 0.7694, + loss: 0.9731049537658691 + } + } + }, + params: { + 'params.yaml': { + epochs: 15, + lr: 0.003, + weight_decay: 0 + } + }, + selected: false, + sha: 'ae4100a4b4a972c3ceffa3062739845d944b3ddf', + starred: false + }, + parentId: '1', + subRows: [] + }, + { + columnFilters: {}, + columnFiltersMeta: {}, + depth: 0, + id: '2', + index: 2, + original: { + Created: '2023-04-17T00:50:06', + branch: 'current', + commit: { + author: 'Matt Seddon', + date: '4 days ago', + message: 'Update dependency dvclive to v2.6.4 (#75)\n\n', + tags: [] + }, + deps: { + data: { + changes: false, + value: 'ab3353d' + }, + 'train.py': { + changes: false, + value: 'f431663' + } + }, + description: 'Update dependency dvclive to v2.6.4 (#75)', + id: '48086f1', + label: '48086f1', + metrics: { + 'training/metrics.json': { + step: 14, + test: { + acc: 0.7735, + loss: 0.9596208930015564 + }, + train: { + acc: 0.7694, + loss: 0.9731049537658691 + } + } + }, + params: { + 'params.yaml': { + epochs: 15, + lr: 0.003, + weight_decay: 0 + } + }, + selected: false, + sha: '48086f1f70b2c535bafd830f7ce956355f6b78ec', + starred: false + }, + subRows: [] + }, + { + depth: 0, + id: '3', + index: 3, + original: { + Created: '2023-04-17T00:49:44', + branch: 'current', + commit: { + author: 'Matt Seddon', + date: '4 days ago', + message: 'Drop checkpoint: true (#74)\n\n', + tags: [] + }, + deps: { + data: { + changes: false, + value: 'ab3353d' + }, + 'train.py': { + changes: false, + value: 'f431663' + } + }, + description: 'Drop checkpoint: true (#74)', + id: '29ecaaf', + label: '29ecaaf', + metrics: { + 'training/metrics.json': { + step: 14, + test: { + acc: 0.7735, + loss: 0.9596208930015564 + }, + train: { + acc: 0.7694, + loss: 0.9731049537658691 + } + } + }, + params: { + 'params.yaml': { + epochs: 15, + lr: 0.003, + weight_decay: 0 + } + }, + selected: false, + sha: '29ecaaf3adf216045e96e81fb8e3027c9122af52', + starred: false + }, + subRows: [] + } + ], + rows: [ + { + depth: 0, + getAllCells: mockedGetAllCells, + getIsExpanded: mockedGetIsExpanded, + id: '0', + index: 0, + original: { + branch: 'current', + deps: { + data: { + changes: false, + value: 'ab3353d' + }, + 'train.py': { + changes: false, + value: 'f431663' + } + }, + id: 'workspace', + label: 'workspace', + metrics: { + 'training/metrics.json': { + step: 14, + test: { + acc: 0.7735, + loss: 0.9596208930015564 + }, + train: { + acc: 0.7694, + loss: 0.9731049537658691 + } + } + }, + params: { + 'params.yaml': { + epochs: 15, + lr: 0.003, + weight_decay: 0 + } + }, + selected: false, + starred: false + }, + subRows: [] + }, + { + depth: 0, + getAllCells: mockedGetAllCells, + getIsExpanded: mockedGetIsExpanded, + id: '1', + index: 1, + original: { + Created: '2023-04-20T05:14:46', + branch: 'current', + commit: { + author: 'Matt Seddon', + date: '31 hours ago', + message: 'Update dependency dvc to v2.55.0 (#76)\n\n', + tags: [] + }, + deps: { + data: { + changes: false, + value: 'ab3353d' + }, + 'train.py': { + changes: false, + value: 'f431663' + } + }, + description: 'Update dependency dvc to v2.55.0 (#76)', + id: 'a9b32d1', + label: 'a9b32d1', + metrics: { + 'training/metrics.json': { + step: 14, + test: { + acc: 0.7735, + loss: 0.9596208930015564 + }, + train: { + acc: 0.7694, + loss: 0.9731049537658691 + } + } + }, + params: { + 'params.yaml': { + epochs: 15, + lr: 0.003, + weight_decay: 0 + } + }, + selected: false, + sha: 'a9b32d14966b9be1396f2211d9eb743359708a07', + starred: false + }, + subRows: [] + }, + { + depth: 1, + getAllCells: mockedGetAllCells, + getIsExpanded: mockedGetIsExpanded, + id: '1.prize-luce', + index: 0, + original: { + Created: '2023-04-21T12:04:32', + branch: 'current', + deps: { + data: { + changes: false, + value: 'ab3353d' + }, + 'train.py': { + changes: false, + value: 'f431663' + } + }, + description: '[prize-luce]', + id: 'prize-luce', + label: 'ae4100a', + metrics: { + 'training/metrics.json': { + step: 14, + test: { + acc: 0.7735, + loss: 0.9596208930015564 + }, + train: { + acc: 0.7694, + loss: 0.9731049537658691 + } + } + }, + params: { + 'params.yaml': { + epochs: 15, + lr: 0.003, + weight_decay: 0 + } + }, + selected: false, + sha: 'ae4100a4b4a972c3ceffa3062739845d944b3ddf', + starred: false + }, + parentId: '1', + subRows: [] + }, + { + depth: 0, + getAllCells: mockedGetAllCells, + getIsExpanded: mockedGetIsExpanded, + id: '2', + index: 2, + original: { + Created: '2023-04-17T00:50:06', + branch: 'current', + commit: { + author: 'Matt Seddon', + date: '4 days ago', + message: 'Update dependency dvclive to v2.6.4 (#75)\n\n', + tags: [] + }, + deps: { + data: { + changes: false, + value: 'ab3353d' + }, + 'train.py': { + changes: false, + value: 'f431663' + } + }, + description: 'Update dependency dvclive to v2.6.4 (#75)', + id: '48086f1', + label: '48086f1', + metrics: { + 'training/metrics.json': { + step: 14, + test: { + acc: 0.7735, + loss: 0.9596208930015564 + }, + train: { + acc: 0.7694, + loss: 0.9731049537658691 + } + } + }, + params: { + 'params.yaml': { + epochs: 15, + lr: 0.003, + weight_decay: 0 + } + }, + selected: false, + sha: '48086f1f70b2c535bafd830f7ce956355f6b78ec', + starred: false + }, + subRows: [] + }, + { + depth: 0, + getAllCells: mockedGetAllCells, + getIsExpanded: mockedGetIsExpanded, + id: '3', + index: 3, + original: { + Created: '2023-04-17T00:49:44', + branch: 'current', + commit: { + author: 'Matt Seddon', + date: '4 days ago', + message: 'Drop checkpoint: true (#74)\n\n', + tags: [] + }, + deps: { + data: { + changes: false, + value: 'ab3353d' + }, + 'train.py': { + changes: false, + value: 'f431663' + } + }, + description: 'Drop checkpoint: true (#74)', + id: '29ecaaf', + label: '29ecaaf', + metrics: { + 'training/metrics.json': { + step: 14, + test: { + acc: 0.7735, + loss: 0.9596208930015564 + }, + train: { + acc: 0.7694, + loss: 0.9731049537658691 + } + } + }, + params: { + 'params.yaml': { + epochs: 15, + lr: 0.003, + weight_decay: 0 + } + }, + selected: false, + sha: '29ecaaf3adf216045e96e81fb8e3027c9122af52', + starred: false + }, + subRows: [] + } + ] + }) + } as unknown as Table + + const renderTableContent = ( + rowsInstance = instance, + branches = ['current'] + ) => { + return render( + + + +
+
+ ) + } + + 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', () => { + const instanceRows = instance.getRowModel() + const multipleBranchesInstance = { + ...instance, + getRowModel: () => ({ + flatRows: instanceRows.flatRows, + rows: [ + ...instanceRows.rows, + ...instanceRows.rows.map(row => ({ + ...row, + id: `${row.id}-new-branch`, + original: { ...row.original, branch: 'new-branch' } + })) + ] + }) + } as unknown as Table + renderTableContent(multipleBranchesInstance, ['current', 'new-branch']) + + expect(screen.getAllByTestId('branch-name').length).toBe(2) + expect(screen.getByText('current')).toBeInTheDocument() + expect(screen.getByText('new-branch')).toBeInTheDocument() + }) +}) diff --git a/webview/src/experiments/components/table/body/TableContent.tsx b/webview/src/experiments/components/table/body/TableContent.tsx index 0fd9d1b80b..b0b7a5631b 100644 --- a/webview/src/experiments/components/table/body/TableContent.tsx +++ b/webview/src/experiments/components/table/body/TableContent.tsx @@ -1,6 +1,8 @@ -import React, { RefObject, useCallback, useContext } from 'react' +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' import { InstanceProp, RowProp } from '../../../util/interfaces' @@ -17,12 +19,10 @@ export const TableContent: React.FC = ({ }) => { const { rows, flatRows } = instance.getRowModel() const { batchSelection, lastSelectedRow } = useContext(RowSelectionContext) - const hasCheckpoints = useSelector( - (state: ExperimentsState) => state.tableData.hasCheckpoints - ) - const hasRunningExperiment = useSelector( - (state: ExperimentsState) => state.tableData.hasRunningExperiment + const { hasCheckpoints, hasRunningExperiment, branches } = useSelector( + (state: ExperimentsState) => state.tableData ) + const batchRowSelection = useCallback( ({ row: { id } }: RowProp) => { const lastSelectedRowId = lastSelectedRow?.row.id ?? '' @@ -54,18 +54,39 @@ export const TableContent: React.FC = ({ return ( <> - {rows.map(row => ( - - ))} + {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) => { + const isFirstRow = + (branchIndex === 0 && i === 1) || (branchIndex !== 0 && i === 0) + return ( + + {isFirstRow && branches.length > 1 && ( + {branch} + )} + + + ) + })} + + + ) + })} ) } diff --git a/webview/src/experiments/components/table/body/WorkspaceRowGroup.tsx b/webview/src/experiments/components/table/body/WorkspaceRowGroup.tsx index ddb73732ca..a44fb0ccb7 100644 --- a/webview/src/experiments/components/table/body/WorkspaceRowGroup.tsx +++ b/webview/src/experiments/components/table/body/WorkspaceRowGroup.tsx @@ -1,15 +1,16 @@ import cx from 'classnames' -import React from 'react' +import React, { PropsWithChildren } from 'react' import { useInView } from 'react-intersection-observer' import { InstanceProp } from '../../../util/interfaces' import styles from '../styles.module.scss' +interface WorkspaceRowGroupProps extends InstanceProp { + root: HTMLElement | null + tableHeaderHeight: number +} + export const WorkspaceRowGroup: React.FC< - { - children: React.ReactNode - root: HTMLElement | null - tableHeaderHeight: number - } & InstanceProp + PropsWithChildren > = ({ children, root, tableHeaderHeight }) => { const [ref, needsShadow] = useInView({ root, diff --git a/webview/src/experiments/components/table/body/branchDivider/BranchDivider.tsx b/webview/src/experiments/components/table/body/branchDivider/BranchDivider.tsx new file mode 100644 index 0000000000..cc3d3deb77 --- /dev/null +++ b/webview/src/experiments/components/table/body/branchDivider/BranchDivider.tsx @@ -0,0 +1,23 @@ +import React, { PropsWithChildren } from 'react' +import styles from './styles.module.scss' +import tablesStyles from '../../styles.module.scss' +import { Icon } from '../../../../../shared/components/Icon' +import { GitMerge } from '../../../../../shared/components/icons' + +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 new file mode 100644 index 0000000000..bf47b5fd39 --- /dev/null +++ b/webview/src/experiments/components/table/body/branchDivider/styles.module.scss @@ -0,0 +1,12 @@ +@import '../../../../../shared/variables.scss'; + +.branchName { + padding: 0 20px 10px; + display: flex; + align-items: center; + gap: 4px; +} + +.icon { + fill: $accent-color; +} diff --git a/webview/src/experiments/components/table/commitsAndBranches/AddAndRemoveBranches.tsx b/webview/src/experiments/components/table/body/commitsAndBranches/AddAndRemoveBranches.tsx similarity index 70% rename from webview/src/experiments/components/table/commitsAndBranches/AddAndRemoveBranches.tsx rename to webview/src/experiments/components/table/body/commitsAndBranches/AddAndRemoveBranches.tsx index 1354c516ea..ae44cafb85 100644 --- a/webview/src/experiments/components/table/commitsAndBranches/AddAndRemoveBranches.tsx +++ b/webview/src/experiments/components/table/body/commitsAndBranches/AddAndRemoveBranches.tsx @@ -1,9 +1,9 @@ import React from 'react' import { useSelector } from 'react-redux' -import styles from '../styles.module.scss' -import { ExperimentsState } from '../../../store' -import { selectBranches } from '../../../util/messages' -import { featureFlag } from '../../../../util/flags' +import styles from './styles.module.scss' +import { ExperimentsState } from '../../../../store' +import { selectBranches } from '../../../../util/messages' +import { featureFlag } from '../../../../../util/flags' export const AddAndRemoveBranches: React.FC = () => { const { hasBranchesToSelect } = useSelector( diff --git a/webview/src/experiments/components/table/body/commitsAndBranches/CommitsAndBranchesNavigation.tsx b/webview/src/experiments/components/table/body/commitsAndBranches/CommitsAndBranchesNavigation.tsx new file mode 100644 index 0000000000..0c84a113a1 --- /dev/null +++ b/webview/src/experiments/components/table/body/commitsAndBranches/CommitsAndBranchesNavigation.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import { useSelector } from 'react-redux' +import { AddAndRemoveBranches } from './AddAndRemoveBranches' +import styles from './styles.module.scss' +import { + showLessCommits, + showMoreCommits, + switchToBranchesView, + switchToCommitsView +} from '../../../../util/messages' +import tableStyles from '../../styles.module.scss' +import { ExperimentsState } from '../../../../store' + +export const CommitsAndBranchesNavigation: React.FC = () => { + const { hasMoreCommits, isBranchesView, isShowingMoreCommits } = useSelector( + (state: ExperimentsState) => state.tableData + ) + + return ( + + + +
+ {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 new file mode 100644 index 0000000000..d95dc83462 --- /dev/null +++ b/webview/src/experiments/components/table/body/commitsAndBranches/styles.module.scss @@ -0,0 +1,28 @@ +@import '../../../../../shared/variables'; + +.commitsAndBranchesNav { + padding: 20px 16px; + width: 100px; + overflow: visible; +} + +.commitsAndBranchesNavButton { + background: transparent; + border: none; + color: $accent-color; + text-decoration: underline; + font-size: 0.6rem; + cursor: pointer; + + &:hover:not(:disabled) { + text-decoration: none; + } + + &:disabled { + opacity: 0.5; + } +} + +.separator::before { + content: '|'; +} diff --git a/webview/src/experiments/components/table/commitsAndBranches/CommitsAndBranchesNavigation.tsx b/webview/src/experiments/components/table/commitsAndBranches/CommitsAndBranchesNavigation.tsx deleted file mode 100644 index 0c76d7e25b..0000000000 --- a/webview/src/experiments/components/table/commitsAndBranches/CommitsAndBranchesNavigation.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react' -import { useSelector } from 'react-redux' -import { AddAndRemoveBranches } from './AddAndRemoveBranches' -import { - showLessCommits, - showMoreCommits, - switchToBranchesView, - switchToCommitsView -} from '../../../util/messages' -import styles from '../styles.module.scss' -import { ExperimentsState } from '../../../store' - -export const CommitsAndBranchesNavigation: React.FC = () => { - const { hasMoreCommits, isBranchesView, isShowingMoreCommits } = useSelector( - (state: ExperimentsState) => state.tableData - ) - - return ( -
- {hasMoreCommits && ( - - )} - {isShowingMoreCommits && ( - - )} - - - - - - -
- ) -} diff --git a/webview/src/experiments/components/table/styles.module.scss b/webview/src/experiments/components/table/styles.module.scss index a49f63ac55..41ff217fbb 100644 --- a/webview/src/experiments/components/table/styles.module.scss +++ b/webview/src/experiments/components/table/styles.module.scss @@ -476,10 +476,12 @@ $badge-size: 0.85rem; .previousCommitsText { font-size: 0.6rem; padding-left: 16px; + font-weight: normal; + text-align: left; } } -.rowGroup:last-child { +.lastRowGroup { & > .experimentsTr:last-child { border-color: $row-bg-color; @@ -834,28 +836,6 @@ $badge-size: 0.85rem; // below table styles -.commitsAndBranchesNav { - padding: 20px 16px; - width: 100%; -} - -.commitsAndBranchesNavButton { - background: transparent; - border: none; - color: $accent-color; - text-decoration: underline; - font-size: 0.6rem; - cursor: pointer; - - &:hover:not(:disabled) { - text-decoration: none; - } - - &:disabled { - opacity: 0.5; - } -} - .buttonAsLink { @extend %link; background: none; @@ -874,7 +854,3 @@ $badge-size: 0.85rem; font-family: var(--vscode-font-family); } } - -.separator::before { - content: '|'; -} diff --git a/webview/src/experiments/state/tableDataSlice.ts b/webview/src/experiments/state/tableDataSlice.ts index a338871fa2..c55c050139 100644 --- a/webview/src/experiments/state/tableDataSlice.ts +++ b/webview/src/experiments/state/tableDataSlice.ts @@ -10,9 +10,11 @@ import { keepReferenceIfEqual } from '../../util/objects' export interface TableDataState extends TableData { hasData?: boolean + branches: string[] } export const tableDataInitialState: TableDataState = { + branches: ['current'], changes: [], columnOrder: [], columnWidths: {}, @@ -103,6 +105,9 @@ export const tableDataSlice = createSlice({ state.rows, action.payload ) as Experiment[] + state.branches = [ + ...new Set(state.rows.map(row => row.branch)) + ] as string[] }, updateSelectedForPlotsCount: (state, action: PayloadAction) => { state.selectedForPlotsCount = action.payload diff --git a/webview/src/shared/components/icons/GitMerge.tsx b/webview/src/shared/components/icons/GitMerge.tsx new file mode 100644 index 0000000000..33462b3f61 --- /dev/null +++ b/webview/src/shared/components/icons/GitMerge.tsx @@ -0,0 +1,17 @@ +import * as React from 'react' +import { SVGProps } from 'react' + +const SvgGitMerge = (props: SVGProps) => ( + + + +) + +export default SvgGitMerge diff --git a/webview/src/shared/components/icons/index.ts b/webview/src/shared/components/icons/index.ts index 60499e9ca7..9583cc086a 100644 --- a/webview/src/shared/components/icons/index.ts +++ b/webview/src/shared/components/icons/index.ts @@ -12,6 +12,7 @@ export { default as Error } from './Error' export { default as Ellipsis } from './Ellipsis' export { default as Filter } from './Filter' export { default as GitCommit } from './GitCommit' +export { default as GitMerge } from './GitMerge' export { default as GraphScatter } from './GraphScatter' export { default as GraphLine } from './GraphLine' export { default as Gripper } from './Gripper' diff --git a/webview/src/stories/Icons.stories.tsx b/webview/src/stories/Icons.stories.tsx index bb5afba173..b7cf970314 100644 --- a/webview/src/stories/Icons.stories.tsx +++ b/webview/src/stories/Icons.stories.tsx @@ -19,6 +19,7 @@ import { Error, Filter, GitCommit, + GitMerge, GraphLine, GraphScatter, Gripper, @@ -83,6 +84,9 @@ const Template: Story = () => { + + + diff --git a/webview/src/stories/Table.stories.tsx b/webview/src/stories/Table.stories.tsx index c96095a6a0..4ae8d0593c 100644 --- a/webview/src/stories/Table.stories.tsx +++ b/webview/src/stories/Table.stories.tsx @@ -2,7 +2,7 @@ import { configureStore } from '@reduxjs/toolkit' import React from 'react' import { Provider } from 'react-redux' import { Meta, Story } from '@storybook/react/types-6-0' -import rowsFixture from 'dvc/src/test/fixtures/expShow/base/rows' +import { rowsFixtureWithBranches } from 'dvc/src/test/fixtures/expShow/base/rows' import columnsFixture from 'dvc/src/test/fixtures/expShow/base/columns' import workspaceChangesFixture from 'dvc/src/test/fixtures/expShow/base/workspaceChanges' import deeplyNestedTableData from 'dvc/src/test/fixtures/expShow/deeplyNested/tableData' @@ -14,6 +14,7 @@ import { ExperimentStatus, isRunning } from 'dvc/src/experiments/webview/contract' +import { EXPERIMENT_WORKSPACE_ID } from 'dvc/src/cli/dvc/contract' import { within, userEvent, @@ -22,7 +23,6 @@ import { } from '@storybook/testing-library' import { addCommitDataToMainBranch } from './util' import Experiments from '../experiments/components/Experiments' - import { experimentsReducers } from '../experiments/store' import { TableDataState } from '../experiments/state/tableDataSlice' import { NORMAL_TOOLTIP_DELAY } from '../shared/components/tooltip/Tooltip' @@ -30,8 +30,10 @@ import { setExperimentsAsSelected, setExperimentsAsStarred } from '../test/tableDataFixture' +import { featureFlag } from '../util/flags' const tableData: TableDataState = { + branches: ['current'], changes: workspaceChangesFixture, columnOrder: [], columnWidths: { @@ -50,10 +52,12 @@ const tableData: TableDataState = { hasValidDvcYaml: true, isBranchesView: false, isShowingMoreCommits: true, - rows: addCommitDataToMainBranch(rowsFixture).map(row => ({ + rows: addCommitDataToMainBranch(rowsFixtureWithBranches).map(row => ({ ...row, + branch: 'current', subRows: row.subRows?.map(experiment => ({ ...experiment, + branch: 'current', starred: experiment.starred || experiment.label === '42b8736' })) })), @@ -67,7 +71,7 @@ const tableData: TableDataState = { const noRunningExperiments = { ...tableData, hasRunningExperiment: false, - rows: addCommitDataToMainBranch(rowsFixture).map(row => ({ + rows: addCommitDataToMainBranch(rowsFixtureWithBranches).map(row => ({ ...row, status: ExperimentStatus.SUCCESS, subRows: row.subRows?.map(experiment => ({ @@ -82,7 +86,7 @@ const noRunningExperiments = { const noRunningExperimentsNoCheckpoints = { ...noRunningExperiments, hasCheckpoints: false, - rows: addCommitDataToMainBranch(rowsFixture).map(row => ({ + rows: addCommitDataToMainBranch(rowsFixtureWithBranches).map(row => ({ ...row, status: ExperimentStatus.SUCCESS, subRows: row.subRows?.map(experiment => ({ @@ -131,6 +135,7 @@ export const WithSurvivalData = Template.bind({}) WithSurvivalData.args = { tableData: { ...survivalTableData, + branches: ['current'], hasData: true, rows: addCommitDataToMainBranch(survivalTableData.rows) } @@ -142,9 +147,12 @@ const tableDataWithSomeSelectedExperiments = setExperimentsAsSelected( ['4fb124a', '42b8736', '1ba7bcd'] ) WithMiddleStates.args = { - tableData: setExperimentsAsStarred(tableDataWithSomeSelectedExperiments, [ - '1ba7bcd' - ]) + tableData: { + ...setExperimentsAsStarred(tableDataWithSomeSelectedExperiments, [ + '1ba7bcd' + ]), + branches: ['current'] + } } WithMiddleStates.play = async ({ canvasElement }) => { await within(canvasElement).findByText('4fb124a') @@ -194,6 +202,7 @@ export const WithAllDataTypes = Template.bind({}) WithAllDataTypes.args = { tableData: { ...dataTypesTableFixture, + branches: ['current'], hasData: true, rows: addCommitDataToMainBranch(dataTypesTableFixture.rows) } @@ -210,6 +219,7 @@ export const WithDeeplyNestedHeaders = Template.bind({}) WithDeeplyNestedHeaders.args = { tableData: { ...deeplyNestedTableData, + branches: ['current'], hasData: true, rows: addCommitDataToMainBranch(deeplyNestedTableData.rows) } @@ -220,7 +230,7 @@ LoadingData.args = { tableData: undefined } export const WithNoExperiments = Template.bind({}) WithNoExperiments.args = { - tableData: { ...tableData, rows: [rowsFixture[0]] } + tableData: { ...tableData, rows: [rowsFixtureWithBranches[0]] } } export const WithNoColumns = Template.bind({}) @@ -284,3 +294,43 @@ Scrolled.parameters = { } } } + +export const WithMultipleBranches = Template.bind({}) +const rowsWithoutWorkspace = survivalTableData.rows.filter( + row => row.id !== EXPERIMENT_WORKSPACE_ID +) +Object.assign(featureFlag, { ADD_REMOVE_BRANCHES: true }) +const branches = ['current', 'other-branch', 'branch-14786'] + +WithMultipleBranches.args = { + tableData: { + ...tableData, + branches, + rows: [ + ...survivalTableData.rows.map(row => ({ + ...row, + branch: branches[0], + subRows: row.subRows?.map(subRow => ({ + ...subRow, + branch: branches[0] + })) + })), + ...rowsWithoutWorkspace.map(row => ({ + ...row, + branch: branches[1], + subRows: row.subRows?.map(subRow => ({ + ...subRow, + branch: branches[1] + })) + })), + ...rowsWithoutWorkspace.map(row => ({ + ...row, + branch: branches[2], + subRows: row.subRows?.map(subRow => ({ + ...subRow, + branch: branches[2] + })) + })) + ] + } +} diff --git a/webview/src/stories/util.ts b/webview/src/stories/util.ts index 1112a41e26..6d02a464d4 100644 --- a/webview/src/stories/util.ts +++ b/webview/src/stories/util.ts @@ -82,5 +82,6 @@ export const addCommitDataToMainBranch = (rows: Commit[]) => date: '4 days ago' } } + row.branch = row.branch || 'current' return row }) diff --git a/webview/src/test/experimentsTable.tsx b/webview/src/test/experimentsTable.tsx index c3f556eac5..b1f0907179 100644 --- a/webview/src/test/experimentsTable.tsx +++ b/webview/src/test/experimentsTable.tsx @@ -38,6 +38,7 @@ export const renderTable = (data = tableDataFixture, noData?: boolean) => { queries: { ...queries, ...customQueries } } ) + !noData && setTableData(data) return renderedTable } diff --git a/webview/src/test/sort.ts b/webview/src/test/sort.ts index 2f64d34b61..7fd558788e 100644 --- a/webview/src/test/sort.ts +++ b/webview/src/test/sort.ts @@ -57,10 +57,12 @@ export const tableData: TableData = { isShowingMoreCommits: true, rows: [ { + branch: 'current', id: EXPERIMENT_WORKSPACE_ID, label: EXPERIMENT_WORKSPACE_ID }, { + branch: 'current', id: 'main', label: 'main' }