Skip to content

Commit b4c3f46

Browse files
authored
Add loading state for sections and images to plots webview (#2865)
* add loading state for sections and images to plots webview * update rule to account for multiple revisions being loaded
1 parent b3762f5 commit b4c3f46

File tree

12 files changed

+168
-39
lines changed

12 files changed

+168
-39
lines changed

extension/src/plots/webview/contract.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export interface TemplatePlotsData {
119119
}
120120

121121
export type ComparisonPlot = {
122-
url: string | undefined
122+
url?: string
123123
revision: string
124124
}
125125

webview/__mocks__/@vscode/webview-ui-toolkit/react.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,7 @@ type MockCheckboxProps = {
2424
export const VSCodeCheckbox: React.FC<MockCheckboxProps> = ({ onClick }) => {
2525
return <input type="checkbox" onClick={onClick} />
2626
}
27+
28+
export const VSCodeProgressRing: React.FC = () => {
29+
return <div />
30+
}

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,46 @@ describe('App', () => {
247247
expect(emptyState).toBeInTheDocument()
248248
})
249249

250+
it('should render loading section states when given a single revision which has not been fetched', async () => {
251+
renderAppWithOptionalData({
252+
checkpoint: null,
253+
comparison: {
254+
plots: [
255+
{
256+
path: 'training/plots/images/misclassified.jpg',
257+
revisions: { ad2b5ec: { revision: 'ad2b5ec' } }
258+
}
259+
],
260+
revisions: [
261+
{
262+
displayColor: '#945dd6',
263+
fetched: false,
264+
group: '[exp-a270a]',
265+
id: 'ad2b5ec854a447d00d9dfa9cdf88211a39a17813',
266+
revision: 'ad2b5ec'
267+
}
268+
],
269+
size: PlotSizeNumber.REGULAR
270+
},
271+
hasPlots: true,
272+
hasSelectedPlots: true,
273+
sectionCollapsed: DEFAULT_SECTION_COLLAPSED,
274+
selectedRevisions: [
275+
{
276+
displayColor: '#945dd6',
277+
fetched: false,
278+
group: '[exp-a270a]',
279+
id: 'ad2b5ec854a447d00d9dfa9cdf88211a39a17813',
280+
revision: 'ad2b5ec'
281+
}
282+
],
283+
template: null
284+
})
285+
const loading = await screen.findAllByText('Loading...')
286+
287+
expect(loading).toHaveLength(3)
288+
})
289+
250290
it('should render the get started buttons when no plots or experiments are selected', async () => {
251291
renderAppWithOptionalData({
252292
checkpoint: null,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Revision } from 'dvc/src/plots/webview/contract'
2+
import React from 'react'
3+
import { EmptyState } from '../../shared/components/emptyState/EmptyState'
4+
5+
export const sectionIsLoading = (selectedRevisions: Revision[]): boolean =>
6+
selectedRevisions.length > 0 &&
7+
!selectedRevisions.some(({ fetched }) => fetched)
8+
9+
export const LoadingSection: React.FC = () => (
10+
<EmptyState isFullScreen={false}>Loading...</EmptyState>
11+
)

webview/src/plots/components/checkpointPlots/CheckpointPlots.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { VirtualizedGrid } from '../../../shared/components/virtualizedGrid/Virt
1717
import { shouldUseVirtualizedGrid } from '../util'
1818
import { PlotsState } from '../../store'
1919
import { changeOrderWithDraggedInfo } from '../../../util/array'
20+
import { LoadingSection, sectionIsLoading } from '../LoadingSection'
2021

2122
interface CheckpointPlotsProps {
2223
plotsIds: string[]
@@ -37,6 +38,10 @@ export const CheckpointPlots: React.FC<CheckpointPlotsProps> = ({
3738
(state: PlotsState) => state.dragAndDrop.draggedRef
3839
)
3940

41+
const selectedRevisions = useSelector(
42+
(state: PlotsState) => state.webview.selectedRevisions
43+
)
44+
4045
useEffect(() => {
4146
setOrder(pastOrder => performSimpleOrderedUpdate(pastOrder, plotsIds))
4247
}, [plotsIds])
@@ -49,6 +54,10 @@ export const CheckpointPlots: React.FC<CheckpointPlotsProps> = ({
4954
})
5055
}
5156

57+
if (sectionIsLoading(selectedRevisions)) {
58+
return <LoadingSection />
59+
}
60+
5261
if (!hasData) {
5362
return <EmptyState isFullScreen={false}>No Plots to Display</EmptyState>
5463
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React from 'react'
2+
import { MessageFromWebviewType } from 'dvc/src/webview/contract'
3+
import { ComparisonPlot } from 'dvc/src/plots/webview/contract'
4+
import styles from './styles.module.scss'
5+
import { RefreshButton } from '../../../shared/components/button/RefreshButton'
6+
import { sendMessage } from '../../../shared/vscode'
7+
8+
type ComparisonTableCellProps = {
9+
path: string
10+
plot?: ComparisonPlot & { fetched: boolean }
11+
}
12+
13+
export const ComparisonTableCell: React.FC<ComparisonTableCellProps> = ({
14+
path,
15+
plot
16+
}) => {
17+
const missing = plot?.fetched && !plot?.url
18+
19+
if (!plot?.fetched) {
20+
return (
21+
<div className={styles.noImageContent}>
22+
<p>Loading...</p>
23+
</div>
24+
)
25+
}
26+
27+
if (missing) {
28+
return (
29+
<div className={styles.noImageContent}>
30+
<p>No Plot to Display.</p>
31+
<RefreshButton
32+
onClick={() =>
33+
sendMessage({
34+
payload: plot.revision,
35+
type: MessageFromWebviewType.REFRESH_REVISION
36+
})
37+
}
38+
/>
39+
</div>
40+
)
41+
}
42+
43+
return (
44+
<img
45+
draggable={false}
46+
src={plot.url}
47+
alt={`Plot of ${path} (${plot.revision})`}
48+
/>
49+
)
50+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe('ComparisonTableRow', () => {
3131
comparisonPlotsFixture.plots.find(
3232
({ path }) => path === join('plots', 'acc.png')
3333
)?.revisions || {}
34-
)
34+
).map(revision => ({ ...revision, fetched: true }))
3535
}
3636

3737
const renderRow = (props = basicProps) =>

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

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import { ComparisonPlot } from 'dvc/src/plots/webview/contract'
2-
import { MessageFromWebviewType } from 'dvc/src/webview/contract'
32
import React, { useState } from 'react'
43
import cx from 'classnames'
54
import { useSelector } from 'react-redux'
65
import styles from './styles.module.scss'
6+
import { ComparisonTableCell } from './ComparisonTableCell'
77
import { Icon } from '../../../shared/components/Icon'
8-
import { RefreshButton } from '../../../shared/components/button/RefreshButton'
9-
import { sendMessage } from '../../../shared/vscode'
108
import { ChevronDown, ChevronRight } from '../../../shared/components/icons'
119
import { PlotsState } from '../../store'
1210
import { CopyButton } from '../../../shared/components/copyButton/CopyButton'
@@ -17,7 +15,7 @@ import Tooltip, {
1715

1816
export interface ComparisonTableRowProps {
1917
path: string
20-
plots: ComparisonPlot[]
18+
plots: (ComparisonPlot & { fetched: boolean })[]
2119
nbColumns: number
2220
pinnedColumn: string
2321
}
@@ -64,42 +62,22 @@ export const ComparisonTableRow: React.FC<ComparisonTableRowProps> = ({
6462
{nbColumns > 1 && pinnedColumn && <td colSpan={nbColumns - 1}></td>}
6563
</tr>
6664
<tr>
67-
{plots.map((plot: ComparisonPlot) => {
65+
{plots.map(plot => {
6866
const isPinned = pinnedColumn === plot.revision
69-
const missing = !plot?.url
70-
7167
return (
7268
<td
7369
key={path + plot.revision}
7470
className={cx({
7571
[styles.pinnedColumnCell]: isPinned,
76-
[styles.missing]: isShown && missing,
72+
[styles.noImage]: isShown && !plot?.url,
7773
[styles.draggedColumn]: draggedId === plot.revision
7874
})}
7975
>
8076
<div
8177
data-testid="row-images"
8278
className={cx(styles.cell, { [styles.cellHidden]: !isShown })}
8379
>
84-
{missing ? (
85-
<div>
86-
<p>No Plot to Display.</p>
87-
<RefreshButton
88-
onClick={() =>
89-
sendMessage({
90-
payload: plot.revision,
91-
type: MessageFromWebviewType.REFRESH_REVISION
92-
})
93-
}
94-
/>
95-
</div>
96-
) : (
97-
<img
98-
draggable={false}
99-
src={plot.url}
100-
alt={`Plot of ${path} (${plot.revision})`}
101-
/>
102-
)}
80+
<ComparisonTableCell path={path} plot={plot} />
10381
</div>
10482
</td>
10583
)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export const ComparisionTableRows: React.FC<ComparisonTableRowsProps> = ({
4646
path={path}
4747
plots={columns.map(column => ({
4848
...revs[column.revision],
49+
fetched: column.fetched,
4950
revision: column.revision
5051
}))}
5152
nbColumns={columns.length}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,18 @@ $gap: 4px;
8888
transform: rotate(0deg);
8989
}
9090

91-
.missing {
91+
.noImage {
9292
background-color: $bg-color;
9393
border-style: solid;
9494
border-width: thin;
9595
border-color: $watermark-color;
9696
}
9797

98+
.noImageContent {
99+
padding-top: 25%;
100+
padding-bottom: 25%;
101+
}
102+
98103
.rowToggler {
99104
border: none;
100105
background: none;

0 commit comments

Comments
 (0)