Dashboard scene: Share modal export (#75640)

* Dashboard scene: Share modal export

* i18n
This commit is contained in:
Dominik Prokop 2023-09-29 09:34:22 +02:00 committed by GitHub
parent 7a38090bc0
commit 933d920121
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 233 additions and 19 deletions

View File

@ -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<DashboardScene>;
isSharingExternally?: boolean;
shouldTrimDefaults?: boolean;
isViewingJSON?: boolean;
}
export class ShareExportTab extends SceneObjectBase<ShareExportTabState> {
static Component = ShareExportTabRenderer;
private _exporter = new DashboardExporter();
constructor(state: Omit<ShareExportTabState, 'panelRef'>) {
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<ShareExportTab>) {
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 && (
<>
<p className="share-modal-info-text">
<Trans i18nKey="share-modal.export.info-text">Export this dashboard.</Trans>
</p>
<VerticalGroup spacing="md">
<Field label={exportExternallyTranslation}>
<Switch
id="share-externally-toggle"
value={isSharingExternally}
onChange={model.onShareExternallyChange}
/>
</Field>
{config.featureToggles.trimDefaults && (
<Field label={exportDefaultTranslation}>
<Switch id="trim-defaults-toggle" value={shouldTrimDefaults} onChange={model.onTrimDefaultsChange} />
</Field>
)}
</VerticalGroup>
<Modal.ButtonRow>
<Button
variant="secondary"
onClick={() => {
modalRef?.resolve().onDismiss();
}}
fill="outline"
>
<Trans i18nKey="share-modal.export.cancel-button">Cancel</Trans>
</Button>
<Button variant="secondary" icon="brackets-curly" onClick={model.onViewJSON}>
<Trans i18nKey="share-modal.export.view-button">View JSON</Trans>
</Button>
<Button variant="primary" icon="save" onClick={() => model.onSaveAsFile()}>
<Trans i18nKey="share-modal.export.save-button">Save to file</Trans>
</Button>
</Modal.ButtonRow>
</>
)}
{isViewingJSON && (
<>
<AutoSizer disableHeight>
{({ width }) => {
if (dashboardJson.value) {
return (
<CodeEditor
value={dashboardJson.value ?? ''}
language="json"
showMiniMap={false}
height="500px"
width={width}
/>
);
}
if (dashboardJson.loading) {
return <div>Loading...</div>;
}
return null;
}}
</AutoSizer>
<Modal.ButtonRow>
<Button variant="secondary" fill="outline" onClick={model.onViewJSON} icon="arrow-left">
<Trans i18nKey="share-modal.export.back-button">Back to export config</Trans>
</Button>
<ClipboardButton
variant="secondary"
icon="copy"
disabled={dashboardJson.loading}
getText={() => dashboardJson.value ?? ''}
>
<Trans i18nKey="share-modal.view-json.copy-button">Copy to Clipboard</Trans>
</ClipboardButton>
<Button variant="primary" icon="save" disabled={dashboardJson.loading} onClick={() => model.onSaveAsFile()}>
<Trans i18nKey="share-modal.export.save-button">Save to file</Trans>
</Button>
</Modal.ButtonRow>
</>
)}
</>
);
}

View File

@ -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<VizPanel>;
dashboardRef: SceneObjectRef<DashboardScene>;
}

View File

@ -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<DashboardScene>;
@ -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<ShareModalState> {
export class ShareModal extends SceneObjectBase<ShareModalState> implements ModalSceneObjectLike {
static Component = SharePanelModalRenderer;
constructor(state: Omit<ShareModalState, 'activeTab'>) {
@ -38,10 +39,14 @@ export class ShareModal extends SceneObjectBase<ShareModalState> {
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<ShareModalState> {
// }
}
onClose = () => {
onDismiss = () => {
const dashboard = getDashboardSceneFor(this);
dashboard.closeModal();
};
@ -110,7 +115,7 @@ function SharePanelModalRenderer({ model }: SceneComponentProps<ShareModal>) {
const currentTab = tabs.find((t) => t.getTabLabel() === activeTab);
return (
<Modal isOpen={true} title={header} onDismiss={model.onClose}>
<Modal isOpen={true} title={header} onDismiss={model.onDismiss}>
<TabContent>{currentTab && <currentTab.Component model={currentTab} />}</TabContent>
</Modal>
);

View File

@ -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<VizPanel>;
}

View File

@ -1,5 +1,13 @@
import { SceneObject, SceneObjectState } from '@grafana/scenes';
import { SceneObject, SceneObjectRef, SceneObjectState } from '@grafana/scenes';
export interface SceneShareTab<T extends SceneObjectState = SceneObjectState> extends SceneObject<T> {
export interface ModalSceneObjectLike {
onDismiss: () => void;
}
export interface SceneShareTabState extends SceneObjectState {
modalRef?: SceneObjectRef<ModalSceneObjectLike>;
}
export interface SceneShareTab<T extends SceneShareTabState = SceneShareTabState> extends SceneObject<T> {
getTabLabel(): string;
}

View File

@ -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 …",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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ę ŧő ƒįľę",

View File

@ -755,6 +755,7 @@
"time-range-description": "将当前相对时间范围转换为绝对时间范围"
},
"export": {
"back-button": "",
"cancel-button": "取消",
"info-text": "导出此仪表板。",
"save-button": "保存至文件",