Skip to content

Commit b91ffed

Browse files
authored
Add manual refresh button to missing plots (#1754)
* add basic refresh button with action to comparison table * add ability to refresh a specific revision * add unit test for button click action * add box to empty plot * update toast text * align images
1 parent 3010d0c commit b91ffed

File tree

16 files changed

+209
-25
lines changed

16 files changed

+209
-25
lines changed

extension/src/plots/index.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { ExperimentsOutput, TEMP_PLOTS_DIR } from '../cli/reader'
3030
import { getModifiedTime, removeDir } from '../fileSystem'
3131
import { sendTelemetryEvent } from '../telemetry'
3232
import { EventName } from '../telemetry/constants'
33+
import { Toast } from '../vscode/toast'
3334

3435
export type PlotsWebview = BaseWebview<TPlotsData>
3536

@@ -228,9 +229,11 @@ export class Plots extends BaseRepository<TPlotsData> {
228229
if (this.webview) {
229230
return {
230231
...plot,
231-
url: `${this.webview.getWebviewUri(plot.url)}?${getModifiedTime(
232-
plot.url
233-
)}`
232+
url: plot.url
233+
? `${this.webview.getWebviewUri(plot.url)}?${getModifiedTime(
234+
plot.url
235+
)}`
236+
: undefined
234237
}
235238
}
236239
}
@@ -263,6 +266,8 @@ export class Plots extends BaseRepository<TPlotsData> {
263266
return this.selectPlotsFromWebview()
264267
case MessageFromWebviewType.SELECT_EXPERIMENTS:
265268
return this.selectExperimentsFromWebview()
269+
case MessageFromWebviewType.REFRESH_REVISION:
270+
return this.attemptToRefreshData(message.payload)
266271
default:
267272
Logger.error(`Unexpected message: ${JSON.stringify(message)}`)
268273
}
@@ -348,6 +353,17 @@ export class Plots extends BaseRepository<TPlotsData> {
348353
)
349354
}
350355

356+
private attemptToRefreshData(revision: string) {
357+
Toast.infoWithOptions(`Attempting to refresh ${revision} plots data.`)
358+
this.plots?.setupManualRefresh(revision)
359+
this.data.managedUpdate()
360+
sendTelemetryEvent(
361+
EventName.VIEWS_PLOTS_MANUAL_REFRESH,
362+
undefined,
363+
undefined
364+
)
365+
}
366+
351367
private sendCheckpointPlotsAndEvent(
352368
event:
353369
| typeof EventName.VIEWS_REORDER_PLOTS_METRICS

extension/src/plots/model/index.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ export class PlotsModel extends ModelWithPersistence {
157157
}
158158
}
159159

160+
public setupManualRefresh(id: string) {
161+
this.deleteRevisionData(id)
162+
}
163+
160164
public getUnfetchedRevisions() {
161165
return this.getSelectedRevisions().filter(
162166
revision => !this.fetchedRevs.has(revision)
@@ -315,14 +319,18 @@ export class PlotsModel extends ModelWithPersistence {
315319
private removeStaleBranches() {
316320
for (const { id, sha } of this.experiments.getBranchRevisions()) {
317321
if (sha && this.branchRevisions[id] !== sha) {
318-
delete this.revisionData[id]
319-
delete this.comparisonData[id]
320-
this.fetchedRevs.delete(id)
322+
this.deleteRevisionData(id)
321323
this.branchRevisions[id] = sha
322324
}
323325
}
324326
}
325327

328+
private deleteRevisionData(id: string) {
329+
delete this.revisionData[id]
330+
delete this.comparisonData[id]
331+
this.fetchedRevs.delete(id)
332+
}
333+
326334
private getSelectedRevisions() {
327335
return this.experiments.getSelectedRevisions().map(({ label }) => label)
328336
}
@@ -369,11 +377,9 @@ export class PlotsModel extends ModelWithPersistence {
369377

370378
for (const revision of selectedRevisions) {
371379
const image = this.comparisonData?.[revision]?.[path]
372-
if (image) {
373-
pathRevisions.revisions[revision] = {
374-
revision,
375-
url: image.url
376-
}
380+
pathRevisions.revisions[revision] = {
381+
revision,
382+
url: image?.url
377383
}
378384
}
379385
acc.push(pathRevisions)

extension/src/plots/webview/contract.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export interface TemplatePlotsData {
126126
}
127127

128128
export type ComparisonPlot = {
129-
url: string
129+
url: string | undefined
130130
revision: string
131131
}
132132

extension/src/telemetry/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export const EventName = Object.assign(
4646
VIEWS_PLOTS_CLOSED: 'views.plots.closed',
4747
VIEWS_PLOTS_CREATED: 'views.plots.created',
4848
VIEWS_PLOTS_FOCUS_CHANGED: 'views.plots.focusChanged',
49+
VIEWS_PLOTS_MANUAL_REFRESH: 'views.plots.manualRefresh',
4950
VIEWS_PLOTS_METRICS_SELECTED: 'views.plots.metricsSelected',
5051
VIEWS_PLOTS_RENAME_SECTION: 'views.plots.sectionRenamed',
5152
VIEWS_PLOTS_REVISIONS_REORDERED: 'views.plots.revisionsReordered',
@@ -190,6 +191,7 @@ export interface IEventNamePropertyMapping {
190191
[EventName.VIEWS_PLOTS_CLOSED]: undefined
191192
[EventName.VIEWS_PLOTS_CREATED]: undefined
192193
[EventName.VIEWS_PLOTS_FOCUS_CHANGED]: WebviewFocusChangedProperties
194+
[EventName.VIEWS_PLOTS_MANUAL_REFRESH]: undefined
193195
[EventName.VIEWS_PLOTS_METRICS_SELECTED]: undefined
194196
[EventName.VIEWS_PLOTS_RENAME_SECTION]: { section: Section }
195197
[EventName.VIEWS_PLOTS_REVISIONS_REORDERED]: undefined

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,42 @@ suite('Plots Test Suite', () => {
519519
)
520520
}).timeout(WEBVIEW_TEST_TIMEOUT)
521521

522+
it('should handle a manual refresh message from the webview', async () => {
523+
const { data, plots, plotsModel, mockPlotsDiff } = await buildPlots(
524+
disposable,
525+
plotsDiffFixture
526+
)
527+
528+
const removeDataSpy = spy(plotsModel, 'setupManualRefresh')
529+
530+
const webview = await plots.showWebview()
531+
mockPlotsDiff.resetHistory()
532+
533+
const mockSendTelemetryEvent = stub(Telemetry, 'sendTelemetryEvent')
534+
const mockMessageReceived = getMessageReceivedEmitter(webview)
535+
536+
const dataUpdateEvent = new Promise(resolve =>
537+
data.onDidUpdate(() => resolve(undefined))
538+
)
539+
540+
mockMessageReceived.fire({
541+
payload: 'main',
542+
type: MessageFromWebviewType.REFRESH_REVISION
543+
})
544+
545+
await dataUpdateEvent
546+
547+
expect(removeDataSpy).to.be.calledOnce
548+
expect(mockSendTelemetryEvent).to.be.calledOnce
549+
expect(mockSendTelemetryEvent).to.be.calledWithExactly(
550+
EventName.VIEWS_PLOTS_MANUAL_REFRESH,
551+
undefined,
552+
undefined
553+
)
554+
expect(mockPlotsDiff).to.be.calledOnce
555+
expect(mockPlotsDiff).to.be.calledWithExactly(dvcDemoPath, 'main')
556+
}).timeout(WEBVIEW_TEST_TIMEOUT)
557+
522558
it('should be able to make the plots webview visible', async () => {
523559
const { plots, messageSpy, mockPlotsDiff } = await buildPlots(
524560
disposable,

extension/src/webview/contract.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export enum MessageFromWebviewType {
2121
REORDER_PLOTS_COMPARISON = 'reorder-plots-comparison',
2222
REORDER_PLOTS_METRICS = 'reorder-plots-metrics',
2323
REORDER_PLOTS_TEMPLATES = 'reorder-plots-templates',
24+
REFRESH_REVISION = 'refresh-revision',
2425
RESIZE_COLUMN = 'resize-column',
2526
RESIZE_PLOTS = 'resize-plots',
2627
SORT_COLUMN = 'sort-column',
@@ -124,6 +125,7 @@ export type MessageFromWebview =
124125
| { type: MessageFromWebviewType.INITIALIZED }
125126
| { type: MessageFromWebviewType.SELECT_EXPERIMENTS }
126127
| { type: MessageFromWebviewType.SELECT_PLOTS }
128+
| { type: MessageFromWebviewType.REFRESH_REVISION; payload: string }
127129

128130
export type MessageToWebview<T extends WebviewData> = {
129131
type: MessageToWebviewType.SET_DATA

webview/src/plots/components/comparisonTable/ComparisonTable.test.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,39 @@ describe('ComparisonTable', () => {
223223
expect(pinnedColumn.textContent).toBe(pinnedRevision)
224224
})
225225

226+
it('should display a refresh button for each revision that has a missing image', () => {
227+
const revisionWithNoData = 'missing-data'
228+
229+
renderTable({
230+
...basicProps,
231+
plots: basicProps.plots.map(({ path, revisions }) => ({
232+
path,
233+
revisions: {
234+
...revisions,
235+
[revisionWithNoData]: { revision: revisionWithNoData, url: undefined }
236+
}
237+
})),
238+
revisions: [
239+
...basicProps.revisions,
240+
{ displayColor: '#f56565', revision: revisionWithNoData }
241+
]
242+
})
243+
244+
const refreshButtons = screen.getAllByText('Refresh')
245+
246+
expect(refreshButtons).toHaveLength(basicProps.plots.length)
247+
248+
for (const button of refreshButtons) {
249+
fireEvent.click(button)
250+
expect(mockPostMessage).toBeCalledTimes(1)
251+
expect(mockPostMessage).toBeCalledWith({
252+
payload: revisionWithNoData,
253+
type: MessageFromWebviewType.REFRESH_REVISION
254+
})
255+
mockPostMessage.mockReset()
256+
}
257+
})
258+
226259
describe('Columns drag and drop', () => {
227260
const pinSecondColumn = () => {
228261
const secondColumn = getPin(screen.getByText(revisions[1]))

webview/src/plots/components/comparisonTable/ComparisonTableRow.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
} from './ComparisonTableRow'
1313
import styles from '../styles.module.scss'
1414

15+
jest.mock('../../../shared/api')
16+
1517
describe('ComparisonTableRow', () => {
1618
afterEach(() => {
1719
cleanup()

webview/src/plots/components/comparisonTable/ComparisonTableRow.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { ComparisonPlot } from 'dvc/src/plots/webview/contract'
2+
import { MessageFromWebviewType } from 'dvc/src/webview/contract'
23
import React, { useState } from 'react'
34
import cx from 'classnames'
45
import styles from './styles.module.scss'
56
import { AllIcons, Icon } from '../../../shared/components/Icon'
7+
import { RefreshButton } from '../../../shared/components/button/RefreshButton'
8+
import { sendMessage } from '../../../shared/vscode'
69

710
export interface ComparisonTableRowProps {
811
path: string
@@ -35,23 +38,33 @@ export const ComparisonTableRow: React.FC<ComparisonTableRowProps> = ({
3538
{nbColumns > 1 && <td colSpan={nbColumns - 1}></td>}
3639
</tr>
3740
<tr>
38-
{plots.map((plot: ComparisonPlot | undefined) => {
39-
const isPinned = pinnedColumn === plot?.revision
41+
{plots.map((plot: ComparisonPlot) => {
42+
const isPinned = pinnedColumn === plot.revision
4043
const missing = !plot?.url
4144

4245
return (
4346
<td
44-
key={path + plot?.revision || 'missing'}
47+
key={path + plot.revision}
4548
className={cx({
4649
[styles.pinnedColumnCell]: isPinned,
47-
[styles.missing]: missing
50+
[styles.missing]: isShown && missing
4851
})}
4952
>
5053
<div
5154
className={cx(styles.cell, { [styles.cellHidden]: !isShown })}
5255
>
5356
{missing ? (
54-
<p>No plot to display.</p>
57+
<div>
58+
<p>No Plot to Display.</p>
59+
<RefreshButton
60+
onClick={() =>
61+
sendMessage({
62+
payload: plot.revision,
63+
type: MessageFromWebviewType.REFRESH_REVISION
64+
})
65+
}
66+
/>
67+
</div>
5568
) : (
5669
<img
5770
src={plot.url}

webview/src/plots/components/comparisonTable/styles.module.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ $gap: 4px;
8383

8484
.missing {
8585
background-color: $bg-color;
86+
border-style: solid;
87+
border-width: thin;
88+
border-color: $watermark-color;
8689
}
8790

8891
.rowToggler {
@@ -148,6 +151,7 @@ $gap: 4px;
148151
.cell img {
149152
width: 100%;
150153
height: auto;
154+
vertical-align: middle;
151155
}
152156

153157
.experimentName {

0 commit comments

Comments
 (0)