mirror of https://github.com/grafana/grafana
ShareDrawer: Share snapshot (#89195)
parent
946545cfc5
commit
399651b9ad
@ -0,0 +1,102 @@ |
||||
import { css } from '@emotion/css'; |
||||
import React from 'react'; |
||||
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data'; |
||||
import { |
||||
Alert, |
||||
Button, |
||||
Divider, |
||||
Field, |
||||
RadioButtonGroup, |
||||
Spinner, |
||||
Stack, |
||||
Text, |
||||
TextLink, |
||||
useStyles2, |
||||
} from '@grafana/ui'; |
||||
import { Input } from '@grafana/ui/src/components/Input/Input'; |
||||
import { t } from '@grafana/ui/src/utils/i18n'; |
||||
import { Trans } from 'app/core/internationalization'; |
||||
|
||||
import { SnapshotSharingOptions } from '../../../../dashboard/services/SnapshotSrv'; |
||||
import { getExpireOptions } from '../../ShareSnapshotTab'; |
||||
|
||||
const SNAPSHOT_URL = 'https://grafana.com/docs/grafana/latest/dashboards/share-dashboards-panels/#publish-a-snapshot'; |
||||
|
||||
interface Props { |
||||
isLoading: boolean; |
||||
name: string; |
||||
selectedExpireOption: SelectableValue<number>; |
||||
sharingOptions?: SnapshotSharingOptions; |
||||
onCancelClick: () => void; |
||||
onCreateClick: (isExternal?: boolean) => void; |
||||
onNameChange: (v: string) => void; |
||||
onExpireChange: (v: number) => void; |
||||
} |
||||
export function CreateSnapshot({ |
||||
name, |
||||
onNameChange, |
||||
onExpireChange, |
||||
selectedExpireOption, |
||||
sharingOptions, |
||||
onCancelClick, |
||||
onCreateClick, |
||||
isLoading, |
||||
}: Props) { |
||||
const styles = useStyles2(getStyles); |
||||
|
||||
return ( |
||||
<div className={styles.container}> |
||||
<Alert severity="info" title={''}> |
||||
<Stack justifyContent="space-between" gap={2} alignItems="center"> |
||||
<Text> |
||||
<Trans i18nKey="snapshot.share.info-alert"> |
||||
A Grafana dashboard snapshot publicly shares a dashboard while removing sensitive data such as queries and |
||||
panel links, leaving only visible metrics and series names. Anyone with the link can access the snapshot. |
||||
</Trans> |
||||
</Text> |
||||
<Button variant="secondary" onClick={() => window.open(SNAPSHOT_URL, '_blank')} type="button"> |
||||
<Trans i18nKey="snapshot.share.learn-more-button">Learn more</Trans> |
||||
</Button> |
||||
</Stack> |
||||
</Alert> |
||||
<Field label={t('snapshot.share.name-label', 'Snapshot name*')}> |
||||
<Input id="snapshot-name-input" defaultValue={name} onBlur={(e) => onNameChange(e.target.value)} /> |
||||
</Field> |
||||
<Field label={t('snapshot.share.expiration-label', 'Expires in')}> |
||||
<RadioButtonGroup<number> |
||||
id="expire-select-input" |
||||
options={getExpireOptions()} |
||||
value={selectedExpireOption?.value} |
||||
onChange={onExpireChange} |
||||
/> |
||||
</Field> |
||||
<Divider /> |
||||
<Stack justifyContent="space-between" direction={{ xs: 'column', xl: 'row' }}> |
||||
<Stack gap={1} flex={1} direction={{ xs: 'column', sm: 'row' }}> |
||||
<Button variant="primary" disabled={isLoading} onClick={() => onCreateClick()}> |
||||
<Trans i18nKey="snapshot.share.local-button">Publish snapshot</Trans> |
||||
</Button> |
||||
{sharingOptions?.externalEnabled && ( |
||||
<Button variant="secondary" disabled={isLoading} onClick={() => onCreateClick(true)}> |
||||
{sharingOptions?.externalSnapshotName} |
||||
</Button> |
||||
)} |
||||
<Button variant="secondary" fill="outline" onClick={onCancelClick}> |
||||
<Trans i18nKey="snapshot.share.cancel-button">Cancel</Trans> |
||||
</Button> |
||||
{isLoading && <Spinner />} |
||||
</Stack> |
||||
<TextLink icon="external-link-alt" href="/dashboard/snapshots"> |
||||
{t('snapshot.share.view-all-button', 'View all snapshots')} |
||||
</TextLink> |
||||
</Stack> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({ |
||||
container: css({ |
||||
paddingBottom: theme.spacing(2), |
||||
}), |
||||
}); |
@ -0,0 +1,91 @@ |
||||
import React, { useState } from 'react'; |
||||
import useAsyncFn from 'react-use/lib/useAsyncFn'; |
||||
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; |
||||
import { SceneComponentProps } from '@grafana/scenes'; |
||||
import { Alert } from '@grafana/ui'; |
||||
import { Trans, t } from 'app/core/internationalization'; |
||||
|
||||
import { ShareDrawerConfirmAction } from '../../ShareDrawer/ShareDrawerConfirmAction'; |
||||
import { ShareSnapshotTab } from '../../ShareSnapshotTab'; |
||||
|
||||
import { CreateSnapshot } from './CreateSnapshot'; |
||||
import { SnapshotActions } from './SnapshotActions'; |
||||
|
||||
const selectors = e2eSelectors.pages.ShareDashboardDrawer.ShareSnapshot; |
||||
|
||||
export class ShareSnapshot extends ShareSnapshotTab { |
||||
static Component = ShareSnapshotRenderer; |
||||
} |
||||
|
||||
function ShareSnapshotRenderer({ model }: SceneComponentProps<ShareSnapshot>) { |
||||
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false); |
||||
const [showDeletedAlert, setShowDeletedAlert] = useState(false); |
||||
const [step, setStep] = useState(1); |
||||
|
||||
const { snapshotName, snapshotSharingOptions, selectedExpireOption, dashboardRef } = model.useState(); |
||||
|
||||
const [snapshotResult, createSnapshot] = useAsyncFn(async (external = false) => { |
||||
const response = await model.onSnapshotCreate(external); |
||||
setStep(2); |
||||
return response; |
||||
}); |
||||
const [deleteSnapshotResult, deleteSnapshot] = useAsyncFn(async (url: string) => { |
||||
const response = await model.onSnapshotDelete(url); |
||||
setStep(1); |
||||
setShowDeleteConfirmation(false); |
||||
setShowDeletedAlert(true); |
||||
return response; |
||||
}); |
||||
|
||||
const onCancelClick = () => { |
||||
dashboardRef.resolve().closeModal(); |
||||
}; |
||||
|
||||
if (showDeleteConfirmation) { |
||||
return ( |
||||
<ShareDrawerConfirmAction |
||||
title={t('snapshot.share.delete-title', 'Delete snapshot')} |
||||
confirmButtonLabel={t('snapshot.share.delete-button', 'Delete snapshot')} |
||||
onConfirm={() => deleteSnapshot(snapshotResult.value?.deleteUrl!)} |
||||
onDismiss={() => setShowDeleteConfirmation(false)} |
||||
description={t('snapshot.share.delete-description', 'Are you sure you want to delete this snapshot?')} |
||||
isActionLoading={deleteSnapshotResult.loading} |
||||
/> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<div data-testid={selectors.container}> |
||||
{step === 1 && ( |
||||
<> |
||||
{showDeletedAlert && ( |
||||
<Alert severity="info" title={''} onRemove={() => setShowDeletedAlert(false)}> |
||||
<Trans i18nKey="snapshot.share.deleted-alert"> |
||||
Your snapshot has been deleted. It might take up to an hour before the snapshot is cleared from any CDN |
||||
caches. |
||||
</Trans> |
||||
</Alert> |
||||
)} |
||||
<CreateSnapshot |
||||
name={snapshotName ?? ''} |
||||
selectedExpireOption={selectedExpireOption} |
||||
sharingOptions={snapshotSharingOptions} |
||||
onNameChange={model.onSnasphotNameChange} |
||||
onCancelClick={onCancelClick} |
||||
onExpireChange={model.onExpireChange} |
||||
onCreateClick={createSnapshot} |
||||
isLoading={snapshotResult.loading} |
||||
/> |
||||
</> |
||||
)} |
||||
{step === 2 && ( |
||||
<SnapshotActions |
||||
url={snapshotResult.value!.url} |
||||
onDeleteClick={() => setShowDeleteConfirmation(true)} |
||||
onNewSnapshotClick={() => setStep(1)} |
||||
/> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,25 @@ |
||||
import React from 'react'; |
||||
|
||||
import { Button, ClipboardButton, Stack } from '@grafana/ui'; |
||||
import { Trans } from 'app/core/internationalization'; |
||||
|
||||
interface Props { |
||||
url: string; |
||||
onDeleteClick: () => void; |
||||
onNewSnapshotClick: () => void; |
||||
} |
||||
export const SnapshotActions = ({ url, onDeleteClick, onNewSnapshotClick }: Props) => { |
||||
return ( |
||||
<Stack justifyContent="flex-start" gap={1} direction={{ xs: 'column', sm: 'row' }}> |
||||
<ClipboardButton icon="link" variant="primary" fill="outline" getText={() => url}> |
||||
<Trans i18nKey="snapshot.share.copy-link-button">Copy link</Trans> |
||||
</ClipboardButton> |
||||
<Button icon="trash-alt" variant="destructive" fill="outline" onClick={onDeleteClick}> |
||||
<Trans i18nKey="snapshot.share.delete-button">Delete snapshot</Trans> |
||||
</Button> |
||||
<Button variant="secondary" fill="solid" onClick={onNewSnapshotClick}> |
||||
<Trans i18nKey="snapshot.share.new-snapshot-button">New snapshot</Trans> |
||||
</Button> |
||||
</Stack> |
||||
); |
||||
}; |
Loading…
Reference in new issue