mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DashboardScene: ShareModal + link sharing (#74955)
* DashboardScene: Panel menu updates, adding explore action * DashboardScene: Panel menu updates, adding explore action * Initial test * Update * share modal * Update * rename * Update tests * Fix test * update * Fix tooltip wording * Update translation file * fix e2e * Extract ShareLinkTab component * rename to overlay --------- Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
parent
6a37a56d68
commit
1d1bdaab37
@ -208,7 +208,7 @@ export const Pages = {
|
||||
linkToRenderedImage: 'Link to rendered image',
|
||||
},
|
||||
ShareDashboardModal: {
|
||||
shareButton: 'Share dashboard or panel',
|
||||
shareButton: 'Share dashboard',
|
||||
PublicDashboard: {
|
||||
Tab: 'Tab Public dashboard',
|
||||
WillBePublicCheckbox: 'data-testid public dashboard will be public checkbox',
|
||||
|
@ -37,8 +37,8 @@ export interface DashboardSceneState extends SceneObjectState {
|
||||
inspectPanelKey?: string;
|
||||
/** Panel to view in full screen */
|
||||
viewPanelKey?: string;
|
||||
/** Scene object that handles the current drawer */
|
||||
drawer?: SceneObject;
|
||||
/** Scene object that handles the current drawer or modal */
|
||||
overlay?: SceneObject;
|
||||
}
|
||||
|
||||
export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
@ -129,7 +129,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
};
|
||||
|
||||
public onSave = () => {
|
||||
this.setState({ drawer: new SaveDashboardDrawer({ dashboardRef: new SceneObjectRef(this) }) });
|
||||
this.setState({ overlay: new SaveDashboardDrawer({ dashboardRef: new SceneObjectRef(this) }) });
|
||||
};
|
||||
|
||||
public getPageNav(location: H.Location) {
|
||||
@ -184,4 +184,12 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
public getInitialState(): DashboardSceneState | undefined {
|
||||
return this._initialState;
|
||||
}
|
||||
|
||||
public showModal(modal: SceneObject) {
|
||||
this.setState({ overlay: modal });
|
||||
}
|
||||
|
||||
public closeModal() {
|
||||
this.setState({ overlay: undefined });
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import { DashboardScene } from './DashboardScene';
|
||||
import { NavToolbarActions } from './NavToolbarActions';
|
||||
|
||||
export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) {
|
||||
const { controls, viewPanelKey: viewPanelId, drawer } = model.useState();
|
||||
const { controls, viewPanelKey: viewPanelId, overlay } = model.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
const location = useLocation();
|
||||
const pageNav = model.getPageNav(location);
|
||||
@ -35,7 +35,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
|
||||
</div>
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
{drawer && <drawer.Component model={drawer} />}
|
||||
{overlay && <overlay.Component model={overlay} />}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
@ -34,10 +34,10 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
||||
}
|
||||
|
||||
update.inspectPanelKey = values.inspect;
|
||||
update.drawer = new PanelInspectDrawer({ panelRef: new SceneObjectRef(panel) });
|
||||
update.overlay = new PanelInspectDrawer({ panelRef: new SceneObjectRef(panel) });
|
||||
} else if (inspectPanelId) {
|
||||
update.inspectPanelKey = undefined;
|
||||
update.drawer = undefined;
|
||||
update.overlay = undefined;
|
||||
}
|
||||
|
||||
// Handle view panel state
|
||||
|
@ -4,8 +4,11 @@ import { locationService } from '@grafana/runtime';
|
||||
import { Button } from '@grafana/ui';
|
||||
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
|
||||
import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/NavToolbarSeparator';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { DashNavButton } from 'app/features/dashboard/components/DashNav/DashNavButton';
|
||||
|
||||
import { ShareModal } from '../sharing/ShareModal';
|
||||
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
|
||||
interface Props {
|
||||
@ -17,6 +20,17 @@ export const NavToolbarActions = React.memo<Props>(({ dashboard }) => {
|
||||
const toolbarActions = (actions ?? []).map((action) => <action.Component key={action.state.key} model={action} />);
|
||||
|
||||
if (uid) {
|
||||
toolbarActions.push(
|
||||
<DashNavButton
|
||||
tooltip={t('dashboard.toolbar.share', 'Share dashboard')}
|
||||
icon="share-alt"
|
||||
iconSize="lg"
|
||||
onClick={() => {
|
||||
dashboard.showModal(new ShareModal({ dashboardRef: dashboard.getRef() }));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
toolbarActions.push(
|
||||
<DashNavButton
|
||||
key="button-scenes"
|
||||
|
@ -38,13 +38,15 @@ describe('panelMenuBehavior', () => {
|
||||
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
||||
expect(menu.state.items?.length).toBe(4);
|
||||
expect(menu.state.items?.length).toBe(5);
|
||||
// verify view panel url keeps url params and adds viewPanel=<panel-key>
|
||||
expect(menu.state.items?.[0].href).toBe('/scenes/dashboard/dash-1?from=now-5m&to=now&viewPanel=panel-12');
|
||||
// verify edit url keeps url time range
|
||||
expect(menu.state.items?.[1].href).toBe('/scenes/dashboard/dash-1/panel-edit/12?from=now-5m&to=now');
|
||||
// verify share
|
||||
expect(menu.state.items?.[2].text).toBe('Share');
|
||||
// verify explore url
|
||||
expect(menu.state.items?.[2].href).toBe('/explore');
|
||||
expect(menu.state.items?.[3].href).toBe('/explore');
|
||||
|
||||
// Verify explore url is called with correct arguments
|
||||
const getExploreArgs: GetExploreUrlArguments = mocks.getExploreUrl.mock.calls[0][0];
|
||||
@ -53,7 +55,7 @@ describe('panelMenuBehavior', () => {
|
||||
expect(getExploreArgs.scopedVars?.__sceneObject?.value).toBe(panel);
|
||||
|
||||
// verify inspect url keeps url params and adds inspect=<panel-key>
|
||||
expect(menu.state.items?.[3].href).toBe('/scenes/dashboard/dash-1?from=now-5m&to=now&inspect=panel-12');
|
||||
expect(menu.state.items?.[4].href).toBe('/scenes/dashboard/dash-1?from=now-5m&to=now&inspect=panel-12');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -6,6 +6,7 @@ import { t } from 'app/core/internationalization';
|
||||
import { getExploreUrl } from 'app/core/utils/explore';
|
||||
import { InspectTab } from 'app/features/inspector/types';
|
||||
|
||||
import { ShareModal } from '../sharing/ShareModal';
|
||||
import { getDashboardUrl, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils';
|
||||
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
@ -47,6 +48,16 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
|
||||
currentQueryParams: location.search,
|
||||
}),
|
||||
});
|
||||
|
||||
items.push({
|
||||
text: t('panel.header-menu.share', `Share`),
|
||||
iconClassName: 'share-alt',
|
||||
onClick: () => {
|
||||
reportInteraction('dashboards_panelheader_menu', { item: 'share' });
|
||||
dashboard.showModal(new ShareModal({ panelRef: panel.getRef(), dashboardRef: dashboard.getRef() }));
|
||||
},
|
||||
shortcut: 'p s',
|
||||
});
|
||||
}
|
||||
|
||||
if (contextSrv.hasAccessToExplore() && !panelPlugin?.meta.skipDataQuery && queryRunner) {
|
||||
|
@ -15,7 +15,7 @@ interface SaveDashboardDrawerState extends SceneObjectState {
|
||||
|
||||
export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerState> {
|
||||
onClose = () => {
|
||||
this.state.dashboardRef.resolve().setState({ drawer: undefined });
|
||||
this.state.dashboardRef.resolve().setState({ overlay: undefined });
|
||||
};
|
||||
|
||||
static Component = ({ model }: SceneComponentProps<SaveDashboardDrawer>) => {
|
||||
|
@ -0,0 +1,121 @@
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { advanceTo, clear } from 'jest-date-mock';
|
||||
import React from 'react';
|
||||
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { SceneGridItem, SceneGridLayout, SceneTimeRange, VizPanel } from '@grafana/scenes';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
|
||||
import { ShareLinkTab } from './ShareLinkTab';
|
||||
|
||||
jest.mock('app/core/utils/shortLinks', () => ({
|
||||
createShortLink: jest.fn().mockResolvedValue(`http://localhost:3000/goto/shortend-uid`),
|
||||
}));
|
||||
|
||||
describe('ShareLinkTab', () => {
|
||||
const fakeCurrentDate = dateTime('2019-02-11T19:00:00.000Z').toDate();
|
||||
|
||||
afterAll(() => {
|
||||
clear();
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
advanceTo(fakeCurrentDate);
|
||||
|
||||
config.appUrl = 'http://dashboards.grafana.com/grafana/';
|
||||
config.rendererAvailable = true;
|
||||
config.bootData.user.orgId = 1;
|
||||
locationService.push('/scenes/dashboard/dash-1?from=now-6h&to=now');
|
||||
});
|
||||
|
||||
describe('with locked time range (absolute) range', () => {
|
||||
it('should generate share url absolute time', async () => {
|
||||
buildAndRenderScenario({});
|
||||
|
||||
expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue(
|
||||
'http://dashboards.grafana.com/grafana/scenes/dashboard/dash-1?from=2019-02-11T13:00:00.000Z&to=2019-02-11T19:00:00.000Z&viewPanel=panel-12'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with disabled locked range range', () => {
|
||||
it('should generate share url with relative time', async () => {
|
||||
const tab = buildAndRenderScenario({});
|
||||
act(() => tab.onToggleLockedTime());
|
||||
|
||||
expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue(
|
||||
'http://dashboards.grafana.com/grafana/scenes/dashboard/dash-1?from=now-6h&to=now&viewPanel=panel-12'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should add theme when specified', async () => {
|
||||
const tab = buildAndRenderScenario({});
|
||||
act(() => tab.onThemeChange('light'));
|
||||
|
||||
expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue(
|
||||
'http://dashboards.grafana.com/grafana/scenes/dashboard/dash-1?from=2019-02-11T13:00:00.000Z&to=2019-02-11T19:00:00.000Z&viewPanel=panel-12&theme=light'
|
||||
);
|
||||
});
|
||||
|
||||
it('should shorten url', async () => {
|
||||
buildAndRenderScenario({});
|
||||
|
||||
await userEvent.click(await screen.findByLabelText('Shorten URL'));
|
||||
|
||||
expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue(
|
||||
`http://localhost:3000/goto/shortend-uid`
|
||||
);
|
||||
});
|
||||
|
||||
it('should generate render url', async () => {
|
||||
buildAndRenderScenario({});
|
||||
|
||||
expect(
|
||||
await screen.findByRole('link', { name: selectors.pages.SharePanelModal.linkToRenderedImage })
|
||||
).toHaveAttribute(
|
||||
'href',
|
||||
'http://dashboards.grafana.com/grafana/render/d-solo/dash-1?from=2019-02-11T13:00:00.000Z&to=2019-02-11T19:00:00.000Z&viewPanel=panel-12&width=1000&height=500&tz=Pacific%2FEaster'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
interface ScenarioOptions {
|
||||
withPanel?: boolean;
|
||||
}
|
||||
|
||||
function buildAndRenderScenario(options: ScenarioOptions) {
|
||||
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 SceneGridItem({
|
||||
key: 'griditem-1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 10,
|
||||
height: 12,
|
||||
body: panel,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const tab = new ShareLinkTab({ dashboardRef: dashboard.getRef(), panelRef: panel.getRef() });
|
||||
|
||||
render(<tab.Component model={tab} />);
|
||||
|
||||
return tab;
|
||||
}
|
255
public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx
Normal file
255
public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx
Normal file
@ -0,0 +1,255 @@
|
||||
import React from 'react';
|
||||
|
||||
import { dateTime, UrlQueryMap } from '@grafana/data';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import {
|
||||
SceneComponentProps,
|
||||
SceneObjectBase,
|
||||
SceneObjectState,
|
||||
SceneObjectRef,
|
||||
VizPanel,
|
||||
sceneGraph,
|
||||
} from '@grafana/scenes';
|
||||
import { TimeZone } from '@grafana/schema';
|
||||
import { Alert, ClipboardButton, Field, FieldSet, Icon, Input, Switch } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
import { createShortLink } from 'app/core/utils/shortLinks';
|
||||
import { ThemePicker } from 'app/features/dashboard/components/ShareModal/ThemePicker';
|
||||
import { trackDashboardSharingActionPerType } from 'app/features/dashboard/components/ShareModal/analytics';
|
||||
import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { getDashboardUrl } from '../utils/utils';
|
||||
|
||||
export interface ShareLinkTabState extends SceneObjectState, ShareOptions {
|
||||
panelRef?: SceneObjectRef<VizPanel>;
|
||||
dashboardRef: SceneObjectRef<DashboardScene>;
|
||||
}
|
||||
|
||||
interface ShareOptions {
|
||||
useLockedTime: boolean;
|
||||
useShortUrl: boolean;
|
||||
selectedTheme: string;
|
||||
shareUrl: string;
|
||||
imageUrl: string;
|
||||
}
|
||||
|
||||
export class ShareLinkTab extends SceneObjectBase<ShareLinkTabState> {
|
||||
static Component = ShareLinkTabRenderer;
|
||||
|
||||
constructor(state: Omit<ShareLinkTabState, keyof ShareOptions>) {
|
||||
super({
|
||||
...state,
|
||||
useLockedTime: true,
|
||||
useShortUrl: false,
|
||||
selectedTheme: 'current',
|
||||
shareUrl: '',
|
||||
imageUrl: '',
|
||||
});
|
||||
|
||||
this.addActivationHandler(() => {
|
||||
this.buildUrl();
|
||||
});
|
||||
}
|
||||
|
||||
async buildUrl() {
|
||||
const { panelRef, dashboardRef, useLockedTime: useAbsoluteTimeRange, useShortUrl, selectedTheme } = this.state;
|
||||
const dashboard = dashboardRef.resolve();
|
||||
const panel = panelRef?.resolve();
|
||||
const location = locationService.getLocation();
|
||||
const timeRange = sceneGraph.getTimeRange(panel ?? dashboard);
|
||||
|
||||
const urlParamsUpdate: UrlQueryMap = {};
|
||||
|
||||
if (panel) {
|
||||
urlParamsUpdate.viewPanel = panel.state.key;
|
||||
}
|
||||
|
||||
if (useAbsoluteTimeRange) {
|
||||
urlParamsUpdate.from = timeRange.state.value.from.toISOString();
|
||||
urlParamsUpdate.to = timeRange.state.value.to.toISOString();
|
||||
}
|
||||
|
||||
if (selectedTheme !== 'current') {
|
||||
urlParamsUpdate.theme = selectedTheme!;
|
||||
}
|
||||
|
||||
let shareUrl = getDashboardUrl({
|
||||
uid: dashboard.state.uid,
|
||||
currentQueryParams: location.search,
|
||||
updateQuery: urlParamsUpdate,
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
if (useShortUrl) {
|
||||
shareUrl = await createShortLink(shareUrl);
|
||||
}
|
||||
|
||||
const imageUrl = getDashboardUrl({
|
||||
uid: dashboard.state.uid,
|
||||
currentQueryParams: location.search,
|
||||
updateQuery: urlParamsUpdate,
|
||||
absolute: true,
|
||||
|
||||
soloRoute: true,
|
||||
render: true,
|
||||
timeZone: getRenderTimeZone(timeRange.getTimeZone()),
|
||||
});
|
||||
|
||||
this.setState({ shareUrl, imageUrl });
|
||||
}
|
||||
|
||||
public getTabLabel() {
|
||||
return t('share-modal.tab-title.link', 'Link');
|
||||
}
|
||||
|
||||
onToggleLockedTime = () => {
|
||||
this.setState({ useLockedTime: !this.state.useLockedTime });
|
||||
this.buildUrl();
|
||||
};
|
||||
|
||||
onUrlShorten = () => {
|
||||
this.setState({ useShortUrl: !this.state.useShortUrl });
|
||||
this.buildUrl();
|
||||
};
|
||||
|
||||
onThemeChange = (value: string) => {
|
||||
this.setState({ selectedTheme: value });
|
||||
this.buildUrl();
|
||||
};
|
||||
|
||||
getShareUrl = () => {
|
||||
return this.state.shareUrl;
|
||||
};
|
||||
|
||||
onCopy() {
|
||||
trackDashboardSharingActionPerType('copy_link', shareDashboardType.link);
|
||||
}
|
||||
}
|
||||
|
||||
function ShareLinkTabRenderer({ model }: SceneComponentProps<ShareLinkTab>) {
|
||||
const state = model.useState();
|
||||
const { panelRef, dashboardRef } = state;
|
||||
|
||||
const dashboard = dashboardRef.resolve();
|
||||
const panel = panelRef?.resolve();
|
||||
|
||||
const timeRange = sceneGraph.getTimeRange(panel ?? dashboard);
|
||||
const isRelativeTime = timeRange.state.to === 'now' ? true : false;
|
||||
|
||||
const { useLockedTime, useShortUrl, selectedTheme, shareUrl, imageUrl } = state;
|
||||
|
||||
const selectors = e2eSelectors.pages.SharePanelModal;
|
||||
const isDashboardSaved = Boolean(dashboard.state.uid);
|
||||
|
||||
const lockTimeRangeLabel = t('share-modal.link.time-range-label', `Lock time range`);
|
||||
|
||||
const lockTimeRangeDescription = t(
|
||||
'share-modal.link.time-range-description',
|
||||
`Transforms the current relative time range to an absolute time range`
|
||||
);
|
||||
|
||||
const shortenURLTranslation = t('share-modal.link.shorten-url', `Shorten URL`);
|
||||
|
||||
const linkURLTranslation = t('share-modal.link.link-url', `Link URL`);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="share-modal-info-text">
|
||||
<Trans i18nKey="share-modal.link.info-text">
|
||||
Create a direct link to this dashboard or panel, customized with the options below.
|
||||
</Trans>
|
||||
</p>
|
||||
<FieldSet>
|
||||
<Field label={lockTimeRangeLabel} description={isRelativeTime ? lockTimeRangeDescription : ''}>
|
||||
<Switch id="share-current-time-range" value={useLockedTime} onChange={model.onToggleLockedTime} />
|
||||
</Field>
|
||||
<ThemePicker selectedTheme={selectedTheme} onChange={model.onThemeChange} />
|
||||
<Field label={shortenURLTranslation}>
|
||||
<Switch id="share-shorten-url" value={useShortUrl} onChange={model.onUrlShorten} />
|
||||
</Field>
|
||||
|
||||
<Field label={linkURLTranslation}>
|
||||
<Input
|
||||
id="link-url-input"
|
||||
value={shareUrl}
|
||||
readOnly
|
||||
addonAfter={
|
||||
<ClipboardButton icon="copy" variant="primary" getText={model.getShareUrl} onClipboardCopy={model.onCopy}>
|
||||
<Trans i18nKey="share-modal.link.copy-link-button">Copy</Trans>
|
||||
</ClipboardButton>
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
|
||||
{panel && config.rendererAvailable && (
|
||||
<>
|
||||
{isDashboardSaved && (
|
||||
<div className="gf-form">
|
||||
<a href={imageUrl} target="_blank" rel="noreferrer" aria-label={selectors.linkToRenderedImage}>
|
||||
<Icon name="camera" />
|
||||
|
||||
<Trans i18nKey="share-modal.link.rendered-image">Direct link rendered image</Trans>
|
||||
</a>
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{panel && !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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function getRenderTimeZone(timeZone: TimeZone): string {
|
||||
const utcOffset = 'UTC' + encodeURIComponent(dateTime().format('Z'));
|
||||
|
||||
if (timeZone === 'utc') {
|
||||
return 'UTC';
|
||||
}
|
||||
|
||||
if (timeZone === 'browser') {
|
||||
if (!window.Intl) {
|
||||
return utcOffset;
|
||||
}
|
||||
|
||||
const dateFormat = window.Intl.DateTimeFormat();
|
||||
const options = dateFormat.resolvedOptions();
|
||||
if (!options.timeZone) {
|
||||
return utcOffset;
|
||||
}
|
||||
|
||||
return options.timeZone;
|
||||
}
|
||||
|
||||
return timeZone;
|
||||
}
|
117
public/app/features/dashboard-scene/sharing/ShareModal.tsx
Normal file
117
public/app/features/dashboard-scene/sharing/ShareModal.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import React, { ComponentProps } from 'react';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { SceneComponentProps, SceneObjectBase, SceneObjectState, VizPanel, SceneObjectRef } from '@grafana/scenes';
|
||||
import { Modal, ModalTabsHeader, TabContent } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
|
||||
import { ShareLinkTab } from './ShareLinkTab';
|
||||
import { ShareSnapshotTab } from './ShareSnapshotTab';
|
||||
import { SceneShareTab } from './types';
|
||||
|
||||
interface ShareModalState extends SceneObjectState {
|
||||
dashboardRef: SceneObjectRef<DashboardScene>;
|
||||
panelRef?: SceneObjectRef<VizPanel>;
|
||||
tabs?: SceneShareTab[];
|
||||
activeTab: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for full dashboard share modal and the panel level share modal
|
||||
*/
|
||||
export class ShareModal extends SceneObjectBase<ShareModalState> {
|
||||
static Component = SharePanelModalRenderer;
|
||||
|
||||
constructor(state: Omit<ShareModalState, 'activeTab'>) {
|
||||
super({
|
||||
...state,
|
||||
activeTab: 'Link',
|
||||
});
|
||||
|
||||
this.addActivationHandler(() => this.buildTabs());
|
||||
}
|
||||
|
||||
private buildTabs() {
|
||||
const { dashboardRef, panelRef } = this.state;
|
||||
|
||||
const tabs: SceneShareTab[] = [new ShareLinkTab({ dashboardRef, panelRef })];
|
||||
|
||||
if (contextSrv.isSignedIn && config.snapshotEnabled) {
|
||||
tabs.push(new ShareSnapshotTab({ panelRef }));
|
||||
}
|
||||
|
||||
this.setState({ tabs });
|
||||
|
||||
// if (panel) {
|
||||
// const embedLabel = t('share-modal.tab-title.embed', 'Embed');
|
||||
// tabs.push({ label: embedLabel, value: shareDashboardType.embed, component: ShareEmbed });
|
||||
|
||||
// if (!isPanelModelLibraryPanel(panel)) {
|
||||
// const libraryPanelLabel = t('share-modal.tab-title.library-panel', 'Library panel');
|
||||
// tabs.push({ label: libraryPanelLabel, value: shareDashboardType.libraryPanel, component: ShareLibraryPanel });
|
||||
// }
|
||||
// tabs.push(...customPanelTabs);
|
||||
// } else {
|
||||
// const exportLabel = t('share-modal.tab-title.export', 'Export');
|
||||
// tabs.push({
|
||||
// label: exportLabel,
|
||||
// value: shareDashboardType.export,
|
||||
// component: ShareExport,
|
||||
// });
|
||||
// tabs.push(...customDashboardTabs);
|
||||
// }
|
||||
|
||||
// if (Boolean(config.featureToggles['publicDashboards'])) {
|
||||
// tabs.push({
|
||||
// label: 'Public dashboard',
|
||||
// value: shareDashboardType.publicDashboard,
|
||||
// component: SharePublicDashboard,
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
onClose = () => {
|
||||
const dashboard = getDashboardSceneFor(this);
|
||||
dashboard.closeModal();
|
||||
};
|
||||
|
||||
onChangeTab: ComponentProps<typeof ModalTabsHeader>['onChangeTab'] = (tab) => {
|
||||
this.setState({ activeTab: tab.value });
|
||||
};
|
||||
}
|
||||
|
||||
function SharePanelModalRenderer({ model }: SceneComponentProps<ShareModal>) {
|
||||
const { panelRef, tabs, activeTab } = model.useState();
|
||||
const title = panelRef ? t('share-modal.panel.title', 'Share Panel') : t('share-modal.dashboard.title', 'Share');
|
||||
|
||||
if (!tabs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modalTabs = tabs?.map((tab) => ({
|
||||
label: tab.getTabLabel(),
|
||||
value: tab.getTabLabel(),
|
||||
}));
|
||||
|
||||
const header = (
|
||||
<ModalTabsHeader
|
||||
title={title}
|
||||
icon="share-alt"
|
||||
tabs={modalTabs}
|
||||
activeTab={activeTab}
|
||||
onChangeTab={model.onChangeTab}
|
||||
/>
|
||||
);
|
||||
|
||||
const currentTab = tabs.find((t) => t.getTabLabel() === activeTab);
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} title={header} onDismiss={model.onClose}>
|
||||
<TabContent>{currentTab && <currentTab.Component model={currentTab} />}</TabContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
import { SceneComponentProps, SceneObjectBase, SceneObjectState, SceneObjectRef, VizPanel } from '@grafana/scenes';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
export interface ShareSnapshotTabState extends SceneObjectState {
|
||||
panelRef?: SceneObjectRef<VizPanel>;
|
||||
}
|
||||
|
||||
export class ShareSnapshotTab extends SceneObjectBase<ShareSnapshotTabState> {
|
||||
public getTabLabel() {
|
||||
return t('share-modal.tab-title.snapshot', 'Snapshot');
|
||||
}
|
||||
|
||||
static Component = ({ model }: SceneComponentProps<ShareSnapshotTab>) => {
|
||||
return <div>Snapshot</div>;
|
||||
};
|
||||
}
|
5
public/app/features/dashboard-scene/sharing/types.ts
Normal file
5
public/app/features/dashboard-scene/sharing/types.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { SceneObject, SceneObjectState } from '@grafana/scenes';
|
||||
|
||||
export interface SceneShareTab<T extends SceneObjectState = SceneObjectState> extends SceneObject<T> {
|
||||
getTabLabel(): string;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { UrlQueryMap, urlUtil } from '@grafana/data';
|
||||
import { locationSearchToObject } from '@grafana/runtime';
|
||||
import { config, locationSearchToObject } from '@grafana/runtime';
|
||||
import {
|
||||
MultiValueVariable,
|
||||
SceneDataTransformer,
|
||||
@ -82,14 +82,35 @@ export interface DashboardUrlOptions {
|
||||
uid?: string;
|
||||
subPath?: string;
|
||||
updateQuery?: UrlQueryMap;
|
||||
/**
|
||||
* Set to location.search to preserve current params
|
||||
*/
|
||||
/** Set to location.search to preserve current params */
|
||||
currentQueryParams: string;
|
||||
/** * Returns solo panel route instead */
|
||||
soloRoute?: boolean;
|
||||
/** return render url */
|
||||
render?: boolean;
|
||||
/** Return an absolute URL */
|
||||
absolute?: boolean;
|
||||
// Add tz to query params
|
||||
timeZone?: string;
|
||||
}
|
||||
|
||||
export function getDashboardUrl(options: DashboardUrlOptions) {
|
||||
const url = `/scenes/dashboard/${options.uid}${options.subPath ?? ''}`;
|
||||
let path = `/scenes/dashboard/${options.uid}${options.subPath ?? ''}`;
|
||||
|
||||
if (options.soloRoute) {
|
||||
path = `/d-solo/${options.uid}${options.subPath ?? ''}`;
|
||||
}
|
||||
|
||||
if (options.render) {
|
||||
path = '/render' + path;
|
||||
|
||||
options.updateQuery = {
|
||||
...options.updateQuery,
|
||||
width: 1000,
|
||||
height: 500,
|
||||
tz: options.timeZone,
|
||||
};
|
||||
}
|
||||
|
||||
const params = options.currentQueryParams ? locationSearchToObject(options.currentQueryParams) : {};
|
||||
|
||||
@ -104,7 +125,13 @@ export function getDashboardUrl(options: DashboardUrlOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
return urlUtil.renderUrl(url, params);
|
||||
const relativeUrl = urlUtil.renderUrl(path, params);
|
||||
|
||||
if (options.absolute) {
|
||||
return config.appUrl + relativeUrl.slice(1);
|
||||
}
|
||||
|
||||
return relativeUrl;
|
||||
}
|
||||
|
||||
export function getMultiVariableValues(variable: MultiValueVariable) {
|
||||
@ -123,15 +150,6 @@ export function getMultiVariableValues(variable: MultiValueVariable) {
|
||||
};
|
||||
}
|
||||
|
||||
export function getDashboardSceneFor(sceneObject: SceneObject): DashboardScene {
|
||||
const root = sceneObject.getRoot();
|
||||
if (root instanceof DashboardScene) {
|
||||
return root;
|
||||
}
|
||||
|
||||
throw new Error('SceneObject root is not a DashboardScene');
|
||||
}
|
||||
|
||||
export function getQueryRunnerFor(sceneObject: SceneObject | undefined): SceneQueryRunner | undefined {
|
||||
if (!sceneObject) {
|
||||
return undefined;
|
||||
@ -147,3 +165,12 @@ export function getQueryRunnerFor(sceneObject: SceneObject | undefined): SceneQu
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getDashboardSceneFor(sceneObject: SceneObject): DashboardScene {
|
||||
const root = sceneObject.getRoot();
|
||||
if (root instanceof DashboardScene) {
|
||||
return root;
|
||||
}
|
||||
|
||||
throw new Error('SceneObject root is not a DashboardScene');
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ export const ShareButton = ({ dashboard }: { dashboard: DashboardModel }) => {
|
||||
|
||||
return (
|
||||
<DashNavButton
|
||||
tooltip={t('dashboard.toolbar.share', 'Share dashboard or panel')}
|
||||
tooltip={t('dashboard.toolbar.share', 'Share dashboard')}
|
||||
icon="share-alt"
|
||||
iconSize="lg"
|
||||
onClick={() => {
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Modal, ModalTabsHeader, TabContent, Themeable2, withTheme2 } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
@ -130,19 +128,12 @@ class UnthemedShareModal extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dashboard, panel, theme } = this.props;
|
||||
const styles = getStyles(theme);
|
||||
const { dashboard, panel } = this.props;
|
||||
const activeTabModel = this.getActiveTab();
|
||||
const ActiveTab = activeTabModel.component;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={true}
|
||||
title={this.renderTitle()}
|
||||
onDismiss={this.props.onDismiss}
|
||||
className={styles.container}
|
||||
contentClassName={styles.content}
|
||||
>
|
||||
<Modal isOpen={true} title={this.renderTitle()} onDismiss={this.props.onDismiss}>
|
||||
<TabContent>
|
||||
<ActiveTab dashboard={dashboard} panel={panel} onDismiss={this.props.onDismiss} />
|
||||
</TabContent>
|
||||
@ -152,16 +143,3 @@ class UnthemedShareModal extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
export const ShareModal = withTheme2(UnthemedShareModal);
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
container: css({
|
||||
label: 'shareModalContainer',
|
||||
paddingTop: theme.spacing(1),
|
||||
}),
|
||||
content: css({
|
||||
label: 'shareModalContent',
|
||||
padding: theme.spacing(3, 2, 2, 2),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
@ -211,7 +211,7 @@
|
||||
"refresh": "Refresh dashboard",
|
||||
"save": "Save dashboard",
|
||||
"settings": "Dashboard settings",
|
||||
"share": "Share dashboard or panel",
|
||||
"share": "Share dashboard",
|
||||
"unmark-favorite": "Unmark as favorite"
|
||||
}
|
||||
},
|
||||
|
@ -211,7 +211,7 @@
|
||||
"refresh": "Ŗęƒřęşĥ đäşĥþőäřđ",
|
||||
"save": "Ŝävę đäşĥþőäřđ",
|
||||
"settings": "Đäşĥþőäřđ şęŧŧįʼnģş",
|
||||
"share": "Ŝĥäřę đäşĥþőäřđ őř päʼnęľ",
|
||||
"share": "Ŝĥäřę đäşĥþőäřđ",
|
||||
"unmark-favorite": "Ůʼnmäřĸ äş ƒävőřįŧę"
|
||||
}
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user