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>
pull/75230/head
Torkel Ödegaard 2 years ago committed by GitHub
parent 6a37a56d68
commit 1d1bdaab37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      packages/grafana-e2e-selectors/src/selectors/pages.ts
  2. 14
      public/app/features/dashboard-scene/scene/DashboardScene.tsx
  3. 4
      public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx
  4. 4
      public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts
  5. 14
      public/app/features/dashboard-scene/scene/NavToolbarActions.tsx
  6. 8
      public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx
  7. 11
      public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx
  8. 2
      public/app/features/dashboard-scene/serialization/SaveDashboardDrawer.tsx
  9. 121
      public/app/features/dashboard-scene/sharing/ShareLinkTab.test.tsx
  10. 255
      public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx
  11. 117
      public/app/features/dashboard-scene/sharing/ShareModal.tsx
  12. 18
      public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx
  13. 5
      public/app/features/dashboard-scene/sharing/types.ts
  14. 0
      public/app/features/dashboard-scene/sharing/utils.ts
  15. 57
      public/app/features/dashboard-scene/utils/utils.ts
  16. 2
      public/app/features/dashboard/components/DashNav/ShareButton.tsx
  17. 26
      public/app/features/dashboard/components/ShareModal/ShareModal.tsx
  18. 2
      public/locales/en-US/grafana.json
  19. 2
      public/locales/pseudo-LOCALE/grafana.json

@ -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;
}

@ -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" />
&nbsp;
<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;
}

@ -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>;
};
}

@ -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…
Cancel
Save