ShareModal: Implement panel embed tab for scenes (#77062)

* ShareModal: Implement panel embed tab for scenes

* Fix url generation

* Locale

---------

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
pull/77042/head^2
kay delaney 2 years ago committed by GitHub
parent 2598ff7c93
commit 4a5b8643ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      public/app/features/dashboard-scene/sharing/ShareModal.tsx
  2. 75
      public/app/features/dashboard-scene/sharing/SharePanelEmbedTab.tsx
  3. 151
      public/app/features/dashboard/components/ShareModal/ShareEmbed.tsx
  4. 14
      public/app/features/dashboard/components/ShareModal/utils.ts
  5. 1
      public/locales/de-DE/grafana.json
  6. 1
      public/locales/en-US/grafana.json
  7. 1
      public/locales/es-ES/grafana.json
  8. 1
      public/locales/fr-FR/grafana.json
  9. 1
      public/locales/pseudo-LOCALE/grafana.json
  10. 1
      public/locales/zh-Hans/grafana.json

@ -11,6 +11,7 @@ import { getDashboardSceneFor } from '../utils/utils';
import { ShareExportTab } from './ShareExportTab'; import { ShareExportTab } from './ShareExportTab';
import { ShareLinkTab } from './ShareLinkTab'; import { ShareLinkTab } from './ShareLinkTab';
import { SharePanelEmbedTab } from './SharePanelEmbedTab';
import { ShareSnapshotTab } from './ShareSnapshotTab'; import { ShareSnapshotTab } from './ShareSnapshotTab';
import { ModalSceneObjectLike, SceneShareTab } from './types'; import { ModalSceneObjectLike, SceneShareTab } from './types';
@ -49,6 +50,10 @@ export class ShareModal extends SceneObjectBase<ShareModalState> implements Moda
tabs.push(new ShareSnapshotTab({ panelRef, dashboardRef, modalRef: this.getRef() })); tabs.push(new ShareSnapshotTab({ panelRef, dashboardRef, modalRef: this.getRef() }));
} }
if (panelRef) {
tabs.push(new SharePanelEmbedTab({ panelRef, dashboardRef }));
}
this.setState({ tabs }); this.setState({ tabs });
// if (panel) { // if (panel) {

@ -0,0 +1,75 @@
import React from 'react';
import { TimeRange } from '@grafana/data';
import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectRef, VizPanel } from '@grafana/scenes';
import { t } from 'app/core/internationalization';
import { ShareEmbed } from 'app/features/dashboard/components/ShareModal/ShareEmbed';
import { buildParams } from 'app/features/dashboard/components/ShareModal/utils';
import { DashboardScene } from '../scene/DashboardScene';
import { getDashboardUrl } from '../utils/urlBuilders';
import { getPanelIdForVizPanel } from '../utils/utils';
import { SceneShareTabState } from './types';
export interface SharePanelEmbedTabState extends SceneShareTabState {
panelRef: SceneObjectRef<VizPanel>;
dashboardRef: SceneObjectRef<DashboardScene>;
}
export class SharePanelEmbedTab extends SceneObjectBase<SharePanelEmbedTabState> {
static Component = SharePanelEmbedTabRenderer;
public constructor(state: SharePanelEmbedTabState) {
super(state);
}
public getTabLabel() {
return t('share-modal.tab-title.panel-embed', 'Embed');
}
}
function SharePanelEmbedTabRenderer({ model }: SceneComponentProps<SharePanelEmbedTab>) {
const { panelRef, dashboardRef } = model.useState();
const p = panelRef.resolve();
const dash = dashboardRef.resolve();
const { uid: dashUid } = dash.useState();
const id = getPanelIdForVizPanel(p);
const timeRangeState = sceneGraph.getTimeRange(p);
return (
<ShareEmbed
panel={{
id,
timeFrom:
typeof timeRangeState.state.value.raw.from === 'string' ? timeRangeState.state.value.raw.from : undefined,
}}
range={timeRangeState.state.value}
dashboard={{ uid: dashUid ?? '', time: timeRangeState.state.value }}
buildIframe={buildIframe}
/>
);
}
function buildIframe(
useCurrentTimeRange: boolean,
dashboardUid: string,
selectedTheme?: string,
panel?: { timeFrom?: string; id: number },
range?: TimeRange
) {
const params = buildParams({ useCurrentTimeRange, selectedTheme, panel, range });
const panelId = params.get('editPanel') ?? params.get('viewPanel') ?? '';
params.set('panelId', panelId);
params.delete('editPanel');
params.delete('viewPanel');
const soloUrl = getDashboardUrl({
absolute: true,
soloRoute: true,
uid: dashboardUid,
currentQueryParams: params.toString(),
});
return `<iframe src="${soloUrl}" width="450" height="200" frameborder="0"></iframe>`;
}

@ -1,6 +1,8 @@
import React, { FormEvent, PureComponent } from 'react'; import React, { FormEvent, useEffect, useState } from 'react';
import { useEffectOnce } from 'react-use';
import { reportInteraction } from '@grafana/runtime/src'; import { RawTimeRange, TimeRange } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { ClipboardButton, Field, Modal, Switch, TextArea } from '@grafana/ui'; import { ClipboardButton, Field, Modal, Switch, TextArea } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization'; import { t, Trans } from 'app/core/internationalization';
@ -8,103 +10,76 @@ import { ThemePicker } from './ThemePicker';
import { ShareModalTabProps } from './types'; import { ShareModalTabProps } from './types';
import { buildIframeHtml } from './utils'; import { buildIframeHtml } from './utils';
interface Props extends ShareModalTabProps {} interface Props extends Omit<ShareModalTabProps, 'panel' | 'dashboard'> {
panel?: { timeFrom?: string; id: number };
interface State { dashboard: { uid: string; time: RawTimeRange };
useCurrentTimeRange: boolean; range?: TimeRange;
selectedTheme: string; buildIframe?: typeof buildIframeHtml;
iframeHtml: string;
} }
export class ShareEmbed extends PureComponent<Props, State> { export function ShareEmbed({ panel, dashboard, range, buildIframe = buildIframeHtml }: Props) {
constructor(props: Props) { const [useCurrentTimeRange, setUseCurrentTimeRange] = useState(true);
super(props); const [selectedTheme, setSelectedTheme] = useState('current');
this.state = { const [iframeHtml, setIframeHtml] = useState('');
useCurrentTimeRange: true,
selectedTheme: 'current',
iframeHtml: '',
};
}
componentDidMount() { useEffectOnce(() => {
reportInteraction('grafana_dashboards_embed_share_viewed'); reportInteraction('grafana_dashboards_embed_share_viewed');
this.buildIframeHtml(); });
}
buildIframeHtml = () => {
const { panel, dashboard } = this.props;
const { useCurrentTimeRange, selectedTheme } = this.state;
const iframeHtml = buildIframeHtml(useCurrentTimeRange, dashboard.uid, selectedTheme, panel);
this.setState({ iframeHtml });
};
onIframeHtmlChange = (event: FormEvent<HTMLTextAreaElement>) => { useEffect(() => {
this.setState({ iframeHtml: event.currentTarget.value }); const newIframeHtml = buildIframe(useCurrentTimeRange, dashboard.uid, selectedTheme, panel, range);
}; setIframeHtml(newIframeHtml);
}, [selectedTheme, useCurrentTimeRange, dashboard, panel, range, buildIframe]);
onUseCurrentTimeRangeChange = () => { const onIframeHtmlChange = (event: FormEvent<HTMLTextAreaElement>) => {
this.setState( setIframeHtml(event.currentTarget.value);
{
useCurrentTimeRange: !this.state.useCurrentTimeRange,
},
this.buildIframeHtml
);
}; };
onThemeChange = (value: string) => { const onUseCurrentTimeRangeChange = () => {
this.setState({ selectedTheme: value }, this.buildIframeHtml); setUseCurrentTimeRange((useCurTimeRange) => !useCurTimeRange);
}; };
getIframeHtml = () => { const onThemeChange = (value: string) => {
return this.state.iframeHtml; setSelectedTheme(value);
}; };
render() { const isRelativeTime = dashboard.time.to === 'now';
const { useCurrentTimeRange, selectedTheme, iframeHtml } = this.state; const timeRangeDescription = isRelativeTime
const isRelativeTime = this.props.dashboard ? this.props.dashboard.time.to === 'now' : false; ? t(
'share-modal.embed.time-range-description',
const timeRangeDescription = isRelativeTime 'Transforms the current relative time range to an absolute time range'
? t( )
'share-modal.embed.time-range-description', : '';
'Transforms the current relative time range to an absolute time range'
)
: '';
return ( return (
<> <>
<p className="share-modal-info-text"> <p className="share-modal-info-text">
<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> </p>
<Field label={t('share-modal.embed.time-range', 'Current time range')} description={timeRangeDescription}> <Field label={t('share-modal.embed.time-range', 'Current time range')} description={timeRangeDescription}>
<Switch <Switch id="share-current-time-range" value={useCurrentTimeRange} onChange={onUseCurrentTimeRangeChange} />
id="share-current-time-range" </Field>
value={useCurrentTimeRange} <ThemePicker selectedTheme={selectedTheme} onChange={onThemeChange} />
onChange={this.onUseCurrentTimeRangeChange} <Field
/> label={t('share-modal.embed.html', 'Embed HTML')}
</Field> description={t(
<ThemePicker selectedTheme={selectedTheme} onChange={this.onThemeChange} /> 'share-modal.embed.html-description',
<Field '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.'
label={t('share-modal.embed.html', 'Embed HTML')} )}
description={t( >
'share-modal.embed.html-description', <TextArea
'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.' data-testid="share-embed-html"
)} id="share-panel-embed-embed-html-textarea"
> rows={5}
<TextArea value={iframeHtml}
data-testid="share-embed-html" onChange={onIframeHtmlChange}
id="share-panel-embed-embed-html-textarea" />
rows={5} </Field>
value={iframeHtml} <Modal.ButtonRow>
onChange={this.onIframeHtmlChange} <ClipboardButton icon="copy" variant="primary" getText={() => iframeHtml}>
/> <Trans i18nKey="share-modal.embed.copy">Copy to clipboard</Trans>
</Field> </ClipboardButton>
<Modal.ButtonRow> </Modal.ButtonRow>
<ClipboardButton icon="copy" variant="primary" getText={this.getIframeHtml}> </>
<Trans i18nKey="share-modal.embed.copy">Copy to clipboard</Trans> );
</ClipboardButton>
</Modal.ButtonRow>
</>
);
}
} }

@ -8,7 +8,7 @@ import { PanelModel } from '../../state';
export interface BuildParamsArgs { export interface BuildParamsArgs {
useCurrentTimeRange: boolean; useCurrentTimeRange: boolean;
selectedTheme?: string; selectedTheme?: string;
panel?: PanelModel; panel?: { timeFrom?: string; id: number };
search?: string; search?: string;
range?: TimeRange; range?: TimeRange;
orgId?: number; orgId?: number;
@ -82,10 +82,11 @@ export function buildSoloUrl(
useCurrentTimeRange: boolean, useCurrentTimeRange: boolean,
dashboardUid: string, dashboardUid: string,
selectedTheme?: string, selectedTheme?: string,
panel?: PanelModel panel?: { timeFrom?: string; id: number },
range?: TimeRange
) { ) {
const baseUrl = buildBaseUrl(); const baseUrl = buildBaseUrl();
const params = buildParams({ useCurrentTimeRange, selectedTheme, panel }); const params = buildParams({ useCurrentTimeRange, selectedTheme, panel, range });
let soloUrl = baseUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/'); let soloUrl = baseUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/');
soloUrl = soloUrl.replace(config.appSubUrl + '/d/', config.appSubUrl + '/d-solo/'); soloUrl = soloUrl.replace(config.appSubUrl + '/d/', config.appSubUrl + '/d-solo/');
@ -121,10 +122,11 @@ export function buildIframeHtml(
useCurrentTimeRange: boolean, useCurrentTimeRange: boolean,
dashboardUid: string, dashboardUid: string,
selectedTheme?: string, selectedTheme?: string,
panel?: PanelModel panel?: { timeFrom?: string; id: number },
range?: TimeRange
) { ) {
let soloUrl = buildSoloUrl(useCurrentTimeRange, dashboardUid, selectedTheme, panel); let soloUrl = buildSoloUrl(useCurrentTimeRange, dashboardUid, selectedTheme, panel, range);
return '<iframe src="' + soloUrl + '" width="450" height="200" frameborder="0"></iframe>'; return `<iframe src="${soloUrl}" width="450" height="200" frameborder="0"></iframe>`;
} }
export function getLocalTimeZone() { export function getLocalTimeZone() {

@ -1090,6 +1090,7 @@
"export": "Exportieren", "export": "Exportieren",
"library-panel": "Bibliotheks-Panel", "library-panel": "Bibliotheks-Panel",
"link": "Link", "link": "Link",
"panel-embed": "",
"snapshot": "Schnappschuss" "snapshot": "Schnappschuss"
}, },
"theme-picker": { "theme-picker": {

@ -1090,6 +1090,7 @@
"export": "Export", "export": "Export",
"library-panel": "Library panel", "library-panel": "Library panel",
"link": "Link", "link": "Link",
"panel-embed": "Embed",
"snapshot": "Snapshot" "snapshot": "Snapshot"
}, },
"theme-picker": { "theme-picker": {

@ -1096,6 +1096,7 @@
"export": "Exportar", "export": "Exportar",
"library-panel": "Panel de librería", "library-panel": "Panel de librería",
"link": "Enlace", "link": "Enlace",
"panel-embed": "",
"snapshot": "Instantánea" "snapshot": "Instantánea"
}, },
"theme-picker": { "theme-picker": {

@ -1096,6 +1096,7 @@
"export": "Exporter", "export": "Exporter",
"library-panel": "Panneau de bibliothèque", "library-panel": "Panneau de bibliothèque",
"link": "Lien", "link": "Lien",
"panel-embed": "",
"snapshot": "Instantané" "snapshot": "Instantané"
}, },
"theme-picker": { "theme-picker": {

@ -1090,6 +1090,7 @@
"export": "Ēχpőřŧ", "export": "Ēχpőřŧ",
"library-panel": "Ŀįþřäřy päʼnęľ", "library-panel": "Ŀįþřäřy päʼnęľ",
"link": "Ŀįʼnĸ", "link": "Ŀįʼnĸ",
"panel-embed": "Ēmþęđ",
"snapshot": "Ŝʼnäpşĥőŧ" "snapshot": "Ŝʼnäpşĥőŧ"
}, },
"theme-picker": { "theme-picker": {

@ -1084,6 +1084,7 @@
"export": "导出", "export": "导出",
"library-panel": "库面板", "library-panel": "库面板",
"link": "链接", "link": "链接",
"panel-embed": "",
"snapshot": "快照" "snapshot": "快照"
}, },
"theme-picker": { "theme-picker": {

Loading…
Cancel
Save