Skip to content

Commit c93f3a8

Browse files
authored
Add multi-select remove to experiments tree (#1810)
* add multi select remove to the experiments tree * refactor and test further * cover case where icon clicked does not belong to a selected experiment * clean up test
1 parent c4a0be3 commit c93f3a8

File tree

4 files changed

+171
-29
lines changed

4 files changed

+171
-29
lines changed

extension/src/cli/executor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,11 @@ export class CliExecutor extends Cli {
7676
)
7777
}
7878

79-
public experimentRemove(cwd: string, experimentName: string) {
79+
public experimentRemove(cwd: string, ...experimentNames: string[]) {
8080
return this.executeExperimentProcess(
8181
cwd,
8282
ExperimentSubCommand.REMOVE,
83-
experimentName
83+
...experimentNames
8484
)
8585
}
8686

extension/src/experiments/model/collect.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { ThemeIcon, TreeItemCollapsibleState, Uri } from 'vscode'
12
import omit from 'lodash.omit'
3+
import { ExperimentType } from '.'
24
import { ExperimentsAccumulator } from './accumulator'
35
import { extractColumns } from '../columns/extract'
46
import { Experiment, ColumnType } from '../webview/contract'
@@ -10,6 +12,23 @@ import {
1012
} from '../../cli/reader'
1113
import { addToMapArray } from '../../util/map'
1214
import { uniqueValues } from '../../util/array'
15+
import { RegisteredCommands } from '../../commands/external'
16+
import { Resource } from '../../resourceLocator'
17+
18+
export type ExperimentItem = {
19+
command?: {
20+
arguments: { dvcRoot: string; id: string }[]
21+
command: RegisteredCommands
22+
title: string
23+
}
24+
dvcRoot: string
25+
description: string | undefined
26+
id: string
27+
label: string
28+
collapsibleState: TreeItemCollapsibleState
29+
type: ExperimentType
30+
iconPath: ThemeIcon | Uri | Resource
31+
}
1332

1433
type ExperimentsObject = { [sha: string]: ExperimentFieldsOrError }
1534

@@ -288,3 +307,49 @@ export const collectMutableRevisions = (
288307

289308
return uniqueValues(acc)
290309
}
310+
311+
type DeletableExperimentAccumulator = { [dvcRoot: string]: Set<string> }
312+
313+
const initializeAccumulatorRoot = (
314+
acc: DeletableExperimentAccumulator,
315+
dvcRoot: string
316+
) => {
317+
if (!acc[dvcRoot]) {
318+
acc[dvcRoot] = new Set<string>()
319+
}
320+
}
321+
322+
const collectExperimentItem = (
323+
acc: DeletableExperimentAccumulator,
324+
deletable: Set<string>,
325+
experimentItem: ExperimentItem
326+
) => {
327+
const { dvcRoot, type, id, label } = experimentItem
328+
if (!deletable.has(type)) {
329+
return
330+
}
331+
initializeAccumulatorRoot(acc, dvcRoot)
332+
if (type === ExperimentType.QUEUED) {
333+
acc[dvcRoot].add(label)
334+
return
335+
}
336+
337+
acc[dvcRoot].add(id)
338+
}
339+
340+
export const collectDeletable = (
341+
experimentItems: (string | ExperimentItem)[]
342+
): DeletableExperimentAccumulator => {
343+
const deletable = new Set([ExperimentType.EXPERIMENT, ExperimentType.QUEUED])
344+
345+
const acc: DeletableExperimentAccumulator = {}
346+
for (const experimentItem of experimentItems) {
347+
if (typeof experimentItem === 'string') {
348+
continue
349+
}
350+
351+
collectExperimentItem(acc, deletable, experimentItem)
352+
}
353+
354+
return acc
355+
}

extension/src/experiments/model/tree.ts

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
Uri
99
} from 'vscode'
1010
import { ExperimentType } from '.'
11+
import { collectDeletable, ExperimentItem } from './collect'
1112
import { MAX_SELECTED_EXPERIMENTS } from './status'
1213
import { WorkspaceExperiments } from '../workspace'
1314
import { sendViewOpenedTelemetryEvent } from '../../telemetry'
@@ -21,21 +22,6 @@ import { sum } from '../../util/math'
2122
import { Title } from '../../vscode/title'
2223
import { Disposable } from '../../class/dispose'
2324

24-
export type ExperimentItem = {
25-
command?: {
26-
arguments: { dvcRoot: string; id: string }[]
27-
command: RegisteredCommands
28-
title: string
29-
}
30-
dvcRoot: string
31-
description: string | undefined
32-
id: string
33-
label: string
34-
collapsibleState: TreeItemCollapsibleState
35-
type: ExperimentType
36-
iconPath: ThemeIcon | Uri | Resource
37-
}
38-
3925
export class ExperimentsTree
4026
extends Disposable
4127
implements TreeDataProvider<string | ExperimentItem>
@@ -60,7 +46,7 @@ export class ExperimentsTree
6046
this.onDidChangeTreeData = experiments.experimentsChanged.event
6147

6248
this.view = this.dispose.track(
63-
createTreeView<ExperimentItem>('dvc.views.experimentsTree', this)
49+
createTreeView<ExperimentItem>('dvc.views.experimentsTree', this, true)
6450
)
6551

6652
this.dispose.track(
@@ -183,12 +169,19 @@ export class ExperimentsTree
183169

184170
internalCommands.registerExternalCommand<ExperimentItem>(
185171
RegisteredCommands.EXPERIMENT_TREE_REMOVE,
186-
({ dvcRoot, id }: ExperimentItem) =>
187-
this.experiments.runCommand(
188-
AvailableCommands.EXPERIMENT_REMOVE,
189-
dvcRoot,
190-
id
191-
)
172+
async experimentItem => {
173+
const selected = [...this.getSelectedExperimentItems(), experimentItem]
174+
175+
const deletable = collectDeletable(selected)
176+
177+
for (const [dvcRoot, ids] of Object.entries(deletable)) {
178+
await this.experiments.runCommand(
179+
AvailableCommands.EXPERIMENT_REMOVE,
180+
dvcRoot,
181+
...ids
182+
)
183+
}
184+
}
192185
)
193186
}
194187

@@ -345,4 +338,8 @@ export class ExperimentsTree
345338
private getDisplayId(type: ExperimentType, label: string, id: string) {
346339
return type === ExperimentType.CHECKPOINT ? label : id
347340
}
341+
342+
private getSelectedExperimentItems() {
343+
return [...this.view.selection]
344+
}
348345
}

extension/src/test/suite/experiments/model/tree.test.ts

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,7 @@ import plotsDiffFixture from '../../../fixtures/plotsDiff/output'
2424
import expShowFixture from '../../../fixtures/expShow/output'
2525
import { Operator } from '../../../../experiments/model/filterBy'
2626
import { joinColumnPath } from '../../../../experiments/columns/paths'
27-
import {
28-
ExperimentItem,
29-
ExperimentsTree
30-
} from '../../../../experiments/model/tree'
27+
import { ExperimentsTree } from '../../../../experiments/model/tree'
3128
import { buildExperiments, buildSingleRepoExperiments } from '../util'
3229
import { ResourceLocator } from '../../../../resourceLocator'
3330
import { InternalCommands } from '../../../../commands/internal'
@@ -42,6 +39,7 @@ import { Param } from '../../../../experiments/model/modify/collect'
4239
import { WorkspaceExperiments } from '../../../../experiments/workspace'
4340
import { ColumnType } from '../../../../experiments/webview/contract'
4441
import { copyOriginalColors } from '../../../../experiments/model/status/colors'
42+
import { ExperimentItem } from '../../../../experiments/model/collect'
4543

4644
suite('Experiments Tree Test Suite', () => {
4745
const disposable = Disposable.fn()
@@ -465,16 +463,53 @@ suite('Experiments Tree Test Suite', () => {
465463
})
466464

467465
it('should be able to remove an experiment with dvc.views.experimentsTree.removeExperiment', async () => {
466+
const mockExperimentId = 'exp-to-remove'
467+
const mockExperiment = {
468+
dvcRoot: dvcDemoPath,
469+
id: mockExperimentId,
470+
type: ExperimentType.EXPERIMENT
471+
}
472+
473+
const mockExperimentRemove = stub(
474+
CliExecutor.prototype,
475+
'experimentRemove'
476+
).resolves('')
477+
478+
stub(
479+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
480+
(ExperimentsTree as any).prototype,
481+
'getSelectedExperimentItems'
482+
).returns([mockExperiment])
483+
484+
await commands.executeCommand(
485+
RegisteredCommands.EXPERIMENT_TREE_REMOVE,
486+
mockExperiment
487+
)
488+
489+
expect(mockExperimentRemove).to.be.calledWithExactly(
490+
dvcDemoPath,
491+
mockExperimentId
492+
)
493+
})
494+
495+
it('should be able to remove the provided experiment with dvc.views.experimentsTree.removeExperiment (if no experiments are selected)', async () => {
468496
const mockExperiment = 'exp-to-remove'
469497

470498
const mockExperimentRemove = stub(
471499
CliExecutor.prototype,
472500
'experimentRemove'
473501
).resolves('')
474502

503+
stub(
504+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
505+
(ExperimentsTree as any).prototype,
506+
'getSelectedExperimentItems'
507+
).returns([])
508+
475509
await commands.executeCommand(RegisteredCommands.EXPERIMENT_TREE_REMOVE, {
476510
dvcRoot: dvcDemoPath,
477-
id: mockExperiment
511+
id: mockExperiment,
512+
type: ExperimentType.EXPERIMENT
478513
})
479514

480515
expect(mockExperimentRemove).to.be.calledWithExactly(
@@ -483,6 +518,51 @@ suite('Experiments Tree Test Suite', () => {
483518
)
484519
})
485520

521+
it('should be able to remove multiple experiments with dvc.views.experimentsTree.removeExperiment', async () => {
522+
const mockExperimentId = 'exp-removed'
523+
const mockQueuedExperimentLabel = 'queued-removed'
524+
525+
const mockExperimentRemove = stub(
526+
CliExecutor.prototype,
527+
'experimentRemove'
528+
).resolves('')
529+
530+
stub(
531+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
532+
(ExperimentsTree as any).prototype,
533+
'getSelectedExperimentItems'
534+
).returns([
535+
dvcDemoPath,
536+
{
537+
dvcRoot: dvcDemoPath,
538+
label: mockQueuedExperimentLabel,
539+
type: ExperimentType.QUEUED
540+
},
541+
{
542+
dvcRoot: dvcDemoPath,
543+
id: 'checkpoint-excluded',
544+
type: ExperimentType.CHECKPOINT
545+
},
546+
{
547+
dvcRoot: dvcDemoPath,
548+
id: 'workspace-excluded',
549+
type: ExperimentType.WORKSPACE
550+
}
551+
])
552+
553+
await commands.executeCommand(RegisteredCommands.EXPERIMENT_TREE_REMOVE, {
554+
dvcRoot: dvcDemoPath,
555+
id: mockExperimentId,
556+
type: ExperimentType.EXPERIMENT
557+
})
558+
559+
expect(mockExperimentRemove).to.be.calledWithExactly(
560+
dvcDemoPath,
561+
mockQueuedExperimentLabel,
562+
mockExperimentId
563+
)
564+
})
565+
486566
it('should be able to apply an experiment to the workspace with dvc.views.experimentsTree.applyExperiment', async () => {
487567
const mockExperiment = 'exp-to-apply'
488568

0 commit comments

Comments
 (0)