diff --git a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx new file mode 100644 index 00000000000..e6a9831ca34 --- /dev/null +++ b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx @@ -0,0 +1,199 @@ +import saveAs from 'file-saver'; +import React from 'react'; +import { useAsync } from 'react-use'; +import AutoSizer from 'react-virtualized-auto-sizer'; + +import { config, getBackendSrv } from '@grafana/runtime'; +import { SceneComponentProps, SceneObjectBase, SceneObjectRef } from '@grafana/scenes'; +import { Button, ClipboardButton, CodeEditor, Field, Modal, Switch, VerticalGroup } from '@grafana/ui'; +import { t, Trans } from 'app/core/internationalization'; +import { DashboardExporter } from 'app/features/dashboard/components/DashExportModal'; +import { trackDashboardSharingActionPerType } from 'app/features/dashboard/components/ShareModal/analytics'; +import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; +import { DashboardModel } from 'app/features/dashboard/state'; + +import { DashboardScene } from '../scene/DashboardScene'; +import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; + +import { SceneShareTabState } from './types'; + +const exportExternallyTranslation = t('share-modal.export.share-externally-label', `Export for sharing externally`); +const exportDefaultTranslation = t('share-modal.export.share-default-label', `Export with default values removed`); + +interface ShareExportTabState extends SceneShareTabState { + dashboardRef: SceneObjectRef; + isSharingExternally?: boolean; + shouldTrimDefaults?: boolean; + isViewingJSON?: boolean; +} + +export class ShareExportTab extends SceneObjectBase { + static Component = ShareExportTabRenderer; + + private _exporter = new DashboardExporter(); + + constructor(state: Omit) { + super({ + isSharingExternally: false, + shouldTrimDefaults: false, + isViewingJSON: false, + ...state, + }); + } + + public getTabLabel() { + return t('share-modal.tab-title.export', 'Export'); + } + + public onShareExternallyChange = () => { + this.setState({ + isSharingExternally: !this.state.isSharingExternally, + }); + }; + + public onTrimDefaultsChange = () => { + this.setState({ + shouldTrimDefaults: !this.state.shouldTrimDefaults, + }); + }; + + public onViewJSON = () => { + this.setState({ + isViewingJSON: !this.state.isViewingJSON, + }); + }; + + public getClipboardText() { + return; + } + + public async getExportableDashboardJson() { + const { dashboardRef, isSharingExternally, shouldTrimDefaults } = this.state; + const saveModel = transformSceneToSaveModel(dashboardRef.resolve()); + + const exportable = isSharingExternally + ? await this._exporter.makeExportable(new DashboardModel(saveModel)) + : saveModel; + + if (shouldTrimDefaults) { + const trimmed = await getBackendSrv().post('/api/dashboards/trim', { dashboard: exportable }); + return trimmed.dashboard; + } else { + return exportable; + } + } + + public async onSaveAsFile() { + const dashboardJson = await this.getExportableDashboardJson(); + const dashboardJsonPretty = JSON.stringify(dashboardJson, null, 2); + + const blob = new Blob([dashboardJsonPretty], { + type: 'application/json;charset=utf-8', + }); + + const time = new Date().getTime(); + saveAs(blob, `${dashboardJson.title}-${time}.json`); + trackDashboardSharingActionPerType('save_export', shareDashboardType.export); + } +} + +function ShareExportTabRenderer({ model }: SceneComponentProps) { + const { isSharingExternally, shouldTrimDefaults, isViewingJSON, modalRef } = model.useState(); + + const dashboardJson = useAsync(async () => { + if (isViewingJSON) { + const json = await model.getExportableDashboardJson(); + return JSON.stringify(json, null, 2); + } + + return ''; + }, [isViewingJSON]); + + return ( + <> + {!isViewingJSON && ( + <> +

+ Export this dashboard. +

+ + + + + + {config.featureToggles.trimDefaults && ( + + + + )} + + + + + + + + + )} + + {isViewingJSON && ( + <> + + {({ width }) => { + if (dashboardJson.value) { + return ( + + ); + } + + if (dashboardJson.loading) { + return
Loading...
; + } + + return null; + }} +
+ + + + dashboardJson.value ?? ''} + > + Copy to Clipboard + + + + + )} + + ); +} diff --git a/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx b/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx index 0c0dda88446..52dc9c2f6ae 100644 --- a/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx @@ -3,14 +3,7 @@ import React from 'react'; import { dateTime, UrlQueryMap } from '@grafana/data'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { config, locationService } from '@grafana/runtime'; -import { - SceneComponentProps, - SceneObjectBase, - SceneObjectState, - SceneObjectRef, - VizPanel, - sceneGraph, -} from '@grafana/scenes'; +import { SceneComponentProps, SceneObjectBase, SceneObjectRef, VizPanel, sceneGraph } from '@grafana/scenes'; import { TimeZone } from '@grafana/schema'; import { Alert, ClipboardButton, Field, FieldSet, Icon, Input, Switch } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; @@ -22,7 +15,8 @@ import { shareDashboardType } from 'app/features/dashboard/components/ShareModal import { DashboardScene } from '../scene/DashboardScene'; import { getDashboardUrl } from '../utils/utils'; -export interface ShareLinkTabState extends SceneObjectState, ShareOptions { +import { SceneShareTabState } from './types'; +export interface ShareLinkTabState extends SceneShareTabState, ShareOptions { panelRef?: SceneObjectRef; dashboardRef: SceneObjectRef; } diff --git a/public/app/features/dashboard-scene/sharing/ShareModal.tsx b/public/app/features/dashboard-scene/sharing/ShareModal.tsx index d92fb0a881d..4bc3b19e84b 100644 --- a/public/app/features/dashboard-scene/sharing/ShareModal.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareModal.tsx @@ -9,9 +9,10 @@ import { t } from 'app/core/internationalization'; import { DashboardScene } from '../scene/DashboardScene'; import { getDashboardSceneFor } from '../utils/utils'; +import { ShareExportTab } from './ShareExportTab'; import { ShareLinkTab } from './ShareLinkTab'; import { ShareSnapshotTab } from './ShareSnapshotTab'; -import { SceneShareTab } from './types'; +import { ModalSceneObjectLike, SceneShareTab } from './types'; interface ShareModalState extends SceneObjectState { dashboardRef: SceneObjectRef; @@ -23,7 +24,7 @@ interface ShareModalState extends SceneObjectState { /** * Used for full dashboard share modal and the panel level share modal */ -export class ShareModal extends SceneObjectBase { +export class ShareModal extends SceneObjectBase implements ModalSceneObjectLike { static Component = SharePanelModalRenderer; constructor(state: Omit) { @@ -38,10 +39,14 @@ export class ShareModal extends SceneObjectBase { private buildTabs() { const { dashboardRef, panelRef } = this.state; - const tabs: SceneShareTab[] = [new ShareLinkTab({ dashboardRef, panelRef })]; + const tabs: SceneShareTab[] = [new ShareLinkTab({ dashboardRef, panelRef, modalRef: this.getRef() })]; + + if (!panelRef) { + tabs.push(new ShareExportTab({ dashboardRef, modalRef: this.getRef() })); + } if (contextSrv.isSignedIn && config.snapshotEnabled) { - tabs.push(new ShareSnapshotTab({ panelRef })); + tabs.push(new ShareSnapshotTab({ panelRef, modalRef: this.getRef() })); } this.setState({ tabs }); @@ -74,7 +79,7 @@ export class ShareModal extends SceneObjectBase { // } } - onClose = () => { + onDismiss = () => { const dashboard = getDashboardSceneFor(this); dashboard.closeModal(); }; @@ -110,7 +115,7 @@ function SharePanelModalRenderer({ model }: SceneComponentProps) { const currentTab = tabs.find((t) => t.getTabLabel() === activeTab); return ( - + {currentTab && } ); diff --git a/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx b/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx index 091ac8484bf..fa3562c8cc8 100644 --- a/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx @@ -1,9 +1,11 @@ import React from 'react'; -import { SceneComponentProps, SceneObjectBase, SceneObjectState, SceneObjectRef, VizPanel } from '@grafana/scenes'; +import { SceneComponentProps, SceneObjectBase, SceneObjectRef, VizPanel } from '@grafana/scenes'; import { t } from 'app/core/internationalization'; -export interface ShareSnapshotTabState extends SceneObjectState { +import { SceneShareTabState } from './types'; + +export interface ShareSnapshotTabState extends SceneShareTabState { panelRef?: SceneObjectRef; } diff --git a/public/app/features/dashboard-scene/sharing/types.ts b/public/app/features/dashboard-scene/sharing/types.ts index d10648f83c1..3dafb3b89f2 100644 --- a/public/app/features/dashboard-scene/sharing/types.ts +++ b/public/app/features/dashboard-scene/sharing/types.ts @@ -1,5 +1,13 @@ -import { SceneObject, SceneObjectState } from '@grafana/scenes'; +import { SceneObject, SceneObjectRef, SceneObjectState } from '@grafana/scenes'; -export interface SceneShareTab extends SceneObject { +export interface ModalSceneObjectLike { + onDismiss: () => void; +} + +export interface SceneShareTabState extends SceneObjectState { + modalRef?: SceneObjectRef; +} + +export interface SceneShareTab extends SceneObject { getTabLabel(): string; } diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index b69fd655363..4a69fc6ae23 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -760,6 +760,7 @@ "time-range-description": "Wandelt den aktuellen relativen Zeitbereich in einen absoluten Zeitbereich um" }, "export": { + "back-button": "", "cancel-button": "Abbrechen", "info-text": "Dieses Dashboard exportieren.", "save-button": "In Datei speichern …", diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 98a8351e233..4e3cbc02e6e 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -760,6 +760,7 @@ "time-range-description": "Transforms the current relative time range to an absolute time range" }, "export": { + "back-button": "Back to export config", "cancel-button": "Cancel", "info-text": "Export this dashboard.", "save-button": "Save to file", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 32a640d1908..6a54352db6c 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -765,6 +765,7 @@ "time-range-description": "Transforma el intervalo de tiempo relativo actual en un intervalo de tiempo absoluto" }, "export": { + "back-button": "", "cancel-button": "Cancelar", "info-text": "Exporte este panel de control.", "save-button": "Guardar en archivo", diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 6cc94dd61c2..800197b1483 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -765,6 +765,7 @@ "time-range-description": "Transforme la période temporelle relative actuelle en une période temporelle absolue" }, "export": { + "back-button": "", "cancel-button": "Annuler", "info-text": "Exporter ce tableau de bord.", "save-button": "Enregistrer dans un fichier", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 0e8bfb89b97..ff6033e16ea 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -760,6 +760,7 @@ "time-range-description": "Ŧřäʼnşƒőřmş ŧĥę čūřřęʼnŧ řęľäŧįvę ŧįmę řäʼnģę ŧő äʼn äþşőľūŧę ŧįmę řäʼnģę" }, "export": { + "back-button": "", "cancel-button": "Cäʼnčęľ", "info-text": "Ēχpőřŧ ŧĥįş đäşĥþőäřđ.", "save-button": "Ŝävę ŧő ƒįľę", diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 13bed6318fb..539ddebd8f6 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -755,6 +755,7 @@ "time-range-description": "将当前相对时间范围转换为绝对时间范围" }, "export": { + "back-button": "", "cancel-button": "取消", "info-text": "导出此仪表板。", "save-button": "保存至文件",