Skip to content

Commit ead0a70

Browse files
committed
add get started component to plots webview
1 parent f2e869a commit ead0a70

File tree

19 files changed

+363
-7
lines changed

19 files changed

+363
-7
lines changed

extension/src/plots/index.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export class Plots extends BaseRepository<TPlotsData> {
4040

4141
private readonly pathsChanged = this.dispose.track(new EventEmitter<void>())
4242

43+
private experiments?: Experiments
44+
4345
private plots?: PlotsModel
4446
private paths?: PathsModel
4547

@@ -80,6 +82,8 @@ export class Plots extends BaseRepository<TPlotsData> {
8082
}
8183

8284
public setExperiments(experiments: Experiments) {
85+
this.experiments = experiments
86+
8387
this.plots = this.dispose.track(
8488
new PlotsModel(this.dvcRoot, experiments, this.workspaceState)
8589
)
@@ -165,6 +169,11 @@ export class Plots extends BaseRepository<TPlotsData> {
165169
this.webview?.show({
166170
checkpoint: this.getCheckpointPlots(),
167171
comparison: this.getComparisonPlots(),
172+
hasPlots: !!this.paths?.hasPaths(),
173+
hasSelectedPlots: definedAndNonEmpty(this.paths?.getSelected()),
174+
hasSelectedRevisions: definedAndNonEmpty(
175+
this.plots?.getSelectedRevisionDetails()
176+
),
168177
sectionCollapsed: this.plots?.getSectionCollapsed(),
169178
template: this.getTemplatePlots()
170179
})
@@ -250,6 +259,10 @@ export class Plots extends BaseRepository<TPlotsData> {
250259
return this.setTemplateOrder(message.payload)
251260
case MessageFromWebviewType.REORDER_PLOTS_METRICS:
252261
return this.setMetricOrder(message.payload)
262+
case MessageFromWebviewType.SELECT_PLOTS:
263+
return this.selectPlotsFromWebview()
264+
case MessageFromWebviewType.SELECT_EXPERIMENTS:
265+
return this.selectExperimentsFromWebview()
253266
default:
254267
Logger.error(`Unexpected message: ${JSON.stringify(message)}`)
255268
}
@@ -321,6 +334,20 @@ export class Plots extends BaseRepository<TPlotsData> {
321334
this.sendCheckpointPlotsAndEvent(EventName.VIEWS_REORDER_PLOTS_METRICS)
322335
}
323336

337+
private selectPlotsFromWebview() {
338+
this.selectPlots()
339+
sendTelemetryEvent(EventName.VIEWS_PLOTS_SELECT_PLOTS, undefined, undefined)
340+
}
341+
342+
private selectExperimentsFromWebview() {
343+
this.experiments?.selectExperiments()
344+
sendTelemetryEvent(
345+
EventName.VIEWS_PLOTS_SELECT_EXPERIMENTS,
346+
undefined,
347+
undefined
348+
)
349+
}
350+
324351
private sendCheckpointPlotsAndEvent(
325352
event:
326353
| typeof EventName.VIEWS_REORDER_PLOTS_METRICS

extension/src/plots/webview/contract.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ export type PlotsData =
130130
| {
131131
comparison?: PlotsComparisonData | null
132132
checkpoint?: CheckpointPlotsData | null
133+
hasPlots?: boolean
134+
hasSelectedPlots?: boolean
135+
hasSelectedRevisions?: boolean
133136
template?: TemplatePlotsData | null
134137
sectionCollapsed?: SectionCollapsed
135138
}

extension/src/telemetry/constants.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export const EventName = Object.assign(
5151
VIEWS_PLOTS_REVISIONS_REORDERED: 'views.plots.revisionsReordered',
5252
VIEWS_PLOTS_SECTION_RESIZED: 'views.plots.sectionResized',
5353
VIEWS_PLOTS_SECTION_TOGGLE: 'views.plots.toggleSection',
54+
VIEWS_PLOTS_SELECT_EXPERIMENTS: 'view.plots.selectExperiments',
55+
VIEWS_PLOTS_SELECT_PLOTS: 'view.plots.selectPlots',
5456
VIEWS_REORDER_PLOTS_METRICS: 'views.plots.metricsReordered',
5557
VIEWS_REORDER_PLOTS_TEMPLATES: 'views.plots.templatesReordered',
5658

@@ -183,12 +185,14 @@ export interface IEventNamePropertyMapping {
183185
[EventName.VIEWS_PLOTS_CLOSED]: undefined
184186
[EventName.VIEWS_PLOTS_CREATED]: undefined
185187
[EventName.VIEWS_PLOTS_FOCUS_CHANGED]: WebviewFocusChangedProperties
186-
[EventName.VIEWS_REORDER_PLOTS_METRICS]: undefined
187188
[EventName.VIEWS_PLOTS_METRICS_SELECTED]: undefined
188-
[EventName.VIEWS_PLOTS_REVISIONS_REORDERED]: undefined
189189
[EventName.VIEWS_PLOTS_RENAME_SECTION]: { section: Section }
190+
[EventName.VIEWS_PLOTS_REVISIONS_REORDERED]: undefined
190191
[EventName.VIEWS_PLOTS_SECTION_RESIZED]: { section: Section; size: PlotSize }
191192
[EventName.VIEWS_PLOTS_SECTION_TOGGLE]: Partial<SectionCollapsed>
193+
[EventName.VIEWS_PLOTS_SELECT_EXPERIMENTS]: undefined
194+
[EventName.VIEWS_PLOTS_SELECT_PLOTS]: undefined
195+
[EventName.VIEWS_REORDER_PLOTS_METRICS]: undefined
192196
[EventName.VIEWS_REORDER_PLOTS_TEMPLATES]: undefined
193197

194198
[EventName.VIEWS_PLOTS_PATH_TREE_OPENED]: DvcRootCount

extension/src/test/suite/plots/index.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,58 @@ suite('Plots Test Suite', () => {
466466
}).timeout(WEBVIEW_TEST_TIMEOUT)
467467
})
468468

469+
it('should handle a select experiments message from the webview', async () => {
470+
const { plots, experiments } = await buildPlots(
471+
disposable,
472+
plotsDiffFixture
473+
)
474+
475+
const mockSelectExperiments = stub(
476+
experiments,
477+
'selectExperiments'
478+
).resolves(undefined)
479+
480+
const webview = await plots.showWebview()
481+
482+
const mockSendTelemetryEvent = stub(Telemetry, 'sendTelemetryEvent')
483+
const mockMessageReceived = getMessageReceivedEmitter(webview)
484+
485+
mockMessageReceived.fire({
486+
type: MessageFromWebviewType.SELECT_EXPERIMENTS
487+
})
488+
489+
expect(mockSelectExperiments).to.be.calledOnce
490+
expect(mockSendTelemetryEvent).to.be.calledOnce
491+
expect(mockSendTelemetryEvent).to.be.calledWithExactly(
492+
EventName.VIEWS_PLOTS_SELECT_EXPERIMENTS,
493+
undefined,
494+
undefined
495+
)
496+
}).timeout(WEBVIEW_TEST_TIMEOUT)
497+
498+
it('should handle a select plots message from the webview', async () => {
499+
const { plots } = await buildPlots(disposable, plotsDiffFixture)
500+
501+
const mockSelectExperiments = stub(plots, 'selectPlots').resolves(undefined)
502+
503+
const webview = await plots.showWebview()
504+
505+
const mockSendTelemetryEvent = stub(Telemetry, 'sendTelemetryEvent')
506+
const mockMessageReceived = getMessageReceivedEmitter(webview)
507+
508+
mockMessageReceived.fire({
509+
type: MessageFromWebviewType.SELECT_PLOTS
510+
})
511+
512+
expect(mockSelectExperiments).to.be.calledOnce
513+
expect(mockSendTelemetryEvent).to.be.calledOnce
514+
expect(mockSendTelemetryEvent).to.be.calledWithExactly(
515+
EventName.VIEWS_PLOTS_SELECT_PLOTS,
516+
undefined,
517+
undefined
518+
)
519+
}).timeout(WEBVIEW_TEST_TIMEOUT)
520+
469521
describe('showWebview', () => {
470522
it('should be able to make the plots webview visible', async () => {
471523
const { plots, messageSpy, mockPlotsDiff } = await buildPlots(
@@ -493,6 +545,9 @@ suite('Plots Test Suite', () => {
493545
const expectedPlotsData: TPlotsData = {
494546
checkpoint: checkpointPlotsFixture,
495547
comparison: comparisonPlotsFixture,
548+
hasPlots: true,
549+
hasSelectedPlots: true,
550+
hasSelectedRevisions: true,
496551
sectionCollapsed: DEFAULT_SECTION_COLLAPSED,
497552
template: templatePlotsFixture
498553
}

extension/src/webview/contract.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export enum MessageFromWebviewType {
2525
RESIZE_PLOTS = 'resize-plots',
2626
SORT_COLUMN = 'sort-column',
2727
TOGGLE_EXPERIMENT = 'toggle-experiment',
28+
SELECT_EXPERIMENTS = 'select-experiments',
29+
SELECT_PLOTS = 'select-plots',
2830
TOGGLE_METRIC = 'toggle-metric',
2931
TOGGLE_PLOTS_SECTION = 'toggle-plots-section',
3032
VARY_EXPERIMENT_PARAMS_AND_QUEUE = 'vary-experiment-params-and-queue',
@@ -115,6 +117,8 @@ export type MessageFromWebview =
115117
payload: PlotSectionRenamedPayload
116118
}
117119
| { type: MessageFromWebviewType.INITIALIZED }
120+
| { type: MessageFromWebviewType.SELECT_EXPERIMENTS }
121+
| { type: MessageFromWebviewType.SELECT_PLOTS }
118122

119123
export type MessageToWebview<T extends WebviewData> = {
120124
type: MessageToWebviewType.SET_DATA
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/* eslint-disable unicorn/filename-case */
2+
import React from 'react'
3+
4+
type MockButtonProps = {
5+
onClick: () => void
6+
children: React.ReactChildren
7+
}
8+
9+
export const VSCodeButton: React.FC<MockButtonProps> = ({
10+
onClick,
11+
children
12+
}: MockButtonProps) => {
13+
return <button onClick={onClick}>{children}</button>
14+
}

webview/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
},
2424
"dependencies": {
2525
"@tippyjs/react": "^4.2.6",
26+
"@vscode/webview-ui-toolkit": "^1.0.0",
2627
"classnames": "^2.2.6",
2728
"react": "^17.0.1",
2829
"react-beautiful-dnd": "^13.1.0",

webview/src/plots/components/App.test.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,35 @@ describe('App', () => {
132132
expect(emptyState).toBeInTheDocument()
133133
})
134134

135+
it('should render the get started buttons when no plots or experiments are selected', async () => {
136+
renderAppWithData({
137+
checkpoint: null,
138+
hasPlots: true,
139+
hasSelectedPlots: false,
140+
hasSelectedRevisions: false,
141+
sectionCollapsed: DEFAULT_SECTION_COLLAPSED
142+
})
143+
const addPlotsButton = await screen.findByText('Add Plots')
144+
const addExperimentsButton = await screen.findByText('Add Experiments')
145+
146+
expect(addPlotsButton).toBeInTheDocument()
147+
expect(addExperimentsButton).toBeInTheDocument()
148+
149+
mockPostMessage.mockReset()
150+
151+
fireEvent.click(addPlotsButton)
152+
153+
expect(mockPostMessage).toBeCalledWith({
154+
type: MessageFromWebviewType.SELECT_PLOTS
155+
})
156+
mockPostMessage.mockReset()
157+
158+
fireEvent.click(addExperimentsButton)
159+
expect(mockPostMessage).toBeCalledWith({
160+
type: MessageFromWebviewType.SELECT_EXPERIMENTS
161+
})
162+
})
163+
135164
it('should render only checkpoint plots when given a message with only checkpoint plots data', () => {
136165
renderAppWithData({
137166
checkpoint: checkpointPlotsFixture,
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React from 'react'
2+
import { MessageFromWebviewType } from 'dvc/src/webview/contract'
3+
import { sendMessage } from '../../shared/vscode'
4+
import { EmptyState } from '../../shared/components/emptyState/EmptyState'
5+
import { StartButton } from '../../shared/components/button/StartButton'
6+
7+
export type GetStartedProps = {
8+
hasPlots?: boolean
9+
hasSelectedPlots?: boolean
10+
hasSelectedRevisions?: boolean
11+
}
12+
13+
export const GetStarted = ({
14+
hasPlots,
15+
hasSelectedPlots,
16+
hasSelectedRevisions
17+
}: GetStartedProps) => {
18+
return (
19+
<EmptyState>
20+
<div>
21+
<p>No Plots to Display</p>
22+
{hasPlots && (
23+
<div>
24+
{!hasSelectedPlots && (
25+
<StartButton
26+
onClick={() =>
27+
sendMessage({
28+
type: MessageFromWebviewType.SELECT_PLOTS
29+
})
30+
}
31+
text={'Add Plots'}
32+
/>
33+
)}
34+
{!hasSelectedRevisions && (
35+
<StartButton
36+
appearance={!hasSelectedPlots ? 'secondary' : 'primary'}
37+
isNested={!hasSelectedPlots}
38+
onClick={() =>
39+
sendMessage({
40+
type: MessageFromWebviewType.SELECT_EXPERIMENTS
41+
})
42+
}
43+
text={'Add Experiments'}
44+
/>
45+
)}
46+
</div>
47+
)}
48+
</div>
49+
</EmptyState>
50+
)
51+
}

webview/src/plots/components/Plots.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React, { useEffect, useRef, useState, useCallback } from 'react'
44
import VegaLite, { VegaLiteProps } from 'react-vega/lib/VegaLite'
55
import { Config } from 'vega-lite'
66
import styles from './styles.module.scss'
7+
import { GetStarted } from './GetStarted'
78
import { CheckpointPlotsWrapper } from './checkpointPlots/CheckpointPlotsWrapper'
89
import { TemplatePlotsWrapper } from './templatePlots/TemplatePlotsWrapper'
910
import { ComparisonTableWrapper } from './comparisonTable/ComparisonTableWrapper'
@@ -54,13 +55,22 @@ export const Plots = ({ state }: { state: PlotsWebviewState }) => {
5455

5556
const {
5657
checkpoint: checkpointPlots,
58+
comparison: comparisonTable,
59+
hasPlots,
60+
hasSelectedPlots,
61+
hasSelectedRevisions,
5762
sectionCollapsed,
58-
template: templatePlots,
59-
comparison: comparisonTable
63+
template: templatePlots
6064
} = data
6165

6266
if (!checkpointPlots && !templatePlots && !comparisonTable) {
63-
return <EmptyState>No Plots to Display</EmptyState>
67+
return (
68+
<GetStarted
69+
hasPlots={hasPlots}
70+
hasSelectedPlots={hasSelectedPlots}
71+
hasSelectedRevisions={hasSelectedRevisions}
72+
/>
73+
)
6474
}
6575

6676
const changeSize = (size: PlotSize, section: Section) => {

0 commit comments

Comments
 (0)