ShareDrawer: Improvements (#93107)

This commit is contained in:
Juan Cabanas 2024-09-09 19:32:45 -03:00 committed by GitHub
parent 40b5c30b94
commit 644a315667
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 164 additions and 129 deletions

View File

@ -211,7 +211,7 @@ export function getNavSubTitle(navId: string | undefined) {
case 'dashboards/snapshots':
return t(
'nav.snapshots.subtitle',
'Interactive, publically available, point-in-time representations of dashboards'
'Interactive, publically available, point-in-time representations of dashboards and panels'
);
case 'dashboards/public':
return config.featureToggles.newDashboardSharingComponent

View File

@ -1,11 +1,13 @@
import { css, cx } from '@emotion/css';
import { Controller, useForm } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { sceneGraph } from '@grafana/scenes';
import { FieldSet, Icon, Label, Spinner, Stack, Text, TimeRangeInput, Tooltip } from '@grafana/ui';
import { FieldSet, Icon, Label, Spinner, Stack, Text, TimeRangeLabel, Tooltip, useStyles2 } from '@grafana/ui';
import { Switch } from '@grafana/ui/src/components/Switch/Switch';
import { contextSrv } from 'app/core/core';
import { Trans, t } from 'app/core/internationalization';
import { t, Trans } from 'app/core/internationalization';
import { publicDashboardApi, useUpdatePublicDashboardMutation } from 'app/features/dashboard/api/publicDashboardApi';
import { ConfigPublicDashboardForm } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard';
import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions';
@ -18,6 +20,7 @@ const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
type FormInput = Omit<ConfigPublicDashboardForm, 'isPaused'>;
export default function ShareConfiguration() {
const styles = useStyles2(getStyles);
const { dashboard } = useShareDrawerContext();
const [update, { isLoading }] = useUpdatePublicDashboardMutation();
@ -119,8 +122,13 @@ export default function ShareConfiguration() {
<Trans i18nKey="public-dashboard.configuration.display-annotations-label">Display annotations</Trans>
</Label>
</Stack>
<Stack gap={1} alignItems="center">
<TimeRangeInput value={timeRange.value} showIcon disabled onChange={() => {}} />
<Stack gap={1} alignItems="flex-start">
<div className={styles.timeRange}>
<Trans i18nKey="public-dashboard.configuration.time-range-label">Time range</Trans>
</div>
<div className={cx(styles.timeRange, styles.timeRangeValue)}>
<TimeRangeLabel value={timeRange.value} />
</div>
<Tooltip
placement="right"
content={t(
@ -128,7 +136,7 @@ export default function ShareConfiguration() {
'The shared dashboard uses the default time range settings of the dashboard'
)}
>
<Icon name="info-circle" size="sm" />
<Icon name="info-circle" size="md" />
</Tooltip>
</Stack>
</Stack>
@ -139,3 +147,13 @@ export default function ShareConfiguration() {
</Stack>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
timeRange: css({
fontSize: theme.typography.bodySmall.fontSize,
fontWeight: theme.typography.bodySmall.fontWeight,
}),
timeRangeValue: css({
color: theme.colors.text.secondary,
}),
});

View File

@ -3,15 +3,15 @@ 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 { Alert, Button, ClipboardButton, Spinner, Stack, TextLink } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { SnapshotSharingOptions } from '../../../../dashboard/services/SnapshotSrv';
import { ShareDrawerConfirmAction } from '../../ShareDrawer/ShareDrawerConfirmAction';
import { ShareSnapshotTab } from '../../ShareSnapshotTab';
import { ShareView } from '../../types';
import { CreateSnapshot } from './CreateSnapshot';
import { SnapshotActions } from './SnapshotActions';
import { UpsertSnapshot } from './UpsertSnapshot';
const selectors = e2eSelectors.pages.ShareDashboardDrawer.ShareSnapshot;
@ -28,7 +28,8 @@ function ShareSnapshotRenderer({ model }: SceneComponentProps<ShareSnapshot>) {
const [showDeletedAlert, setShowDeletedAlert] = useState(false);
const [step, setStep] = useState(1);
const { snapshotName, snapshotSharingOptions, selectedExpireOption, panelRef, onDismiss } = model.useState();
const { snapshotName, snapshotSharingOptions, selectedExpireOption, panelRef, onDismiss, dashboardRef } =
model.useState();
const [snapshotResult, createSnapshot] = useAsyncFn(async (external = false) => {
const response = await model.onSnapshotCreate(external);
@ -47,12 +48,22 @@ function ShareSnapshotRenderer({ model }: SceneComponentProps<ShareSnapshot>) {
onDismiss?.();
};
const reset = () => {
model.onSnasphotNameChange(dashboardRef.resolve().state.title);
setStep(1);
};
const onDeleteSnapshotClick = async () => {
await deleteSnapshot(snapshotResult.value?.deleteUrl!);
reset();
};
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!)}
onConfirm={onDeleteSnapshotClick}
onDismiss={() => setShowDeleteConfirmation(false)}
description={t('snapshot.share.delete-description', 'Are you sure you want to delete this snapshot?')}
isActionLoading={deleteSnapshotResult.loading}
@ -62,36 +73,95 @@ function ShareSnapshotRenderer({ model }: SceneComponentProps<ShareSnapshot>) {
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}
panelRef={panelRef}
/>
</>
)}
{step === 2 && (
<SnapshotActions
url={snapshotResult.value!.url}
onDeleteClick={() => setShowDeleteConfirmation(true)}
onNewSnapshotClick={() => setStep(1)}
/>
)}
<>
{step === 1 && showDeletedAlert && (
<Alert severity="info" title={''} onRemove={() => setShowDeletedAlert(false)}>
<Trans i18nKey="snapshot.share.deleted-alert">
Snapshot deleted. It could take an hour to be cleared from CDN caches.
</Trans>
</Alert>
)}
<UpsertSnapshot
name={snapshotName ?? ''}
selectedExpireOption={selectedExpireOption}
onNameChange={model.onSnasphotNameChange}
onExpireChange={model.onExpireChange}
disableInputs={step === 2}
panelRef={panelRef}
>
<Stack justifyContent="space-between" gap={{ xs: 2 }} direction={{ xs: 'column', xl: 'row' }}>
{step === 1 ? (
<CreateSnapshotActions
onCreateClick={createSnapshot}
isLoading={snapshotResult.loading}
onCancelClick={onCancelClick}
sharingOptions={snapshotSharingOptions}
/>
) : (
step === 2 &&
snapshotResult.value && (
<UpsertSnapshotActions
url={snapshotResult.value!.url}
onDeleteClick={() => setShowDeleteConfirmation(true)}
onNewSnapshotClick={reset}
/>
)
)}
<TextLink icon="external-link-alt" href="/dashboard/snapshots" external>
{t('snapshot.share.view-all-button', 'View all snapshots')}
</TextLink>
</Stack>
</UpsertSnapshot>
</>
</div>
);
}
const CreateSnapshotActions = ({
isLoading,
onCreateClick,
onCancelClick,
sharingOptions,
}: {
isLoading: boolean;
sharingOptions?: SnapshotSharingOptions;
onCancelClick: () => void;
onCreateClick: (isExternal?: boolean) => void;
}) => (
<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>
);
const UpsertSnapshotActions = ({
url,
onDeleteClick,
onNewSnapshotClick,
}: {
url: string;
onDeleteClick: () => void;
onNewSnapshotClick: () => void;
}) => (
<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>
);

View File

@ -1,23 +0,0 @@
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>
);
};

View File

@ -1,24 +1,13 @@
import { css } from '@emotion/css';
import { PropsWithChildren } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { SceneObjectRef, VizPanel } from '@grafana/scenes';
import {
Alert,
Button,
Divider,
Field,
RadioButtonGroup,
Spinner,
Stack,
Text,
TextLink,
useStyles2,
} from '@grafana/ui';
import { Alert, Button, Divider, Field, RadioButtonGroup, Stack, Text, 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 DASHBOARD_SNAPSHOT_URL =
@ -27,27 +16,22 @@ const PANEL_SNAPSHOT_URL =
'https://grafana.com/docs/grafana/latest/dashboards/share-dashboards-panels/#publish-a-snapshot-1';
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;
panelRef?: SceneObjectRef<VizPanel>;
disableInputs: boolean;
}
export function CreateSnapshot({
export function UpsertSnapshot({
name,
onNameChange,
onExpireChange,
selectedExpireOption,
sharingOptions,
onCancelClick,
onCreateClick,
isLoading,
panelRef,
}: Props) {
disableInputs,
children,
}: Props & PropsWithChildren) {
const styles = useStyles2(getStyles);
return (
@ -77,37 +61,21 @@ export function CreateSnapshot({
</Button>
</Stack>
</Alert>
<Field label={t('snapshot.share.name-label', 'Snapshot name')}>
<Input id="snapshot-name-input" defaultValue={name} onChange={(e) => onNameChange(e.currentTarget.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>
<fieldset disabled={disableInputs}>
<Field label={t('snapshot.share.name-label', 'Snapshot name')}>
<Input id="snapshot-name-input" value={name} onChange={(e) => onNameChange(e.currentTarget.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>
</fieldset>
<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>
{children}
</div>
);
}

View File

@ -90,7 +90,7 @@ export class ShareSnapshotTab extends SceneObjectBase<ShareSnapshotTabState> imp
}
public onSnasphotNameChange = (snapshotName: string) => {
this.setState({ snapshotName: snapshotName.trim() });
this.setState({ snapshotName });
};
public onExpireChange = (option: number) => {
@ -105,7 +105,7 @@ export class ShareSnapshotTab extends SceneObjectBase<ShareSnapshotTabState> imp
const saveModel = transformSceneToSaveModel(dashboardRef.resolve(), true);
return trimDashboardForSnapshot(
this.state.snapshotName || '',
this.state.snapshotName.trim() || '',
timeRange.state.value,
saveModel,
panelRef?.resolve()

View File

@ -65,7 +65,7 @@ export function ShareEmbed({ panel, dashboard, range, onCancelClick, buildIframe
return (
<>
<p>
<Trans i18nKey="share-modal.embed.info">Generate HTML for embedding an iframe with this panel.</Trans>
<Trans i18nKey="share-modal.embed.info">Generate HTML for embedding an iframe with this panel</Trans>
</p>
<Field>
<Stack gap={1} alignItems="start">
@ -90,7 +90,7 @@ export function ShareEmbed({ panel, dashboard, range, onCancelClick, buildIframe
label={t('share-modal.embed.html', 'Embed HTML')}
description={t(
'share-modal.embed.html-description',
'The HTML code below can be pasted and included in another web page. Unless anonymous access is enabled, the user viewing that page need to be signed into Grafana for the graph to load.'
'The HTML code below can be pasted and included in another web page. Unless anonymous access is enabled, the users viewing that page need to be signed into Grafana for the graph to load.'
)}
>
<TextArea

View File

@ -1709,7 +1709,7 @@
"title": "SLO"
},
"snapshots": {
"subtitle": "Interactive, publically available, point-in-time representations of dashboards",
"subtitle": "Interactive, publically available, point-in-time representations of dashboards and panels",
"title": "Snapshots"
},
"starred": {
@ -1943,6 +1943,7 @@
"success-resume": "Your dashboard access has been resumed",
"success-update": "Settings have been successfully updated",
"success-update-old": "Public dashboard updated!",
"time-range-label": "Time range",
"time-range-tooltip": "The shared dashboard uses the default time range settings of the dashboard"
},
"create-page": {
@ -2276,8 +2277,8 @@
"embed": {
"copy": "Copy to clipboard",
"html": "Embed HTML",
"html-description": "The HTML code below can be pasted and included in another web page. Unless anonymous access is enabled, the user viewing that page need to be signed into Grafana for the graph to load.",
"info": "Generate HTML for embedding an iframe with this panel.",
"html-description": "The HTML code below can be pasted and included in another web page. Unless anonymous access is enabled, the users viewing that page need to be signed into Grafana for the graph to load.",
"info": "Generate HTML for embedding an iframe with this panel",
"time-range": "Lock time range"
},
"export": {
@ -2455,7 +2456,7 @@
"delete-button": "Delete snapshot",
"delete-description": "Are you sure you want to delete this snapshot?",
"delete-title": "Delete snapshot",
"deleted-alert": "Your snapshot has been deleted. It might take up to an hour before the snapshot is cleared from any CDN caches.",
"deleted-alert": "Snapshot deleted. It could take an hour to be cleared from CDN caches.",
"expiration-label": "Expires in",
"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.",
"learn-more-button": "Learn more",

View File

@ -1709,7 +1709,7 @@
"title": "ŜĿØ"
},
"snapshots": {
"subtitle": "Ĩʼnŧęřäčŧįvę, pūþľįčäľľy äväįľäþľę, pőįʼnŧ-įʼn-ŧįmę řępřęşęʼnŧäŧįőʼnş őƒ đäşĥþőäřđş",
"subtitle": "Ĩʼnŧęřäčŧįvę, pūþľįčäľľy äväįľäþľę, pőįʼnŧ-įʼn-ŧįmę řępřęşęʼnŧäŧįőʼnş őƒ đäşĥþőäřđş äʼnđ päʼnęľş",
"title": "Ŝʼnäpşĥőŧş"
},
"starred": {
@ -1943,6 +1943,7 @@
"success-resume": "Ÿőūř đäşĥþőäřđ äččęşş ĥäş þęęʼn řęşūmęđ",
"success-update": "Ŝęŧŧįʼnģş ĥävę þęęʼn şūččęşşƒūľľy ūpđäŧęđ",
"success-update-old": "Pūþľįč đäşĥþőäřđ ūpđäŧęđ!",
"time-range-label": "Ŧįmę řäʼnģę",
"time-range-tooltip": "Ŧĥę şĥäřęđ đäşĥþőäřđ ūşęş ŧĥę đęƒäūľŧ ŧįmę řäʼnģę şęŧŧįʼnģş őƒ ŧĥę đäşĥþőäřđ"
},
"create-page": {
@ -2276,8 +2277,8 @@
"embed": {
"copy": "Cőpy ŧő čľįpþőäřđ",
"html": "Ēmþęđ ĦŦMĿ",
"html-description": "Ŧĥę ĦŦMĿ čőđę þęľőŵ čäʼn þę päşŧęđ äʼnđ įʼnčľūđęđ įʼn äʼnőŧĥęř ŵęþ päģę. Ůʼnľęşş äʼnőʼnymőūş äččęşş įş ęʼnäþľęđ, ŧĥę ūşęř vįęŵįʼnģ ŧĥäŧ päģę ʼnęęđ ŧő þę şįģʼnęđ įʼnŧő Ğřäƒäʼnä ƒőř ŧĥę ģřäpĥ ŧő ľőäđ.",
"info": "Ğęʼnęřäŧę ĦŦMĿ ƒőř ęmþęđđįʼnģ äʼn įƒřämę ŵįŧĥ ŧĥįş päʼnęľ.",
"html-description": "Ŧĥę ĦŦMĿ čőđę þęľőŵ čäʼn þę päşŧęđ äʼnđ įʼnčľūđęđ įʼn äʼnőŧĥęř ŵęþ päģę. Ůʼnľęşş äʼnőʼnymőūş äččęşş įş ęʼnäþľęđ, ŧĥę ūşęřş vįęŵįʼnģ ŧĥäŧ päģę ʼnęęđ ŧő þę şįģʼnęđ įʼnŧő Ğřäƒäʼnä ƒőř ŧĥę ģřäpĥ ŧő ľőäđ.",
"info": "Ğęʼnęřäŧę ĦŦMĿ ƒőř ęmþęđđįʼnģ äʼn įƒřämę ŵįŧĥ ŧĥįş päʼnęľ",
"time-range": "Ŀőčĸ ŧįmę řäʼnģę"
},
"export": {
@ -2455,7 +2456,7 @@
"delete-button": "Đęľęŧę şʼnäpşĥőŧ",
"delete-description": "Åřę yőū şūřę yőū ŵäʼnŧ ŧő đęľęŧę ŧĥįş şʼnäpşĥőŧ?",
"delete-title": "Đęľęŧę şʼnäpşĥőŧ",
"deleted-alert": "Ÿőūř şʼnäpşĥőŧ ĥäş þęęʼn đęľęŧęđ. Ĩŧ mįģĥŧ ŧäĸę ūp ŧő äʼn ĥőūř þęƒőřę ŧĥę şʼnäpşĥőŧ įş čľęäřęđ ƒřőm äʼny CĐŃ čäčĥęş.",
"deleted-alert": "Ŝʼnäpşĥőŧ đęľęŧęđ. Ĩŧ čőūľđ ŧäĸę äʼn ĥőūř ŧő þę čľęäřęđ ƒřőm CĐŃ čäčĥęş.",
"expiration-label": "Ēχpįřęş įʼn",
"info-alert": "Å Ğřäƒäʼnä đäşĥþőäřđ şʼnäpşĥőŧ pūþľįčľy şĥäřęş ä đäşĥþőäřđ ŵĥįľę řęmővįʼnģ şęʼnşįŧįvę đäŧä şūčĥ äş qūęřįęş äʼnđ päʼnęľ ľįʼnĸş, ľęävįʼnģ őʼnľy vįşįþľę męŧřįčş äʼnđ şęřįęş ʼnämęş. Åʼnyőʼnę ŵįŧĥ ŧĥę ľįʼnĸ čäʼn äččęşş ŧĥę şʼnäpşĥőŧ.",
"learn-more-button": "Ŀęäřʼn mőřę",