mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
ShareModal: Export options (JSON) (#87082)
* Adding new export button * Create Export as JSON drawer * update scene drawer and add css * update css * Update ExportAsJson to be regular react component * add tests to export menu and button * add tests * prettier and lint * fix translations * update translation * Apply suggestions from code review Co-authored-by: Juan Cabanas <juan.cabanas@grafana.com> * delete extra file * Update to use SceneObject * add spinner * Rename ExportAsJSON.tsx to ExportAsJson.tsx * update i18n * Upate texts * small fixes from code review * add space * i18n * fix build issues * changes from review feedback * update test * update test --------- Co-authored-by: Juan Cabanas <juan.cabanas@grafana.com>
This commit is contained in:
parent
79092ebc6a
commit
058538287f
@ -69,6 +69,14 @@ export const Pages = {
|
||||
shareSnapshot: 'data-testid new share button share snapshot',
|
||||
},
|
||||
},
|
||||
NewExportButton: {
|
||||
container: 'data-testid new export button',
|
||||
arrowMenu: 'data-testid new export button arrow menu',
|
||||
Menu: {
|
||||
container: 'data-testid new export button menu',
|
||||
exportAsJson: 'data-testid new export button export as json',
|
||||
},
|
||||
},
|
||||
playlistControls: {
|
||||
prev: 'data-testid playlist previous dashboard button',
|
||||
stop: 'data-testid playlist stop dashboard button',
|
||||
@ -287,6 +295,16 @@ export const Pages = {
|
||||
container: 'data-testid share snapshot drawer container',
|
||||
},
|
||||
},
|
||||
ExportDashboardDrawer: {
|
||||
ExportAsJson: {
|
||||
container: 'data-testid export as Json drawer container',
|
||||
codeEditor: 'data-testid export as Json code editor',
|
||||
exportExternallyToggle: 'data-testid export externally toggle type select',
|
||||
saveToFileButton: 'data-testid save to file button',
|
||||
copyToClipboardButton: 'data-testid copy to clipboard button',
|
||||
cancelButton: 'data-testid cancel button',
|
||||
},
|
||||
},
|
||||
PublicDashboard: {
|
||||
page: 'public-dashboard-page',
|
||||
NotAvailable: {
|
||||
|
@ -153,6 +153,8 @@ describe('NavToolbarActions', () => {
|
||||
expect(await screen.findByText('Share')).toBeInTheDocument();
|
||||
const newShareButton = screen.queryByTestId(selectors.pages.Dashboard.DashNav.newShareButton.container);
|
||||
expect(newShareButton).not.toBeInTheDocument();
|
||||
const newExportButton = screen.queryByTestId(selectors.pages.Dashboard.DashNav.NewExportButton.container);
|
||||
expect(newExportButton).not.toBeInTheDocument();
|
||||
});
|
||||
it('Should show new share button when newDashboardSharingComponent FF is enabled', async () => {
|
||||
config.featureToggles.newDashboardSharingComponent = true;
|
||||
@ -162,6 +164,12 @@ describe('NavToolbarActions', () => {
|
||||
const newShareButton = screen.getByTestId(selectors.pages.Dashboard.DashNav.newShareButton.container);
|
||||
expect(newShareButton).toBeInTheDocument();
|
||||
});
|
||||
it('Should show new export button when newDashboardSharingComponent FF is enabled', async () => {
|
||||
config.featureToggles.newDashboardSharingComponent = true;
|
||||
setup();
|
||||
const newExportButton = screen.getByTestId(selectors.pages.Dashboard.DashNav.NewExportButton.container);
|
||||
expect(newExportButton).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Snapshot', () => {
|
||||
|
@ -24,6 +24,7 @@ import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
||||
|
||||
import { PanelEditor, buildPanelEditScene } from '../panel-edit/PanelEditor';
|
||||
import ExportButton from '../sharing/ExportButton/ExportButton';
|
||||
import ShareButton from '../sharing/ShareButton/ShareButton';
|
||||
import { ShareModal } from '../sharing/ShareModal';
|
||||
import { DashboardInteractions } from '../utils/interactions';
|
||||
@ -373,7 +374,13 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
});
|
||||
|
||||
toolbarActions.push({
|
||||
group: 'new-share-dashboard-button',
|
||||
group: 'new-share-dashboard-buttons',
|
||||
condition: config.featureToggles.newDashboardSharingComponent && showShareButton,
|
||||
render: () => <ExportButton key="new-export-dashboard-button" dashboard={dashboard} />,
|
||||
});
|
||||
|
||||
toolbarActions.push({
|
||||
group: 'new-share-dashboard-buttons',
|
||||
condition: config.featureToggles.newDashboardSharingComponent && showShareButton,
|
||||
render: () => <ShareButton key="new-share-dashboard-button" dashboard={dashboard} />,
|
||||
});
|
||||
|
@ -0,0 +1,110 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useAsync } from 'react-use';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
||||
import { SceneComponentProps } from '@grafana/scenes';
|
||||
import { Button, ClipboardButton, CodeEditor, Label, Spinner, Stack, Switch, useStyles2 } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import { getDashboardSceneFor } from '../../utils/utils';
|
||||
import { ShareExportTab } from '../ShareExportTab';
|
||||
|
||||
const selector = e2eSelectors.pages.ExportDashboardDrawer.ExportAsJson;
|
||||
|
||||
export class ExportAsJson extends ShareExportTab {
|
||||
static Component = ExportAsJsonRenderer;
|
||||
}
|
||||
|
||||
function ExportAsJsonRenderer({ model }: SceneComponentProps<ExportAsJson>) {
|
||||
const dashboard = getDashboardSceneFor(model);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const { isSharingExternally } = model.useState();
|
||||
|
||||
const dashboardJson = useAsync(async () => {
|
||||
const json = await model.getExportableDashboardJson();
|
||||
return JSON.stringify(json, null, 2);
|
||||
}, [isSharingExternally]);
|
||||
|
||||
const switchLabel = t('export.json.export-externally-label', 'Export the dashboard to use in another instance');
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
<Trans i18nKey="export.json.info-text">
|
||||
Copy or download a JSON file containing the JSON of your dashboard
|
||||
</Trans>
|
||||
</p>
|
||||
<Stack gap={1} alignItems="center">
|
||||
<Switch
|
||||
label={switchLabel}
|
||||
data-testid={selector.exportExternallyToggle}
|
||||
id="export-externally-toggle"
|
||||
value={isSharingExternally}
|
||||
onChange={model.onShareExternallyChange}
|
||||
/>
|
||||
<Label>{switchLabel}</Label>
|
||||
</Stack>
|
||||
<AutoSizer disableHeight className={styles.codeEditorBox} data-testid={selector.codeEditor}>
|
||||
{({ width }) => {
|
||||
if (dashboardJson.value) {
|
||||
return (
|
||||
<CodeEditor
|
||||
value={dashboardJson.value}
|
||||
language="json"
|
||||
showMiniMap={false}
|
||||
height="500px"
|
||||
width={width}
|
||||
readOnly={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return dashboardJson.loading && <Spinner />;
|
||||
}}
|
||||
</AutoSizer>
|
||||
<div className={styles.container}>
|
||||
<Stack gap={1} flex={1} direction={{ xs: 'column', sm: 'row' }}>
|
||||
<Button
|
||||
data-testid={selector.saveToFileButton}
|
||||
variant="primary"
|
||||
icon="download-alt"
|
||||
onClick={model.onSaveAsFile}
|
||||
>
|
||||
<Trans i18nKey="export.json.download-button">Download file</Trans>
|
||||
</Button>
|
||||
<ClipboardButton
|
||||
data-testid={selector.copyToClipboardButton}
|
||||
variant="secondary"
|
||||
icon="copy"
|
||||
disabled={dashboardJson.loading}
|
||||
getText={() => dashboardJson.value ?? ''}
|
||||
>
|
||||
<Trans i18nKey="export.json.copy-button">Copy to clipboard</Trans>
|
||||
</ClipboardButton>
|
||||
<Button
|
||||
data-testid={selector.cancelButton}
|
||||
variant="secondary"
|
||||
onClick={() => dashboard.closeModal()}
|
||||
fill="outline"
|
||||
>
|
||||
<Trans i18nKey="export.json.cancel-button">Cancel</Trans>
|
||||
</Button>
|
||||
</Stack>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
codeEditorBox: css({
|
||||
margin: `${theme.spacing(2)} 0`,
|
||||
}),
|
||||
container: css({
|
||||
paddingBottom: theme.spacing(2),
|
||||
}),
|
||||
};
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
||||
import { SceneGridLayout, SceneTimeRange, VizPanel } from '@grafana/scenes';
|
||||
|
||||
import { DashboardGridItem } from '../../scene/DashboardGridItem';
|
||||
import { DashboardScene } from '../../scene/DashboardScene';
|
||||
|
||||
import ExportButton from './ExportButton';
|
||||
|
||||
const selector = e2eSelectors.pages.Dashboard.DashNav.NewExportButton;
|
||||
|
||||
describe('ExportButton', () => {
|
||||
it('should render Export menu', async () => {
|
||||
setup();
|
||||
expect(await screen.findByTestId(selector.arrowMenu)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render menu when arrow button clicked', async () => {
|
||||
setup();
|
||||
|
||||
const arrowMenu = await screen.findByTestId(selector.arrowMenu);
|
||||
await userEvent.click(arrowMenu);
|
||||
|
||||
expect(await screen.findByTestId(selector.Menu.container)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
function setup() {
|
||||
const panel = new VizPanel({
|
||||
title: 'Panel A',
|
||||
pluginId: 'table',
|
||||
key: 'panel-12',
|
||||
});
|
||||
|
||||
const dashboard = new DashboardScene({
|
||||
title: 'hello',
|
||||
uid: 'dash-1',
|
||||
$timeRange: new SceneTimeRange({}),
|
||||
body: new SceneGridLayout({
|
||||
children: [
|
||||
new DashboardGridItem({
|
||||
key: 'griditem-1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 10,
|
||||
height: 12,
|
||||
body: panel,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
render(<ExportButton dashboard={dashboard} />);
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
||||
import { Button, ButtonGroup, Dropdown, Icon } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import { DashboardScene } from '../../scene/DashboardScene';
|
||||
|
||||
import ExportMenu from './ExportMenu';
|
||||
|
||||
const newExportButtonSelector = e2eSelectors.pages.Dashboard.DashNav.NewExportButton;
|
||||
|
||||
export default function ExportButton({ dashboard }: { dashboard: DashboardScene }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const onMenuClick = useCallback((isOpen: boolean) => {
|
||||
setIsOpen(isOpen);
|
||||
}, []);
|
||||
|
||||
const MenuActions = () => <ExportMenu dashboard={dashboard} />;
|
||||
|
||||
return (
|
||||
<ButtonGroup data-testid={newExportButtonSelector.container}>
|
||||
<Dropdown overlay={MenuActions} placement="bottom-end" onVisibleChange={onMenuClick}>
|
||||
<Button
|
||||
data-testid={newExportButtonSelector.arrowMenu}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
fill="solid"
|
||||
tooltip={t('export.menu.export-as-json-tooltip', 'Export')}
|
||||
>
|
||||
<Trans i18nKey="export.menu.export-as-json-label">Export</Trans>
|
||||
<Icon name={isOpen ? 'angle-up' : 'angle-down'} size="lg" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
||||
import { SceneGridLayout, SceneTimeRange, VizPanel } from '@grafana/scenes';
|
||||
|
||||
import { DashboardGridItem } from '../../scene/DashboardGridItem';
|
||||
import { DashboardScene } from '../../scene/DashboardScene';
|
||||
|
||||
import ExportMenu from './ExportMenu';
|
||||
|
||||
const selector = e2eSelectors.pages.Dashboard.DashNav.NewExportButton.Menu;
|
||||
|
||||
describe('ExportMenu', () => {
|
||||
it('should render menu items', async () => {
|
||||
setup();
|
||||
expect(await screen.findByTestId(selector.exportAsJson)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
function setup() {
|
||||
const panel = new VizPanel({
|
||||
title: 'Panel A',
|
||||
pluginId: 'table',
|
||||
key: 'panel-12',
|
||||
});
|
||||
const dashboard = new DashboardScene({
|
||||
title: 'hello',
|
||||
uid: 'dash-1',
|
||||
$timeRange: new SceneTimeRange({}),
|
||||
body: new SceneGridLayout({
|
||||
children: [
|
||||
new DashboardGridItem({
|
||||
key: 'griditem-1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 10,
|
||||
height: 12,
|
||||
body: panel,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
render(<ExportMenu dashboard={dashboard} />);
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
||||
import { Menu } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { DashboardScene } from '../../scene/DashboardScene';
|
||||
import { ShareDrawer } from '../ShareDrawer/ShareDrawer';
|
||||
|
||||
import { ExportAsJson } from './ExportAsJson';
|
||||
|
||||
const newExportButtonSelector = e2eSelectors.pages.Dashboard.DashNav.NewExportButton.Menu;
|
||||
|
||||
export default function ExportMenu({ dashboard }: { dashboard: DashboardScene }) {
|
||||
const onExportAsJsonClick = () => {
|
||||
const drawer = new ShareDrawer({
|
||||
title: t('export.json.title', 'Save dashboard JSON'),
|
||||
body: new ExportAsJson({}),
|
||||
});
|
||||
|
||||
dashboard.showModal(drawer);
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu data-testid={newExportButtonSelector.container}>
|
||||
<Menu.Item
|
||||
testId={newExportButtonSelector.exportAsJson}
|
||||
label={t('share-dashboard.menu.export-json-title', 'Export as JSON')}
|
||||
icon="arrow"
|
||||
onClick={onExportAsJsonClick}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
}
|
@ -16,7 +16,7 @@ import { getDashboardSceneFor } from '../utils/utils';
|
||||
|
||||
import { SceneShareTabState } from './types';
|
||||
|
||||
interface ShareExportTabState extends SceneShareTabState {
|
||||
export interface ShareExportTabState extends SceneShareTabState {
|
||||
isSharingExternally?: boolean;
|
||||
isViewingJSON?: boolean;
|
||||
}
|
||||
@ -55,7 +55,7 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> {
|
||||
return;
|
||||
}
|
||||
|
||||
public async getExportableDashboardJson() {
|
||||
public getExportableDashboardJson = async () => {
|
||||
const { isSharingExternally } = this.state;
|
||||
const saveModel = transformSceneToSaveModel(getDashboardSceneFor(this));
|
||||
|
||||
@ -70,9 +70,9 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> {
|
||||
: saveModel;
|
||||
|
||||
return exportable;
|
||||
}
|
||||
};
|
||||
|
||||
public async onSaveAsFile() {
|
||||
public onSaveAsFile = async () => {
|
||||
const dashboardJson = await this.getExportableDashboardJson();
|
||||
const dashboardJsonPretty = JSON.stringify(dashboardJson, null, 2);
|
||||
const { isSharingExternally } = this.state;
|
||||
@ -90,7 +90,7 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> {
|
||||
DashboardInteractions.exportDownloadJsonClicked({
|
||||
externally: isSharingExternally,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function ShareExportTabRenderer({ model }: SceneComponentProps<ShareExportTab>) {
|
||||
|
@ -686,6 +686,20 @@
|
||||
"split-widen": "Widen pane"
|
||||
}
|
||||
},
|
||||
"export": {
|
||||
"json": {
|
||||
"cancel-button": "Cancel",
|
||||
"copy-button": "Copy to clipboard",
|
||||
"download-button": "Download file",
|
||||
"export-externally-label": "Export the dashboard to use in another instance",
|
||||
"info-text": "Copy or download a JSON file containing the JSON of your dashboard",
|
||||
"title": "Save dashboard JSON"
|
||||
},
|
||||
"menu": {
|
||||
"export-as-json-label": "Export",
|
||||
"export-as-json-tooltip": "Export"
|
||||
}
|
||||
},
|
||||
"folder-picker": {
|
||||
"loading": "Loading folders..."
|
||||
},
|
||||
@ -1729,6 +1743,7 @@
|
||||
},
|
||||
"share-dashboard": {
|
||||
"menu": {
|
||||
"export-json-title": "Export as JSON",
|
||||
"share-externally-title": "Share externally",
|
||||
"share-internally-description": "Advanced settings",
|
||||
"share-internally-title": "Share internally",
|
||||
|
@ -686,6 +686,20 @@
|
||||
"split-widen": "Ŵįđęʼn päʼnę"
|
||||
}
|
||||
},
|
||||
"export": {
|
||||
"json": {
|
||||
"cancel-button": "Cäʼnčęľ",
|
||||
"copy-button": "Cőpy ŧő čľįpþőäřđ",
|
||||
"download-button": "Đőŵʼnľőäđ ƒįľę",
|
||||
"export-externally-label": "Ēχpőřŧ ŧĥę đäşĥþőäřđ ŧő ūşę įʼn äʼnőŧĥęř įʼnşŧäʼnčę",
|
||||
"info-text": "Cőpy őř đőŵʼnľőäđ ä ĴŜØŃ ƒįľę čőʼnŧäįʼnįʼnģ ŧĥę ĴŜØŃ őƒ yőūř đäşĥþőäřđ",
|
||||
"title": "Ŝävę đäşĥþőäřđ ĴŜØŃ"
|
||||
},
|
||||
"menu": {
|
||||
"export-as-json-label": "Ēχpőřŧ",
|
||||
"export-as-json-tooltip": "Ēχpőřŧ"
|
||||
}
|
||||
},
|
||||
"folder-picker": {
|
||||
"loading": "Ŀőäđįʼnģ ƒőľđęřş..."
|
||||
},
|
||||
@ -1729,6 +1743,7 @@
|
||||
},
|
||||
"share-dashboard": {
|
||||
"menu": {
|
||||
"export-json-title": "Ēχpőřŧ äş ĴŜØŃ",
|
||||
"share-externally-title": "Ŝĥäřę ęχŧęřʼnäľľy",
|
||||
"share-internally-description": "Åđväʼnčęđ şęŧŧįʼnģş",
|
||||
"share-internally-title": "Ŝĥäřę įʼnŧęřʼnäľľy",
|
||||
|
Loading…
Reference in New Issue
Block a user