Skip to content

Commit d6f245b

Browse files
sroy3mattseddon
andauthored
Add experiments from plots ribbon (#1798)
* Add plots ribbon * Fix tests and event name * Add tests * Add react-virtualized * Add extension test * Add button in plots ribbon to add experiments * Add tests * Add refresh all button * add refresh all visible action to plots (#1808) Co-authored-by: mattseddon <[email protected]>
1 parent 1a2d5c2 commit d6f245b

File tree

9 files changed

+174
-22
lines changed

9 files changed

+174
-22
lines changed

extension/src/plots/index.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,9 @@ export class Plots extends BaseRepository<TPlotsData> {
267267
case MessageFromWebviewType.SELECT_EXPERIMENTS:
268268
return this.selectExperimentsFromWebview()
269269
case MessageFromWebviewType.REFRESH_REVISION:
270-
return this.attemptToRefreshData(message.payload)
270+
return this.attemptToRefreshRevData(message.payload)
271+
case MessageFromWebviewType.REFRESH_REVISIONS:
272+
return this.attemptToRefreshSelectedData(message.payload)
271273
case MessageFromWebviewType.TOGGLE_EXPERIMENT:
272274
return this.setExperimentStatus(message.payload)
273275
default:
@@ -364,13 +366,26 @@ export class Plots extends BaseRepository<TPlotsData> {
364366
)
365367
}
366368

367-
private attemptToRefreshData(revision: string) {
368-
Toast.infoWithOptions(`Attempting to refresh ${revision} plots data.`)
369+
private attemptToRefreshRevData(revision: string) {
370+
Toast.infoWithOptions(`Attempting to refresh plots data for ${revision}.`)
369371
this.plots?.setupManualRefresh(revision)
370372
this.data.managedUpdate()
371373
sendTelemetryEvent(
372374
EventName.VIEWS_PLOTS_MANUAL_REFRESH,
373-
undefined,
375+
{ revisions: 1 },
376+
undefined
377+
)
378+
}
379+
380+
private attemptToRefreshSelectedData(revisions: string[]) {
381+
Toast.infoWithOptions('Attempting to refresh visible plots data.')
382+
for (const revision of revisions) {
383+
this.plots?.setupManualRefresh(revision)
384+
}
385+
this.data.managedUpdate()
386+
sendTelemetryEvent(
387+
EventName.VIEWS_PLOTS_MANUAL_REFRESH,
388+
{ revisions: revisions.length },
374389
undefined
375390
)
376391
}

extension/src/telemetry/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ export interface IEventNamePropertyMapping {
205205
[EventName.VIEWS_PLOTS_CLOSED]: undefined
206206
[EventName.VIEWS_PLOTS_CREATED]: undefined
207207
[EventName.VIEWS_PLOTS_FOCUS_CHANGED]: WebviewFocusChangedProperties
208-
[EventName.VIEWS_PLOTS_MANUAL_REFRESH]: undefined
208+
[EventName.VIEWS_PLOTS_MANUAL_REFRESH]: { revisions: number }
209209
[EventName.VIEWS_PLOTS_METRICS_SELECTED]: undefined
210210
[EventName.VIEWS_PLOTS_RENAME_SECTION]: { section: Section }
211211
[EventName.VIEWS_PLOTS_REVISIONS_REORDERED]: undefined

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

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,7 @@ suite('Plots Test Suite', () => {
619619
)
620620
}).timeout(WEBVIEW_TEST_TIMEOUT)
621621

622-
it('should handle a manual refresh message from the webview', async () => {
622+
it('should handle a message to manually refresh a revision from the webview', async () => {
623623
const { data, plots, plotsModel, mockPlotsDiff } = await buildPlots(
624624
disposable,
625625
plotsDiffFixture
@@ -648,13 +648,53 @@ suite('Plots Test Suite', () => {
648648
expect(mockSendTelemetryEvent).to.be.calledOnce
649649
expect(mockSendTelemetryEvent).to.be.calledWithExactly(
650650
EventName.VIEWS_PLOTS_MANUAL_REFRESH,
651-
undefined,
651+
{ revisions: 1 },
652652
undefined
653653
)
654654
expect(mockPlotsDiff).to.be.calledOnce
655655
expect(mockPlotsDiff).to.be.calledWithExactly(dvcDemoPath, 'main')
656656
}).timeout(WEBVIEW_TEST_TIMEOUT)
657657

658+
it('should handle a message to manually refresh all visible plots from the webview', async () => {
659+
const { data, plots, mockPlotsDiff } = await buildPlots(
660+
disposable,
661+
plotsDiffFixture
662+
)
663+
664+
const webview = await plots.showWebview()
665+
mockPlotsDiff.resetHistory()
666+
667+
const mockSendTelemetryEvent = stub(Telemetry, 'sendTelemetryEvent')
668+
const mockMessageReceived = getMessageReceivedEmitter(webview)
669+
670+
const dataUpdateEvent = new Promise(resolve =>
671+
data.onDidUpdate(() => resolve(undefined))
672+
)
673+
674+
mockMessageReceived.fire({
675+
payload: ['1ba7bcd', '42b8736', '4fb124a', 'main', 'workspace'],
676+
type: MessageFromWebviewType.REFRESH_REVISIONS
677+
})
678+
679+
await dataUpdateEvent
680+
681+
expect(mockSendTelemetryEvent).to.be.calledOnce
682+
expect(mockSendTelemetryEvent).to.be.calledWithExactly(
683+
EventName.VIEWS_PLOTS_MANUAL_REFRESH,
684+
{ revisions: 5 },
685+
undefined
686+
)
687+
expect(mockPlotsDiff).to.be.calledOnce
688+
expect(mockPlotsDiff).to.be.calledWithExactly(
689+
dvcDemoPath,
690+
'1ba7bcd',
691+
'42b8736',
692+
'4fb124a',
693+
'main',
694+
'workspace'
695+
)
696+
}).timeout(WEBVIEW_TEST_TIMEOUT)
697+
658698
it('should be able to make the plots webview visible', async () => {
659699
const { plots, messageSpy, mockPlotsDiff } = await buildPlots(
660700
disposable,

extension/src/webview/contract.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export enum MessageFromWebviewType {
2323
REORDER_PLOTS_METRICS = 'reorder-plots-metrics',
2424
REORDER_PLOTS_TEMPLATES = 'reorder-plots-templates',
2525
REFRESH_REVISION = 'refresh-revision',
26+
REFRESH_REVISIONS = 'refresh-revisions',
2627
RESIZE_COLUMN = 'resize-column',
2728
RESIZE_PLOTS = 'resize-plots',
2829
SORT_COLUMN = 'sort-column',
@@ -137,6 +138,7 @@ export type MessageFromWebview =
137138
| { type: MessageFromWebviewType.SELECT_EXPERIMENTS }
138139
| { type: MessageFromWebviewType.SELECT_PLOTS }
139140
| { type: MessageFromWebviewType.REFRESH_REVISION; payload: string }
141+
| { type: MessageFromWebviewType.REFRESH_REVISIONS; payload: string[] }
140142
| { type: MessageFromWebviewType.SELECT_COLUMNS }
141143

142144
export type MessageToWebview<T extends WebviewData> = {

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

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,9 +1552,10 @@ describe('App', () => {
15521552
})
15531553
const ribbon = screen.getByTestId('ribbon')
15541554

1555-
const revisions = within(ribbon)
1556-
.getAllByRole('listitem')
1557-
.map(item => item.textContent)
1555+
const revisionBlocks = within(ribbon).getAllByRole('listitem')
1556+
revisionBlocks.shift() // Remove filter button
1557+
revisionBlocks.shift() // Remove refresh all
1558+
const revisions = revisionBlocks.map(item => item.textContent)
15581559
expect(revisions).toStrictEqual(
15591560
comparisonTableFixture.revisions.map(rev =>
15601561
rev.group ? rev.group.slice(1, -1) + rev.revision : rev.revision
@@ -1580,6 +1581,54 @@ describe('App', () => {
15801581
})
15811582
})
15821583

1584+
it('should display the number of experiments selected', () => {
1585+
renderAppWithData({
1586+
comparison: comparisonTableFixture,
1587+
sectionCollapsed: DEFAULT_SECTION_COLLAPSED
1588+
})
1589+
1590+
expect(
1591+
screen.getByText(`${comparisonTableFixture.revisions.length} of 7`)
1592+
).toBeInTheDocument()
1593+
})
1594+
1595+
it('should send a message to select the revisions when clicking the filter button', () => {
1596+
renderAppWithData({
1597+
comparison: comparisonTableFixture,
1598+
sectionCollapsed: DEFAULT_SECTION_COLLAPSED
1599+
})
1600+
1601+
const filterButton = within(screen.getByTestId('ribbon')).getAllByRole(
1602+
'button'
1603+
)[0]
1604+
1605+
fireEvent.click(filterButton)
1606+
1607+
expect(mockPostMessage).toBeCalledWith({
1608+
type: MessageFromWebviewType.SELECT_EXPERIMENTS
1609+
})
1610+
})
1611+
1612+
it('should send a message to refresh each revision when clicking the refresh all button', () => {
1613+
renderAppWithData({
1614+
comparison: comparisonTableFixture,
1615+
sectionCollapsed: DEFAULT_SECTION_COLLAPSED
1616+
})
1617+
1618+
const refreshAllButton = within(
1619+
screen.getByTestId('ribbon')
1620+
).getAllByRole('button')[1]
1621+
1622+
mockPostMessage.mockReset()
1623+
fireEvent.click(refreshAllButton)
1624+
1625+
expect(mockPostMessage).toHaveBeenCalledTimes(1)
1626+
expect(mockPostMessage).toBeCalledWith({
1627+
payload: ['workspace', 'main', '4fb124a', '42b8736', '1ba7bcd'],
1628+
type: MessageFromWebviewType.REFRESH_REVISIONS
1629+
})
1630+
})
1631+
15831632
describe('Copy button', () => {
15841633
const mockWriteText = jest.fn()
15851634
Object.assign(navigator, {
@@ -1588,7 +1637,17 @@ describe('App', () => {
15881637
}
15891638
})
15901639

1591-
it('should copy the experiment name when clicking the text', () => {
1640+
beforeAll(() => {
1641+
jest.useFakeTimers()
1642+
})
1643+
1644+
afterAll(() => {
1645+
jest.useRealTimers()
1646+
})
1647+
1648+
it('should copy the experiment name when clicking the text', async () => {
1649+
mockWriteText.mockResolvedValueOnce('success')
1650+
15921651
renderAppWithData({
15931652
comparison: comparisonTableFixture,
15941653
sectionCollapsed: DEFAULT_SECTION_COLLAPSED
@@ -1598,14 +1657,14 @@ describe('App', () => {
15981657
screen.getByTestId('ribbon-main')
15991658
).getAllByRole('button')[0]
16001659

1660+
fireEvent.mouseEnter(mainNameButton, { bubbles: true })
16011661
fireEvent.click(mainNameButton)
16021662

16031663
expect(mockWriteText).toBeCalledWith('main')
1664+
await screen.findByText(CopyTooltip.COPIED)
16041665
})
16051666

16061667
it('should display that the experiment was copied when clicking the text', async () => {
1607-
jest.useFakeTimers()
1608-
16091668
mockWriteText.mockResolvedValueOnce('success')
16101669

16111670
renderAppWithData({
@@ -1621,11 +1680,10 @@ describe('App', () => {
16211680
fireEvent.click(mainNameButton)
16221681

16231682
expect(await screen.findByText(CopyTooltip.COPIED)).toBeInTheDocument()
1624-
jest.useRealTimers()
16251683
})
16261684

1627-
it('should display copy again when hovering the text 2s after clicking the text', () => {
1628-
jest.useFakeTimers()
1685+
it('should display copy again when hovering the text 2s after clicking the text', async () => {
1686+
mockWriteText.mockResolvedValueOnce('success')
16291687

16301688
renderAppWithData({
16311689
comparison: comparisonTableFixture,
@@ -1636,13 +1694,12 @@ describe('App', () => {
16361694
screen.getByTestId('ribbon-main')
16371695
).getAllByRole('button')[0]
16381696

1639-
fireEvent.click(mainNameButton)
16401697
fireEvent.mouseEnter(mainNameButton, { bubbles: true })
1698+
fireEvent.click(mainNameButton)
16411699

16421700
jest.advanceTimersByTime(2001)
16431701

1644-
expect(screen.getByText(CopyTooltip.NORMAL)).toBeInTheDocument()
1645-
jest.useRealTimers()
1702+
expect(await screen.findByText(CopyTooltip.NORMAL)).toBeInTheDocument()
16461703
})
16471704
})
16481705
})

webview/src/plots/components/ribbon/Ribbon.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,52 @@ import React from 'react'
44
import styles from './styles.module.scss'
55
import { RibbonBlock } from './RibbonBlock'
66
import { sendMessage } from '../../../shared/vscode'
7+
import { AllIcons } from '../../../shared/components/Icon'
8+
import { IconButton } from '../../../shared/components/button/IconButton'
79

810
interface RibbonProps {
911
revisions: Revision[]
1012
}
1113

14+
const MAX_NB_EXP = 7
15+
1216
export const Ribbon: React.FC<RibbonProps> = ({ revisions }) => {
1317
const removeRevision = (revision: string) => {
1418
sendMessage({
1519
payload: revision,
1620
type: MessageFromWebviewType.TOGGLE_EXPERIMENT
1721
})
1822
}
23+
24+
const refreshRevisions = () =>
25+
sendMessage({
26+
payload: revisions.map(({ revision }) => revision),
27+
type: MessageFromWebviewType.REFRESH_REVISIONS
28+
})
29+
30+
const selectRevisions = () => {
31+
sendMessage({
32+
type: MessageFromWebviewType.SELECT_EXPERIMENTS
33+
})
34+
}
35+
1936
return (
2037
<ul className={styles.list} data-testid="ribbon">
38+
<li className={styles.addButtonWrapper}>
39+
<IconButton
40+
onClick={selectRevisions}
41+
icon={AllIcons.LINES}
42+
text={`${revisions.length} of ${MAX_NB_EXP}`}
43+
/>
44+
</li>
45+
<li className={styles.addButtonWrapper}>
46+
<IconButton
47+
onClick={refreshRevisions}
48+
icon={AllIcons.REFRESH}
49+
text="Refresh All"
50+
appearance="secondary"
51+
/>
52+
</li>
2153
{revisions.map(revision => (
2254
<RibbonBlock
2355
revision={revision}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,8 @@
5757
padding: 10px 15px;
5858
margin: 0;
5959
}
60+
61+
.addButtonWrapper {
62+
background-color: $accent-color;
63+
list-style: none;
64+
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,15 +277,15 @@ $gap: 20px;
277277
.dropTarget {
278278
height: auto;
279279
opacity: 0.5;
280-
border: 3px dashed var(--vscode-focusBorder);
280+
border: 3px dashed $accent-color;
281281
display: flex;
282282
justify-content: center;
283283
align-items: center;
284284
}
285285

286286
.dropIcon {
287287
border-radius: 100%;
288-
border: 3px solid var(--vscode-focusBorder);
288+
border: 3px solid $accent-color;
289289
padding: 20px;
290290
}
291291

@@ -298,7 +298,7 @@ $gap: 20px;
298298
box-sizing: content-box;
299299

300300
path {
301-
fill: var(--vscode-focusBorder);
301+
fill: $accent-color;
302302
}
303303
}
304304

webview/src/shared/variables.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ $header-border-color: var(--vscode-tree-tableColumnsBorder);
1616
$meta-cell-color: var(--vscode-descriptionForeground);
1717

1818
$hover-background-color: var(--vscode-list-hoverBackground);
19+
$accent-color: var(--vscode-focusBorder);

0 commit comments

Comments
 (0)