mirror of https://github.com/grafana/grafana
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>pull/89995/head
parent
79092ebc6a
commit
058538287f
@ -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> |
||||
); |
||||
} |
Loading…
Reference in new issue