diff --git a/packages/grafana-data/src/types/dataLink.ts b/packages/grafana-data/src/types/dataLink.ts index 16d4edbff43..815b67f0352 100644 --- a/packages/grafana-data/src/types/dataLink.ts +++ b/packages/grafana-data/src/types/dataLink.ts @@ -102,6 +102,14 @@ export interface LinkModel { // When a click callback exists, this is passed the raw mouse|react event onClick?: (e: any, origin?: any) => void; oneClick?: boolean; + + /** + * @alpha + */ + interpolatedParams?: { + query?: DataQuery; + timeRange?: TimeRange; + }; } /** diff --git a/packages/grafana-data/src/utils/dataLinks.test.ts b/packages/grafana-data/src/utils/dataLinks.test.ts index 945e88aa1e0..86e01f4139c 100644 --- a/packages/grafana-data/src/utils/dataLinks.test.ts +++ b/packages/grafana-data/src/utils/dataLinks.test.ts @@ -48,6 +48,14 @@ describe('mapInternalLinkToExplore', () => { title: 'dsName', href: `/explore?left=${encodeURIComponent('{"datasource":"uid","queries":[{"query":"12344"}]}')}`, onClick: undefined, + interpolatedParams: { + query: { + query: '12344', + datasource: { + uid: 'uid', + }, + }, + }, }) ); }); @@ -130,6 +138,14 @@ describe('mapInternalLinkToExplore', () => { replaceVariables: (val, scopedVars) => val.replace(/\$var/g, scopedVars!['var1']!.value), }); + const query = { + query: 'val1 val1', + $var: 'foo', + nested: { something: 'val1' }, + num: 1, + arr: ['val1', 'non var'], + }; + expect(decodeURIComponent(link.href)).toEqual( `/explore?left=${JSON.stringify({ range: { @@ -137,16 +153,17 @@ describe('mapInternalLinkToExplore', () => { to: DATE_AS_MS, }, datasource: 'uid', - queries: [ - { - query: 'val1 val1', - $var: 'foo', - nested: { something: 'val1' }, - num: 1, - arr: ['val1', 'non var'], - }, - ], + queries: [query], })}` ); + + expect(link.interpolatedParams?.query).toEqual({ + datasource: { + uid: 'uid', + }, + ...query, + }); + + expect(link.interpolatedParams?.timeRange).toEqual(TIME_RANGE); }); }); diff --git a/packages/grafana-data/src/utils/dataLinks.ts b/packages/grafana-data/src/utils/dataLinks.ts index 51dac431148..ece36cf525a 100644 --- a/packages/grafana-data/src/utils/dataLinks.ts +++ b/packages/grafana-data/src/utils/dataLinks.ts @@ -47,6 +47,20 @@ export function mapInternalLinkToExplore(options: LinkToExploreOptions): LinkMod const interpolatedCorrelationData = interpolateObject(link.meta?.correlationData, scopedVars, replaceVariables); const title = link.title ? link.title : internalLink.datasourceName; + const interpolatedParams = interpolatedQuery + ? { + query: { + ...interpolatedQuery, + // data source is defined in a separate property in DataLink, we ensure it's put back together after interpolation + datasource: { + ...interpolatedQuery.datasource, + uid: internalLink.datasourceUid, + }, + }, + ...(range && { timeRange: range }), + } + : undefined; + return { title: replaceVariables(title, scopedVars), // In this case this is meant to be internal link (opens split view by default) the href will also points @@ -72,6 +86,7 @@ export function mapInternalLinkToExplore(options: LinkToExploreOptions): LinkMod : undefined, target: link?.targetBlank ? '_blank' : '_self', origin: field, + ...(interpolatedParams && { interpolatedParams }), }; } diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/ShareSpanButton.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/ShareSpanButton.tsx index 23b76bb4b97..cca6e4f3562 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/ShareSpanButton.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/ShareSpanButton.tsx @@ -8,12 +8,13 @@ type Props = { export function ShareSpanButton(props: Props) { const { focusSpanLink } = props; + const { interpolatedParams, ...linkProps } = focusSpanLink ?? {}; return ( {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} { // click handling logic copied from react router: // https://github.com/remix-run/react-router/blob/997b4d67e506d39ac6571cb369d6d2d6b3dda557/packages/react-router-dom/index.tsx#L392-L394s diff --git a/public/app/features/logs/components/panel/LogLineDetails.test.tsx b/public/app/features/logs/components/panel/LogLineDetails.test.tsx index a56166a2c9d..c3156bbf80c 100644 --- a/public/app/features/logs/components/panel/LogLineDetails.test.tsx +++ b/public/app/features/logs/components/panel/LogLineDetails.test.tsx @@ -688,7 +688,14 @@ describe('LogLineDetails', () => { if (field.config && field.config.links) { return field.config.links.map((link) => { return { - href: '/explore?left=%7B%22range%22%3A%7B%22from%22%3A%22now-15m%22%2C%22to%22%3A%22now%22%7D%2C%22datasource%22%3A%22fetpfiwe8asqoe%22%2C%22queries%22%3A%5B%7B%22query%22%3A%22abcd1234%22%2C%22queryType%22%3A%22traceql%22%7D%5D%7D', + href: '/explore', + interpolatedParams: { + query: { + refId: 'A', + query: 'abcd1234', + queryType: 'traceql', + }, + }, title: 'tempo', target: '_blank', origin: field, @@ -750,7 +757,14 @@ describe('LogLineDetails', () => { if (field.config && field.config.links) { return field.config.links.map((link) => { return { - href: '/explore?left=%7B%22range%22%3A%7B%22from%22%3A%22now-15m%22%2C%22to%22%3A%22now%22%7D%2C%22datasource%22%3A%22fetpfiwe8asqoe%22%2C%22queries%22%3A%5B%7B%22query%22%3A%22abcd1234%22%2C%22queryType%22%3A%22traceql%22%7D%5D%7D', + href: '/explore', + interpolatedParams: { + query: { + refId: 'A', + query: 'abcd1234', + queryType: 'traceql', + }, + }, title: 'tempo', target: '_blank', origin: field, diff --git a/public/app/features/logs/components/panel/links.ts b/public/app/features/logs/components/panel/links.ts index 8996d6d2125..a714f977f3c 100644 --- a/public/app/features/logs/components/panel/links.ts +++ b/public/app/features/logs/components/panel/links.ts @@ -18,11 +18,16 @@ export function getTempoTraceFromLinks(fields: FieldDef[]) { } function getTempoTraceFromLink(link: LinkModel) { - const queryData = getDataSourceAndQueryFromLink(link); - if (!queryData || queryData.queryType !== 'traceql') { - return null; + if (link.interpolatedParams?.query && isTempoQuery(link.interpolatedParams.query)) { + const query = link.interpolatedParams.query; + return { + dsUID: query.datasource?.uid || '', + query: query.query, + queryType: query.queryType || '', + }; + } else { + return undefined; } - return queryData; } export type EmbeddedInternalLink = { @@ -31,31 +36,14 @@ export type EmbeddedInternalLink = { queryType: string; }; -function getDataSourceAndQueryFromLink(link: LinkModel): EmbeddedInternalLink | null { - if (!link.href) { - return null; +type TempoQuery = { + query: string; + queryType: string; +}; + +const isTempoQuery = (query: unknown): query is TempoQuery => { + if (!query || typeof query !== 'object') { + return false; } - const paramsStrings = link.href.split('?')[1]; - if (!paramsStrings) { - return null; - } - const params = Object.values(Object.fromEntries(new URLSearchParams(paramsStrings))); - try { - const parsed = JSON.parse(params[0]); - const dsUID: string = 'datasource' in parsed && parsed.datasource ? parsed.datasource.toString() : ''; - const query: string = - 'queries' in parsed && Array.isArray(parsed.queries) && 'query' in parsed.queries[0] && parsed.queries[0].query - ? parsed.queries[0].query.toString() - : ''; - const queryType = - 'queryType' in parsed.queries[0] && parsed.queries[0].queryType ? parsed.queries[0].queryType.toString() : ''; - return dsUID && query && queryType - ? { - dsUID, - query, - queryType, - } - : null; - } catch (e) {} - return null; -} + return 'query' in query && 'queryType' in query; +};