Data Links: Store interpolated query in link models (#110912)

* Data Links: Store interpolated query in link models

* Fix types

* Fix type guard

* Fix linting

* Fix tests

* Fix tests

* Fix tests

* Fix linting

* Simplify the code

* Linting

* Linting

* Revert redundant changes

* Update test

* Infer types

* Linting

* Fix null check

* Rename prop

* Rename prop

* Rename prop

* Drop interpolatedParams from span link

* Drop interpolatedParams from span link
This commit is contained in:
Piotr Jamróz
2025-09-17 16:36:53 +02:00
committed by GitHub
parent abcf939a0f
commit 911df89725
6 changed files with 86 additions and 43 deletions
@@ -102,6 +102,14 @@ export interface LinkModel<T = any> {
// 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;
};
}
/**
@@ -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);
});
});
@@ -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 }),
};
}
@@ -8,12 +8,13 @@ type Props = {
export function ShareSpanButton(props: Props) {
const { focusSpanLink } = props;
const { interpolatedParams, ...linkProps } = focusSpanLink ?? {};
return (
<span>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
data-testid="share-span-button"
{...focusSpanLink}
{...linkProps}
onClick={(e) => {
// click handling logic copied from react router:
// https://github.com/remix-run/react-router/blob/997b4d67e506d39ac6571cb369d6d2d6b3dda557/packages/react-router-dom/index.tsx#L392-L394s
@@ -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,
@@ -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;
};