Dashboard: Fixes url links to embedded panels in scene based dashboards (#109837)

* Dashboard: Fixes url links to embedded panels in scene based dashboards

* fix
This commit is contained in:
Torkel Ödegaard
2025-08-19 15:07:09 +02:00
committed by GitHub
parent 2a617abd77
commit 9ea3c3a9a2
9 changed files with 105 additions and 74 deletions
+4
View File
@@ -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.
@@ -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) {
@@ -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<D
updatedBy: metadata.annotations?.[AnnoKeyUpdatedBy],
folderUid: metadata.annotations?.[AnnoKeyFolder],
isSnapshot: Boolean(metadata.annotations?.[AnnoKeyDashboardIsSnapshot]),
isEmbedded: Boolean(metadata.annotations?.[AnnoKeyEmbedded]),
// UI-only metadata, ref: DashboardModel.initMeta
showSettings: Boolean(dto.access.canEdit),
@@ -8,7 +8,7 @@ import { buildParams, shareDashboardType } from 'app/features/dashboard/componen
import { DashboardScene } from '../scene/DashboardScene';
import { PanelTimeRange } from '../scene/PanelTimeRange';
import { getDashboardUrl } from '../utils/getDashboardUrl';
import { getDashboardSceneFor, getPanelIdForVizPanel } from '../utils/utils';
import { getDashboardSceneFor } from '../utils/utils';
import { SceneShareTabState } from './types';
@@ -37,17 +37,14 @@ function SharePanelEmbedTabRenderer({ model }: SceneComponentProps<SharePanelEmb
const dash = getDashboardSceneFor(model);
const { uid: dashUid } = dash.useState();
const id = getPanelIdForVizPanel(p);
const timeRangeState = sceneGraph.getTimeRange(p);
const timeFrom = timeRangeState instanceof PanelTimeRange ? timeRangeState.state.timeFrom : undefined;
return (
<ShareEmbed
panel={{
id,
timeFrom,
}}
panelId={p.state.key!}
timeFrom={timeFrom}
range={timeRangeState.state.value}
dashboard={{ uid: dashUid ?? '', time: timeRangeState.state.value }}
buildIframe={getIframeBuilder(dash)}
@@ -62,12 +59,13 @@ const getIframeBuilder =
useCurrentTimeRange: boolean,
_dashboardUid: string,
selectedTheme?: string,
panel?: { timeFrom?: string; id: number },
panelId?: string,
timeFrom?: string,
range?: TimeRange
) => {
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');
@@ -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(<ShareEmbed dashboard={mockDashboard} panel={mockPanel} />);
render(<ShareEmbed dashboard={mockDashboard} panelId={'mockPanelId'} />);
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(<ShareEmbed dashboard={mockDashboard} panel={mockPanel} />);
render(<ShareEmbed dashboard={mockDashboard} panelId={'mockPanelId'} />);
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(<ShareEmbed dashboard={mockDashboard} panel={mockPanel} />);
render(<ShareEmbed dashboard={mockDashboard} panelId={'mockPanelId'} />);
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(<ShareEmbed dashboard={mockDashboard} panel={mockPanel} />);
render(<ShareEmbed dashboard={mockDashboard} panelId={'mockPanelId'} />);
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`
);
});
});
@@ -11,22 +11,30 @@ import { ShareModalTabProps } from './types';
import { buildIframeHtml, getTrackingSource } from './utils';
interface Props extends Omit<ShareModalTabProps, 'panel' | 'dashboard'> {
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<HTMLTextAreaElement>) => {
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),
});
}}
>
@@ -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 (
<ShareEmbed
dashboard={props.dashboard}
panelId={String(props.panel?.id)}
timeFrom={props.panel?.timeFrom}
onDismiss={props.onDismiss}
/>
);
}
interface Props extends Themeable2 {
dashboard: DashboardModel;
panel?: PanelModel;
@@ -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);
}
@@ -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 `<iframe src="${soloUrl}" width="450" height="200" frameborder="0"></iframe>`;
}
@@ -163,9 +182,7 @@ export function getLocalTimeZone() {
return '&tz=' + encodeURIComponent(options.timeZone);
}
export const getTrackingSource = (
panel?: PanelModel | SceneObjectRef<VizPanel> | { timeFrom?: string; id: number }
) => {
export const getTrackingSource = (panel?: Object | undefined) => {
return panel ? 'panel' : 'dashboard';
};