diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index cd6189821e6..fae9f32db26 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -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', diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index fb745d83459..3d49708827d 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -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 { @@ -129,7 +129,7 @@ export class DashboardScene extends SceneObjectBase { }; 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 { public getInitialState(): DashboardSceneState | undefined { return this._initialState; } + + public showModal(modal: SceneObject) { + this.setState({ overlay: modal }); + } + + public closeModal() { + this.setState({ overlay: undefined }); + } } diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx index e02344c6938..63bb7df05e6 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx @@ -11,7 +11,7 @@ import { DashboardScene } from './DashboardScene'; import { NavToolbarActions } from './NavToolbarActions'; export function DashboardSceneRenderer({ model }: SceneComponentProps) { - 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 - {drawer && } + {overlay && } ); } diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts index aa344b4cea2..f94a248ab65 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts @@ -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 diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 85c3e32a480..48e7e46c1f7 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -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(({ dashboard }) => { const toolbarActions = (actions ?? []).map((action) => ); if (uid) { + toolbarActions.push( + { + dashboard.showModal(new ShareModal({ dashboardRef: dashboard.getRef() })); + }} + /> + ); + toolbarActions.push( { 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= 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= - 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'); }); }); diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx index b5d95b0b307..aa03646e03a 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx @@ -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) { diff --git a/public/app/features/dashboard-scene/serialization/SaveDashboardDrawer.tsx b/public/app/features/dashboard-scene/serialization/SaveDashboardDrawer.tsx index 6aa8098565a..92068f84576 100644 --- a/public/app/features/dashboard-scene/serialization/SaveDashboardDrawer.tsx +++ b/public/app/features/dashboard-scene/serialization/SaveDashboardDrawer.tsx @@ -15,7 +15,7 @@ interface SaveDashboardDrawerState extends SceneObjectState { export class SaveDashboardDrawer extends SceneObjectBase { onClose = () => { - this.state.dashboardRef.resolve().setState({ drawer: undefined }); + this.state.dashboardRef.resolve().setState({ overlay: undefined }); }; static Component = ({ model }: SceneComponentProps) => { diff --git a/public/app/features/dashboard-scene/sharing/ShareLinkTab.test.tsx b/public/app/features/dashboard-scene/sharing/ShareLinkTab.test.tsx new file mode 100644 index 00000000000..1cfe3167fc6 --- /dev/null +++ b/public/app/features/dashboard-scene/sharing/ShareLinkTab.test.tsx @@ -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(); + + return tab; +} diff --git a/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx b/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx new file mode 100644 index 00000000000..0c0dda88446 --- /dev/null +++ b/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx @@ -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; + dashboardRef: SceneObjectRef; +} + +interface ShareOptions { + useLockedTime: boolean; + useShortUrl: boolean; + selectedTheme: string; + shareUrl: string; + imageUrl: string; +} + +export class ShareLinkTab extends SceneObjectBase { + static Component = ShareLinkTabRenderer; + + constructor(state: Omit) { + 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) { + 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 ( + <> +

+ + Create a direct link to this dashboard or panel, customized with the options below. + +

+
+ + + + + + + + + + + Copy + + } + /> + +
+ + {panel && config.rendererAvailable && ( + <> + {isDashboardSaved && ( + + )} + + {!isDashboardSaved && ( + + + To render a panel image, you must save the dashboard first. + + + )} + + )} + + {panel && !config.rendererAvailable && ( + + + To render a panel image, you must install the + + Grafana image renderer plugin + + . Please contact your Grafana administrator to install the plugin. + + + )} + + ); +} + +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; +} diff --git a/public/app/features/dashboard-scene/sharing/ShareModal.tsx b/public/app/features/dashboard-scene/sharing/ShareModal.tsx new file mode 100644 index 00000000000..d92fb0a881d --- /dev/null +++ b/public/app/features/dashboard-scene/sharing/ShareModal.tsx @@ -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; + panelRef?: SceneObjectRef; + tabs?: SceneShareTab[]; + activeTab: string; +} + +/** + * Used for full dashboard share modal and the panel level share modal + */ +export class ShareModal extends SceneObjectBase { + static Component = SharePanelModalRenderer; + + constructor(state: Omit) { + 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['onChangeTab'] = (tab) => { + this.setState({ activeTab: tab.value }); + }; +} + +function SharePanelModalRenderer({ model }: SceneComponentProps) { + 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 = ( + + ); + + const currentTab = tabs.find((t) => t.getTabLabel() === activeTab); + + return ( + + {currentTab && } + + ); +} diff --git a/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx b/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx new file mode 100644 index 00000000000..091ac8484bf --- /dev/null +++ b/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx @@ -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; +} + +export class ShareSnapshotTab extends SceneObjectBase { + public getTabLabel() { + return t('share-modal.tab-title.snapshot', 'Snapshot'); + } + + static Component = ({ model }: SceneComponentProps) => { + return
Snapshot
; + }; +} diff --git a/public/app/features/dashboard-scene/sharing/types.ts b/public/app/features/dashboard-scene/sharing/types.ts new file mode 100644 index 00000000000..d10648f83c1 --- /dev/null +++ b/public/app/features/dashboard-scene/sharing/types.ts @@ -0,0 +1,5 @@ +import { SceneObject, SceneObjectState } from '@grafana/scenes'; + +export interface SceneShareTab extends SceneObject { + getTabLabel(): string; +} diff --git a/public/app/features/dashboard-scene/sharing/utils.ts b/public/app/features/dashboard-scene/sharing/utils.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index b9780b58961..68f94780821 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -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'); +} diff --git a/public/app/features/dashboard/components/DashNav/ShareButton.tsx b/public/app/features/dashboard/components/DashNav/ShareButton.tsx index f762b1de090..f21b9f560ad 100644 --- a/public/app/features/dashboard/components/DashNav/ShareButton.tsx +++ b/public/app/features/dashboard/components/DashNav/ShareButton.tsx @@ -28,7 +28,7 @@ export const ShareButton = ({ dashboard }: { dashboard: DashboardModel }) => { return ( { diff --git a/public/app/features/dashboard/components/ShareModal/ShareModal.tsx b/public/app/features/dashboard/components/ShareModal/ShareModal.tsx index 1f05fccbb9e..7e3d9fe0887 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareModal.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareModal.tsx @@ -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 { } 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 ( - + @@ -152,16 +143,3 @@ class UnthemedShareModal extends React.Component { } 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), - }), - }; -}; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index bda6c9dbe90..d25c14e18a5 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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" } }, diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 6229a841b09..6cad22e9d37 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -211,7 +211,7 @@ "refresh": "Ŗęƒřęşĥ đäşĥþőäřđ", "save": "Ŝävę đäşĥþőäřđ", "settings": "Đäşĥþőäřđ şęŧŧįʼnģş", - "share": "Ŝĥäřę đäşĥþőäřđ őř päʼnęľ", + "share": "Ŝĥäřę đäşĥþőäřđ", "unmark-favorite": "Ůʼnmäřĸ äş ƒävőřįŧę" } },