Skip to content

Commit 5770507

Browse files
authored
Add tags filters to experiments table (#4882)
1 parent 2665032 commit 5770507

File tree

9 files changed

+171
-17
lines changed

9 files changed

+171
-17
lines changed

extension/src/experiments/columns/like.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,35 @@ import { Column } from '../webview/contract'
22

33
export type ColumnLike = {
44
firstValueType?: string
5+
description?: string
56
label: string
67
path: string
78
}
89

9-
const starredColumnLike: ColumnLike = {
10+
export const starredColumnLike: ColumnLike = {
11+
description: '$(star-full)',
1012
firstValueType: 'boolean',
11-
label: '$(star-full)',
13+
label: 'Starred',
1214
path: 'starred'
1315
}
1416

15-
export const addStarredToColumns = (
16-
columns: Column[] | undefined
17+
export const tagsColumnLike: ColumnLike = {
18+
description: '$(git-commit)',
19+
firstValueType: 'tags',
20+
label: 'Git Tag',
21+
path: 'Git Tag'
22+
}
23+
24+
export const addToColumns = (
25+
columns: Column[] | undefined,
26+
...columnLikes: ColumnLike[]
1727
): ColumnLike[] | undefined => {
1828
if (!columns?.length) {
1929
return
2030
}
2131

2232
return [
23-
starredColumnLike,
33+
...columnLikes,
2434
...columns.map(({ label, path, firstValueType }) => ({
2535
firstValueType,
2636
label,

extension/src/experiments/columns/quickPick.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ColumnLike } from './like'
1+
import { ColumnLike, starredColumnLike, tagsColumnLike } from './like'
22
import { definedAndNonEmpty } from '../../util/array'
33
import {
44
QuickPickOptionsWithTitle,
@@ -16,9 +16,11 @@ export const pickFromColumnLikes = (
1616

1717
const items = []
1818
for (const columnLike of columnLikes) {
19-
if (columnLike.path === 'starred') {
19+
if (
20+
[starredColumnLike.path, tagsColumnLike.path].includes(columnLike.path)
21+
) {
2022
items.push({
21-
description: columnLike.path,
23+
description: columnLike.description,
2224
label: columnLike.label,
2325
value: columnLike
2426
})

extension/src/experiments/index.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import {
77
workspace
88
} from 'vscode'
99
import omit from 'lodash.omit'
10-
import { ColumnLike, addStarredToColumns } from './columns/like'
10+
import {
11+
ColumnLike,
12+
addToColumns,
13+
starredColumnLike,
14+
tagsColumnLike
15+
} from './columns/like'
1116
import { setContextForEditorTitleIcons } from './context'
1217
import { ExperimentsModel } from './model'
1318
import { collectRemoteExpShas } from './model/collect'
@@ -291,7 +296,7 @@ export class Experiments extends BaseRepository<TableData> {
291296

292297
public async addSort() {
293298
const columns = this.columns.getTerminalNodes()
294-
const columnLikes = addStarredToColumns(columns)
299+
const columnLikes = addToColumns(columns, starredColumnLike)
295300

296301
const sortToAdd = await pickSortToAdd(columnLikes)
297302
if (!sortToAdd) {
@@ -773,7 +778,7 @@ export class Experiments extends BaseRepository<TableData> {
773778
return overrideColumn
774779
}
775780
const columns = this.columns.getTerminalNodes()
776-
const columnLikes = addStarredToColumns(columns)
781+
const columnLikes = addToColumns(columns, starredColumnLike, tagsColumnLike)
777782
return pickColumnToFilter(columnLikes)
778783
}
779784

extension/src/experiments/model/filterBy/index.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { FilterDefinition, filterExperiment, Operator } from '.'
33
import rowsFixture from '../../../test/fixtures/expShow/base/rows'
44
import { buildDepPath, buildMetricOrParamPath } from '../../columns/paths'
55
import { Experiment, ColumnType } from '../../webview/contract'
6+
import { tagsColumnLike } from '../../columns/like'
67

78
describe('filterExperiment', () => {
89
const paramsFile = 'params.yaml'
@@ -292,4 +293,70 @@ describe('filterExperiment', () => {
292293
)
293294
).toBeUndefined()
294295
})
296+
297+
it('should correctly filter by tags using the equal operator', () => {
298+
const main = { ...rowsFixture[1] }
299+
;(main.commit as { tags: string[] }).tags = ['0.9.3', 'model@v1']
300+
const tagFilter = [
301+
{
302+
operator: Operator.EQUAL,
303+
path: tagsColumnLike.path,
304+
value: '0.9.3'
305+
}
306+
]
307+
const unfiltered = filterExperiment(tagFilter, main)
308+
expect(unfiltered).toStrictEqual(main)
309+
310+
expect((main?.subRows || []).length > 0).toBe(true)
311+
312+
for (const experiment of main.subRows || []) {
313+
expect(filterExperiment(tagFilter, experiment)).toBeUndefined()
314+
}
315+
316+
expect(
317+
filterExperiment(
318+
[
319+
{
320+
operator: Operator.EQUAL,
321+
path: tagsColumnLike.path,
322+
value: '0.9'
323+
}
324+
],
325+
main
326+
)
327+
).toBeUndefined()
328+
})
329+
330+
it('should correctly filter by tags using the contains operator', () => {
331+
const main = { ...rowsFixture[1] }
332+
;(main.commit as { tags: string[] }).tags = ['0.9.3', 'model@v1', 'a-tag']
333+
const tagFilter = [
334+
{
335+
operator: Operator.CONTAINS,
336+
path: tagsColumnLike.path,
337+
value: '0.9'
338+
}
339+
]
340+
const unfiltered = filterExperiment(tagFilter, main)
341+
expect(unfiltered).toStrictEqual(main)
342+
343+
expect((main?.subRows || []).length > 0).toBe(true)
344+
345+
for (const experiment of main.subRows || []) {
346+
expect(filterExperiment(tagFilter, experiment)).toBeUndefined()
347+
}
348+
349+
expect(
350+
filterExperiment(
351+
[
352+
{
353+
operator: Operator.CONTAINS,
354+
path: tagsColumnLike.path,
355+
value: '0.9.4'
356+
}
357+
],
358+
main
359+
)
360+
).toBeUndefined()
361+
})
295362
})

extension/src/experiments/model/filterBy/index.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Experiment } from '../../webview/contract'
33
import { definedAndNonEmpty } from '../../../util/array'
44
import { splitColumnPath } from '../../columns/paths'
55
import { getValue } from '../../columns/util'
6+
import { tagsColumnLike } from '../../columns/like'
67

78
export enum Operator {
89
EQUAL = '=',
@@ -33,7 +34,7 @@ export const isDateOperator = (operator: Operator): boolean =>
3334
export interface FilterDefinition {
3435
path: string
3536
operator: Operator
36-
value: string | number | undefined
37+
value: string | number | undefined | string[]
3738
}
3839

3940
const stringContains = (
@@ -60,6 +61,7 @@ const evaluate = <T extends string | number | boolean>(
6061
if (valueToEvaluate === undefined) {
6162
return operator !== Operator.NOT_MISSING
6263
}
64+
6365
switch (operator) {
6466
case Operator.NOT_MISSING:
6567
return true
@@ -105,12 +107,31 @@ const evaluate = <T extends string | number | boolean>(
105107
}
106108
}
107109

110+
const evaluateTagsFilter = (
111+
experiment: Experiment,
112+
filter: FilterDefinition
113+
) => {
114+
const values = experiment.commit?.tags
115+
if (!values || values.length === 0) {
116+
return true
117+
}
118+
if (Array.isArray(values)) {
119+
return !values.some(value =>
120+
evaluate<typeof value>(value, filter.operator, filter.value as string)
121+
)
122+
}
123+
}
124+
108125
const buildFilter =
109126
(
110127
filterDefinitions: FilterDefinition[]
111128
): ((experiment: Experiment) => boolean) =>
112129
experiment => {
113130
const firstFailure = filterDefinitions.find(filter => {
131+
if (filter.path === tagsColumnLike.path) {
132+
return evaluateTagsFilter(experiment, filter)
133+
}
134+
114135
const pathArray = splitColumnPath(filter.path)
115136
const value = getValue(experiment, pathArray)
116137

extension/src/experiments/model/filterBy/quickPick.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,18 @@ export const OPERATORS = [
9292
label: Operator.ON_DATE,
9393
types: [ColumnType.TIMESTAMP],
9494
value: Operator.ON_DATE
95+
},
96+
{
97+
description: 'Git Tag Equal',
98+
label: '=',
99+
types: ['tags'],
100+
value: Operator.EQUAL
101+
},
102+
{
103+
description: 'Git Tag Contains',
104+
label: Operator.CONTAINS,
105+
types: ['tags'],
106+
value: Operator.CONTAINS
95107
}
96108
]
97109

extension/src/test/fixtures/expShow/base/gitLog.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { COMMITS_SEPARATOR } from '../../../../cli/git/constants'
44
const data = `${expShowFixture[1].rev}
55
github-actions[bot]
66
6 hours ago
7-
refNames:tag: 0.9.3, origin/main, origin/HEAD, main
7+
refNames:HEAD, tag: 0.9.3, origin/main, origin/HEAD, main
88
message:Update version and CHANGELOG for release (#4022)
99
1010
Co-authored-by: Olivaw[bot] <[email protected]>${COMMITS_SEPARATOR}${expShowFixture[2].rev}

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
ValueTree,
4646
FileDataOrError
4747
} from '../../../../../cli/dvc/contract'
48+
import { tagsColumnLike } from '../../../../../experiments/columns/like'
4849

4950
suite('Experiments Filter By Tree Test Suite', () => {
5051
const disposable = Disposable.fn()
@@ -368,6 +369,36 @@ suite('Experiments Filter By Tree Test Suite', () => {
368369
expect(messageSpy).to.be.calledWithMatch(filteredTableData)
369370
}).timeout(WEBVIEW_TEST_TIMEOUT)
370371

372+
it('should be able to filter to tagged commits', async () => {
373+
const { experiments, messageSpy } =
374+
await stubWorkspaceGettersWebview(disposable)
375+
376+
const messageSent = waitForSpyCall(messageSpy, messageSpy.callCount)
377+
await addFilterViaQuickInput(experiments, {
378+
operator: Operator.CONTAINS,
379+
path: tagsColumnLike.path,
380+
value: '9.3'
381+
})
382+
383+
const [workspace, main] = rowsFixture
384+
385+
const mainNoSubRows = { ...main }
386+
delete mainNoSubRows.subRows
387+
388+
const filteredRows = [workspace, mainNoSubRows]
389+
390+
const filteredTableData: Partial<TableData> = {
391+
changes: workspaceChangesFixture,
392+
columnOrder: columnsOrderFixture,
393+
columns: columnsFixture,
394+
filters: ['Git Tag'],
395+
rows: filteredRows
396+
}
397+
398+
await messageSent
399+
expect(messageSpy).to.be.calledWithMatch(filteredTableData)
400+
}).timeout(WEBVIEW_TEST_TIMEOUT)
401+
371402
it('should provide a shortcut to filter to starred experiments', async () => {
372403
const { experimentsModel } = await stubWorkspaceGetters(disposable)
373404

extension/src/test/suite/experiments/model/filterBy/util.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,22 @@ import { FilterDefinition } from '../../../../../experiments/model/filterBy'
55
import { experimentsUpdatedEvent } from '../../../util'
66
import { Experiments } from '../../../../../experiments'
77
import { RegisteredCommands } from '../../../../../commands/external'
8-
import { addStarredToColumns } from '../../../../../experiments/columns/like'
8+
import {
9+
addToColumns,
10+
starredColumnLike,
11+
tagsColumnLike
12+
} from '../../../../../experiments/columns/like'
913

1014
export const mockQuickInputFilter = (
1115
fixtureFilter: FilterDefinition,
1216
mockShowQuickPick = stub(window, 'showQuickPick'),
1317
mockShowInputBox = stub(window, 'showInputBox')
1418
) => {
15-
const column = addStarredToColumns(columnsFixture)?.find(
16-
column => column.path === fixtureFilter.path
17-
)
19+
const column = addToColumns(
20+
columnsFixture,
21+
starredColumnLike,
22+
tagsColumnLike
23+
)?.find(column => column.path === fixtureFilter.path)
1824
mockShowQuickPick
1925
.onFirstCall()
2026
.resolves({ value: column } as unknown as QuickPickItem)

0 commit comments

Comments
 (0)