Skip to content

Commit a520571

Browse files
authored
Add tooltips to experiments tree (#2706)
* add md table to experiment tooltip, showing values of first three columns
1 parent 3e4e100 commit a520571

File tree

9 files changed

+314
-43
lines changed

9 files changed

+314
-43
lines changed

extension/src/experiments/columns/model.test.ts

Lines changed: 83 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Disposable, Disposer } from '@hediet/std/disposable'
12
import { ColumnsModel } from './model'
23
import { appendColumnToPath, buildMetricOrParamPath } from './paths'
34
import { timestampColumn } from './constants'
@@ -20,89 +21,139 @@ import dataTypesOutputFixture from '../../test/fixtures/expShow/dataTypes/output
2021
import survivalOutputFixture from '../../test/fixtures/expShow/survival/output'
2122
import survivalColumnsFixture from '../../test/fixtures/expShow/survival/columns'
2223
import { getConfigValue } from '../../vscode/config'
24+
import { buildMockedEventEmitter } from '../../test/util/jest'
2325

2426
jest.mock('../../vscode/config')
27+
jest.mock('@hediet/std/disposable')
2528

2629
const mockedGetConfigValue = jest.mocked(getConfigValue)
30+
const mockedDisposable = jest.mocked(Disposable)
2731

2832
beforeEach(() => {
2933
jest.resetAllMocks()
3034
mockedGetConfigValue.mockReturnValue(5)
35+
36+
mockedDisposable.fn.mockReturnValue({
37+
track: function <T>(disposable: T): T {
38+
return disposable
39+
}
40+
} as unknown as (() => void) & Disposer)
3141
})
3242

3343
describe('ColumnsModel', () => {
3444
const exampleDvcRoot = 'test'
45+
const mockedColumnsOrderChanged = buildMockedEventEmitter<void>()
3546

3647
it('should return the expected columns when given the default output fixture', async () => {
37-
const model = new ColumnsModel('', buildMockMemento())
48+
const model = new ColumnsModel(
49+
'',
50+
buildMockMemento(),
51+
mockedColumnsOrderChanged
52+
)
3853
await model.transformAndSet(outputFixture)
3954
expect(mockedGetConfigValue).toHaveBeenCalled()
4055
expect(model.getSelected()).toStrictEqual(columnsFixture)
4156
})
4257

4358
it('should return the expected columns when given the survival output fixture', async () => {
44-
const model = new ColumnsModel('', buildMockMemento())
59+
const model = new ColumnsModel(
60+
'',
61+
buildMockMemento(),
62+
mockedColumnsOrderChanged
63+
)
4564
await model.transformAndSet(survivalOutputFixture)
4665
expect(model.getSelected()).toStrictEqual(survivalColumnsFixture)
4766
})
4867

4968
it('should return the expected columns when given the deeply nested output fixture', async () => {
50-
const model = new ColumnsModel('', buildMockMemento())
69+
const model = new ColumnsModel(
70+
'',
71+
buildMockMemento(),
72+
mockedColumnsOrderChanged
73+
)
5174
await model.transformAndSet(deeplyNestedOutputFixture)
5275
expect(mockedGetConfigValue).toHaveBeenCalled()
5376
expect(model.getSelected()).toStrictEqual(deeplyNestedColumnsFixture)
5477
})
5578

5679
it('should return the expected columns when the max depth config is set to 10', async () => {
5780
mockedGetConfigValue.mockReturnValue(10)
58-
const model = new ColumnsModel('', buildMockMemento())
81+
const model = new ColumnsModel(
82+
'',
83+
buildMockMemento(),
84+
mockedColumnsOrderChanged
85+
)
5986
await model.transformAndSet(deeplyNestedOutputFixture)
6087
expect(mockedGetConfigValue).toHaveBeenCalled()
6188
expect(model.getSelected()).toStrictEqual(deeplyNestedColumnsWithHeightOf10)
6289
})
6390

6491
it('should return the expected columns when the max depth config is set to 3', async () => {
6592
mockedGetConfigValue.mockReturnValue(3)
66-
const model = new ColumnsModel('', buildMockMemento())
93+
const model = new ColumnsModel(
94+
'',
95+
buildMockMemento(),
96+
mockedColumnsOrderChanged
97+
)
6798
await model.transformAndSet(deeplyNestedOutputFixture)
6899
expect(mockedGetConfigValue).toHaveBeenCalled()
69100
expect(model.getSelected()).toStrictEqual(deeplyNestedColumnsWithHeightOf3)
70101
})
71102

72103
it('should return the expected columns when the max depth config is set to 2', async () => {
73104
mockedGetConfigValue.mockReturnValue(2)
74-
const model = new ColumnsModel('', buildMockMemento())
105+
const model = new ColumnsModel(
106+
'',
107+
buildMockMemento(),
108+
mockedColumnsOrderChanged
109+
)
75110
await model.transformAndSet(deeplyNestedOutputFixture)
76111
expect(mockedGetConfigValue).toHaveBeenCalled()
77112
expect(model.getSelected()).toStrictEqual(deeplyNestedColumnsWithHeightOf2)
78113
})
79114

80115
it('should return the expected columns when the max depth config is set to 1', async () => {
81116
mockedGetConfigValue.mockReturnValue(1)
82-
const model = new ColumnsModel('', buildMockMemento())
117+
const model = new ColumnsModel(
118+
'',
119+
buildMockMemento(),
120+
mockedColumnsOrderChanged
121+
)
83122
await model.transformAndSet(deeplyNestedOutputFixture)
84123
expect(mockedGetConfigValue).toHaveBeenCalled()
85124
expect(model.getSelected()).toStrictEqual(deeplyNestedColumnsWithHeightOf1)
86125
})
87126

88127
it('should return the expected columns when the max depth config is set to 0', async () => {
89128
mockedGetConfigValue.mockReturnValue(0)
90-
const model = new ColumnsModel('', buildMockMemento())
129+
const model = new ColumnsModel(
130+
'',
131+
buildMockMemento(),
132+
mockedColumnsOrderChanged
133+
)
91134
await model.transformAndSet(deeplyNestedOutputFixture)
92135
expect(mockedGetConfigValue).toHaveBeenCalled()
93136
expect(model.getSelected()).toStrictEqual(deeplyNestedColumnsWithHeightOf10)
94137
})
95138

96139
it('should return the expected columns when the max depth config is set to -1', async () => {
97140
mockedGetConfigValue.mockReturnValue(-1)
98-
const model = new ColumnsModel('', buildMockMemento())
141+
const model = new ColumnsModel(
142+
'',
143+
buildMockMemento(),
144+
mockedColumnsOrderChanged
145+
)
99146
await model.transformAndSet(deeplyNestedOutputFixture)
100147
expect(mockedGetConfigValue).toHaveBeenCalled()
101148
expect(model.getSelected()).toStrictEqual(deeplyNestedColumnsWithHeightOf10)
102149
})
103150

104151
it('should return the expected columns when given the data types output fixture', async () => {
105-
const model = new ColumnsModel('', buildMockMemento())
152+
const model = new ColumnsModel(
153+
'',
154+
buildMockMemento(),
155+
mockedColumnsOrderChanged
156+
)
106157
await model.transformAndSet(dataTypesOutputFixture)
107158
expect(model.getSelected()).toStrictEqual(dataTypesColumnsFixture)
108159
})
@@ -129,7 +180,11 @@ describe('ColumnsModel', () => {
129180
}
130181
}
131182
it('Shows all items when given no persisted status', async () => {
132-
const model = new ColumnsModel(exampleDvcRoot, buildMockMemento())
183+
const model = new ColumnsModel(
184+
exampleDvcRoot,
185+
buildMockMemento(),
186+
mockedColumnsOrderChanged
187+
)
133188
await model.transformAndSet(exampleData)
134189
expect(model.getSelected()).toStrictEqual([
135190
timestampColumn,
@@ -161,7 +216,8 @@ describe('ColumnsModel', () => {
161216
[paramsDotYamlPath]: Status.INDETERMINATE,
162217
[testParamPath]: Status.UNSELECTED
163218
}
164-
})
219+
}),
220+
mockedColumnsOrderChanged
165221
)
166222
await model.transformAndSet(exampleData)
167223
expect(model.getSelected()).toStrictEqual([
@@ -189,7 +245,8 @@ describe('ColumnsModel', () => {
189245
buildMockMemento({
190246
[PersistenceKey.METRICS_AND_PARAMS_COLUMN_ORDER + exampleDvcRoot]:
191247
persistedState
192-
})
248+
}),
249+
mockedColumnsOrderChanged
193250
)
194251
expect(model.getColumnOrder()).toStrictEqual(persistedState)
195252
})
@@ -209,7 +266,8 @@ describe('ColumnsModel', () => {
209266
buildMockMemento({
210267
[PersistenceKey.METRICS_AND_PARAMS_COLUMN_ORDER + exampleDvcRoot]:
211268
persistedState
212-
})
269+
}),
270+
mockedColumnsOrderChanged
213271
)
214272

215273
expect(model.getFirstThreeColumnOrder()).toStrictEqual(
@@ -218,7 +276,11 @@ describe('ColumnsModel', () => {
218276
})
219277

220278
it('should return first three columns collected from data if state is empty', async () => {
221-
const model = new ColumnsModel(exampleDvcRoot, buildMockMemento())
279+
const model = new ColumnsModel(
280+
exampleDvcRoot,
281+
buildMockMemento(),
282+
mockedColumnsOrderChanged
283+
)
222284
await model.transformAndSet(outputFixture)
223285

224286
expect(model.getFirstThreeColumnOrder()).toStrictEqual([
@@ -237,7 +299,8 @@ describe('ColumnsModel', () => {
237299
{ path: 'B', width: 0 },
238300
{ path: 'C', width: 0 }
239301
]
240-
})
302+
}),
303+
mockedColumnsOrderChanged
241304
)
242305
const newState = ['C', 'B', 'A']
243306
model.setColumnOrder(newState)
@@ -257,7 +320,8 @@ describe('ColumnsModel', () => {
257320
buildMockMemento({
258321
[PersistenceKey.METRICS_AND_PARAMS_COLUMN_ORDER + exampleDvcRoot]:
259322
persistedState
260-
})
323+
}),
324+
mockedColumnsOrderChanged
261325
)
262326
expect(model.getColumnOrder()).toStrictEqual(persistedState)
263327
})
@@ -273,7 +337,8 @@ describe('ColumnsModel', () => {
273337
buildMockMemento({
274338
[PersistenceKey.METRICS_AND_PARAMS_COLUMN_ORDER + exampleDvcRoot]:
275339
persistedState
276-
})
340+
}),
341+
mockedColumnsOrderChanged
277342
)
278343
const changedColumnId = 'C'
279344
const expectedWidth = 77

extension/src/experiments/columns/model.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
1-
import { Memento } from 'vscode'
1+
import { EventEmitter, Memento } from 'vscode'
22
import { collectChanges, collectColumns, collectParamsFiles } from './collect'
33
import { Column, ColumnType } from '../webview/contract'
44
import { ExperimentsOutput } from '../../cli/dvc/contract'
55
import { PersistenceKey } from '../../persistence/constants'
66
import { PathSelectionModel } from '../../path/selection/model'
77

88
export class ColumnsModel extends PathSelectionModel<Column> {
9+
private columnsOrderChanged: EventEmitter<void>
910
private columnOrderState: string[] = []
1011
private columnWidthsState: Record<string, number> = {}
1112
private columnsChanges: string[] = []
1213
private paramsFiles = new Set<string>()
1314

14-
constructor(dvcRoot: string, workspaceState: Memento) {
15+
constructor(
16+
dvcRoot: string,
17+
workspaceState: Memento,
18+
columnsOrderChanged: EventEmitter<void>
19+
) {
1520
super(dvcRoot, workspaceState, PersistenceKey.METRICS_AND_PARAMS_STATUS)
1621

1722
this.columnOrderState = this.revive(
@@ -22,6 +27,7 @@ export class ColumnsModel extends PathSelectionModel<Column> {
2227
PersistenceKey.METRICS_AND_PARAMS_COLUMN_WIDTHS,
2328
{}
2429
)
30+
this.columnsOrderChanged = columnsOrderChanged
2531
}
2632

2733
public getColumnOrder(): string[] {
@@ -59,6 +65,7 @@ export class ColumnsModel extends PathSelectionModel<Column> {
5965
PersistenceKey.METRICS_AND_PARAMS_COLUMN_ORDER,
6066
this.getColumnOrder()
6167
)
68+
this.columnsOrderChanged.fire()
6269
}
6370

6471
public setColumnWidth(id: string, width: number) {

extension/src/experiments/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export class Experiments extends BaseRepository<TableData> {
5454
public readonly onDidChangeIsParamsFileFocused: Event<string | undefined>
5555
public readonly onDidChangeExperiments: Event<ExperimentsOutput | void>
5656
public readonly onDidChangeColumns: Event<void>
57+
public readonly onDidChangeColumnOrder: Event<void>
5758
public readonly onDidChangeCheckpoints: Event<void>
5859

5960
public readonly viewKey = ViewKey.EXPERIMENTS
@@ -78,6 +79,10 @@ export class Experiments extends BaseRepository<TableData> {
7879
)
7980

8081
private readonly columnsChanged = this.dispose.track(new EventEmitter<void>())
82+
private readonly columnsOrderChanged = this.dispose.track(
83+
new EventEmitter<void>()
84+
)
85+
8186
private readonly decorationProvider = this.dispose.track(
8287
new DecorationProvider()
8388
)
@@ -101,13 +106,16 @@ export class Experiments extends BaseRepository<TableData> {
101106
this.onDidChangeIsParamsFileFocused = this.paramsFileFocused.event
102107
this.onDidChangeExperiments = this.experimentsChanged.event
103108
this.onDidChangeColumns = this.columnsChanged.event
109+
this.onDidChangeColumnOrder = this.columnsOrderChanged.event
104110
this.onDidChangeCheckpoints = this.checkpointsChanged.event
105111

106112
this.experiments = this.dispose.track(
107113
new ExperimentsModel(dvcRoot, workspaceState)
108114
)
109115

110-
this.columns = this.dispose.track(new ColumnsModel(dvcRoot, workspaceState))
116+
this.columns = this.dispose.track(
117+
new ColumnsModel(dvcRoot, workspaceState, this.columnsOrderChanged)
118+
)
111119

112120
this.checkpoints = this.dispose.track(new CheckpointsModel())
113121

@@ -460,6 +468,10 @@ export class Experiments extends BaseRepository<TableData> {
460468
return this.experiments.hasRunningExperiment()
461469
}
462470

471+
public getFirstThreeColumnOrder() {
472+
return this.columns.getFirstThreeColumnOrder()
473+
}
474+
463475
private setupInitialData() {
464476
const waitForInitialData = this.dispose.track(
465477
this.onDidChangeExperiments(() => {

extension/src/experiments/model/quickPicks.ts

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { QuickPickItemKind } from 'vscode'
22
import omit from 'lodash.omit'
3-
import get from 'lodash.get'
43
import { ExperimentWithCheckpoints } from '.'
54
import { MAX_SELECTED_EXPERIMENTS } from './status'
5+
import { getDataFromColumnPath } from './util'
66
import { definedAndNonEmpty } from '../../util/array'
77
import {
88
QuickPickItemWithValue,
@@ -11,8 +11,6 @@ import {
1111
import { Toast } from '../../vscode/toast'
1212
import { Experiment } from '../webview/contract'
1313
import { Title } from '../../vscode/title'
14-
import { splitColumnPath } from '../columns/paths'
15-
import { formatDate } from '../../util/date'
1614
import { truncate, truncateFromLeft } from '../../util/string'
1715

1816
type QuickPickItemAccumulator = {
@@ -28,25 +26,16 @@ const getSeparator = (experiment: Experiment) => ({
2826

2927
const getItem = (experiment: Experiment, firstThreeColumnOrder: string[]) => ({
3028
detail: firstThreeColumnOrder
31-
.map(name => {
32-
const splitUpName = splitColumnPath(name)
33-
const collectedVal = get(experiment, splitUpName)
34-
if (!collectedVal) {
35-
return null
36-
}
37-
const value =
38-
name === 'Created'
39-
? formatDate(collectedVal)
40-
: collectedVal?.value || collectedVal
41-
29+
.map(path => {
30+
const { splitUpPath, value } = getDataFromColumnPath(experiment, path)
4231
const truncatedKey = truncateFromLeft(
43-
splitUpName[splitUpName.length - 1],
32+
splitUpPath[splitUpPath.length - 1],
4433
15
4534
)
4635
const truncatedVal =
4736
typeof value === 'number' ? truncate(String(value), 7) : value
4837

49-
return `${truncatedKey}:${truncatedVal}`
38+
return value ? `${truncatedKey}:${truncatedVal}` : ''
5039
})
5140
.filter(Boolean)
5241
.join(', '),

0 commit comments

Comments
 (0)