diff --git a/.codeclimate.json b/.codeclimate.json index 0cb09b2635..a190afe9db 100644 --- a/.codeclimate.json +++ b/.codeclimate.json @@ -8,20 +8,29 @@ "demo/", "extension/src/test/", "languageServer/src/test/", - "webview/src/shared/components/icons/" + "webview/src/shared/components/icons/", + "webview/src/test/mockRowModel.ts" ], "checks": { "file-lines": { - "config": { "threshold": 300 } + "config": { + "threshold": 300 + } + }, + "method-count": { + "config": { + "threshold": 30 + } }, - "method-count": { "config": { "threshold": 30 } }, "method-lines": { "config": { "threshold": 40 } }, "method-complexity": { - "config": { "threshold": 6 } + "config": { + "threshold": 6 + } } } } diff --git a/extension/src/experiments/columns/collect/order.ts b/extension/src/experiments/columns/collect/order.ts index d23adb6601..5f44934477 100644 --- a/extension/src/experiments/columns/collect/order.ts +++ b/extension/src/experiments/columns/collect/order.ts @@ -1,5 +1,9 @@ import { Column, ColumnType } from '../../webview/contract' -import { EXPERIMENT_COLUMN_ID } from '../constants' +import { + EXPERIMENT_COLUMN_ID, + BRANCH_COLUMN_ID, + COMMIT_COLUMN_ID +} from '../constants' export const collectColumnOrder = async ( existingColumnOrder: string[], @@ -25,6 +29,14 @@ export const collectColumnOrder = async ( existingColumnOrder.unshift(EXPERIMENT_COLUMN_ID) } + if (!existingColumnOrder.includes(BRANCH_COLUMN_ID)) { + existingColumnOrder.splice(1, 0, BRANCH_COLUMN_ID) + } + + if (!existingColumnOrder.includes(COMMIT_COLUMN_ID)) { + existingColumnOrder.splice(2, 0, COMMIT_COLUMN_ID) + } + return [ ...existingColumnOrder, ...acc.timestamp, diff --git a/extension/src/experiments/columns/constants.ts b/extension/src/experiments/columns/constants.ts index e41af45edd..c716d6f051 100644 --- a/extension/src/experiments/columns/constants.ts +++ b/extension/src/experiments/columns/constants.ts @@ -11,3 +11,12 @@ export const timestampColumn: Column = { } export const EXPERIMENT_COLUMN_ID = 'id' + +export const COMMIT_COLUMN_ID = 'commit' +export const BRANCH_COLUMN_ID = 'branch' + +export const DEFAULT_COLUMN_IDS = [ + EXPERIMENT_COLUMN_ID, + BRANCH_COLUMN_ID, + COMMIT_COLUMN_ID +] diff --git a/extension/src/experiments/columns/model.ts b/extension/src/experiments/columns/model.ts index 8808067bec..9fd901da76 100644 --- a/extension/src/experiments/columns/model.ts +++ b/extension/src/experiments/columns/model.ts @@ -12,6 +12,11 @@ import { limitSummaryOrder } from './util' import { collectColumnOrder } from './collect/order' +import { + BRANCH_COLUMN_ID, + COMMIT_COLUMN_ID, + DEFAULT_COLUMN_IDS +} from './constants' import { Column, ColumnType } from '../webview/contract' import { ExpShowOutput } from '../../cli/dvc/contract' import { PersistenceKey } from '../../persistence/constants' @@ -104,10 +109,10 @@ export class ColumnsModel extends PathSelectionModel { public selectFirst(firstColumns: string[]) { const columnOrder = [ - 'id', + ...DEFAULT_COLUMN_IDS, ...firstColumns, ...this.getColumnOrder().filter( - column => !['id', ...firstColumns].includes(column) + column => ![...DEFAULT_COLUMN_IDS, ...firstColumns].includes(column) ) ] this.setColumnOrder(columnOrder) @@ -201,6 +206,13 @@ export class ColumnsModel extends PathSelectionModel { return this.setColumnOrderFromData(selectedColumns) } } + + const maybeMissingDefaultColumns = [COMMIT_COLUMN_ID, BRANCH_COLUMN_ID] + for (const id of maybeMissingDefaultColumns) { + if (!this.columnOrderState.includes(id)) { + return this.setColumnOrderFromData(selectedColumns) + } + } } private transformAndSetChanges(data: ExpShowOutput) { diff --git a/extension/src/experiments/model/index.ts b/extension/src/experiments/model/index.ts index c086520487..ac70b7ab28 100644 --- a/extension/src/experiments/model/index.ts +++ b/extension/src/experiments/model/index.ts @@ -438,11 +438,19 @@ export class ExperimentsModel extends ModelWithPersistence { } public getRowData() { - const commitsBySha = this.applyFiltersToCommits() + const workspaceRow = { + branch: WORKSPACE_BRANCH, + ...this.addDetails(this.workspace) + } + const sorts = this.getSorts() + const flattenRowData = sorts.length > 0 + if (flattenRowData) { + return this.getFlattenedRowData(workspaceRow) + } + + const commitsBySha: { [sha: string]: Commit } = this.applyFiltersToCommits() + const rows: Commit[] = [workspaceRow] - const rows: Commit[] = [ - { branch: WORKSPACE_BRANCH, ...this.addDetails(this.workspace) } - ] for (const { branch, sha } of this.rowOrder) { const commit = commitsBySha[sha] if (!commit) { @@ -829,4 +837,42 @@ export class ExperimentsModel extends ModelWithPersistence { } return commitsBySha } + + private applyFiltersToFlattenedCommits() { + const commitsBySha: { [sha: string]: Commit[] } = {} + const filters = this.getFilters() + + for (const commit of this.commits) { + const commitWithSelectedAndStarred = this.addDetails(commit) + const experiments = this.getExperimentsByCommit( + commitWithSelectedAndStarred + ) + + commitsBySha[commit.sha as string] = [ + commitWithSelectedAndStarred, + ...(experiments || []) + ].filter(exp => !!filterExperiment(filters, exp)) + } + + return commitsBySha + } + + private getFlattenedRowData(workspaceRow: Commit): Commit[] { + const commitsBySha: { [sha: string]: Commit[] } = + this.applyFiltersToFlattenedCommits() + const rows = [] + + for (const { branch, sha } of this.rowOrder) { + const commitsAndExps = commitsBySha[sha] + if (!commitsAndExps) { + continue + } + + rows.push( + ...commitsAndExps.map(commitOrExp => ({ ...commitOrExp, branch })) + ) + } + + return [workspaceRow, ...sortExperiments(this.getSorts(), rows)] + } } diff --git a/extension/src/test/fixtures/expShow/base/columns.ts b/extension/src/test/fixtures/expShow/base/columns.ts index 6ffda369b1..93a882953b 100644 --- a/extension/src/test/fixtures/expShow/base/columns.ts +++ b/extension/src/test/fixtures/expShow/base/columns.ts @@ -10,6 +10,8 @@ const nestedParamsFile = join('nested', 'params.yaml') export const dataColumnOrder: string[] = [ 'id', + 'branch', + 'commit', 'Created', 'metrics:summary.json:accuracy', 'metrics:summary.json:loss', diff --git a/extension/src/test/fixtures/expShow/sorted/columns.ts b/extension/src/test/fixtures/expShow/sorted/columns.ts new file mode 100644 index 0000000000..58b940ba5b --- /dev/null +++ b/extension/src/test/fixtures/expShow/sorted/columns.ts @@ -0,0 +1,199 @@ +import { join } from 'path' +import { Column, ColumnType } from '../../../../experiments/webview/contract' +import { buildMetricOrParamPath } from '../../../../experiments/columns/paths' +import { timestampColumn } from '../../../../experiments/columns/constants' + +const nestedParamsFile = join('nested', 'params.yaml') + +const data: Column[] = [ + timestampColumn, + { + type: ColumnType.METRICS, + hasChildren: true, + label: 'summary.json', + parentPath: buildMetricOrParamPath(ColumnType.METRICS), + path: buildMetricOrParamPath(ColumnType.METRICS, 'summary.json') + }, + { + type: ColumnType.METRICS, + hasChildren: false, + label: 'loss', + parentPath: buildMetricOrParamPath(ColumnType.METRICS, 'summary.json'), + path: buildMetricOrParamPath(ColumnType.METRICS, 'summary.json', 'loss'), + pathArray: [ColumnType.METRICS, 'summary.json', 'loss'], + firstValueType: 'number' + }, + { + type: ColumnType.METRICS, + hasChildren: false, + label: 'accuracy', + parentPath: buildMetricOrParamPath(ColumnType.METRICS, 'summary.json'), + path: buildMetricOrParamPath( + ColumnType.METRICS, + 'summary.json', + 'accuracy' + ), + pathArray: [ColumnType.METRICS, 'summary.json', 'accuracy'], + firstValueType: 'number' + }, + { + type: ColumnType.METRICS, + hasChildren: false, + label: 'val_loss', + parentPath: buildMetricOrParamPath(ColumnType.METRICS, 'summary.json'), + path: buildMetricOrParamPath( + ColumnType.METRICS, + 'summary.json', + 'val_loss' + ), + pathArray: [ColumnType.METRICS, 'summary.json', 'val_loss'], + firstValueType: 'number' + }, + { + type: ColumnType.METRICS, + hasChildren: false, + label: 'val_accuracy', + parentPath: buildMetricOrParamPath(ColumnType.METRICS, 'summary.json'), + path: buildMetricOrParamPath( + ColumnType.METRICS, + 'summary.json', + 'val_accuracy' + ), + pathArray: [ColumnType.METRICS, 'summary.json', 'val_accuracy'], + firstValueType: 'number' + }, + { + type: ColumnType.PARAMS, + hasChildren: true, + label: 'params.yaml', + parentPath: ColumnType.PARAMS, + path: buildMetricOrParamPath(ColumnType.PARAMS, 'params.yaml') + }, + { + type: ColumnType.PARAMS, + hasChildren: false, + label: 'code_names', + parentPath: buildMetricOrParamPath(ColumnType.PARAMS, 'params.yaml'), + path: buildMetricOrParamPath( + ColumnType.PARAMS, + 'params.yaml', + 'code_names' + ), + pathArray: [ColumnType.PARAMS, 'params.yaml', 'code_names'], + firstValueType: 'array' + }, + { + type: ColumnType.PARAMS, + hasChildren: false, + label: 'epochs', + parentPath: buildMetricOrParamPath(ColumnType.PARAMS, 'params.yaml'), + path: buildMetricOrParamPath(ColumnType.PARAMS, 'params.yaml', 'epochs'), + pathArray: [ColumnType.PARAMS, 'params.yaml', 'epochs'], + firstValueType: 'number' + }, + { + type: ColumnType.PARAMS, + hasChildren: false, + label: 'learning_rate', + parentPath: buildMetricOrParamPath(ColumnType.PARAMS, 'params.yaml'), + path: buildMetricOrParamPath( + ColumnType.PARAMS, + 'params.yaml', + 'learning_rate' + ), + pathArray: [ColumnType.PARAMS, 'params.yaml', 'learning_rate'], + firstValueType: 'number' + }, + { + type: ColumnType.PARAMS, + hasChildren: false, + label: 'dvc_logs_dir', + parentPath: buildMetricOrParamPath(ColumnType.PARAMS, 'params.yaml'), + path: buildMetricOrParamPath( + ColumnType.PARAMS, + 'params.yaml', + 'dvc_logs_dir' + ), + pathArray: [ColumnType.PARAMS, 'params.yaml', 'dvc_logs_dir'], + firstValueType: 'string' + }, + { + type: ColumnType.PARAMS, + hasChildren: false, + label: 'log_file', + parentPath: buildMetricOrParamPath(ColumnType.PARAMS, 'params.yaml'), + path: buildMetricOrParamPath(ColumnType.PARAMS, 'params.yaml', 'log_file'), + pathArray: [ColumnType.PARAMS, 'params.yaml', 'log_file'], + firstValueType: 'string' + }, + { + type: ColumnType.PARAMS, + hasChildren: false, + label: 'dropout', + parentPath: buildMetricOrParamPath(ColumnType.PARAMS, 'params.yaml'), + path: buildMetricOrParamPath(ColumnType.PARAMS, 'params.yaml', 'dropout'), + pathArray: [ColumnType.PARAMS, 'params.yaml', 'dropout'], + firstValueType: 'number' + }, + { + type: ColumnType.PARAMS, + hasChildren: true, + label: 'process', + parentPath: buildMetricOrParamPath(ColumnType.PARAMS, 'params.yaml'), + path: buildMetricOrParamPath(ColumnType.PARAMS, 'params.yaml', 'process') + }, + { + type: ColumnType.PARAMS, + hasChildren: false, + label: 'threshold', + parentPath: buildMetricOrParamPath( + ColumnType.PARAMS, + 'params.yaml', + 'process' + ), + path: buildMetricOrParamPath( + ColumnType.PARAMS, + 'params.yaml', + 'process', + 'threshold' + ), + pathArray: [ColumnType.PARAMS, 'params.yaml', 'process', 'threshold'], + firstValueType: 'number' + }, + { + type: ColumnType.PARAMS, + hasChildren: true, + label: nestedParamsFile, + parentPath: ColumnType.PARAMS, + path: buildMetricOrParamPath(ColumnType.PARAMS, nestedParamsFile) + }, + { + type: ColumnType.PARAMS, + hasChildren: false, + label: 'test', + parentPath: buildMetricOrParamPath(ColumnType.PARAMS, nestedParamsFile), + path: buildMetricOrParamPath(ColumnType.PARAMS, nestedParamsFile, 'test'), + pathArray: [ColumnType.PARAMS, nestedParamsFile, 'test'], + firstValueType: 'boolean' + }, + { + type: ColumnType.PARAMS, + hasChildren: false, + label: 'test_arg', + parentPath: buildMetricOrParamPath( + ColumnType.PARAMS, + 'params.yaml', + 'process' + ), + path: buildMetricOrParamPath( + ColumnType.PARAMS, + 'params.yaml', + 'process', + 'test_arg' + ), + pathArray: [ColumnType.PARAMS, 'params.yaml', 'process', 'test_arg'], + firstValueType: 'string' + } +] + +export default data diff --git a/extension/src/test/fixtures/expShow/sorted/rows.ts b/extension/src/test/fixtures/expShow/sorted/rows.ts new file mode 100644 index 0000000000..0a3ecc5f20 --- /dev/null +++ b/extension/src/test/fixtures/expShow/sorted/rows.ts @@ -0,0 +1,348 @@ +import { join } from '../../../util/path' +import { + Commit, + GitRemoteStatus, + StudioLinkType, + WORKSPACE_BRANCH +} from '../../../../experiments/webview/contract' +import { + ExecutorStatus, + EXPERIMENT_WORKSPACE_ID, + Executor +} from '../../../../cli/dvc/contract' + +const rowsFixture: Commit[] = [ + { + branch: WORKSPACE_BRANCH, + commit: undefined, + description: undefined, + displayColor: undefined, + executor: Executor.WORKSPACE, + id: EXPERIMENT_WORKSPACE_ID, + label: EXPERIMENT_WORKSPACE_ID, + metrics: { + 'summary.json': { + loss: 1.775016188621521, + accuracy: 0.5926499962806702, + val_loss: 1.7233840227127075, + val_accuracy: 0.6704000234603882 + } + }, + params: { + 'params.yaml': { + code_names: [0, 1], + epochs: 5, + learning_rate: 2.1e-7, + dvc_logs_dir: 'dvc_logs', + log_file: 'logs.csv', + dropout: 0.124, + process: { threshold: 0.85 } + }, + [join('nested', 'params.yaml')]: { + test: true + } + }, + executorStatus: ExecutorStatus.RUNNING, + selected: false, + starred: false + }, + { + branch: 'main', + commit: { + author: 'github-actions[bot]', + date: '6 hours ago', + message: + 'Update version and CHANGELOG for release (#4022)\n\nCo-authored-by: Olivaw[bot] ', + tags: ['0.9.3'] + }, + description: 'Update version and CHANGELOG for release (#4022) ...', + displayColor: undefined, + id: 'main', + label: 'main', + metrics: { + 'summary.json': { + loss: 2.048856019973755, + accuracy: 0.3484833240509033, + val_loss: 1.9979369640350342, + val_accuracy: 0.4277999997138977 + } + }, + params: { + 'params.yaml': { + code_names: [0, 1], + epochs: 5, + learning_rate: 2.1e-7, + dvc_logs_dir: 'dvc_logs', + log_file: 'logs.csv', + dropout: 0.122, + process: { threshold: 0.86, test_arg: 'string' } + }, + [join('nested', 'params.yaml')]: { + test: true + } + }, + selected: false, + sha: '53c3851f46955fa3e2b8f6e1c52999acc8c9ea77', + starred: false, + Created: '2020-11-21T19:58:22' + }, + { + branch: 'main', + baselineSha: '53c3851f46955fa3e2b8f6e1c52999acc8c9ea77', + displayColor: undefined, + description: '[exp-83425]', + executor: Executor.WORKSPACE, + id: 'exp-83425', + label: EXPERIMENT_WORKSPACE_ID, + metrics: { + 'summary.json': { + loss: 1.775016188621521, + accuracy: 0.5926499962806702, + val_loss: 1.7233840227127075, + val_accuracy: 0.6704000234603882 + } + }, + params: { + 'params.yaml': { + code_names: [0, 1], + epochs: 5, + learning_rate: 2.1e-7, + dvc_logs_dir: 'dvc_logs', + log_file: 'logs.csv', + dropout: 0.124, + process: { threshold: 0.85 } + }, + [join('nested', 'params.yaml')]: { + test: true + } + }, + selected: true, + starred: false, + executorStatus: ExecutorStatus.RUNNING, + Created: '2020-12-29T15:27:02' + }, + { + branch: 'main', + baselineSha: '53c3851f46955fa3e2b8f6e1c52999acc8c9ea77', + displayColor: undefined, + description: '[exp-f13bca]', + id: 'exp-f13bca', + error: "unable to read: 'summary.json', JSON file structure is corrupted", + label: 'f0f9186', + metrics: {}, + params: { + 'params.yaml': { + code_names: [0, 1], + epochs: 5, + learning_rate: 2.1e-7, + dvc_logs_dir: 'dvc_logs', + log_file: 'logs.csv', + dropout: 0.124, + process: { threshold: 0.85 } + }, + [join('nested', 'params.yaml')]: { + test: true + } + }, + gitRemoteStatus: GitRemoteStatus.NOT_ON_REMOTE, + selected: false, + sha: 'f0f918662b4f8c47819ca154a23029bf9b47d4f3', + starred: false, + Created: '2020-12-29T15:26:36' + }, + { + branch: 'main', + baselineSha: '53c3851f46955fa3e2b8f6e1c52999acc8c9ea77', + displayColor: undefined, + error: 'Experiment run failed.', + id: '55d492c', + label: '55d492c', + metrics: {}, + params: { + 'params.yaml': { + code_names: [0, 2], + epochs: 5, + learning_rate: 2.1e-7, + dvc_logs_dir: 'dvc_logs', + log_file: 'logs.csv', + dropout: 0.125, + process: { threshold: 0.85 } + }, + [join('nested', 'params.yaml')]: { + test: true + } + }, + selected: false, + executorStatus: ExecutorStatus.FAILED, + sha: '55d492c9c633912685351b32df91bfe1f9ecefb9', + starred: false, + Created: '2020-12-29T15:25:27' + }, + { + branch: 'main', + commit: { + author: 'Julie G', + date: '6 hours ago', + message: + 'Improve "Get Started" walkthrough (#4020)\n\n* don\'t show walkthrough in sidebar welcome section\n* move admonition in command palette walkthrough step', + tags: [] + }, + description: 'Improve "Get Started" walkthrough (#4020) ...', + displayColor: undefined, + id: 'fe2919b', + label: 'fe2919b', + metrics: { + 'summary.json': { + loss: 2.048856019973755, + accuracy: 0.3484833240509033, + val_loss: 1.9979369640350342, + val_accuracy: 0.4277999997138977 + } + }, + params: { + 'params.yaml': { + code_names: [0, 1], + epochs: 5, + learning_rate: 2.1e-7, + dvc_logs_dir: 'dvc_logs', + log_file: 'logs.csv', + dropout: 0.122, + process: { threshold: 0.86, test_arg: 'string' } + }, + [join('nested', 'params.yaml')]: { + test: true + } + }, + selected: false, + sha: 'fe2919bb4394b30494bea905c253e10077b9a1bd', + starred: false, + Created: '2020-11-21T19:58:22' + }, + { + branch: 'main', + commit: { + author: 'Matt Seddon', + date: '8 hours ago', + message: + 'Add capabilities to text mentioning storage provider extensions (#4015)\n', + tags: [] + }, + description: + 'Add capabilities to text mentioning storage provider extensions (#4015)', + displayColor: undefined, + id: '7df876c', + label: '7df876c', + metrics: { + 'summary.json': { + loss: 2.048856019973755, + accuracy: 0.3484833240509033, + val_loss: 1.9979369640350342, + val_accuracy: 0.4277999997138977 + } + }, + params: { + 'params.yaml': { + code_names: [0, 1], + epochs: 5, + learning_rate: 2.1e-7, + dvc_logs_dir: 'dvc_logs', + log_file: 'logs.csv', + dropout: 0.122, + process: { threshold: 0.86, test_arg: 'string' } + }, + [join('nested', 'params.yaml')]: { + test: true + } + }, + selected: false, + sha: '7df876cb5147800cd3e489d563bc6dcd67188621', + starred: false, + Created: '2020-11-21T19:58:22' + }, + { + branch: 'main', + baselineSha: '53c3851f46955fa3e2b8f6e1c52999acc8c9ea77', + displayColor: undefined, + description: '[exp-e7a67]', + executor: Executor.DVC_TASK, + id: 'exp-e7a67', + label: '4fb124a', + metrics: { + 'summary.json': { + loss: 2.0205044746398926, + accuracy: 0.3724166750907898, + val_loss: 1.9979370832443237, + val_accuracy: 0.4277999997138977 + } + }, + params: { + 'params.yaml': { + code_names: [0, 1], + epochs: 2, + learning_rate: 2e-12, + dvc_logs_dir: 'dvc_logs', + log_file: 'logs.csv', + dropout: 0.15, + process: { threshold: 0.86, test_arg: 3 } + }, + [join('nested', 'params.yaml')]: { + test: true + } + }, + executorStatus: ExecutorStatus.RUNNING, + selected: false, + sha: '4fb124aebddb2adf1545030907687fa9a4c80e70', + starred: false, + Created: '2020-12-29T15:31:52' + }, + { + branch: 'main', + baselineSha: '53c3851f46955fa3e2b8f6e1c52999acc8c9ea77', + displayColor: undefined, + description: '[test-branch]', + id: 'test-branch', + label: '42b8736', + metrics: { + 'summary.json': { + loss: 1.9293040037155151, + accuracy: 0.4668000042438507, + val_loss: 1.8770883083343506, + val_accuracy: 0.5608000159263611 + } + }, + params: { + 'params.yaml': { + code_names: [0, 1], + epochs: 2, + learning_rate: 2.2e-7, + dvc_logs_dir: 'dvc_logs', + log_file: 'logs.csv', + dropout: 0.122, + process: { threshold: 0.86, test_arg: 'string' } + }, + [join('nested', 'params.yaml')]: { + test: true + } + }, + gitRemoteStatus: GitRemoteStatus.ON_REMOTE, + studioLinkType: StudioLinkType.PUSHED, + selected: false, + sha: '42b8736b08170529903cd203a1f40382a4b4a8cd', + starred: false, + Created: '2020-12-29T15:28:59' + }, + { + branch: 'main', + baselineSha: '53c3851f46955fa3e2b8f6e1c52999acc8c9ea77', + displayColor: undefined, + id: '489fd8b', + sha: '489fd8bdaa709f7330aac342e051a9431c625481', + label: '489fd8b', + error: "unable to read: 'params.yaml', YAML file structure is corrupted", + selected: false, + starred: false, + executorStatus: ExecutorStatus.FAILED + } +] + +export default rowsFixture diff --git a/extension/src/test/fixtures/expShow/sorted/tableData.ts b/extension/src/test/fixtures/expShow/sorted/tableData.ts new file mode 100644 index 0000000000..97b1035cc3 --- /dev/null +++ b/extension/src/test/fixtures/expShow/sorted/tableData.ts @@ -0,0 +1,19 @@ +import columns from './columns' +import rows from './rows' +import defaultData from '../base/tableData' +import { TableData } from '../../../../experiments/webview/contract' + +const data: TableData = { + ...defaultData, + columns, + rows, + selectedForPlotsCount: 0, + sorts: [ + { + path: 'params:params.yaml:epochs', + descending: true + } + ] +} + +export default data diff --git a/extension/src/test/suite/experiments/columns/tree.test.ts b/extension/src/test/suite/experiments/columns/tree.test.ts index 13ded49a0f..448e6b9fa1 100644 --- a/extension/src/test/suite/experiments/columns/tree.test.ts +++ b/extension/src/test/suite/experiments/columns/tree.test.ts @@ -363,8 +363,9 @@ suite('Experiments Columns Tree Test Suite', () => { const firstColumns = [] const otherColumns = [] + const defaultColumns = ['id', 'branch', 'commit'] for (const column of columnsOrder) { - if (column === 'id') { + if (defaultColumns.includes(column)) { continue } if ( @@ -410,7 +411,7 @@ suite('Experiments Columns Tree Test Suite', () => { ]) expect(columnsModel.getColumnOrder()).to.deep.equal([ - 'id', + ...defaultColumns, ...firstColumns, ...otherColumns ]) diff --git a/extension/src/test/suite/experiments/index.test.ts b/extension/src/test/suite/experiments/index.test.ts index 6c8c749023..35885c7c85 100644 --- a/extension/src/test/suite/experiments/index.test.ts +++ b/extension/src/test/suite/experiments/index.test.ts @@ -1096,9 +1096,12 @@ suite('Experiments Test Suite', () => { }) await messageSent - const [id, firstColumn] = messageSpy.lastCall.args[0].columnOrder + const [id, branch, commit, firstColumn] = + messageSpy.lastCall.args[0].columnOrder expect(id).to.equal('id') + expect(commit).to.equal('commit') + expect(branch).to.equal('branch') expect(firstColumn).to.equal(movedColumn) }).timeout(WEBVIEW_TEST_TIMEOUT) @@ -1126,8 +1129,11 @@ suite('Experiments Test Suite', () => { expect(paramsYamlColumns).to.be.greaterThan(6) - const [id, ...columns] = messageSpy.lastCall.args[0].columnOrder + const [id, branch, commit, ...columns] = + messageSpy.lastCall.args[0].columnOrder expect(id).to.equal('id') + expect(branch).to.equal('branch') + expect(commit).to.equal('commit') let params = 0 let other = 0 @@ -1720,60 +1726,63 @@ suite('Experiments Test Suite', () => { }) describe('Sorting', () => { - it('should be able to sort', async () => { - const { experiments, messageSpy } = await buildExperimentsWebview({ - disposer: disposable, - availableNbCommits: { main: 20 }, - expShow: generateTestExpShowOutput( - {}, + const mockExpShowOutput = generateTestExpShowOutput( + {}, + { + rev: '2d879497587b80b2d9e61f072d9dbe9c07a65357', + experiments: [ { - rev: '2d879497587b80b2d9e61f072d9dbe9c07a65357', - experiments: [ - { - params: { - 'params.yaml': { - data: { - test: 2 - } - } + params: { + 'params.yaml': { + data: { + test: 2 } - }, - { - params: { - 'params.yaml': { - data: { - test: 1 - } - } + } + } + }, + { + params: { + 'params.yaml': { + data: { + test: 1 } - }, - { - params: { - 'params.yaml': { - data: { - test: 3 - } - } + } + } + }, + { + params: { + 'params.yaml': { + data: { + test: 3 } } - ] + } } - ), + ], + data: { params: { 'params.yaml': { data: { test: 5 } } } } + } + ) + + const getIds = (rows: Commit[]) => + rows.map(({ id, subRows }) => { + const data: { id: string; subRows?: string[] } = { id } + + if (subRows) { + data.subRows = subRows.map(({ id }) => id) + } + return data + }) + + it('should be able to flatten the table rows and sort', async () => { + const { experiments, messageSpy } = await buildExperimentsWebview({ + disposer: disposable, + availableNbCommits: { main: 20 }, + expShow: mockExpShowOutput, rowOrder: [ { sha: '2d879497587b80b2d9e61f072d9dbe9c07a65357', branch: 'main' } ] }) - const getIds = (rows: Commit[]) => - rows.map(({ id, subRows }) => { - const data: { id: string; subRows?: string[] } = { id } - - if (subRows) { - data.subRows = subRows.map(({ id }) => id) - } - return data - }) - const { rows, sorts: noSorts } = messageSpy.lastCall.args[0] expect(getIds(rows)).to.deep.equal([ @@ -1816,13 +1825,85 @@ suite('Experiments Test Suite', () => { expect(getIds(sortedRows)).to.deep.equal([ { id: EXPERIMENT_WORKSPACE_ID }, { - id: '2d879497587b80b2d9e61f072d9dbe9c07a65357', - subRows: ['exp-2', 'exp-1', 'exp-3'] + id: 'exp-2' + }, + { + id: 'exp-1' + }, + { + id: 'exp-3' + }, + { + id: '2d879497587b80b2d9e61f072d9dbe9c07a65357' } ]) expect(sorts).to.deep.equal([{ descending: false, path: sortPath }]) }).timeout(WEBVIEW_TEST_TIMEOUT) + + it('should be able to filter out parent commit rows when sorted', async () => { + const { experiments, experimentsModel, messageSpy } = + await buildExperimentsWebview({ + disposer: disposable, + availableNbCommits: { main: 20 }, + expShow: mockExpShowOutput, + rowOrder: [ + { sha: '2d879497587b80b2d9e61f072d9dbe9c07a65357', branch: 'main' } + ] + }) + + const { rows, sorts: noSorts } = messageSpy.lastCall.args[0] + + expect(getIds(rows)).to.deep.equal([ + { id: EXPERIMENT_WORKSPACE_ID }, + { + id: '2d879497587b80b2d9e61f072d9dbe9c07a65357', + subRows: ['exp-1', 'exp-2', 'exp-3'] + } + ]) + + expect(noSorts).to.deep.equal([]) + + const paramPath = buildMetricOrParamPath( + ColumnType.PARAMS, + 'params.yaml', + 'test' + ) + + stub(experimentsModel, 'getFilters').returns([ + { + operator: Operator.LESS_THAN, + path: paramPath, + value: 4 + } + ]) + stub(SortQuickPicks, 'pickSortToAdd') + .onFirstCall() + .resolves({ descending: true, path: paramPath }) + + messageSpy.resetHistory() + const messageSent = waitForSpyCall(messageSpy, messageSpy.callCount) + + await experiments.addSort() + await messageSent + + const { rows: sortedRows, sorts } = messageSpy.lastCall.args[0] + + expect(getIds(sortedRows)).to.deep.equal([ + { id: EXPERIMENT_WORKSPACE_ID }, + { + id: 'exp-3' + }, + { + id: 'exp-1' + }, + { + id: 'exp-2' + } + ]) + + expect(sorts).to.deep.equal([{ descending: true, path: paramPath }]) + }).timeout(WEBVIEW_TEST_TIMEOUT) }) describe('persisted state', () => { diff --git a/extension/src/test/suite/experiments/model/sortBy/tree.test.ts b/extension/src/test/suite/experiments/model/sortBy/tree.test.ts index e191844099..e36b7c4bbc 100644 --- a/extension/src/test/suite/experiments/model/sortBy/tree.test.ts +++ b/extension/src/test/suite/experiments/model/sortBy/tree.test.ts @@ -95,6 +95,7 @@ suite('Experiments Sort By Tree Test Suite', () => { return closeAllEditors() }) + // eslint-disable-next-line sonarjs/cognitive-complexity describe('ExperimentsSortByTree', () => { it('should appear in the UI', async () => { await expect( @@ -161,6 +162,21 @@ suite('Experiments Sort By Tree Test Suite', () => { get(exp, selector) ) + const getSortedParamsArray = (selector = testParamPathArray) => { + const rows = messageSpy.getCall(-1).firstArg.rows + const params = [] + + for (const row of rows) { + const param = get(row, selector) + + if (param) { + params.push(param) + } + } + + return params + } + stub(WorkspaceExperiments.prototype, 'getDvcRoots').returns([dvcDemoPath]) stub(WorkspaceExperiments.prototype, 'getOnlyOrPickProject').resolves( dvcDemoPath @@ -176,9 +192,10 @@ suite('Experiments Sort By Tree Test Suite', () => { ) await tableChangedPromise mockShowQuickPick.reset() - expect(getParamsArray(), 'single sort with table command').to.deep.equal([ - 1, 2, 3, 4 - ]) + expect( + getSortedParamsArray(), + 'single sort with table command' + ).to.deep.equal([1, 2, 3, 4]) const tableSortRemoved = experimentsUpdatedEvent(experiments) @@ -193,7 +210,7 @@ suite('Experiments Sort By Tree Test Suite', () => { await addSortWithMocks(otherTestParamPath, false) expect( - getParamsArray(), + getSortedParamsArray(), `row order is maintained after applying a sort on ${otherTestParamPath}` ).to.deep.equal([1, 3, 2, 4]) @@ -212,7 +229,7 @@ suite('Experiments Sort By Tree Test Suite', () => { } ]) expect( - getParamsArray(), + getSortedParamsArray(), 'the result of both sorts is sent to the webview' ).to.deep.equal([3, 1, 4, 2]) @@ -231,7 +248,7 @@ suite('Experiments Sort By Tree Test Suite', () => { } ]) expect( - getParamsArray(), + getSortedParamsArray(), 'the result of the switched sort is sent to the webview' ).to.deep.equal([4, 2, 3, 1]) @@ -243,7 +260,7 @@ suite('Experiments Sort By Tree Test Suite', () => { } ) expect( - getParamsArray(), + getSortedParamsArray(), 'when removing a sort that changes the order of ties, those ties should reflect their original order' ).to.deep.equal([2, 4, 1, 3]) diff --git a/webview/src/experiments/components/App.test.tsx b/webview/src/experiments/components/App.test.tsx index f9a7fbd80f..51f9254c03 100644 --- a/webview/src/experiments/components/App.test.tsx +++ b/webview/src/experiments/components/App.test.tsx @@ -8,6 +8,7 @@ import { } from '@testing-library/react' import '@testing-library/jest-dom' import tableDataFixture from 'dvc/src/test/fixtures/expShow/base/tableData' +import sortedTableData from 'dvc/src/test/fixtures/expShow/sorted/tableData' import { MessageFromWebviewType } from 'dvc/src/webview/contract' import { Column, @@ -26,7 +27,7 @@ import { commonColumnFields, expectHeaders, getHeaders, - tableData as sortingTableDataFixture + tableData as simplifiedSortedTableDataFixture } from '../../test/sort' import { NORMAL_TOOLTIP_DELAY, @@ -62,6 +63,11 @@ const tableStateFixture = { columnData: collectColumnData(tableDataFixture.columns) } +const sortedTableStateFixture = { + ...sortedTableData, + columnData: collectColumnData(sortedTableData.columns) +} + jest.mock('../../shared/api') jest.mock('../../util/styles') jest.mock('./overflowHoverTooltip/useIsFullyContained', () => ({ @@ -100,6 +106,19 @@ describe('App', () => { expect(noColumnsState).toBeInTheDocument() }) + it('should show the no columns selected empty state when there are no columns provided and the table is sorted', () => { + const { columns } = tableStateFixture + const sortPath = columns[columns.length - 1].path + renderTable({ + ...tableDataFixture, + columns: [], + sorts: [{ descending: true, path: sortPath }] + }) + + const noColumnsState = screen.queryByText('No Columns Selected.') + expect(noColumnsState).toBeInTheDocument() + }) + it('should not show the no columns selected empty state when only the timestamp column is provided', () => { renderTable({ ...tableStateFixture, @@ -145,9 +164,9 @@ describe('App', () => { const { getDraggableHeaderFromText } = renderTableWithSortingData() setTableData({ - ...sortingTableDataFixture, + ...simplifiedSortedTableDataFixture, columns: [ - ...sortingTableDataFixture.columns, + ...simplifiedSortedTableDataFixture.columns, { ...commonColumnFields, id: 'D', @@ -165,9 +184,51 @@ describe('App', () => { await expectHeaders(['A', 'C', 'D', 'B']) }) + it('should add a "branch/tags" column if the table is sorted', () => { + renderTable(sortedTableStateFixture) + + const branchHeader = screen.getByTestId('header-branch') + expect(branchHeader).toBeInTheDocument() + + const branchHeaderTextContent = + within(branchHeader).getByText('Branch/Tags') + expect(branchHeader).toBeInTheDocument() + + fireEvent.mouseEnter(branchHeaderTextContent, { bubbles: true }) + expect(screen.getByRole('tooltip')).toBeInTheDocument() + expect(screen.getByRole('tooltip')).toHaveTextContent( + 'The table has limited functionality while sorted. Clear all sorts to have nested rows and increase/decrease commits.' + ) + + expect(screen.getByTestId('branch___main').textContent).toStrictEqual( + 'main' + ) + }) + + it('should add a "parent" column if the table is sorted', () => { + renderTable(sortedTableStateFixture) + + const commitHeader = screen.getByTestId('header-commit') + expect(commitHeader).toBeInTheDocument() + + const commitHeaderTextContent = within(commitHeader).getByText('Parent') + expect(commitHeader).toBeInTheDocument() + + fireEvent.mouseEnter(commitHeaderTextContent, { bubbles: true }) + expect(screen.getByRole('tooltip')).toBeInTheDocument() + expect(screen.getByRole('tooltip')).toHaveTextContent( + 'The table has limited functionality while sorted. Clear all sorts to have nested rows and increase/decrease commits.' + ) + + expect(screen.getByTestId('commit___main').textContent).toStrictEqual('') + expect(screen.getByTestId('commit___exp-83425').textContent).toStrictEqual( + '53c3851' + ) + }) + it('should be able to move columns to the start', async () => { renderTable({ - ...sortingTableDataFixture, + ...simplifiedSortedTableDataFixture, columnOrder: ['id', 'Created', 'params:A', 'params:B', 'params:C'] }) @@ -176,7 +237,7 @@ describe('App', () => { const moveBCtoStart = ['id', 'params:B', 'params:C', 'Created', 'params:A'] setTableData({ - ...sortingTableDataFixture, + ...simplifiedSortedTableDataFixture, columnOrder: moveBCtoStart }) diff --git a/webview/src/experiments/components/Experiments.tsx b/webview/src/experiments/components/Experiments.tsx index 7fc4e7579b..79af4d5f2e 100644 --- a/webview/src/experiments/components/Experiments.tsx +++ b/webview/src/experiments/components/Experiments.tsx @@ -26,6 +26,7 @@ import { WebviewWrapper } from '../../shared/components/webviewWrapper/WebviewWr import { EmptyState } from '../../shared/components/emptyState/EmptyState' import { ExperimentsState } from '../store' import { resizeColumn } from '../util/messages' +import { isDefaultColumn } from '../util/columns' const DEFAULT_COLUMN_WIDTH = 90 const MINIMUM_COLUMN_WIDTH = 90 @@ -57,12 +58,15 @@ export const ExperimentsTable: React.FC = () => { columnOrder: columnOrderData, columnWidths, hasConfig, - rows: data + rows: data, + sorts } = useSelector((state: ExperimentsState) => state.tableData) const [expanded, setExpanded] = useState({}) - const [columns, setColumns] = useState(buildColumns(columnData)) + const [columns, setColumns] = useState( + buildColumns(columnData, sorts.length > 0) + ) const [columnSizing, setColumnSizing] = useState(columnWidths) const [columnOrder, setColumnOrder] = @@ -74,8 +78,8 @@ export const ExperimentsTable: React.FC = () => { }, [columnSizing, columnWidths]) useEffect(() => { - setColumns(buildColumns(columnData)) - }, [columnData]) + setColumns(buildColumns(columnData, sorts.length > 0)) + }, [columnData, sorts]) useEffect(() => { setColumnOrder(columnOrderData) @@ -114,7 +118,9 @@ export const ExperimentsTable: React.FC = () => { toggleAllRowsExpanded() }, [toggleAllRowsExpanded]) - const hasOnlyDefaultColumns = columns.length <= 1 + const hasOnlyDefaultColumns = columns.every( + ({ id }) => id && isDefaultColumn(id) + ) if (hasOnlyDefaultColumns) { return } diff --git a/webview/src/experiments/components/table/body/SortedTableContent.tsx b/webview/src/experiments/components/table/body/SortedTableContent.tsx new file mode 100644 index 0000000000..12d2a8081c --- /dev/null +++ b/webview/src/experiments/components/table/body/SortedTableContent.tsx @@ -0,0 +1,30 @@ +import React, { Fragment, RefObject } from 'react' +import { Row } from '@tanstack/react-table' +import { Experiment } from 'dvc/src/experiments/webview/contract' +import { TableBody } from './TableBody' + +interface SortedTableContentProps { + rows: Row[] + tableRef: RefObject + tableHeadHeight: number +} + +export const SortedTableContent: React.FC = ({ + rows, + tableHeadHeight, + tableRef +}) => { + return ( + <> + {rows.map((row, i) => ( + + ))} + + ) +} diff --git a/webview/src/experiments/components/table/body/TableContent.test.tsx b/webview/src/experiments/components/table/body/TableContent.test.tsx index 9aeebd61f7..70abe96e3b 100644 --- a/webview/src/experiments/components/table/body/TableContent.test.tsx +++ b/webview/src/experiments/components/table/body/TableContent.test.tsx @@ -1,14 +1,18 @@ import '@testing-library/jest-dom' import { render, screen } from '@testing-library/react' import React, { createRef } from 'react' -import { Table } from '@tanstack/react-table' +import { Table, Row } 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 { mockRowModel } from '../../../../test/mockRowModel' import { experimentsReducers } from '../../../store' -import { collectColumnData } from '../../../state/tableDataSlice' +import { + TableDataState, + collectColumnData +} from '../../../state/tableDataSlice' const tableStateData = { ...tableData, @@ -19,802 +23,50 @@ jest.mock('../../../../shared/api') jest.mock('./NestedRow') jest.mock('./Row') +const getMockFlattenedRowModel = () => { + const flatRows: Row[] = [] + const rows: Row[] = [] + + for (const flatRow of mockRowModel.flatRows) { + const { subRows } = flatRow + + flatRows.push( + { + ...flatRow, + originalSubRows: undefined, + subRows: [] + } as unknown as Row, + ...(subRows as unknown as Row[]) + ) + } + + for (const row of mockRowModel.rows) { + const { subRows } = row + + rows.push( + { + ...row, + originalSubRows: undefined, + subRows: [] + } as unknown as Row, + ...(subRows as unknown as Row[]) + ) + } + + return { flatRows, rows } +} + 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: 'main', - 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: 'main', - 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: 'main', - 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: 'main', - 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: 'main', - 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: 'main', - 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: 'main', - 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: 'main', - 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: 'main', - 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: 'main', - 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: 'main', - 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: 'main', - 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: 'main', - 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: 'main', - 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: [] - } - ] + ...mockRowModel }) } as unknown as Table - const renderTableContent = (rowsInstance = instance) => { + const renderTableContent = ( + rowsInstance = instance, + tableData: TableDataState = tableStateData + ) => { const { rows, flatRows } = rowsInstance.getRowModel() return render( @@ -837,7 +89,7 @@ describe('TableContent', () => { ), selectedRows: {} }, - tableData: tableStateData + tableData }, reducer: experimentsReducers })} @@ -875,4 +127,17 @@ describe('TableContent', () => { expect(screen.getByText('main')).toBeInTheDocument() expect(screen.getByText('new-branch')).toBeInTheDocument() }) + + it('should not add branch rows when the table is sorted', () => { + const multipleBranchesInstance = { + ...instance, + getRowModel: getMockFlattenedRowModel + } as unknown as Table + renderTableContent(multipleBranchesInstance, { + ...tableStateData, + sorts: [{ descending: true, path: 'path' }] + }) + + expect(screen.queryByTestId('branch-name')).not.toBeInTheDocument() + }) }) diff --git a/webview/src/experiments/components/table/body/TableContent.tsx b/webview/src/experiments/components/table/body/TableContent.tsx index ce484e20ac..950c23c471 100644 --- a/webview/src/experiments/components/table/body/TableContent.tsx +++ b/webview/src/experiments/components/table/body/TableContent.tsx @@ -1,9 +1,12 @@ import React, { Fragment, RefObject } from 'react' import { Row } from '@tanstack/react-table' +import { useSelector } from 'react-redux' import { Experiment } from 'dvc/src/experiments/webview/contract' import { TableBody } from './TableBody' +import { SortedTableContent } from './SortedTableContent' import { collectBranchWithRows } from './util' import { BranchDivider } from './branchDivider/BranchDivider' +import { ExperimentsState } from '../../../store' interface TableContentProps { rows: Row[] @@ -16,6 +19,18 @@ export const TableContent: React.FC = ({ tableHeadHeight, tableRef }) => { + const sorts = useSelector((state: ExperimentsState) => state.tableData.sorts) + + if (sorts.length > 0) { + return ( + + ) + } + return ( <> {collectBranchWithRows(rows).map(([branch, branchRows]) => { diff --git a/webview/src/experiments/components/table/body/columns/Columns.tsx b/webview/src/experiments/components/table/body/columns/Columns.tsx index 6422fa1b46..4e9c8b7d3d 100644 --- a/webview/src/experiments/components/table/body/columns/Columns.tsx +++ b/webview/src/experiments/components/table/body/columns/Columns.tsx @@ -8,7 +8,11 @@ import { CellContext } from '@tanstack/react-table' import { ColumnType, Experiment } from 'dvc/src/experiments/webview/contract' -import { EXPERIMENT_COLUMN_ID } from 'dvc/src/experiments/columns/constants' +import { + EXPERIMENT_COLUMN_ID, + BRANCH_COLUMN_ID, + COMMIT_COLUMN_ID +} from 'dvc/src/experiments/columns/constants' import { Header } from '../../content/Header' import { Cell, CellValue } from '../../content/Cell' import { Column, Columns } from '../../../../state/tableDataSlice' @@ -16,6 +20,9 @@ import { DateCellContents } from '../../content/DateCellContent' import { TimestampHeader } from '../../content/TimestampHeader' import { ExperimentCell } from '../../content/ExperimentCell' import { ExperimentHeader } from '../../content/ExperimentHeader' +import { SortedTableHeader } from '../../content/SortedTableHeader' +import { BranchCellContent } from '../../content/BranchCellContent' +import { CommitCellContent } from '../../content/CommitCellContent' export type ColumnWithGroup = ColumnDef & { group: ColumnType @@ -23,29 +30,57 @@ export type ColumnWithGroup = ColumnDef & { const columnHelper = createColumnHelper() -const getDefaultColumn = () => - columnHelper.accessor(() => EXPERIMENT_COLUMN_ID, { - cell: (cell: CellContext) => { - const { - row: { - original: { label, description, commit, sha, error } - } - } = cell as unknown as CellContext - return ( - - ) - }, - header: ExperimentHeader, - id: EXPERIMENT_COLUMN_ID, - minSize: 230, - size: 240 - }) +const getDefaultColumns = (flattenTable: boolean) => { + const columns = [ + columnHelper.accessor(() => EXPERIMENT_COLUMN_ID, { + cell: (cell: CellContext) => { + const { + row: { + original: { label, description, commit, sha, error } + } + } = cell as unknown as CellContext + return ( + + ) + }, + header: ExperimentHeader, + id: EXPERIMENT_COLUMN_ID, + minSize: 230, + size: 240 + }) + ] + + if (flattenTable) { + columns.push( + columnHelper.accessor(() => BRANCH_COLUMN_ID, { + cell: BranchCellContent as unknown as React.FC< + CellContext + >, + header: () => , + id: BRANCH_COLUMN_ID, + minSize: 115, + size: 115 + }), + columnHelper.accessor(() => COMMIT_COLUMN_ID, { + cell: CommitCellContent as unknown as React.FC< + CellContext + >, + header: () => , + id: COMMIT_COLUMN_ID, + minSize: 80, + size: 80 + }) + ) + } + + return columns +} const buildAccessor: (valuePath: string[]) => AccessorFn = pathArray => originalRow => { @@ -126,9 +161,12 @@ const buildColumnsType = ( .filter(Boolean) as TableColumn[] } -export const buildColumns = (columns: Columns): ColumnDef[] => { +export const buildColumns = ( + columns: Columns, + flattenTable: boolean +): ColumnDef[] => { return [ - getDefaultColumn(), + ...getDefaultColumns(flattenTable), ...getTimestampColumn(columns), ...buildColumnsType(columns, ColumnType.METRICS), ...buildColumnsType(columns, ColumnType.PARAMS), diff --git a/webview/src/experiments/components/table/content/BranchCellContent.tsx b/webview/src/experiments/components/table/content/BranchCellContent.tsx new file mode 100644 index 0000000000..a0d52becd4 --- /dev/null +++ b/webview/src/experiments/components/table/content/BranchCellContent.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import { Experiment } from 'dvc/src/experiments/webview/contract' +import { CellContext } from '@tanstack/react-table' +import { CellValue } from './Cell' +import styles from '../styles.module.scss' +import { Icon } from '../../../../shared/components/Icon' +import { GitMerge } from '../../../../shared/components/icons' + +export const BranchCellContent: React.FC< + CellContext +> = cell => { + const { + row: { + original: { branch } + } + } = cell as unknown as CellContext + + if (!branch) { + return ( +
+ ) + } + + return ( +
+
+ + {branch} +
+
+ ) +} diff --git a/webview/src/experiments/components/table/content/CommitCellContent.tsx b/webview/src/experiments/components/table/content/CommitCellContent.tsx new file mode 100644 index 0000000000..2aebe0b160 --- /dev/null +++ b/webview/src/experiments/components/table/content/CommitCellContent.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import { Experiment } from 'dvc/src/experiments/webview/contract' +import { CellContext } from '@tanstack/react-table' +import { CellValue } from './Cell' +import styles from '../styles.module.scss' +import { Icon } from '../../../../shared/components/Icon' +import { GitCommit } from '../../../../shared/components/icons' + +export const CommitCellContent: React.FC< + CellContext +> = cell => { + const { + row: { + original: { baselineSha } + } + } = cell as unknown as CellContext + const labelSha = baselineSha?.slice(0, 7) + + if (!labelSha) { + return ( +
+ ) + } + + return ( +
+
+ + {labelSha} +
+
+ ) +} diff --git a/webview/src/experiments/components/table/content/SortedTableHeader.tsx b/webview/src/experiments/components/table/content/SortedTableHeader.tsx new file mode 100644 index 0000000000..b02f5d039c --- /dev/null +++ b/webview/src/experiments/components/table/content/SortedTableHeader.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import styles from '../styles.module.scss' +import { Icon } from '../../../../shared/components/Icon' +import { Info } from '../../../../shared/components/icons' +import Tooltip from '../../../../shared/components/tooltip/Tooltip' + +export const SortedTableHeader: React.FC<{ name: string }> = ({ name }) => ( + + + + {name} + + + + +) diff --git a/webview/src/experiments/components/table/header/ContextMenuContent.tsx b/webview/src/experiments/components/table/header/ContextMenuContent.tsx index 413b1d6540..7700e9096d 100644 --- a/webview/src/experiments/components/table/header/ContextMenuContent.tsx +++ b/webview/src/experiments/components/table/header/ContextMenuContent.tsx @@ -4,7 +4,7 @@ import React, { useMemo } from 'react' import { Header } from '@tanstack/react-table' import { useSelector } from 'react-redux' import { SortDefinition } from 'dvc/src/experiments/model/sortBy' -import { SortOrder, getSortDetails, isFromExperimentColumn } from './util' +import { SortOrder, getSortDetails, isFromDefaultColumn } from './util' import { MessagesMenu } from '../../../../shared/components/messagesMenu/MessagesMenu' import { MessagesMenuOptionProps } from '../../../../shared/components/messagesMenu/MessagesMenuOption' import { ExperimentsState } from '../../../store' @@ -57,7 +57,7 @@ const getFilterDetails = ( const id = header.column.id const canFilter = - !isFromExperimentColumn(header) && header.column.columns.length <= 1 + !isFromDefaultColumn(header) && header.column.columns.length <= 1 return { canFilter, isFiltered: filters.includes(id) } } @@ -73,7 +73,7 @@ const getMenuOptions = ( return [ { - disabled: isFromExperimentColumn(header), + disabled: isFromDefaultColumn(header), id: 'hide', label: 'Hide', message: { @@ -82,7 +82,7 @@ const getMenuOptions = ( } }, { - disabled: isFromExperimentColumn(header), + disabled: isFromDefaultColumn(header), id: 'move-to-start', label: 'Move to Start', message: { diff --git a/webview/src/experiments/components/table/header/TableHead.tsx b/webview/src/experiments/components/table/header/TableHead.tsx index 4c1afa9dde..a110f60473 100644 --- a/webview/src/experiments/components/table/header/TableHead.tsx +++ b/webview/src/experiments/components/table/header/TableHead.tsx @@ -13,7 +13,7 @@ import { ExperimentsState } from '../../../store' import { leafColumnIds, reorderColumnIds, - isExperimentColumn + isDefaultColumn } from '../../../util/columns' import { DragFunction } from '../../../../shared/components/dragDrop/Draggable' import styles from '../styles.module.scss' @@ -80,7 +80,7 @@ export const TableHead = ({ const onDragEnter = (e: DragEvent) => { findDisplacedHeader(e.currentTarget.id, displacedHeader => { - if (!isExperimentColumn(displacedHeader.id)) { + if (!isDefaultColumn(displacedHeader.id)) { dispatch(setDropTarget(displacedHeader.id)) } }) diff --git a/webview/src/experiments/components/table/header/TableHeaderCell.tsx b/webview/src/experiments/components/table/header/TableHeaderCell.tsx index 11eb9c6e69..97d045233f 100644 --- a/webview/src/experiments/components/table/header/TableHeaderCell.tsx +++ b/webview/src/experiments/components/table/header/TableHeaderCell.tsx @@ -8,7 +8,11 @@ import { TableHeaderCellContents } from './TableHeaderCellContents' import { ContextMenuContent } from './ContextMenuContent' import { getSortDetails } from './util' import styles from '../styles.module.scss' -import { isExperimentColumn, isFirstLevelHeader } from '../../../util/columns' +import { + isDefaultColumn, + isExperimentColumn, + isFirstLevelHeader +} from '../../../util/columns' import { ExperimentsState } from '../../../store' import { ContextMenu } from '../../../../shared/components/contextMenu/ContextMenu' import { DragFunction } from '../../../../shared/components/dragDrop/Draggable' @@ -117,7 +121,7 @@ export const TableHeaderCell: React.FC<{ const { isSortable, sortOrder } = useMemo(() => { return getSortDetails(header, sorts) }, [header, sorts]) - const isDraggable = !isPlaceholder && !isExperimentColumn(id) + const isDraggable = !isPlaceholder && !isDefaultColumn(id) const hasFilter = !!(header.id && filters.includes(header.id)) diff --git a/webview/src/experiments/components/table/header/util.ts b/webview/src/experiments/components/table/header/util.ts index adb63293ac..8d8b80bc23 100644 --- a/webview/src/experiments/components/table/header/util.ts +++ b/webview/src/experiments/components/table/header/util.ts @@ -1,4 +1,5 @@ import { Header } from '@tanstack/react-table' +import { DEFAULT_COLUMN_IDS } from 'dvc/src/experiments/columns/constants' import { SortDefinition } from 'dvc/src/experiments/model/sortBy' import { Experiment } from 'dvc/src/experiments/webview/contract' @@ -14,15 +15,22 @@ const possibleOrders = { undefined: SortOrder.NONE } as const -export const isFromExperimentColumn = (header: Header) => - header.column.id === 'id' || header.column.id.startsWith('id_placeholder') +export const isFromDefaultColumn = (header: Header) => { + const headerId = header.column.id + + for (const id of DEFAULT_COLUMN_IDS) { + if (headerId === id || headerId.startsWith(`${id}_placeholder`)) { + return true + } + } +} export const getSortDetails = ( header: Header, sorts: SortDefinition[] ): { id: string; isSortable: boolean; sortOrder: SortOrder } => { - const isNotExperiments = !isFromExperimentColumn(header) - const isSortable = isNotExperiments && header.column.columns.length <= 1 + const isNotDefaultColumn = !isFromDefaultColumn(header) + const isSortable = isNotDefaultColumn && header.column.columns.length <= 1 const baseColumn = header.headerGroup.headers.find( h => h.column.id === header.placeholderId diff --git a/webview/src/experiments/components/table/styles.module.scss b/webview/src/experiments/components/table/styles.module.scss index 3f9b1b3507..a57f78b551 100644 --- a/webview/src/experiments/components/table/styles.module.scss +++ b/webview/src/experiments/components/table/styles.module.scss @@ -419,6 +419,22 @@ $badge-size: 0.85rem; text-overflow: ellipsis; } +.headerWithTooltip { + display: flex; +} + +.headerWithTooltipContents { + display: flex; + gap: 2px; + align-items: center; + + svg { + width: 16px; + height: 16px; + fill: $accent-color; + } +} + .headerCellText { @extend %truncateLeftParent; @extend %headerCellPadding; @@ -497,7 +513,9 @@ $badge-size: 0.85rem; visibility: visible; } - .timestampInnerCell { + .timestampInnerCell, + .branchInnerCell, + .commitInnerCell { height: 42px; } } @@ -838,7 +856,9 @@ $badge-size: 0.85rem; } } -.timestampInnerCell { +.timestampInnerCell, +.branchInnerCell, +.commitInnerCell { @extend %baseInnerCell; @extend %truncateLeftParent; @@ -859,6 +879,28 @@ $badge-size: 0.85rem; font-size: 0.9em; } +.commitInnerCell .cellContents { + font-size: 0.7rem; + + > * { + vertical-align: middle; + } +} + +.branchInnerCell .cellContents { + font-size: 0.65rem; + font-weight: 600; + + > * { + vertical-align: middle; + } + + svg { + fill: $accent-color; + margin-right: 1px; + } +} + .cellTooltip { padding: 2px 6px; } diff --git a/webview/src/experiments/util/columns.ts b/webview/src/experiments/util/columns.ts index 1ff639bfba..a8a04018fd 100644 --- a/webview/src/experiments/util/columns.ts +++ b/webview/src/experiments/util/columns.ts @@ -1,6 +1,9 @@ import { Experiment } from 'dvc/src/experiments/webview/contract' import { Header } from '@tanstack/react-table' -import { EXPERIMENT_COLUMN_ID } from 'dvc/src/experiments/columns/constants' +import { + EXPERIMENT_COLUMN_ID, + DEFAULT_COLUMN_IDS +} from 'dvc/src/experiments/columns/constants' export const isFirstLevelHeader = (id: string) => id.split(':').length - 1 === 1 @@ -80,3 +83,5 @@ export const leafColumnIds = ( export const isExperimentColumn = (id: string): boolean => id === EXPERIMENT_COLUMN_ID + +export const isDefaultColumn = (id: string) => DEFAULT_COLUMN_IDS.includes(id) diff --git a/webview/src/stories/Table.stories.tsx b/webview/src/stories/Table.stories.tsx index b607d9bb26..fbf826fed7 100644 --- a/webview/src/stories/Table.stories.tsx +++ b/webview/src/stories/Table.stories.tsx @@ -8,6 +8,7 @@ import workspaceChangesFixture from 'dvc/src/test/fixtures/expShow/base/workspac import deeplyNestedTableData from 'dvc/src/test/fixtures/expShow/deeplyNested/tableData' import dataTypesTableFixture from 'dvc/src/test/fixtures/expShow/dataTypes/tableData' import survivalTableData from 'dvc/src/test/fixtures/expShow/survival/tableData' +import sortedTableData from 'dvc/src/test/fixtures/expShow/sorted/tableData' import { timestampColumn } from 'dvc/src/experiments/columns/constants' import { delay } from 'dvc/src/util/time' import { @@ -61,10 +62,7 @@ const tableData = getTableState({ selectedBranches: [], selectedForPlotsCount: 2, showOnlyChanged: false, - sorts: [ - { descending: true, path: 'params:params.yaml:epochs' }, - { descending: false, path: 'params:params.yaml:log_file' } - ] + sorts: [] }) const noRunningExperiments = { @@ -313,6 +311,11 @@ WithNoSortsOrFilters.args = { } } +export const WithSortedRows = Template.bind({}) +WithSortedRows.args = { + tableData: getTableState(sortedTableData) +} + export const Scrolled: StoryFn<{ tableData: TableDataState }> = ({ tableData }) => { diff --git a/webview/src/test/mockRowModel.ts b/webview/src/test/mockRowModel.ts new file mode 100644 index 0000000000..d1995e153f --- /dev/null +++ b/webview/src/test/mockRowModel.ts @@ -0,0 +1,797 @@ +/* eslint-disable sonarjs/no-duplicate-string */ + +import { Row } from '@tanstack/react-table' +import { Experiment } from 'dvc/src/experiments/webview/contract' + +const mockedGetIsExpanded = jest.fn() +const mockedGetAllCells = jest.fn().mockReturnValue([1, 2, 3, 4, 5]) // Only needed as length + +export const mockRowModel = { + flatRows: [ + { + columnFilters: {}, + columnFiltersMeta: {}, + depth: 0, + id: '1', + index: 1, + original: { + Created: '2023-04-20T05:14:46', + branch: 'main', + 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: 'main', + 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: 'main', + 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: 'main', + 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: 'main', + 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: 'main', + 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: 'main', + 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: 'main', + 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: 'main', + 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: 'main', + 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: 'main', + 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: 'main', + 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: 'main', + 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: 'main', + 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 { flatRows: Row[]; rows: Row[] }