diff --git a/public/app/features/apiserver/types.ts b/public/app/features/apiserver/types.ts index 4ab780003ab..5dc944cfe2f 100644 --- a/public/app/features/apiserver/types.ts +++ b/public/app/features/apiserver/types.ts @@ -71,6 +71,8 @@ export const AnnoKeyDashboardGnetId = 'grafana.app/dashboard-gnet-id'; export const AnnoKeyFolderTitle = 'grafana.app/folderTitle'; /** @deprecated NOT A REAL annotation -- this is just a shim */ export const AnnoKeyFolderUrl = 'grafana.app/folderUrl'; +/** @deprecated NOT A REAL annotation -- this is just a shim */ +export const AnnoKeyEmbedded = 'grafana.app/embedded'; /** @experimental only provided by proxies for setup with reloadDashboardsOnParamsChange toggle on */ /** Not intended to be used in production, we will be removing this in short-term future */ @@ -108,6 +110,8 @@ type GrafanaClientAnnotations = { [AnnoKeySavedFromUI]?: string; [AnnoKeyDashboardIsSnapshot]?: string; [AnnoKeyDashboardSnapshotOriginalUrl]?: string; + [AnnoKeyEmbedded]?: string; + [AnnoKeyGrantPermissions]?: string; // TODO: This should be provided by the API // This is the dashboard ID for the Gcom API. This set when a dashboard is created through importing a dashboard from Grafana.com. diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index 18b4098fa9c..663677dbaea 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -8,6 +8,7 @@ import { StateManagerBase } from 'app/core/services/StateManagerBase'; import { getMessageFromError, getMessageIdFromError, getStatusFromError } from 'app/core/utils/errors'; import { startMeasure, stopMeasure } from 'app/core/utils/metrics'; import { + AnnoKeyEmbedded, AnnoKeyFolder, AnnoKeyManagerIdentity, AnnoKeyManagerKind, @@ -639,8 +640,8 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan rsp = await this.dashboardLoader.loadDashboard(type || 'db', slug || '', uid); if (route === DashboardRoutes.Embedded) { - throw new Error('Method not implemented.'); - // rsp.meta.isEmbedded = true; + rsp.metadata.annotations = rsp.metadata.annotations || {}; + rsp.metadata.annotations[AnnoKeyEmbedded] = 'embedded'; } } if (rsp.access.url && route === DashboardRoutes.Normal) { diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 2b6dc7d5864..2a4c05839c2 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -49,6 +49,7 @@ import { AnnoKeyUpdatedTimestamp, AnnoKeyDashboardIsSnapshot, DeprecatedInternalId, + AnnoKeyEmbedded, } from 'app/features/apiserver/types'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import { @@ -139,6 +140,7 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo { - const params = buildParams({ useCurrentTimeRange, selectedTheme, panel, range }); - const panelId = params.get('editPanel') ?? params.get('viewPanel') ?? ''; - params.set('panelId', panelId); + const params = buildParams({ useCurrentTimeRange, selectedTheme, panelId, timeFrom, range }); + const editOrViewPanel = params.get('editPanel') ?? params.get('viewPanel') ?? ''; + params.set('panelId', editOrViewPanel); params.delete('editPanel'); params.delete('viewPanel'); params.set('__feature.dashboardSceneSolo', 'true'); diff --git a/public/app/features/dashboard/components/ShareModal/ShareEmbed.test.tsx b/public/app/features/dashboard/components/ShareModal/ShareEmbed.test.tsx index aad5b7554e6..377671419df 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareEmbed.test.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareEmbed.test.tsx @@ -5,7 +5,6 @@ import { setEchoSrv } from '@grafana/runtime'; import config from 'app/core/config'; import { Echo } from '../../../../core/services/echo/Echo'; -import { PanelModel } from '../../state/PanelModel'; import { createDashboardModelFixture } from '../../state/__fixtures__/dashboardFixtures'; import { ShareEmbed } from './ShareEmbed'; @@ -72,16 +71,13 @@ describe('ShareEmbed', () => { const mockDashboard = createDashboardModelFixture({ uid: 'mockDashboardUid', }); - const mockPanel = new PanelModel({ - id: 'mockPanelId', - }); mockLocationHref(`http://dashboards.grafana.com/d/${mockDashboard.uid}?orgId=1`); - render(); + render(); const embedUrl = screen.getByTestId('share-embed-html'); expect(embedUrl).toBeInTheDocument(); expect(embedUrl).toHaveTextContent( - `http://dashboards.grafana.com/d-solo/${mockDashboard.uid}?orgId=1&from=1000&to=2000&panelId=${mockPanel.id}` + `http://dashboards.grafana.com/d-solo/${mockDashboard.uid}?orgId=1&from=1000&to=2000&panelId=mockPanelId` ); }); @@ -90,15 +86,13 @@ describe('ShareEmbed', () => { const mockDashboard = createDashboardModelFixture({ uid: 'mockDashboardUid', }); - const mockPanel = new PanelModel({ - id: 'mockPanelId', - }); - render(); + + render(); const embedUrl = screen.getByTestId('share-embed-html'); expect(embedUrl).toBeInTheDocument(); expect(embedUrl).toHaveTextContent( - `http://dashboards.grafana.com/d-solo/${mockDashboard.uid}?orgId=1&from=1000&to=2000&panelId=${mockPanel.id}` + `http://dashboards.grafana.com/d-solo/${mockDashboard.uid}?orgId=1&from=1000&to=2000&panelId=mockPanelId` ); }); @@ -108,15 +102,13 @@ describe('ShareEmbed', () => { const mockDashboard = createDashboardModelFixture({ uid: 'mockDashboardUid', }); - const mockPanel = new PanelModel({ - id: 'mockPanelId', - }); - render(); + + render(); const embedUrl = screen.getByTestId('share-embed-html'); expect(embedUrl).toBeInTheDocument(); expect(embedUrl).toHaveTextContent( - `http://dashboards.grafana.com/dashboard-solo/snapshot/${mockSlug}?orgId=1&from=1000&to=2000&panelId=${mockPanel.id}` + `http://dashboards.grafana.com/dashboard-solo/snapshot/${mockSlug}?orgId=1&from=1000&to=2000&panelId=mockPanelId` ); }); @@ -126,15 +118,13 @@ describe('ShareEmbed', () => { const mockDashboard = createDashboardModelFixture({ uid: 'mockDashboardUid', }); - const mockPanel = new PanelModel({ - id: 'mockPanelId', - }); - render(); + + render(); const embedUrl = screen.getByTestId('share-embed-html'); expect(embedUrl).toBeInTheDocument(); expect(embedUrl).toHaveTextContent( - `http://dashboards.grafana.com/dashboard-solo/script/${mockSlug}?orgId=1&from=1000&to=2000&panelId=${mockPanel.id}` + `http://dashboards.grafana.com/dashboard-solo/script/${mockSlug}?orgId=1&from=1000&to=2000&panelId=mockPanelId` ); }); }); diff --git a/public/app/features/dashboard/components/ShareModal/ShareEmbed.tsx b/public/app/features/dashboard/components/ShareModal/ShareEmbed.tsx index e5442f3d0a5..2bc55485818 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareEmbed.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareEmbed.tsx @@ -11,22 +11,30 @@ import { ShareModalTabProps } from './types'; import { buildIframeHtml, getTrackingSource } from './utils'; interface Props extends Omit { - panel?: { timeFrom?: string; id: number }; + panelId: string; + timeFrom?: string; dashboard: { uid: string; time: RawTimeRange }; range?: TimeRange; buildIframe?: typeof buildIframeHtml; onCancelClick?: () => void; } -export function ShareEmbed({ panel, dashboard, range, onCancelClick, buildIframe = buildIframeHtml }: Props) { +export function ShareEmbed({ + panelId, + timeFrom, + dashboard, + range, + onCancelClick, + buildIframe = buildIframeHtml, +}: Props) { const [useCurrentTimeRange, setUseCurrentTimeRange] = useState(true); const [selectedTheme, setSelectedTheme] = useState('current'); const [iframeHtml, setIframeHtml] = useState(''); useEffect(() => { - const newIframeHtml = buildIframe(useCurrentTimeRange, dashboard.uid, selectedTheme, panel, range); + const newIframeHtml = buildIframe(useCurrentTimeRange, dashboard.uid, selectedTheme, panelId, timeFrom, range); setIframeHtml(newIframeHtml); - }, [selectedTheme, useCurrentTimeRange, dashboard, panel, range, buildIframe]); + }, [selectedTheme, useCurrentTimeRange, dashboard, panelId, timeFrom, range, buildIframe]); const onIframeHtmlChange = (event: FormEvent) => { setIframeHtml(event.currentTarget.value); @@ -49,7 +57,7 @@ export function ShareEmbed({ panel, dashboard, range, onCancelClick, buildIframe DashboardInteractions.embedSnippetCopy({ currentTimeRange: useCurrentTimeRange, theme: selectedTheme, - shareResource: getTrackingSource(panel), + shareResource: getTrackingSource(panelId), }); }} > diff --git a/public/app/features/dashboard/components/ShareModal/ShareModal.tsx b/public/app/features/dashboard/components/ShareModal/ShareModal.tsx index 16d6220adcc..d8762d837b7 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareModal.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareModal.tsx @@ -17,7 +17,7 @@ import { ShareExport } from './ShareExport'; import { ShareLibraryPanel } from './ShareLibraryPanel'; import { ShareLink } from './ShareLink'; import { ShareSnapshot } from './ShareSnapshot'; -import { ShareModalTabModel } from './types'; +import { ShareModalTabModel, ShareModalTabProps } from './types'; import { getTrackingSource, shareDashboardType } from './utils'; const customDashboardTabs: ShareModalTabModel[] = []; @@ -46,7 +46,7 @@ function getTabs(canEditDashboard: boolean, panel?: PanelModel, activeTab?: stri if (panel) { const embedLabel = t('share-modal.tab-title.embed', 'Embed'); - tabs.push({ label: embedLabel, value: shareDashboardType.embed, component: ShareEmbed }); + tabs.push({ label: embedLabel, value: shareDashboardType.embed, component: ShareEmbedTab }); if (!isPanelModelLibraryPanel(panel)) { const libraryPanelLabel = t('share-modal.tab-title.library-panel', 'Library panel'); @@ -79,6 +79,17 @@ function getTabs(canEditDashboard: boolean, panel?: PanelModel, activeTab?: stri }; } +function ShareEmbedTab(props: ShareModalTabProps) { + return ( + + ); +} + interface Props extends Themeable2 { dashboard: DashboardModel; panel?: PanelModel; diff --git a/public/app/features/dashboard/components/ShareModal/utils.test.ts b/public/app/features/dashboard/components/ShareModal/utils.test.ts index f6aba62c4ff..5864123c146 100644 --- a/public/app/features/dashboard/components/ShareModal/utils.test.ts +++ b/public/app/features/dashboard/components/ShareModal/utils.test.ts @@ -4,42 +4,42 @@ import { buildParams } from './utils'; describe('buildParams', () => { it.each` - search | useCurrentTimeRange | selectedTheme | panel | expected + search | useCurrentTimeRange | selectedTheme | panelId | expected ${''} | ${true} | ${'current'} | ${undefined} | ${'from=1000&to=2000&orgId=2'} - ${''} | ${true} | ${'current'} | ${{ id: 3 }} | ${'from=1000&to=2000&orgId=2&viewPanel=3'} + ${''} | ${true} | ${'current'} | ${'3'} | ${'from=1000&to=2000&orgId=2&viewPanel=3'} ${''} | ${true} | ${'light'} | ${undefined} | ${'from=1000&to=2000&orgId=2&theme=light'} - ${''} | ${true} | ${'light'} | ${{ id: 3 }} | ${'from=1000&to=2000&orgId=2&theme=light&viewPanel=3'} + ${''} | ${true} | ${'light'} | ${'3'} | ${'from=1000&to=2000&orgId=2&theme=light&viewPanel=3'} ${''} | ${false} | ${'current'} | ${undefined} | ${'orgId=2'} - ${''} | ${false} | ${'current'} | ${{ id: 3 }} | ${'orgId=2&viewPanel=3'} + ${''} | ${false} | ${'current'} | ${'3'} | ${'orgId=2&viewPanel=3'} ${''} | ${false} | ${'light'} | ${undefined} | ${'orgId=2&theme=light'} - ${''} | ${false} | ${'light'} | ${{ id: 3 }} | ${'orgId=2&theme=light&viewPanel=3'} + ${''} | ${false} | ${'light'} | ${'3'} | ${'orgId=2&theme=light&viewPanel=3'} ${'editPanel=4'} | ${true} | ${'current'} | ${undefined} | ${'editPanel=4&from=1000&to=2000&orgId=2'} - ${'editPanel=4'} | ${true} | ${'current'} | ${{ id: 3 }} | ${'editPanel=4&from=1000&to=2000&orgId=2'} + ${'editPanel=4'} | ${true} | ${'current'} | ${'3'} | ${'editPanel=4&from=1000&to=2000&orgId=2'} ${'editPanel=4'} | ${true} | ${'light'} | ${undefined} | ${'editPanel=4&from=1000&to=2000&orgId=2&theme=light'} - ${'editPanel=4'} | ${true} | ${'light'} | ${{ id: 3 }} | ${'editPanel=4&from=1000&to=2000&orgId=2&theme=light'} + ${'editPanel=4'} | ${true} | ${'light'} | ${'3'} | ${'editPanel=4&from=1000&to=2000&orgId=2&theme=light'} ${'editPanel=4'} | ${false} | ${'current'} | ${undefined} | ${'editPanel=4&orgId=2'} - ${'editPanel=4'} | ${false} | ${'current'} | ${{ id: 3 }} | ${'editPanel=4&orgId=2'} + ${'editPanel=4'} | ${false} | ${'current'} | ${'3'} | ${'editPanel=4&orgId=2'} ${'editPanel=4'} | ${false} | ${'light'} | ${undefined} | ${'editPanel=4&orgId=2&theme=light'} - ${'editPanel=4'} | ${false} | ${'light'} | ${{ id: 3 }} | ${'editPanel=4&orgId=2&theme=light'} + ${'editPanel=4'} | ${false} | ${'light'} | ${'3'} | ${'editPanel=4&orgId=2&theme=light'} ${'var=%2B1&var=a+value+with+spaces&var=true'} | ${true} | ${'current'} | ${undefined} | ${'var=%2B1&var=a+value+with+spaces&var=true&from=1000&to=2000&orgId=2'} - ${'var=%2B1&var=a+value+with+spaces&var=true'} | ${true} | ${'current'} | ${{ id: 3 }} | ${'var=%2B1&var=a+value+with+spaces&var=true&from=1000&to=2000&orgId=2&viewPanel=3'} + ${'var=%2B1&var=a+value+with+spaces&var=true'} | ${true} | ${'current'} | ${'3'} | ${'var=%2B1&var=a+value+with+spaces&var=true&from=1000&to=2000&orgId=2&viewPanel=3'} ${'var=%2B1&var=a+value+with+spaces&var=true'} | ${true} | ${'light'} | ${undefined} | ${'var=%2B1&var=a+value+with+spaces&var=true&from=1000&to=2000&orgId=2&theme=light'} - ${'var=%2B1&var=a+value+with+spaces&var=true'} | ${true} | ${'light'} | ${{ id: 3 }} | ${'var=%2B1&var=a+value+with+spaces&var=true&from=1000&to=2000&orgId=2&theme=light&viewPanel=3'} + ${'var=%2B1&var=a+value+with+spaces&var=true'} | ${true} | ${'light'} | ${'3'} | ${'var=%2B1&var=a+value+with+spaces&var=true&from=1000&to=2000&orgId=2&theme=light&viewPanel=3'} ${'var=%2B1&var=a+value+with+spaces&var=true'} | ${false} | ${'current'} | ${undefined} | ${'var=%2B1&var=a+value+with+spaces&var=true&orgId=2'} - ${'var=%2B1&var=a+value+with+spaces&var=true'} | ${false} | ${'current'} | ${{ id: 3 }} | ${'var=%2B1&var=a+value+with+spaces&var=true&orgId=2&viewPanel=3'} + ${'var=%2B1&var=a+value+with+spaces&var=true'} | ${false} | ${'current'} | ${'3'} | ${'var=%2B1&var=a+value+with+spaces&var=true&orgId=2&viewPanel=3'} ${'var=%2B1&var=a+value+with+spaces&var=true'} | ${false} | ${'light'} | ${undefined} | ${'var=%2B1&var=a+value+with+spaces&var=true&orgId=2&theme=light'} - ${'var=%2B1&var=a+value+with+spaces&var=true'} | ${false} | ${'light'} | ${{ id: 3 }} | ${'var=%2B1&var=a+value+with+spaces&var=true&orgId=2&theme=light&viewPanel=3'} + ${'var=%2B1&var=a+value+with+spaces&var=true'} | ${false} | ${'light'} | ${'3'} | ${'var=%2B1&var=a+value+with+spaces&var=true&orgId=2&theme=light&viewPanel=3'} ${'auth_token=1234'} | ${true} | ${'current'} | ${undefined} | ${'from=1000&to=2000&orgId=2'} `( "when called with search: '$search' and useCurrentTimeRange: '$useCurrentTimeRange' and selectedTheme: '$selectedTheme' and panel: '$panel'then result should be '$expected'", - ({ search, useCurrentTimeRange, selectedTheme, panel, expected }) => { + ({ search, useCurrentTimeRange, selectedTheme, panelId, expected }) => { const range: TimeRange = { from: 1000, to: 2000, raw: { from: 'now-6h', to: 'now' }, } as unknown as TimeRange; const orgId = 2; - const result = buildParams({ useCurrentTimeRange, selectedTheme, panel, search, range, orgId }); + const result = buildParams({ useCurrentTimeRange, selectedTheme, panelId, search, range, orgId }); expect(result.toString()).toEqual(expected); } diff --git a/public/app/features/dashboard/components/ShareModal/utils.ts b/public/app/features/dashboard/components/ShareModal/utils.ts index 9693afc8e0f..5770afefafe 100644 --- a/public/app/features/dashboard/components/ShareModal/utils.ts +++ b/public/app/features/dashboard/components/ShareModal/utils.ts @@ -1,6 +1,5 @@ import { dateTime, locationUtil, TimeRange, urlUtil, rangeUtil } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { SceneObjectRef, VizPanel } from '@grafana/scenes'; import { createShortLink } from 'app/core/utils/shortLinks'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; @@ -9,7 +8,8 @@ import { PanelModel } from '../../state/PanelModel'; export interface BuildParamsArgs { useCurrentTimeRange: boolean; selectedTheme?: string; - panel?: { timeFrom?: string; id: number }; + panelId?: string; + timeFrom?: string; search?: string; range?: TimeRange; orgId?: number; @@ -18,17 +18,17 @@ export interface BuildParamsArgs { export function buildParams({ useCurrentTimeRange, selectedTheme, - panel, + panelId, + timeFrom, search = window.location.search, range = getTimeSrv().timeRange(), orgId = config.bootData.user.orgId, }: BuildParamsArgs): URLSearchParams { const searchParams = new URLSearchParams(search); - const relative = panel?.timeFrom; // Use panel's relative time if it's set - if (relative) { - const { from, to } = rangeUtil.describeTextRange(relative); + if (timeFrom) { + const { from, to } = rangeUtil.describeTextRange(timeFrom); searchParams.set('from', from); searchParams.set('to', to); } else { @@ -46,8 +46,8 @@ export function buildParams({ searchParams.set('theme', selectedTheme!); } - if (panel && !searchParams.has('editPanel')) { - searchParams.set('viewPanel', String(panel.id)); + if (panelId && !searchParams.has('editPanel')) { + searchParams.set('viewPanel', panelId); } // Token is unique to the authenticated identity and should not be shared with the URL, @@ -78,7 +78,12 @@ export async function buildShareUrl( shortenUrl?: boolean ) { const baseUrl = buildBaseUrl(); - const params = buildParams({ useCurrentTimeRange, selectedTheme, panel }); + const params = buildParams({ + useCurrentTimeRange, + selectedTheme, + panelId: panel?.id ? String(panel.id) : undefined, + timeFrom: panel?.timeFrom, + }); const shareUrl = urlUtil.appendQueryToUrl(baseUrl, params.toString()); if (shortenUrl) { return await createShortLink(shareUrl); @@ -90,11 +95,18 @@ export function buildSoloUrl( useCurrentTimeRange: boolean, dashboardUid: string, selectedTheme?: string, - panel?: { timeFrom?: string; id: number }, + panelId?: string, + timeFrom?: string, range?: TimeRange ) { const baseUrl = buildBaseUrl(); - const params = buildParams({ useCurrentTimeRange, selectedTheme, panel, range }); + const params = buildParams({ + useCurrentTimeRange, + selectedTheme, + panelId: panelId, + timeFrom, + range, + }); let soloUrl = baseUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/'); soloUrl = soloUrl.replace(config.appSubUrl + '/d/', config.appSubUrl + '/d-solo/'); @@ -105,8 +117,8 @@ export function buildSoloUrl( soloUrl = `${config.appUrl}d-solo/${dashboardUid}`; } - const panelId = params.get('editPanel') ?? params.get('viewPanel') ?? ''; - params.set('panelId', panelId); + const editOrViewPanel = params.get('editPanel') ?? params.get('viewPanel') ?? ''; + params.set('panelId', editOrViewPanel); params.delete('editPanel'); params.delete('viewPanel'); @@ -119,7 +131,13 @@ export function buildImageUrl( selectedTheme?: string, panel?: PanelModel ) { - let soloUrl = buildSoloUrl(useCurrentTimeRange, dashboardUid, selectedTheme, panel); + let soloUrl = buildSoloUrl( + useCurrentTimeRange, + dashboardUid, + selectedTheme, + panel?.id ? String(panel.id) : undefined, + panel?.timeFrom + ); let imageUrl = soloUrl.replace(config.appSubUrl + '/dashboard-solo/', config.appSubUrl + '/render/dashboard-solo/'); imageUrl = imageUrl.replace(config.appSubUrl + '/d-solo/', config.appSubUrl + '/render/d-solo/'); imageUrl += @@ -135,10 +153,11 @@ export function buildIframeHtml( useCurrentTimeRange: boolean, dashboardUid: string, selectedTheme?: string, - panel?: { timeFrom?: string; id: number }, + panelId?: string, + timeFrom?: string, range?: TimeRange ) { - let soloUrl = buildSoloUrl(useCurrentTimeRange, dashboardUid, selectedTheme, panel, range); + let soloUrl = buildSoloUrl(useCurrentTimeRange, dashboardUid, selectedTheme, panelId, timeFrom, range); return ``; } @@ -163,9 +182,7 @@ export function getLocalTimeZone() { return '&tz=' + encodeURIComponent(options.timeZone); } -export const getTrackingSource = ( - panel?: PanelModel | SceneObjectRef | { timeFrom?: string; id: number } -) => { +export const getTrackingSource = (panel?: Object | undefined) => { return panel ? 'panel' : 'dashboard'; };