diff --git a/extension/package.json b/extension/package.json index 28f1f4ac98..298538c106 100644 --- a/extension/package.json +++ b/extension/package.json @@ -1669,6 +1669,7 @@ "execa": "5.1.1", "fs-extra": "11.1.1", "js-yaml": "4.1.0", + "json-2-csv": "4.0.0", "json5": "2.2.3", "lodash.clonedeep": "4.5.0", "lodash.get": "4.4.2", diff --git a/extension/src/fileSystem/index.test.ts b/extension/src/fileSystem/index.test.ts index bbff1bb29e..2e37addaf9 100644 --- a/extension/src/fileSystem/index.test.ts +++ b/extension/src/fileSystem/index.test.ts @@ -15,7 +15,8 @@ import { isSameOrChild, getModifiedTime, findOrCreateDvcYamlFile, - writeJson + writeJson, + writeCsv } from '.' import { dvcDemoPath } from '../test/util' import { DOT_DVC } from '../cli/dvc/constants' @@ -71,6 +72,21 @@ describe('writeJson', () => { }) }) +describe('writeCsv', () => { + it('should write csv into given file', async () => { + await writeCsv('file-name.csv', [ + { nested: { string: 'string1' }, value: 3 }, + { nested: { string: 'string2' }, value: 4 }, + { nested: { string: 'string3' }, value: 6 } + ]) + + expect(mockedWriteFileSync).toHaveBeenCalledWith( + 'file-name.csv', + 'nested.string,value\nstring1,3\nstring2,4\nstring3,6' + ) + }) +}) + describe('findDvcRootPaths', () => { it('should find the dvc root if it exists in the given folder', async () => { const dvcRoots = await findDvcRootPaths(dvcDemoPath) diff --git a/extension/src/fileSystem/index.ts b/extension/src/fileSystem/index.ts index 633260eee5..ab24794ef2 100644 --- a/extension/src/fileSystem/index.ts +++ b/extension/src/fileSystem/index.ts @@ -20,6 +20,7 @@ import { } from 'fs-extra' import { load } from 'js-yaml' import { Uri, workspace, window, commands, ViewColumn } from 'vscode' +import { json2csv } from 'json-2-csv' import { standardizePath } from './path' import { definedAndNonEmpty, sortCollectedArray } from '../util/array' import { Logger } from '../common/logger' @@ -208,7 +209,9 @@ export const loadJson = (path: string): T | undefined => { } } -export const writeJson = >( +export const writeJson = < + T extends Record | Array> +>( path: string, obj: T, format = false @@ -218,6 +221,15 @@ export const writeJson = >( return writeFileSync(path, json) } +export const writeCsv = async ( + path: string, + arr: Array> +) => { + ensureFileSync(path) + const csv = await json2csv(arr) + return writeFileSync(path, csv) +} + export const getPidFromFile = async ( path: string ): Promise => { @@ -269,7 +281,8 @@ export const getBinDisplayText = ( : path } -export const showSaveDialog = ( - defaultUri: Uri, - filters?: { [name: string]: string[] } -) => window.showSaveDialog({ defaultUri, filters }) +export const showSaveDialog = (fileName: string, extname: string) => + window.showSaveDialog({ + defaultUri: Uri.file(fileName), + filters: { [extname.toUpperCase()]: [extname] } + }) diff --git a/extension/src/plots/model/collect.ts b/extension/src/plots/model/collect.ts index c6046d501f..6f63c0eb0a 100644 --- a/extension/src/plots/model/collect.ts +++ b/extension/src/plots/model/collect.ts @@ -111,7 +111,7 @@ export const collectCustomPlots = ({ export const collectCustomPlotRawData = ( orderValue: CustomPlotsOrderValue, experiments: Experiment[] -) => { +): Array> => { const { metric, param } = orderValue const metricPath = getFullValuePath(ColumnType.METRICS, metric) const paramPath = getFullValuePath(ColumnType.PARAMS, param) @@ -472,7 +472,7 @@ export const collectSelectedTemplatePlotRawData = ({ multiSourceEncodingUpdate ) - return datapoints + return datapoints as unknown as Array> } export const collectOrderedRevisions = ( diff --git a/extension/src/plots/model/index.ts b/extension/src/plots/model/index.ts index 9ac72265b6..e2c83f99aa 100644 --- a/extension/src/plots/model/index.ts +++ b/extension/src/plots/model/index.ts @@ -57,7 +57,7 @@ import { } from '../multiSource/collect' import { isDvcError } from '../../cli/dvc/reader' import { ErrorsModel } from '../errors/model' -import { openFileInEditor, writeJson } from '../../fileSystem' +import { openFileInEditor, writeCsv, writeJson } from '../../fileSystem' import { Toast } from '../../vscode/toast' export class PlotsModel extends ModelWithPersistence { @@ -227,21 +227,15 @@ export class PlotsModel extends ModelWithPersistence { return selectedRevisions } - public savePlotData(plotId: string, filePath: string) { - const foundCustomPlot = this.customPlotsOrder.find( - ({ metric, param }) => getCustomPlotId(metric, param) === plotId - ) - - const rawData = foundCustomPlot - ? this.getCustomPlotData(foundCustomPlot) - : this.getSelectedTemplatePlotData(plotId) + public savePlotDataAsJson(filePath: string, plotId: string) { + void this.savePlotData(filePath, plotId, data => { + writeJson(filePath, data, true) + return Promise.resolve() + }) + } - try { - writeJson(filePath, rawData as unknown as Record, true) - void openFileInEditor(filePath) - } catch { - void Toast.showError('Cannot write to file') - } + public savePlotDataAsCsv(filePath: string, plotId: string) { + void this.savePlotData(filePath, plotId, data => writeCsv(filePath, data)) } public getTemplatePlots( @@ -479,4 +473,25 @@ export class PlotsModel extends ModelWithPersistence { return collectCustomPlotRawData(orderValue, experiments) } + + private async savePlotData( + filePath: string, + plotId: string, + writeToFile: (rawData: Array>) => Promise + ) { + const foundCustomPlot = this.customPlotsOrder.find( + ({ metric, param }) => getCustomPlotId(metric, param) === plotId + ) + + const rawData = foundCustomPlot + ? this.getCustomPlotData(foundCustomPlot) + : this.getSelectedTemplatePlotData(plotId) + + try { + await writeToFile(rawData) + void openFileInEditor(filePath) + } catch { + void Toast.showError('Cannot write to file') + } + } } diff --git a/extension/src/plots/webview/messages.ts b/extension/src/plots/webview/messages.ts index c45ed24b66..cf0a0e2de1 100644 --- a/extension/src/plots/webview/messages.ts +++ b/extension/src/plots/webview/messages.ts @@ -1,4 +1,4 @@ -import { Uri, commands } from 'vscode' +import { commands } from 'vscode' import isEmpty from 'lodash.isempty' import { ComparisonPlot, @@ -82,8 +82,10 @@ export class WebviewMessages { RegisteredCommands.PLOTS_CUSTOM_ADD, this.dvcRoot ) - case MessageFromWebviewType.EXPORT_PLOT_DATA: - return this.exportPlotData(message.payload) + case MessageFromWebviewType.EXPORT_PLOT_DATA_AS_CSV: + return this.exportPlotDataAsCsv(message.payload) + case MessageFromWebviewType.EXPORT_PLOT_DATA_AS_JSON: + return this.exportPlotDataAsJson(message.payload) case MessageFromWebviewType.RESIZE_PLOTS: return this.setPlotSize( message.payload.section, @@ -344,19 +346,35 @@ export class WebviewMessages { return this.plots.getCustomPlots() || null } - private async exportPlotData(plotId: string) { - const file = await showSaveDialog(Uri.file('data.json'), { JSON: ['json'] }) + private async exportPlotDataAsJson(plotId: string) { + const file = await showSaveDialog('data.json', 'json') if (!file) { return } sendTelemetryEvent( - EventName.VIEWS_PLOTS_EXPORT_PLOT_DATA, + EventName.VIEWS_PLOTS_EXPORT_PLOT_DATA_AS_JSON, undefined, undefined ) - this.plots.savePlotData(plotId, file.path) + void this.plots.savePlotDataAsJson(file.path, plotId) + } + + private async exportPlotDataAsCsv(plotId: string) { + const file = await showSaveDialog('data.csv', 'csv') + + if (!file) { + return + } + + sendTelemetryEvent( + EventName.VIEWS_PLOTS_EXPORT_PLOT_DATA_AS_CSV, + undefined, + undefined + ) + + void this.plots.savePlotDataAsCsv(file.path, plotId) } } diff --git a/extension/src/telemetry/constants.ts b/extension/src/telemetry/constants.ts index 14b59dbda3..9212b1e823 100644 --- a/extension/src/telemetry/constants.ts +++ b/extension/src/telemetry/constants.ts @@ -71,7 +71,8 @@ export const EventName = Object.assign( 'views.plots.comparisonRowsReordered', VIEWS_PLOTS_CREATED: 'views.plots.created', VIEWS_PLOTS_EXPERIMENT_TOGGLE: 'views.plots.toggleExperimentStatus', - VIEWS_PLOTS_EXPORT_PLOT_DATA: 'views.plots.exportPlotData', + VIEWS_PLOTS_EXPORT_PLOT_DATA_AS_CSV: 'views.plots.exportPlotDataAsCsv', + VIEWS_PLOTS_EXPORT_PLOT_DATA_AS_JSON: 'views.plots.exportPlotDataAsJson', VIEWS_PLOTS_FOCUS_CHANGED: 'views.plots.focusChanged', VIEWS_PLOTS_REVISIONS_REORDERED: 'views.plots.revisionsReordered', VIEWS_PLOTS_SECTION_RESIZED: 'views.plots.sectionResized', @@ -267,7 +268,8 @@ export interface IEventNamePropertyMapping { [EventName.VIEWS_PLOTS_SELECT_EXPERIMENTS]: undefined [EventName.VIEWS_PLOTS_SELECT_PLOTS]: undefined [EventName.VIEWS_PLOTS_EXPERIMENT_TOGGLE]: undefined - [EventName.VIEWS_PLOTS_EXPORT_PLOT_DATA]: undefined + [EventName.VIEWS_PLOTS_EXPORT_PLOT_DATA_AS_CSV]: undefined + [EventName.VIEWS_PLOTS_EXPORT_PLOT_DATA_AS_JSON]: undefined [EventName.VIEWS_PLOTS_ZOOM_PLOT]: { isImage: boolean } [EventName.VIEWS_REORDER_PLOTS_CUSTOM]: undefined diff --git a/extension/src/test/suite/plots/index.test.ts b/extension/src/test/suite/plots/index.test.ts index c9a5418376..8f6b4ef87c 100644 --- a/extension/src/test/suite/plots/index.test.ts +++ b/extension/src/test/suite/plots/index.test.ts @@ -2,7 +2,7 @@ import { join } from 'path' import { afterEach, beforeEach, describe, it, suite } from 'mocha' import { expect } from 'chai' import { restore, spy, stub } from 'sinon' -import { commands, Uri, window } from 'vscode' +import { commands, TextDocument, Uri, window } from 'vscode' import isEqual from 'lodash.isequal' import { buildPlots } from '../plots/util' import { Disposable } from '../../../extension' @@ -475,23 +475,25 @@ suite('Plots Test Suite', () => { ) }).timeout(WEBVIEW_TEST_TIMEOUT) - it('should handle an export template plot data message from the webview', async () => { + it('should handle an export plot data as json message from the webview', async () => { const { plots } = await buildPlots({ disposer: disposable, plotsDiff: plotsDiffFixture }) const webview = await plots.showWebview() - const mockSendTelemetryEvent = stub(Telemetry, 'sendTelemetryEvent') const mockMessageReceived = getMessageReceivedEmitter(webview) + const customPlot = customPlotsFixture.plots[0] const mockShowSaveDialog = stub(window, 'showSaveDialog') const mockWriteJson = stub(FileSystem, 'writeJson') const mockOpenFile = stub(FileSystem, 'openFileInEditor') const exportFile = Uri.file('raw-data.json') - const templatePlot = templatePlotsFixture.plots[0].entries[0] + const mockShowInformationMessage = stub(window, 'showErrorMessage') + + mockShowSaveDialog.resolves(exportFile) - const undefinedFileEvent = new Promise(resolve => + const fileCancelledEvent = new Promise(resolve => mockShowSaveDialog.onFirstCall().callsFake(() => { resolve(undefined) return Promise.resolve(undefined) @@ -499,129 +501,119 @@ suite('Plots Test Suite', () => { ) mockMessageReceived.fire({ - payload: templatePlot.id, - type: MessageFromWebviewType.EXPORT_PLOT_DATA + payload: customPlot.id, + type: MessageFromWebviewType.EXPORT_PLOT_DATA_AS_JSON }) - await undefinedFileEvent + await fileCancelledEvent - expect(mockWriteJson).not.to.be.called - expect(mockOpenFile).not.to.be.called expect(mockSendTelemetryEvent).not.to.be.called + expect(mockWriteJson).not.to.be.called - const exportFileEvent = new Promise(resolve => - mockShowSaveDialog.onSecondCall().callsFake(() => { + const jsonWriteErrorEvent = new Promise(resolve => + mockWriteJson.onFirstCall().callsFake(() => { resolve(undefined) - return Promise.resolve(exportFile) + throw new Error('file failed to write') }) ) mockMessageReceived.fire({ - payload: templatePlot.id, - type: MessageFromWebviewType.EXPORT_PLOT_DATA + payload: customPlot.id, + type: MessageFromWebviewType.EXPORT_PLOT_DATA_AS_JSON }) - await exportFileEvent - - expect(mockWriteJson).to.be.calledOnce - expect(mockWriteJson).to.be.calledWithExactly( - exportFile.path, - (templatePlot.content.data as { values: unknown[] }).values, - true - ) - expect(mockOpenFile).to.be.calledOnce - expect(mockOpenFile).to.calledWithExactly(exportFile.path) - expect(mockSendTelemetryEvent).to.be.called - expect(mockSendTelemetryEvent).to.be.calledWithExactly( - EventName.VIEWS_PLOTS_EXPORT_PLOT_DATA, - undefined, - undefined - ) - }).timeout(WEBVIEW_TEST_TIMEOUT) - - it('should handle an export custom plot data message from the webview', async () => { - const { plots } = await buildPlots({ - disposer: disposable, - plotsDiff: plotsDiffFixture - }) + await jsonWriteErrorEvent - const webview = await plots.showWebview() + expect(mockOpenFile).not.to.be.called + expect(mockShowInformationMessage).to.be.called - const mockSendTelemetryEvent = stub(Telemetry, 'sendTelemetryEvent') - const mockMessageReceived = getMessageReceivedEmitter(webview) - const mockShowSaveDialog = stub(window, 'showSaveDialog') - const mockWriteJson = stub(FileSystem, 'writeJson') - const mockOpenFile = stub(FileSystem, 'openFileInEditor') - const exportFile = Uri.file('raw-data.json') - const exportFileEvent = new Promise(resolve => - mockShowSaveDialog.callsFake(() => { + const openFileEvent = new Promise(resolve => + mockOpenFile.onFirstCall().callsFake(() => { resolve(undefined) - return Promise.resolve(exportFile) + return Promise.resolve(undefined as unknown as TextDocument) }) ) - const customPlot = customPlotsFixture.plots[0] mockMessageReceived.fire({ payload: customPlot.id, - type: MessageFromWebviewType.EXPORT_PLOT_DATA + type: MessageFromWebviewType.EXPORT_PLOT_DATA_AS_JSON }) - await exportFileEvent + await openFileEvent - expect(mockWriteJson).to.be.calledOnce expect(mockWriteJson).to.be.calledWithExactly( exportFile.path, customPlot.values, true ) - expect(mockOpenFile).to.be.calledOnce expect(mockOpenFile).to.calledWithExactly(exportFile.path) - expect(mockSendTelemetryEvent).to.be.calledOnce expect(mockSendTelemetryEvent).to.be.calledWithExactly( - EventName.VIEWS_PLOTS_EXPORT_PLOT_DATA, + EventName.VIEWS_PLOTS_EXPORT_PLOT_DATA_AS_JSON, undefined, undefined ) - }).timeout(WEBVIEW_TEST_TIMEOUT) + }) - it('should handle errors being thrown during file writing when exporting plot data', async () => { + it('should handle an export plot data as csv message from the webview', async () => { const { plots } = await buildPlots({ disposer: disposable, plotsDiff: plotsDiffFixture }) const webview = await plots.showWebview() - + const mockSendTelemetryEvent = stub(Telemetry, 'sendTelemetryEvent') const mockMessageReceived = getMessageReceivedEmitter(webview) const mockShowSaveDialog = stub(window, 'showSaveDialog') + const mockWriteCsv = stub(FileSystem, 'writeCsv') const mockOpenFile = stub(FileSystem, 'openFileInEditor') - const exportFile = Uri.file('raw-data.json') + const exportFile = Uri.file('raw-data.csv') const templatePlot = templatePlotsFixture.plots[0].entries[0] - const mockShowInformationMessage = stub(window, 'showErrorMessage') - const mockWriteJson = stub(FileSystem, 'writeJson') - .onFirstCall() - .callsFake(() => { - throw new Error('failed to convert obj to json') - }) - const exportFileEvent = new Promise(resolve => + mockShowSaveDialog.resolves(exportFile) + + const fileCancelledEvent = new Promise(resolve => mockShowSaveDialog.onFirstCall().callsFake(() => { resolve(undefined) - return Promise.resolve(exportFile) + return Promise.resolve(undefined) }) ) mockMessageReceived.fire({ payload: templatePlot.id, - type: MessageFromWebviewType.EXPORT_PLOT_DATA + type: MessageFromWebviewType.EXPORT_PLOT_DATA_AS_CSV }) - await exportFileEvent + await fileCancelledEvent - expect(mockWriteJson).to.be.called + expect(mockSendTelemetryEvent).not.to.be.called + expect(mockWriteCsv).not.to.be.called expect(mockOpenFile).not.to.be.called - expect(mockShowInformationMessage).to.be.called - }).timeout(WEBVIEW_TEST_TIMEOUT) + + const openFileEvent = new Promise(resolve => + mockOpenFile.onFirstCall().callsFake(() => { + resolve(undefined) + return Promise.resolve(undefined as unknown as TextDocument) + }) + ) + + mockMessageReceived.fire({ + payload: templatePlot.id, + type: MessageFromWebviewType.EXPORT_PLOT_DATA_AS_CSV + }) + + await openFileEvent + + expect(mockWriteCsv).to.be.calledWithExactly( + exportFile.path, + (templatePlot.content.data as { values: unknown[] }).values + ) + expect(mockOpenFile).to.calledWithExactly(exportFile.path) + expect(mockSendTelemetryEvent).to.be.calledWithExactly( + EventName.VIEWS_PLOTS_EXPORT_PLOT_DATA_AS_CSV, + undefined, + undefined + ) + }) it('should handle a custom plots reordered message from the webview', async () => { const { plots, plotsModel, messageSpy } = await buildPlots({ diff --git a/extension/src/webview/contract.ts b/extension/src/webview/contract.ts index b0cdf40fcd..00e427363b 100644 --- a/extension/src/webview/contract.ts +++ b/extension/src/webview/contract.ts @@ -18,7 +18,8 @@ export enum MessageFromWebviewType { ADD_STARRED_EXPERIMENT_FILTER = 'add-starred-experiment-filter', ADD_CUSTOM_PLOT = 'add-custom-plot', CREATE_BRANCH_FROM_EXPERIMENT = 'create-branch-from-experiment', - EXPORT_PLOT_DATA = 'export-plot-data', + EXPORT_PLOT_DATA_AS_JSON = 'export-plot-data-as-json', + EXPORT_PLOT_DATA_AS_CSV = 'export-plot-data-as-csv', FOCUS_FILTERS_TREE = 'focus-filters-tree', FOCUS_SORTS_TREE = 'focus-sorts-tree', OPEN_EXPERIMENTS_WEBVIEW = 'open-experiments-webview', @@ -99,7 +100,11 @@ export type MessageFromWebview = type: MessageFromWebviewType.ADD_CUSTOM_PLOT } | { - type: MessageFromWebviewType.EXPORT_PLOT_DATA + type: MessageFromWebviewType.EXPORT_PLOT_DATA_AS_JSON + payload: string + } + | { + type: MessageFromWebviewType.EXPORT_PLOT_DATA_AS_CSV payload: string } | { diff --git a/webview/src/plots/components/App.test.tsx b/webview/src/plots/components/App.test.tsx index 551f4b2326..c13df7d22a 100644 --- a/webview/src/plots/components/App.test.tsx +++ b/webview/src/plots/components/App.test.tsx @@ -1442,7 +1442,7 @@ describe('App', () => { expect(screen.getByTestId('modal')).toBeInTheDocument() }) - it('should add a "export raw data" action to zoomed in plot modal', async () => { + it('should add a "save as json" action to zoomed in plot modal', async () => { renderAppWithOptionalData({ template: complexTemplatePlotsFixture }) @@ -1455,7 +1455,7 @@ describe('App', () => { const modal = screen.getByTestId('modal') - const customAction = await within(modal).findByText('Save Raw Data') + const customAction = await within(modal).findByText('Save as JSON') expect(customAction).toBeInTheDocument() @@ -1463,7 +1463,32 @@ describe('App', () => { expect(mockPostMessage).toHaveBeenCalledWith({ payload: complexTemplatePlotsFixture.plots[0].entries[0].id, - type: MessageFromWebviewType.EXPORT_PLOT_DATA + type: MessageFromWebviewType.EXPORT_PLOT_DATA_AS_JSON + }) + }) + + it('should add a "save as csv" action to zoomed in plot modal', async () => { + renderAppWithOptionalData({ + template: complexTemplatePlotsFixture + }) + + expect(screen.queryByTestId('modal')).not.toBeInTheDocument() + + const plot = within(screen.getAllByTestId(/^plot_/)[0]).getByRole('button') + + fireEvent.click(plot) + + const modal = screen.getByTestId('modal') + + const customAction = await within(modal).findByText('Save as CSV') + + expect(customAction).toBeInTheDocument() + + fireEvent.click(customAction) + + expect(mockPostMessage).toHaveBeenCalledWith({ + payload: complexTemplatePlotsFixture.plots[0].entries[0].id, + type: MessageFromWebviewType.EXPORT_PLOT_DATA_AS_CSV }) }) diff --git a/webview/src/plots/components/ZoomedInPlot.tsx b/webview/src/plots/components/ZoomedInPlot.tsx index e280f268e3..5742413ded 100644 --- a/webview/src/plots/components/ZoomedInPlot.tsx +++ b/webview/src/plots/components/ZoomedInPlot.tsx @@ -6,13 +6,27 @@ import cloneDeep from 'lodash.clonedeep' import { reverseOfLegendSuppressionUpdate } from 'dvc/src/plots/vega/util' import styles from './styles.module.scss' import { getThemeValue, ThemeProperty } from '../../util/styles' -import { exportPlotData } from '../util/messages' +import { exportPlotDataAsCsv, exportPlotDataAsJson } from '../util/messages' type ZoomedInPlotProps = { id: string props: VegaLiteProps } +const appendActionToVega = ( + type: string, + vegaActions: HTMLDivElement, + onClick: () => void +) => { + const rawDataAction = document.createElement('a') + rawDataAction.textContent = `Save as ${type}` + rawDataAction.addEventListener('click', () => { + onClick() + }) + rawDataAction.classList.add(styles.vegaCustomAction) + vegaActions.append(rawDataAction) +} + export const ZoomedInPlot: React.FC = ({ id, props @@ -29,14 +43,13 @@ export const ZoomedInPlot: React.FC = ({ }, []) const onNewView = () => { - const actions = zoomedInPlotRef.current?.querySelector('.vega-actions') - const rawDataAction = document.createElement('a') - rawDataAction.textContent = 'Save Raw Data' - rawDataAction.addEventListener('click', () => { - exportPlotData(id) - }) - rawDataAction.classList.add(styles.vegaCustomAction) - actions?.append(rawDataAction) + const actions: HTMLDivElement | null | undefined = + zoomedInPlotRef.current?.querySelector('.vega-actions') + if (!actions) { + return + } + appendActionToVega('JSON', actions, () => exportPlotDataAsJson(id)) + appendActionToVega('CSV', actions, () => exportPlotDataAsCsv(id)) } return ( diff --git a/webview/src/plots/util/messages.ts b/webview/src/plots/util/messages.ts index 77c9775bc2..e16969e8cb 100644 --- a/webview/src/plots/util/messages.ts +++ b/webview/src/plots/util/messages.ts @@ -89,9 +89,16 @@ export const togglePlotsSection = ( export const zoomPlot = (imagePath?: string) => sendMessage({ payload: imagePath, type: MessageFromWebviewType.ZOOM_PLOT }) -export const exportPlotData = (id: string) => { +export const exportPlotDataAsCsv = (id: string) => { sendMessage({ payload: id, - type: MessageFromWebviewType.EXPORT_PLOT_DATA + type: MessageFromWebviewType.EXPORT_PLOT_DATA_AS_CSV + }) +} + +export const exportPlotDataAsJson = (id: string) => { + sendMessage({ + payload: id, + type: MessageFromWebviewType.EXPORT_PLOT_DATA_AS_JSON }) } diff --git a/yarn.lock b/yarn.lock index e189a83dac..6eca0643c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9257,6 +9257,11 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +deeks@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/deeks/-/deeks-3.0.0.tgz#1f66b6c78b6daf39a90a39389592e12397990a9b" + integrity sha512-go4YE5vDMzDNzC9OqK7iIQd6agUUqZdBiw3TD2niRxsKGdGdB1FW8eZdrWsyCRZCNkBjV5Vutcx5uT1AikEPCg== + deep-eql@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" @@ -9617,6 +9622,11 @@ dns-packet@^5.2.2: dependencies: "@leichtgewicht/ip-codec" "^2.0.1" +doc-path@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/doc-path/-/doc-path-4.0.0.tgz#8edad83bdc31da53f7bf0b0b3c08d7b5f53517dd" + integrity sha512-By/OF7rbws/bgExPZW10gHASoy3sACwXKKpKBIzYk/E19nX8q8IbILbreCeCYAVFIcO9FOxDjFYbm45wvn4I6w== + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -13988,6 +13998,14 @@ jsesc@~0.5.0: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= +json-2-csv@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/json-2-csv/-/json-2-csv-4.0.0.tgz#b65cf5256f003cc64b241d0a21f68df8347e9150" + integrity sha512-r8WXuuLgVbeufMuCyzUXSp8VP9YK4BTuzM7rTG0X85MPKuHKa8JD6LHrp0rEKVCtBSgHA3LMu/VjrZHx+851DQ== + dependencies: + deeks "3.0.0" + doc-path "4.0.0" + json-buffer@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"