Skip to content

Commit 66242bf

Browse files
authored
Open comparison table images when clicking on them (#3467)
* Open comparison table images when clicking on them * Fix and add tests * Use join as join function for resourceUrl * Apply review comments * Fix tests * Add getWebviewUri inside of tests
1 parent 0113642 commit 66242bf

File tree

9 files changed

+99
-19
lines changed

9 files changed

+99
-19
lines changed

extension/src/fileSystem/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
writeFileSync
1111
} from 'fs-extra'
1212
import { load } from 'js-yaml'
13-
import { Uri, workspace, window } from 'vscode'
13+
import { Uri, workspace, window, commands, ViewColumn } from 'vscode'
1414
import { standardizePath } from './path'
1515
import { definedAndNonEmpty } from '../util/array'
1616
import { Logger } from '../common/logger'
@@ -141,6 +141,11 @@ export const openFileInEditor = async (filePath: string) => {
141141
return document
142142
}
143143

144+
export const openImageFileInEditor = async (imagePath: string) =>
145+
await commands.executeCommand('vscode.open', Uri.file(imagePath), {
146+
viewColumn: ViewColumn.Beside
147+
})
148+
144149
export const findOrCreateDvcYamlFile = (
145150
cwd: string,
146151
trainingScript: string,

extension/src/plots/webview/messages.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
import { PlotsModel } from '../model'
2222
import { PathsModel } from '../paths/model'
2323
import { BaseWebview } from '../../webview'
24-
import { getModifiedTime } from '../../fileSystem'
24+
import { getModifiedTime, openImageFileInEditor } from '../../fileSystem'
2525
import { pickCustomPlots, pickMetricAndParam } from '../model/quickPick'
2626
import { Title } from '../../vscode/title'
2727
import { ColumnType } from '../../experiments/webview/contract'
@@ -112,9 +112,13 @@ export class WebviewMessages {
112112
case MessageFromWebviewType.TOGGLE_EXPERIMENT:
113113
return this.setExperimentStatus(message.payload)
114114
case MessageFromWebviewType.ZOOM_PLOT:
115+
if (message.payload) {
116+
const imagePath = this.revertCorrectUrl(message.payload)
117+
void openImageFileInEditor(imagePath)
118+
}
115119
return sendTelemetryEvent(
116120
EventName.VIEWS_PLOTS_ZOOM_PLOT,
117-
undefined,
121+
{ isImage: !!message.payload },
118122
undefined
119123
)
120124
default:
@@ -418,6 +422,15 @@ export class WebviewMessages {
418422
}
419423
}
420424

425+
private revertCorrectUrl(url: string) {
426+
const webview = this.getWebview()
427+
if (webview) {
428+
const toRemove = webview.getWebviewUri('')
429+
return url.replace(toRemove, '').split('?')[0]
430+
}
431+
return url
432+
}
433+
421434
private getCheckpointPlots() {
422435
return this.plots.getCheckpointPlots() || null
423436
}

extension/src/telemetry/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ export interface IEventNamePropertyMapping {
258258
[EventName.VIEWS_PLOTS_SELECT_EXPERIMENTS]: undefined
259259
[EventName.VIEWS_PLOTS_SELECT_PLOTS]: undefined
260260
[EventName.VIEWS_PLOTS_EXPERIMENT_TOGGLE]: undefined
261-
[EventName.VIEWS_PLOTS_ZOOM_PLOT]: undefined
261+
[EventName.VIEWS_PLOTS_ZOOM_PLOT]: { isImage: boolean }
262262
[EventName.VIEWS_REORDER_PLOTS_METRICS]: undefined
263263
[EventName.VIEWS_REORDER_PLOTS_CUSTOM]: undefined
264264
[EventName.VIEWS_REORDER_PLOTS_TEMPLATES]: undefined

extension/src/test/fixtures/plotsDiff/index.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -449,11 +449,8 @@ const getImageData = (baseUrl: string, joinFunc = join) => ({
449449
]
450450
})
451451

452-
export const getOutput = (
453-
baseUrl: string,
454-
joinFunc?: (...args: string[]) => string
455-
): PlotsOutput => ({
456-
...getImageData(baseUrl, joinFunc),
452+
export const getOutput = (baseUrl: string): PlotsOutput => ({
453+
...getImageData(baseUrl),
457454
...basicVega,
458455
...require('./vega').default
459456
})
@@ -689,14 +686,18 @@ export const getComparisonWebviewMessage = (
689686
joinFunc?: (...args: string[]) => string
690687
): PlotsComparisonData => {
691688
const plotAcc = [] as ComparisonPlots
689+
692690
for (const [path, plots] of Object.entries(getImageData(baseUrl, joinFunc))) {
693691
const revisionsAcc: ComparisonRevisionData = {}
694692
for (const { url, revisions } of plots) {
695693
const revision = replaceCommitCLIId(revisions?.[0])
696694
if (!revision) {
697695
continue
698696
}
699-
revisionsAcc[revision] = { url: `${url}?${MOCK_IMAGE_MTIME}`, revision }
697+
revisionsAcc[revision] = {
698+
url: `${url}?${MOCK_IMAGE_MTIME}`,
699+
revision
700+
}
700701
}
701702

702703
plotAcc.push({ path, revisions: revisionsAcc })

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

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import cloneDeep from 'lodash.clonedeep'
44
import { afterEach, beforeEach, describe, it, suite } from 'mocha'
55
import { expect } from 'chai'
66
import { restore, spy, stub } from 'sinon'
7+
import { commands, Uri } from 'vscode'
78
import { buildPlots } from '../plots/util'
89
import { Disposable } from '../../../extension'
910
import expShowFixtureWithoutErrors from '../../fixtures/expShow/base/noErrors'
@@ -529,11 +530,53 @@ suite('Plots Test Suite', () => {
529530
expect(mockSendTelemetryEvent).to.be.calledOnce
530531
expect(mockSendTelemetryEvent).to.be.calledWithExactly(
531532
EventName.VIEWS_PLOTS_ZOOM_PLOT,
532-
undefined,
533+
{ isImage: false },
534+
undefined
535+
)
536+
}).timeout(WEBVIEW_TEST_TIMEOUT)
537+
538+
it('should handle a plot zoomed message from the webview for an image', async () => {
539+
const { plots } = await buildPlots(disposable, plotsDiffFixture)
540+
541+
const webview = await plots.showWebview()
542+
543+
const mockSendTelemetryEvent = stub(Telemetry, 'sendTelemetryEvent')
544+
const mockMessageReceived = getMessageReceivedEmitter(webview)
545+
546+
mockMessageReceived.fire({
547+
payload: webview.getWebviewUri('a/path.jpg'),
548+
type: MessageFromWebviewType.ZOOM_PLOT
549+
})
550+
551+
expect(mockSendTelemetryEvent).to.be.calledOnce
552+
expect(mockSendTelemetryEvent).to.be.calledWithExactly(
553+
EventName.VIEWS_PLOTS_ZOOM_PLOT,
554+
{ isImage: true },
533555
undefined
534556
)
535557
}).timeout(WEBVIEW_TEST_TIMEOUT)
536558

559+
it('should open an image when receiving a plot zoomed message from the webview with a payload', async () => {
560+
const { plots } = await buildPlots(disposable, plotsDiffFixture)
561+
562+
const webview = await plots.showWebview()
563+
const imagePath = 'some/path/image.jpg'
564+
565+
stub(Telemetry, 'sendTelemetryEvent')
566+
const mockExecuteCommands = stub(commands, 'executeCommand')
567+
const mockMessageReceived = getMessageReceivedEmitter(webview)
568+
569+
mockMessageReceived.fire({
570+
payload: webview.getWebviewUri(imagePath),
571+
type: MessageFromWebviewType.ZOOM_PLOT
572+
})
573+
574+
expect(mockExecuteCommands).to.be.calledWith(
575+
'vscode.open',
576+
Uri.file(imagePath)
577+
)
578+
}).timeout(WEBVIEW_TEST_TIMEOUT)
579+
537580
it('should handle a custom plots reordered message from the webview', async () => {
538581
const { plots, plotsModel, messageSpy } = await buildPlots(
539582
disposable,

extension/src/webview/contract.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ export type MessageFromWebview =
238238
| { type: MessageFromWebviewType.OPEN_STUDIO_PROFILE }
239239
| { type: MessageFromWebviewType.SAVE_STUDIO_TOKEN }
240240
| { type: MessageFromWebviewType.ADD_CONFIGURATION }
241-
| { type: MessageFromWebviewType.ZOOM_PLOT }
241+
| { type: MessageFromWebviewType.ZOOM_PLOT; payload?: string }
242242
| { type: MessageFromWebviewType.OPEN_EXPERIMENTS_WEBVIEW }
243243

244244
export type MessageToWebview<T extends WebviewData> = {

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,6 +1436,21 @@ describe('App', () => {
14361436
})
14371437
})
14381438

1439+
it('should send a message with the plot path when a comparison table plot is zoomed', () => {
1440+
renderAppWithOptionalData({
1441+
comparison: comparisonTableFixture
1442+
})
1443+
1444+
const plot = screen.getAllByTestId('image-plot-button')[0]
1445+
1446+
fireEvent.click(plot)
1447+
1448+
expect(mockPostMessage).toHaveBeenCalledWith({
1449+
payload: comparisonTableFixture.plots[0].revisions.workspace.url,
1450+
type: MessageFromWebviewType.ZOOM_PLOT
1451+
})
1452+
})
1453+
14391454
it('should open a modal with the plot zoomed in when clicking a checkpoint plot', () => {
14401455
renderAppWithOptionalData({
14411456
checkpoint: checkpointPlotsFixture

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ComparisonPlot } from 'dvc/src/plots/webview/contract'
44
import styles from './styles.module.scss'
55
import { RefreshButton } from '../../../shared/components/button/RefreshButton'
66
import { sendMessage } from '../../../shared/vscode'
7+
import { zoomPlot } from '../messages'
78

89
type ComparisonTableCellProps = {
910
path: string
@@ -41,10 +42,12 @@ export const ComparisonTableCell: React.FC<ComparisonTableCellProps> = ({
4142
}
4243

4344
return (
44-
<img
45-
draggable={false}
46-
src={plot.url}
47-
alt={`Plot of ${path} (${plot.revision})`}
48-
/>
45+
<button onClick={() => zoomPlot(plot.url)} data-testid="image-plot-button">
46+
<img
47+
draggable={false}
48+
src={plot.url}
49+
alt={`Plot of ${path} (${plot.revision})`}
50+
/>
51+
</button>
4952
)
5053
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { MessageFromWebviewType } from 'dvc/src/webview/contract'
22
import { sendMessage } from '../../shared/vscode'
33

4-
export const zoomPlot = () =>
5-
sendMessage({ type: MessageFromWebviewType.ZOOM_PLOT })
4+
export const zoomPlot = (imagePath?: string) =>
5+
sendMessage({ payload: imagePath, type: MessageFromWebviewType.ZOOM_PLOT })

0 commit comments

Comments
 (0)