Skip to content

Commit 2da53ae

Browse files
authored
Add plots error message below ribbon (#5165)
1 parent 590aa4b commit 2da53ae

File tree

17 files changed

+385
-26
lines changed

17 files changed

+385
-26
lines changed

extension/src/plots/errors/model.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Disposable } from '../../class/dispose'
1010
import { DvcError, PlotsOutputOrError } from '../../cli/dvc/contract'
1111
import { isDvcError } from '../../cli/dvc/reader'
1212
import { getCliErrorLabel } from '../../tree'
13+
import { PlotErrors } from '../webview/contract'
1314

1415
export class ErrorsModel extends Disposable {
1516
private readonly dvcRoot: string
@@ -66,6 +67,17 @@ export class ErrorsModel extends Disposable {
6667
return [...acc]
6768
}
6869

70+
public getErrorsByPath(paths: string[], selectedRevisions: string[]) {
71+
const errors: PlotErrors = []
72+
for (const path of paths) {
73+
const pathErrors = this.getPathErrors(path, selectedRevisions)
74+
if (pathErrors) {
75+
errors.push({ path, revs: pathErrors })
76+
}
77+
}
78+
return errors
79+
}
80+
6981
public hasCliError() {
7082
return !!this.getCliError()
7183
}

extension/src/plots/paths/model.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,22 @@ export class PathsModel extends PathSelectionModel<PlotPath> {
142142
return false
143143
}
144144

145+
public getSelectedPlotPaths() {
146+
const revisionPaths = this.data.filter(element =>
147+
this.hasRevisions(element)
148+
)
149+
150+
const paths: string[] = []
151+
152+
for (const { path } of revisionPaths) {
153+
if (this.status[path] === Status.SELECTED) {
154+
paths.push(path)
155+
}
156+
}
157+
158+
return paths
159+
}
160+
145161
public getTemplateOrder(): TemplateOrder {
146162
return collectTemplateOrder(
147163
this.getPathsByType(PathType.TEMPLATE_SINGLE),

extension/src/plots/webview/contract.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,18 @@ export type ComparisonPlot = {
163163
imgs: ComparisonPlotImg[]
164164
}
165165

166+
export type PlotErrors = {
167+
path: string
168+
revs: { rev: string; msg: string }[]
169+
}[]
170+
166171
export enum PlotsDataKeys {
167172
COMPARISON = 'comparison',
168173
CLI_ERROR = 'cliError',
169174
CUSTOM = 'custom',
170175
HAS_UNSELECTED_PLOTS = 'hasUnselectedPlots',
171176
HAS_PLOTS = 'hasPlots',
177+
PLOT_ERRORS = 'plotErrors',
172178
SELECTED_REVISIONS = 'selectedRevisions',
173179
TEMPLATE = 'template',
174180
SECTION_COLLAPSED = 'sectionCollapsed',
@@ -188,6 +194,7 @@ export type PlotsData =
188194
[PlotsDataKeys.SECTION_COLLAPSED]?: SectionCollapsed
189195
[PlotsDataKeys.SHOW_TOO_MANY_TEMPLATE_PLOTS]?: boolean
190196
[PlotsDataKeys.SHOW_TOO_MANY_COMPARISON_IMAGES]?: boolean
197+
[PlotsDataKeys.PLOT_ERRORS]?: PlotErrors
191198
}
192199
| undefined
193200

extension/src/plots/webview/messages.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,15 +174,20 @@ export class WebviewMessages {
174174
hasPlots,
175175
hasUnselectedPlots,
176176
sectionCollapsed,
177-
template
177+
template,
178+
plotErrors
178179
] = await Promise.all([
179180
this.errors.getCliError()?.error || null,
180181
this.getComparisonPlots(),
181182
this.getCustomPlots(),
182183
!!this.paths.hasPaths(),
183184
this.paths.getHasUnselectedPlots(),
184185
this.plots.getSectionCollapsed(),
185-
this.getTemplatePlots(selectedRevisions)
186+
this.getTemplatePlots(selectedRevisions),
187+
this.errors.getErrorsByPath(
188+
this.paths.getSelectedPlotPaths(),
189+
this.plots.getSelectedRevisionIds()
190+
)
186191
])
187192
const shouldShowTooManyTemplatePlotsMessage =
188193
this.shouldShowTooManyPlotsMessage([
@@ -198,6 +203,7 @@ export class WebviewMessages {
198203
custom,
199204
hasPlots,
200205
hasUnselectedPlots,
206+
plotErrors,
201207
sectionCollapsed,
202208
selectedRevisions,
203209
shouldShowTooManyComparisonImagesMessage,

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,6 +1104,66 @@ suite('Plots Test Suite', () => {
11041104
).not.to.contain(brokenExp)
11051105
})
11061106

1107+
it('should send plot errors to the webview', async () => {
1108+
const accPngPath = join('plots', 'acc.png')
1109+
const accPng = [
1110+
...plotsDiffFixture.data[join('plots', 'acc.png')]
1111+
] as ImagePlot[]
1112+
const lossTsvPath = join('logs', 'loss.tsv')
1113+
const lossTsv = [
1114+
...plotsDiffFixture.data[lossTsvPath]
1115+
] as TemplatePlotOutput[]
1116+
1117+
const plotsDiffOutput = {
1118+
data: {
1119+
[accPngPath]: accPng,
1120+
[lossTsvPath]: lossTsv
1121+
},
1122+
errors: [
1123+
{
1124+
msg: 'File not found',
1125+
name: accPngPath,
1126+
rev: 'workspace',
1127+
type: 'FileNotFoundError'
1128+
},
1129+
{
1130+
msg: 'Could not find provided field',
1131+
name: lossTsvPath,
1132+
rev: 'workspace',
1133+
type: 'FieldNotFoundError'
1134+
},
1135+
{
1136+
msg: 'Could not find provided field',
1137+
name: lossTsvPath,
1138+
rev: 'main',
1139+
type: 'FieldNotFoundError'
1140+
}
1141+
]
1142+
}
1143+
const { messageSpy } = await buildPlotsWebview({
1144+
disposer: disposable,
1145+
plotsDiff: plotsDiffOutput
1146+
})
1147+
1148+
const expectedPlotsData: TPlotsData = {
1149+
plotErrors: [
1150+
{
1151+
path: accPngPath,
1152+
revs: [{ msg: 'File not found', rev: 'workspace' }]
1153+
},
1154+
{
1155+
path: lossTsvPath,
1156+
revs: [
1157+
{ msg: 'Could not find provided field', rev: 'workspace' },
1158+
{ msg: 'Could not find provided field', rev: 'main' }
1159+
]
1160+
}
1161+
]
1162+
}
1163+
1164+
expect(messageSpy).to.be.calledWithMatch(expectedPlotsData)
1165+
}).timeout(WEBVIEW_TEST_TIMEOUT)
1166+
11071167
it('should handle a toggle experiment message from the webview', async () => {
11081168
const { experiments, mockMessageReceived } = await buildPlotsWebview({
11091169
disposer: disposable,

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2796,6 +2796,49 @@ describe('App', () => {
27962796
})
27972797
expect(screen.queryByText('!')).not.toBeInTheDocument()
27982798
})
2799+
2800+
it('should show an error banner when there are plot errors found', () => {
2801+
renderAppWithOptionalData({
2802+
comparison: comparisonTableFixture,
2803+
plotErrors: [
2804+
{ path: 'image-path', revs: [{ msg: 'error', rev: 'main' }] }
2805+
]
2806+
})
2807+
2808+
expect(screen.getByText('Show error')).toBeInTheDocument()
2809+
2810+
sendSetDataMessage({
2811+
plotErrors: [
2812+
{ path: 'image-path', revs: [{ msg: 'error', rev: 'main' }] },
2813+
{ path: 'second-image-path', revs: [{ msg: 'error', rev: 'main' }] }
2814+
]
2815+
})
2816+
2817+
expect(screen.getByText('Show 2 errors')).toBeInTheDocument()
2818+
})
2819+
2820+
it('should show a button that opens an error modal when there are plot errors found', () => {
2821+
renderAppWithOptionalData({
2822+
comparison: comparisonTableFixture,
2823+
plotErrors: [
2824+
{ path: 'image-path', revs: [{ msg: 'error', rev: 'main' }] }
2825+
]
2826+
})
2827+
2828+
const showErrorsBtn = screen.getByText('Show error')
2829+
2830+
expect(showErrorsBtn).toBeInTheDocument()
2831+
2832+
fireEvent.click(showErrorsBtn)
2833+
2834+
expect(screen.getByTestId('modal')).toBeInTheDocument()
2835+
2836+
const modalContents = screen.getByTestId('errors-modal')
2837+
2838+
expect(within(modalContents).getByText('image-path')).toBeInTheDocument()
2839+
expect(within(modalContents).getByText('main')).toBeInTheDocument()
2840+
expect(within(modalContents).getByText('error')).toBeInTheDocument()
2841+
})
27992842
})
28002843

28012844
describe('Vega panels', () => {

webview/src/plots/components/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
updateCliError,
3131
updateHasPlots,
3232
updateHasUnselectedPlots,
33+
updatePlotErrors,
3334
updateSelectedRevisions
3435
} from './webviewSlice'
3536
import { PlotsDispatch } from '../store'
@@ -81,6 +82,9 @@ export const feedStore = (
8182
case PlotsDataKeys.HAS_UNSELECTED_PLOTS:
8283
dispatch(updateHasUnselectedPlots(!!data.data[key]))
8384
continue
85+
case PlotsDataKeys.PLOT_ERRORS:
86+
dispatch(updatePlotErrors(data.data[key]))
87+
continue
8488
case PlotsDataKeys.SELECTED_REVISIONS:
8589
dispatch(updateSelectedRevisions(data.data[key]))
8690
continue
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import React from 'react'
2+
import { useSelector } from 'react-redux'
3+
import styles from './styles.module.scss'
4+
import { Error } from '../../shared/components/icons'
5+
import { PlotsState } from '../store'
6+
import { useModalOpenClass } from '../hooks/useModalOpenClass'
7+
8+
export const ErrorsModal: React.FC = () => {
9+
const errors = useSelector((state: PlotsState) => state.webview.plotErrors)
10+
useModalOpenClass()
11+
12+
return (
13+
<div className={styles.errorsModal} data-testid="errors-modal">
14+
<h3 className={styles.errorsModalTitle}>
15+
<Error className={styles.errorsModalIcon} width="20" height="20" />
16+
Errors
17+
</h3>
18+
<table>
19+
<tbody>
20+
{errors.map(({ path, revs }) => (
21+
<React.Fragment key={path}>
22+
<tr>
23+
<th colSpan={2} className={styles.errorsModalPlot}>
24+
{path}
25+
</th>
26+
</tr>
27+
{revs.map(({ rev, msg }) => (
28+
<tr key={`${rev}-${msg}`}>
29+
<td className={styles.errorsModalRev}>{rev}</td>
30+
<td className={styles.errorsModalMsgs}>{msg}</td>
31+
</tr>
32+
))}
33+
</React.Fragment>
34+
))}
35+
</tbody>
36+
</table>
37+
</div>
38+
)
39+
}

webview/src/plots/components/PlotsContent.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,31 @@ import { useSelector, useDispatch } from 'react-redux'
33
import { ErrorState } from './emptyState/ErrorState'
44
import { GetStarted } from './emptyState/GetStarted'
55
import { ZoomedInPlot } from './ZoomedInPlot'
6+
import { ErrorsModal } from './ErrorsModal'
67
import { CustomPlotsWrapper } from './customPlots/CustomPlotsWrapper'
78
import { TemplatePlotsWrapper } from './templatePlots/TemplatePlotsWrapper'
89
import { ComparisonTableWrapper } from './comparisonTable/ComparisonTableWrapper'
910
import { Ribbon } from './ribbon/Ribbon'
10-
import { setMaxNbPlotsPerRow, setZoomedInPlot } from './webviewSlice'
11+
import {
12+
setMaxNbPlotsPerRow,
13+
setZoomedInPlot,
14+
setShowErrorsModal
15+
} from './webviewSlice'
1116
import styles from './styles.module.scss'
1217
import { EmptyState } from '../../shared/components/emptyState/EmptyState'
1318
import { Modal } from '../../shared/components/modal/Modal'
1419
import { PlotsState } from '../store'
1520

1621
export const PlotsContent = () => {
1722
const dispatch = useDispatch()
18-
const { hasData, hasPlots, hasUnselectedPlots, zoomedInPlot, cliError } =
19-
useSelector((state: PlotsState) => state.webview)
23+
const {
24+
hasData,
25+
hasPlots,
26+
hasUnselectedPlots,
27+
zoomedInPlot,
28+
cliError,
29+
showErrorsModal
30+
} = useSelector((state: PlotsState) => state.webview)
2031
const hasComparisonData = useSelector(
2132
(state: PlotsState) => state.comparison.hasData
2233
)
@@ -63,6 +74,16 @@ export const PlotsContent = () => {
6374
</Modal>
6475
)
6576

77+
const errorsModal = showErrorsModal && (
78+
<Modal
79+
onClose={() => {
80+
dispatch(setShowErrorsModal(false))
81+
}}
82+
>
83+
<ErrorsModal />
84+
</Modal>
85+
)
86+
6687
const hasCustomPlots = customPlotIds.length > 0
6788

6889
if (cliError) {
@@ -93,6 +114,7 @@ export const PlotsContent = () => {
93114
<ComparisonTableWrapper />
94115
<CustomPlotsWrapper />
95116
{modal}
117+
{errorsModal}
96118
</div>
97119
)
98120
}

webview/src/plots/components/ZoomedInPlot.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useRef } from 'react'
1+
import React, { useRef } from 'react'
22
import { PlotsSection } from 'dvc/src/plots/webview/contract'
33
import { View } from 'react-vega'
44
import { ExtendedVegaLite } from './vegaLite/ExtendedVegaLite'
@@ -15,6 +15,7 @@ import {
1515
exportPlotDataAsJson,
1616
exportPlotDataAsTsv
1717
} from '../util/messages'
18+
import { useModalOpenClass } from '../hooks/useModalOpenClass'
1819

1920
type ZoomedInPlotProps = {
2021
id: string
@@ -43,15 +44,7 @@ export const ZoomedInPlot: React.FC<ZoomedInPlotProps> = ({
4344
openActionsMenu
4445
}: ZoomedInPlotProps) => {
4546
const zoomedInPlotRef = useRef<HTMLDivElement>(null)
46-
47-
useEffect(() => {
48-
const modalOpenClass = 'modalOpen'
49-
document.body.classList.add(modalOpenClass)
50-
51-
return () => {
52-
document.body.classList.remove(modalOpenClass)
53-
}
54-
}, [])
47+
useModalOpenClass()
5548

5649
const onNewView = (view: View) => {
5750
const actions: HTMLDivElement | null | undefined =

0 commit comments

Comments
 (0)