ShareDrawer: Share link panel (#90549)

This commit is contained in:
Juan Cabanas 2024-07-26 17:04:34 -03:00 committed by GitHub
parent 90349b21f7
commit 397dfaf679
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 318 additions and 78 deletions

View File

@ -1,15 +1,15 @@
import {
getTimeZone,
InterpolateFunction,
LinkModel,
PanelMenuItem,
PanelPlugin,
PluginExtensionPanelContext,
PluginExtensionPoints,
getTimeZone,
urlUtil,
} from '@grafana/data';
import { config, getPluginLinkExtensions, locationService } from '@grafana/runtime';
import { LocalValueVariable, SceneGridRow, VizPanel, VizPanelMenu, sceneGraph } from '@grafana/scenes';
import { LocalValueVariable, sceneGraph, SceneGridRow, VizPanel, VizPanelMenu } from '@grafana/scenes';
import { DataQuery, OptionsWithLegend } from '@grafana/schema';
import appEvents from 'app/core/app_events';
import { t } from 'app/core/internationalization';
@ -21,7 +21,9 @@ import { createExtensionSubMenu } from 'app/features/plugins/extensions/utils';
import { addDataTrailPanelAction } from 'app/features/trails/Integrations/dashboardIntegration';
import { ShowConfirmModalEvent } from 'app/types/events';
import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer';
import { ShareModal } from '../sharing/ShareModal';
import { SharePanelInternally } from '../sharing/panel-share/SharePanelInternally';
import { DashboardInteractions } from '../utils/interactions';
import { getEditPanelUrl, getInspectUrl, getViewPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders';
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils';
@ -79,15 +81,42 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
});
}
items.push({
text: t('panel.header-menu.share', `Share`),
iconClassName: 'share-alt',
onClick: () => {
DashboardInteractions.panelMenuItemClicked('share');
dashboard.showModal(new ShareModal({ panelRef: panel.getRef() }));
},
shortcut: 'p s',
});
if (config.featureToggles.newDashboardSharingComponent) {
const subMenu: PanelMenuItem[] = [];
subMenu.push({
text: t('share-panel.menu.share-link-title', 'Share link'),
iconClassName: 'link',
shortcut: 'p u',
onClick: () => {
const drawer = new ShareDrawer({
title: t('share-panel.drawer.share-link-title', 'Link settings'),
body: new SharePanelInternally({ panelRef: panel.getRef() }),
});
dashboard.showModal(drawer);
},
});
items.push({
type: 'submenu',
text: t('panel.header-menu.share', 'Share'),
iconClassName: 'share-alt',
subMenu,
onClick: (e) => {
e.preventDefault();
},
});
} else {
items.push({
text: t('panel.header-menu.share', 'Share'),
iconClassName: 'share-alt',
onClick: () => {
DashboardInteractions.panelMenuItemClicked('share');
dashboard.showModal(new ShareModal({ panelRef: panel.getRef() }));
},
shortcut: 'p s',
});
}
if (dashboard.state.isEditing && !isRepeat && !isEditingPanel) {
moreSubMenu.push({

View File

@ -1,10 +1,13 @@
import { SetPanelAttentionEvent } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { config, locationService } from '@grafana/runtime';
import { sceneGraph, VizPanel } from '@grafana/scenes';
import appEvents from 'app/core/app_events';
import { t } from 'app/core/internationalization';
import { KeybindingSet } from 'app/core/services/KeybindingSet';
import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer';
import { ShareModal } from '../sharing/ShareModal';
import { SharePanelInternally } from '../sharing/panel-share/SharePanelInternally';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { getEditPanelUrl, getInspectUrl, getViewPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders';
import { getPanelIdForVizPanel } from '../utils/utils';
@ -45,12 +48,26 @@ export function setupKeyboardShortcuts(scene: DashboardScene) {
});
// Panel share
keybindings.addBinding({
key: 'p s',
onTrigger: withFocusedPanel(scene, async (vizPanel: VizPanel) => {
scene.showModal(new ShareModal({ panelRef: vizPanel.getRef() }));
}),
});
if (config.featureToggles.newDashboardSharingComponent) {
keybindings.addBinding({
key: 'p u',
onTrigger: withFocusedPanel(scene, async (vizPanel: VizPanel) => {
const drawer = new ShareDrawer({
title: t('share-panel.drawer.share-link-title', 'Link settings'),
body: new SharePanelInternally({ panelRef: vizPanel.getRef() }),
});
scene.showModal(drawer);
}),
});
} else {
keybindings.addBinding({
key: 'p s',
onTrigger: withFocusedPanel(scene, async (vizPanel: VizPanel) => {
scene.showModal(new ShareModal({ panelRef: vizPanel.getRef() }));
}),
});
}
// Panel inspect
keybindings.addBinding({

View File

@ -2,12 +2,12 @@ import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneComponentProps, SceneObjectRef, VizPanel } from '@grafana/scenes';
import { Alert, ClipboardButton, Divider, Label, Spinner, Stack, Switch, Text, useStyles2 } from '@grafana/ui';
import { Alert, ClipboardButton, Divider, Text, useStyles2 } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { ThemePicker } from 'app/features/dashboard/components/ShareModal/ThemePicker';
import ShareInternallyConfiguration from '../../ShareInternallyConfiguration';
import { ShareLinkTab } from '../../ShareLinkTab';
import { getShareLinkConfiguration } from '../utils';
import { getShareLinkConfiguration, updateShareLinkConfiguration } from '../utils';
export class ShareInternally extends ShareLinkTab {
static Component = ShareInternallyRenderer;
@ -20,6 +20,39 @@ export class ShareInternally extends ShareLinkTab {
useShortUrl,
selectedTheme: theme,
});
this.onToggleLockedTime = this.onToggleLockedTime.bind(this);
this.onUrlShorten = this.onUrlShorten.bind(this);
this.onThemeChange = this.onThemeChange.bind(this);
}
async onToggleLockedTime() {
const useLockedTime = !this.state.useLockedTime;
updateShareLinkConfiguration({
useAbsoluteTimeRange: useLockedTime,
useShortUrl: this.state.useShortUrl,
theme: this.state.selectedTheme,
});
await super.onToggleLockedTime();
}
async onUrlShorten() {
const useShortUrl = !this.state.useShortUrl;
updateShareLinkConfiguration({
useShortUrl,
useAbsoluteTimeRange: this.state.useLockedTime,
theme: this.state.selectedTheme,
});
await super.onUrlShorten();
}
async onThemeChange(value: string) {
updateShareLinkConfiguration({
theme: value,
useShortUrl: this.state.useShortUrl,
useAbsoluteTimeRange: this.state.useLockedTime,
});
await super.onThemeChange(value);
}
}
@ -42,41 +75,15 @@ function ShareInternallyRenderer({ model }: SceneComponentProps<ShareInternally>
</Trans>
</Text>
</div>
<Stack justifyContent="space-between">
<Stack gap={2} direction="column">
<Stack gap={1} direction="column">
<Stack gap={1} alignItems="start">
<Switch
label={t('link.share.time-range-label', 'Lock time range')}
id="share-current-time-range"
value={useLockedTime}
onChange={model.onToggleLockedTime}
/>
<Label
description={t(
'link.share.time-range-description',
'Change the current relative time range to an absolute time range'
)}
>
<Trans i18nKey="link.share.time-range-label">Lock time range</Trans>
</Label>
</Stack>
<Stack gap={1} alignItems="start">
<Switch
id="share-short-url"
value={useShortUrl}
label={t('link.share.short-url-label', 'Shorten link')}
onChange={model.onUrlShorten}
/>
<Label>
<Trans i18nKey="link.share.short-url-label">Shorten link</Trans>
</Label>
</Stack>
</Stack>
<ThemePicker selectedTheme={selectedTheme} onChange={model.onThemeChange} />
</Stack>
{isBuildUrlLoading && <Spinner />}
</Stack>
<ShareInternallyConfiguration
useLockedTime={useLockedTime}
onToggleLockedTime={model.onToggleLockedTime}
useShortUrl={useShortUrl}
onUrlShorten={model.onUrlShorten}
selectedTheme={selectedTheme}
onChangeTheme={model.onThemeChange}
isLoading={isBuildUrlLoading}
/>
<Divider spacing={1} />
<ClipboardButton
icon="link"

View File

@ -0,0 +1,62 @@
import { Label, Spinner, Stack, Switch } from '@grafana/ui';
import { t, Trans } from '../../../core/internationalization';
import { ThemePicker } from '../../dashboard/components/ShareModal/ThemePicker';
interface Props {
useLockedTime: boolean;
onToggleLockedTime: () => void;
useShortUrl: boolean;
onUrlShorten: () => void;
selectedTheme: string;
onChangeTheme: (v: string) => void;
isLoading: boolean;
}
export default function ShareInternallyConfiguration({
useLockedTime,
onToggleLockedTime,
useShortUrl,
onUrlShorten,
onChangeTheme,
selectedTheme,
isLoading,
}: Props) {
return (
<Stack justifyContent="space-between">
<Stack gap={2} direction="column">
<Stack gap={1} direction="column">
<Stack gap={1} alignItems="start">
<Switch
label={t('link.share.time-range-label', 'Lock time range')}
id="share-current-time-range"
value={useLockedTime}
onChange={onToggleLockedTime}
/>
<Label
description={t(
'link.share.time-range-description',
'Change the current relative time range to an absolute time range'
)}
>
<Trans i18nKey="link.share.time-range-label">Lock time range</Trans>
</Label>
</Stack>
<Stack gap={1} alignItems="start">
<Switch
id="share-short-url"
value={useShortUrl}
label={t('link.share.short-url-label', 'Shorten link')}
onChange={onUrlShorten}
/>
<Label>
<Trans i18nKey="link.share.short-url-label">Shorten link</Trans>
</Label>
</Stack>
</Stack>
<ThemePicker selectedTheme={selectedTheme} onChange={onChangeTheme} />
</Stack>
{isLoading && <Spinner />}
</Stack>
);
}

View File

@ -13,7 +13,6 @@ import { DashboardInteractions } from '../utils/interactions';
import { getDashboardUrl } from '../utils/urlBuilders';
import { getDashboardSceneFor } from '../utils/utils';
import { updateShareLinkConfiguration } from './ShareButton/utils';
import { SceneShareTabState } from './types';
export interface ShareLinkTabState extends SceneShareTabState, ShareOptions {
panelRef?: SceneObjectRef<VizPanel>;
@ -50,6 +49,10 @@ export class ShareLinkTab extends SceneObjectBase<ShareLinkTabState> {
this.addActivationHandler(() => {
this.buildUrl();
});
this.onToggleLockedTime = this.onToggleLockedTime.bind(this);
this.onUrlShorten = this.onUrlShorten.bind(this);
this.onThemeChange = this.onThemeChange.bind(this);
}
async buildUrl() {
@ -94,37 +97,22 @@ export class ShareLinkTab extends SceneObjectBase<ShareLinkTabState> {
return t('share-modal.tab-title.link', 'Link');
}
onToggleLockedTime = async () => {
async onToggleLockedTime() {
const useLockedTime = !this.state.useLockedTime;
updateShareLinkConfiguration({
useAbsoluteTimeRange: useLockedTime,
useShortUrl: this.state.useShortUrl,
theme: this.state.selectedTheme,
});
this.setState({ useLockedTime });
await this.buildUrl();
};
}
onUrlShorten = async () => {
async onUrlShorten() {
const useShortUrl = !this.state.useShortUrl;
this.setState({ useShortUrl });
updateShareLinkConfiguration({
useShortUrl,
useAbsoluteTimeRange: this.state.useLockedTime,
theme: this.state.selectedTheme,
});
await this.buildUrl();
};
}
onThemeChange = async (value: string) => {
async onThemeChange(value: string) {
this.setState({ selectedTheme: value });
updateShareLinkConfiguration({
theme: value,
useShortUrl: this.state.useShortUrl,
useAbsoluteTimeRange: this.state.useLockedTime,
});
await this.buildUrl();
};
}
getShareUrl = () => {
return this.state.shareUrl;

View File

@ -0,0 +1,113 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime';
import { SceneComponentProps, SceneObjectRef, VizPanel } from '@grafana/scenes';
import { Alert, ClipboardButton, Divider, LinkButton, Stack, Text, useStyles2 } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { getDashboardSceneFor } from '../../utils/utils';
import ShareInternallyConfiguration from '../ShareInternallyConfiguration';
import { ShareLinkTab } from '../ShareLinkTab';
export class SharePanelInternally extends ShareLinkTab {
static Component = SharePanelInternallyRenderer;
constructor({ panelRef }: { panelRef?: SceneObjectRef<VizPanel> }) {
super({
panelRef,
});
}
}
function SharePanelInternallyRenderer({ model }: SceneComponentProps<SharePanelInternally>) {
const styles = useStyles2(getStyles);
const { useLockedTime, useShortUrl, selectedTheme, isBuildUrlLoading, imageUrl } = model.useState();
const dashboard = getDashboardSceneFor(model);
const isDashboardSaved = Boolean(dashboard.state.uid);
return (
<>
<div className={styles.configDescription}>
<Text variant="body">
<Trans i18nKey="link.share-panel.config-description">
Create a personalized, direct link to share your panel within your organization, with the following
customization settings:
</Trans>
</Text>
</div>
<ShareInternallyConfiguration
useLockedTime={useLockedTime}
onToggleLockedTime={() => model.onToggleLockedTime()}
useShortUrl={useShortUrl}
onUrlShorten={() => model.onUrlShorten()}
selectedTheme={selectedTheme}
onChangeTheme={(t) => model.onThemeChange(t)}
isLoading={isBuildUrlLoading}
/>
<Divider spacing={1} />
<Stack gap={2} direction="column">
<div className={styles.buttonsContainer}>
<Stack gap={1} flex={1} direction={{ xs: 'column', sm: 'row' }}>
<ClipboardButton
icon="link"
variant="primary"
fill="outline"
disabled={isBuildUrlLoading}
getText={model.getShareUrl}
onClipboardCopy={model.onCopy}
>
<Trans i18nKey="link.share.copy-link-button">Copy link</Trans>
</ClipboardButton>
<LinkButton
href={imageUrl}
icon="external-link-alt"
target="_blank"
variant="secondary"
fill="solid"
disabled={!config.rendererAvailable || !isDashboardSaved}
>
<Trans i18nKey="link.share-panel.render-image">Render image</Trans>
</LinkButton>
</Stack>
</div>
{!isDashboardSaved && (
<Alert severity="info" title={t('share-modal.link.save-alert', 'Dashboard is not saved')} bottomSpacing={0}>
<Trans i18nKey="share-modal.link.save-dashboard">
To render a panel image, you must save the dashboard first.
</Trans>
</Alert>
)}
{!config.rendererAvailable && (
<Alert
severity="info"
title={t('share-modal.link.render-alert', 'Image renderer plugin not installed')}
bottomSpacing={0}
>
<Trans i18nKey="share-modal.link.render-instructions">
To render a panel image, you must install the{' '}
<a
href="https://grafana.com/grafana/plugins/grafana-image-renderer"
target="_blank"
rel="noopener noreferrer"
className="external-link"
>
Grafana image renderer plugin
</a>
. Please contact your Grafana administrator to install the plugin.
</Trans>
</Alert>
)}
</Stack>
</>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
configDescription: css({
marginBottom: theme.spacing(2),
}),
buttonsContainer: css({
marginTop: theme.spacing(2),
}),
});

View File

@ -953,6 +953,10 @@
"short-url-label": "Shorten link",
"time-range-description": "Change the current relative time range to an absolute time range",
"time-range-label": "Lock time range"
},
"share-panel": {
"config-description": "Create a personalized, direct link to share your panel within your organization, with the following customization settings:",
"render-image": "Render image"
}
},
"login": {
@ -2009,6 +2013,14 @@
"copy-button": "Copy to Clipboard"
}
},
"share-panel": {
"drawer": {
"share-link-title": "Link settings"
},
"menu": {
"share-link-title": "Share link"
}
},
"share-playlist": {
"checkbox-description": "Panel heights will be adjusted to fit screen size",
"checkbox-label": "Autofit",

View File

@ -953,6 +953,10 @@
"short-url-label": "Ŝĥőřŧęʼn ľįʼnĸ",
"time-range-description": "Cĥäʼnģę ŧĥę čūřřęʼnŧ řęľäŧįvę ŧįmę řäʼnģę ŧő äʼn äþşőľūŧę ŧįmę řäʼnģę",
"time-range-label": "Ŀőčĸ ŧįmę řäʼnģę"
},
"share-panel": {
"config-description": "Cřęäŧę ä pęřşőʼnäľįžęđ, đįřęčŧ ľįʼnĸ ŧő şĥäřę yőūř päʼnęľ ŵįŧĥįʼn yőūř őřģäʼnįžäŧįőʼn, ŵįŧĥ ŧĥę ƒőľľőŵįʼnģ čūşŧőmįžäŧįőʼn şęŧŧįʼnģş:",
"render-image": "Ŗęʼnđęř įmäģę"
}
},
"login": {
@ -2009,6 +2013,14 @@
"copy-button": "Cőpy ŧő Cľįpþőäřđ"
}
},
"share-panel": {
"drawer": {
"share-link-title": "Ŀįʼnĸ şęŧŧįʼnģş"
},
"menu": {
"share-link-title": "Ŝĥäřę ľįʼnĸ"
}
},
"share-playlist": {
"checkbox-description": "Päʼnęľ ĥęįģĥŧş ŵįľľ þę äđĵūşŧęđ ŧő ƒįŧ şčřęęʼn şįžę",
"checkbox-label": "Åūŧőƒįŧ",