diff --git a/extension/src/experiments/columns/paths.ts b/extension/src/experiments/columns/paths.ts index a113930c28..43b97bca0a 100644 --- a/extension/src/experiments/columns/paths.ts +++ b/extension/src/experiments/columns/paths.ts @@ -47,12 +47,13 @@ export const splitColumnPath = (path: string) => { if (!fileSegment) { return [baseSegment] } + const cleanFileSegment = fileSegment.replace(/_\d+$/g, '') if (!paramPath) { - return [baseSegment, fileSegment] + return [baseSegment, cleanFileSegment] } return [ baseSegment, - fileSegment, + cleanFileSegment, ...paramPath.split(METRIC_PARAM_SEPARATOR).map(decodeColumn) ] } diff --git a/extension/src/experiments/index.ts b/extension/src/experiments/index.ts index cdaa6839f2..759c6ef200 100644 --- a/extension/src/experiments/index.ts +++ b/extension/src/experiments/index.ts @@ -1,4 +1,5 @@ -import { Event, EventEmitter, Memento, window } from 'vscode' +import { join } from 'path' +import { Event, EventEmitter, Memento, Uri, ViewColumn, window } from 'vscode' import { ExperimentsModel } from './model' import { pickExperiments } from './model/quickPicks' import { pickAndModifyParams } from './model/queue/quickPick' @@ -15,6 +16,7 @@ import { ExperimentsData } from './data' import { askToDisableAutoApplyFilters } from './toast' import { Experiment, ColumnType, TableData } from './webview/contract' import { SortDefinition } from './model/sortBy' +import { splitColumnPath } from './columns/paths' import { ResourceLocator } from '../resourceLocator' import { AvailableCommands, @@ -380,6 +382,18 @@ export class Experiments extends BaseRepository { ) } + private async openParamsFileToTheSide(path: string) { + const [, fileSegment] = splitColumnPath(path) + await window.showTextDocument(Uri.file(join(this.dvcRoot, fileSegment)), { + viewColumn: ViewColumn.Beside + }) + sendTelemetryEvent( + EventName.VIEWS_EXPERIMENTS_TABLE_OPEN_PARAMS_FILE, + { path }, + undefined + ) + } + private setupInitialData() { const waitForInitialData = this.dispose.track( this.onDidChangeExperiments(() => { @@ -440,6 +454,8 @@ export class Experiments extends BaseRepository { return this.setExperimentStatus(message.payload) case MessageFromWebviewType.HIDE_EXPERIMENTS_TABLE_COLUMN: return this.hideTableColumn(message.payload) + case MessageFromWebviewType.OPEN_PARAMS_FILE_TO_THE_SIDE: + return this.openParamsFileToTheSide(message.payload) case MessageFromWebviewType.SORT_COLUMN: return this.addColumnSort(message.payload) case MessageFromWebviewType.REMOVE_COLUMN_SORT: diff --git a/extension/src/telemetry/constants.ts b/extension/src/telemetry/constants.ts index 2fcf7558e1..e8f53dfb07 100644 --- a/extension/src/telemetry/constants.ts +++ b/extension/src/telemetry/constants.ts @@ -37,6 +37,8 @@ export const EventName = Object.assign( VIEWS_EXPERIMENTS_TABLE_FOCUS_CHANGED: 'views.experimentsTable.focusChanged', VIEWS_EXPERIMENTS_TABLE_HIDE_COLUMN: 'views.experimentsTable.columnHidden', + VIEWS_EXPERIMENTS_TABLE_OPEN_PARAMS_FILE: + 'views.experimentsTable.paramsFileOpened', VIEWS_EXPERIMENTS_TABLE_REMOVE_COLUMN_SORT: 'views.experimentsTable.columnSortRemoved', VIEWS_EXPERIMENTS_TABLE_RESIZE_COLUMN: @@ -196,6 +198,9 @@ export interface IEventNamePropertyMapping { path: string } [EventName.VIEWS_EXPERIMENTS_TABLE_SELECT_COLUMNS]: undefined + [EventName.VIEWS_EXPERIMENTS_TABLE_OPEN_PARAMS_FILE]: { + path: string + } [EventName.VIEWS_PLOTS_CLOSED]: undefined [EventName.VIEWS_PLOTS_CREATED]: undefined diff --git a/extension/src/test/suite/experiments/index.test.ts b/extension/src/test/suite/experiments/index.test.ts index 0f179d6b05..f1e32d8adc 100644 --- a/extension/src/test/suite/experiments/index.test.ts +++ b/extension/src/test/suite/experiments/index.test.ts @@ -8,7 +8,8 @@ import { commands, workspace, Uri, - QuickPickItem + QuickPickItem, + ViewColumn } from 'vscode' import { buildExperiments } from './util' import { Disposable } from '../../../extension' @@ -390,6 +391,50 @@ suite('Experiments Test Suite', () => { ) }) + it('should be able to handle a message to open the source params file from a column path', async () => { + const { experiments } = setupExperimentsAndMockCommands() + + const mockShowTextDocument = stub(window, 'showTextDocument') + const webview = await experiments.showWebview() + const mockMessageReceived = getMessageReceivedEmitter(webview) + const mockColumnId = 'params:params.yaml_5' + + mockMessageReceived.fire({ + payload: mockColumnId, + type: MessageFromWebviewType.OPEN_PARAMS_FILE_TO_THE_SIDE + }) + + expect(mockShowTextDocument).to.be.calledOnce + expect(mockShowTextDocument).to.be.calledWithExactly( + Uri.file(join(dvcDemoPath, 'params.yaml')), + { + viewColumn: ViewColumn.Beside + } + ) + }) + + it('should be able to handle a message to open different params files than the default one', async () => { + const { experiments } = setupExperimentsAndMockCommands() + + const mockShowTextDocument = stub(window, 'showTextDocument') + const webview = await experiments.showWebview() + const mockMessageReceived = getMessageReceivedEmitter(webview) + const mockColumnId = 'params:params_alt.json_5:nested1.nested2' + + mockMessageReceived.fire({ + payload: mockColumnId, + type: MessageFromWebviewType.OPEN_PARAMS_FILE_TO_THE_SIDE + }) + + expect(mockShowTextDocument).to.be.calledOnce + expect(mockShowTextDocument).to.be.calledWithExactly( + Uri.file(join(dvcDemoPath, 'params_alt.json')), + { + viewColumn: ViewColumn.Beside + } + ) + }) + it('should be able to handle a message to apply an experiment to workspace', async () => { const { experiments, mockExecuteCommand } = setupExperimentsAndMockCommands() diff --git a/extension/src/webview/contract.ts b/extension/src/webview/contract.ts index eb5b927a3f..4c9fbfc05e 100644 --- a/extension/src/webview/contract.ts +++ b/extension/src/webview/contract.ts @@ -14,6 +14,7 @@ export enum MessageFromWebviewType { INITIALIZED = 'initialized', APPLY_EXPERIMENT_TO_WORKSPACE = 'apply-experiment-to-workspace', CREATE_BRANCH_FROM_EXPERIMENT = 'create-branch-from-experiment', + OPEN_PARAMS_FILE_TO_THE_SIDE = 'open-params-file-to-the-side', REMOVE_COLUMN_SORT = 'remove-column-sort', REMOVE_EXPERIMENT = 'remove-experiment', RENAME_SECTION = 'rename-section', @@ -68,6 +69,10 @@ export type MessageFromWebview = type: MessageFromWebviewType.HIDE_EXPERIMENTS_TABLE_COLUMN payload: string } + | { + type: MessageFromWebviewType.OPEN_PARAMS_FILE_TO_THE_SIDE + payload: string + } | { type: MessageFromWebviewType.APPLY_EXPERIMENT_TO_WORKSPACE payload: string diff --git a/webview/src/experiments/components/table/Row.tsx b/webview/src/experiments/components/table/Row.tsx index d80ba36a89..4a9392e970 100644 --- a/webview/src/experiments/components/table/Row.tsx +++ b/webview/src/experiments/components/table/Row.tsx @@ -9,6 +9,7 @@ import { sendMessage } from '../../../shared/vscode' import { ContextMenu } from '../../../shared/components/contextMenu/ContextMenu' import { MessagesMenu } from '../../../shared/components/messagesMenu/MessagesMenu' import { MessagesMenuOptionProps } from '../../../shared/components/messagesMenu/MessagesMenuOption' +import { pushIf } from '../../../util/array' const getExperimentTypeClass = ({ running, queued, selected }: Experiment) => { if (running) { @@ -52,10 +53,7 @@ export const RowContextMenu: React.FC = ({ const contextMenuOptions = React.useMemo(() => { const menuOptions: MessagesMenuOptionProps[] = [] - const pushIf = (condition: boolean, options: MessagesMenuOptionProps[]) => - condition && menuOptions.push(...options) - - pushIf(!queued && !isWorkspace && depth > 0, [ + pushIf(menuOptions, !queued && !isWorkspace && depth > 0, [ experimentMenuOption( id, 'Apply to Workspace', @@ -68,7 +66,7 @@ export const RowContextMenu: React.FC = ({ ) ]) - pushIf(depth === 1, [ + pushIf(menuOptions, depth === 1, [ experimentMenuOption( id, 'Remove', @@ -78,7 +76,7 @@ export const RowContextMenu: React.FC = ({ const isNotCheckpoint = depth <= 1 || isWorkspace - pushIf(isNotCheckpoint, [ + pushIf(menuOptions, isNotCheckpoint, [ experimentMenuOption( id, projectHasCheckpoints ? 'Modify and Resume' : 'Modify and Run', @@ -86,7 +84,7 @@ export const RowContextMenu: React.FC = ({ ) ]) - pushIf(isNotCheckpoint && projectHasCheckpoints, [ + pushIf(menuOptions, isNotCheckpoint && projectHasCheckpoints, [ experimentMenuOption( id, 'Modify, Reset and Run', @@ -94,7 +92,7 @@ export const RowContextMenu: React.FC = ({ ) ]) - pushIf(isNotCheckpoint, [ + pushIf(menuOptions, isNotCheckpoint, [ experimentMenuOption( id, 'Modify and Queue', diff --git a/webview/src/experiments/components/table/TableHeader.tsx b/webview/src/experiments/components/table/TableHeader.tsx index 1177b79cf8..bfad394ee7 100644 --- a/webview/src/experiments/components/table/TableHeader.tsx +++ b/webview/src/experiments/components/table/TableHeader.tsx @@ -21,6 +21,7 @@ import { OnDrop } from '../../../shared/components/dragDrop/DragDropWorkbench' import { MessagesMenu } from '../../../shared/components/messagesMenu/MessagesMenu' +import { MessagesMenuOptionProps } from '../../../shared/components/messagesMenu/MessagesMenuOption' export const ColumnDragHandle: React.FC<{ disabled: boolean @@ -197,6 +198,31 @@ export const TableHeader: React.FC = ({ }) } + const contextMenuOptions: MessagesMenuOptionProps[] = React.useMemo(() => { + const menuOptions: MessagesMenuOptionProps[] = [ + { + hidden: !!column.headers, + id: 'hide-column', + label: 'Hide Column', + message: { + payload: column.id, + type: MessageFromWebviewType.HIDE_EXPERIMENTS_TABLE_COLUMN + } + }, + { + hidden: column.group !== ColumnType.PARAMS, + id: 'open-to-the-side', + label: 'Open to the Side', + message: { + payload: column.id, + type: MessageFromWebviewType.OPEN_PARAMS_FILE_TO_THE_SIDE + } + } + ] + + return menuOptions + }, [column]) + return ( = ({ onDragOver={onDragOver} onDragStart={onDragStart} onDrop={onDrop} - menuDisabled={!isSortable} + menuDisabled={!isSortable && column.group !== ColumnType.PARAMS} menuContent={
- { - setColumnSort(order) - }} - /> - - + {isSortable && ( +
+ { + setColumnSort(order) + }} + /> + +
+ )} +
} /> diff --git a/webview/src/shared/components/messagesMenu/MessagesMenu.tsx b/webview/src/shared/components/messagesMenu/MessagesMenu.tsx index 42a3be3e70..f5a68e39c2 100644 --- a/webview/src/shared/components/messagesMenu/MessagesMenu.tsx +++ b/webview/src/shared/components/messagesMenu/MessagesMenu.tsx @@ -10,8 +10,10 @@ export interface MessagesMenuProps { export const MessagesMenu: React.FC = ({ options }) => (
- {options.map((option, i) => ( - - ))} + {options + .filter(({ hidden }) => !hidden) + .map((option, i) => ( + + ))}
) diff --git a/webview/src/shared/components/messagesMenu/MessagesMenuOption.tsx b/webview/src/shared/components/messagesMenu/MessagesMenuOption.tsx index 2eaf4067b9..bd2bb6c495 100644 --- a/webview/src/shared/components/messagesMenu/MessagesMenuOption.tsx +++ b/webview/src/shared/components/messagesMenu/MessagesMenuOption.tsx @@ -7,6 +7,7 @@ export interface MessagesMenuOptionProps { id: string label: string message: MessageFromWebview + hidden?: boolean } interface MessagesMenuOptionAllProps extends MessagesMenuOptionProps { diff --git a/webview/src/util/array.ts b/webview/src/util/array.ts new file mode 100644 index 0000000000..1f8c5e7ab4 --- /dev/null +++ b/webview/src/util/array.ts @@ -0,0 +1,2 @@ +export const pushIf = (array: T[], condition: boolean, elements: T[]) => + condition && array.push(...elements)