Tempo / Trace Viewer: Implement deep linking to spans

This commit is contained in:
Erin Noe-Payne
2022-01-24 10:49:35 -05:00
committed by GitHub
parent fdeaf7a5c4
commit ac945fb6e1
24 changed files with 472 additions and 61 deletions

View File

@@ -644,7 +644,7 @@ describe('getLinksSupplier', () => {
expect(links[0]).toEqual(
expect.objectContaining({
title: 'testDS',
href: `/explore?left=${encodeURIComponent('{"datasource":"testDS","queries":["12345"]}')}`,
href: `/explore?left=${encodeURIComponent('{"datasource":"testDS","queries":["12345"],"panelsState":{}}')}`,
onClick: undefined,
})
);

View File

@@ -1,5 +1,6 @@
import { DataQuery } from './query';
import { InterpolateFunction } from './panel';
import { ExplorePanelsState } from './explore';
/**
* Callback info for DataLink click events
@@ -44,6 +45,7 @@ export interface InternalDataLink<T extends DataQuery = any> {
query: T;
datasourceUid: string;
datasourceName: string;
panelsState?: ExplorePanelsState;
}
export type LinkTarget = '_blank' | '_self' | undefined;

View File

@@ -1,3 +1,4 @@
import { PreferredVisualisationType } from './data';
import { DataQuery } from './query';
import { RawTimeRange, TimeRange } from './time';
@@ -10,11 +11,20 @@ export interface ExploreUrlState<T extends DataQuery = AnyQuery> {
range: RawTimeRange;
originPanelId?: number;
context?: string;
panelsState?: ExplorePanelsState;
}
export interface ExplorePanelsState extends Partial<Record<PreferredVisualisationType, {}>> {
trace?: ExploreTracePanelState;
}
export interface ExploreTracePanelState {
spanId?: string;
}
/**
* SplitOpen type is used in Explore and related components.
*/
export type SplitOpen = <T extends DataQuery = any>(
options?: { datasourceUid: string; query: T; range?: TimeRange } | undefined
options?: { datasourceUid: string; query: T; range?: TimeRange; panelsState?: ExplorePanelsState } | undefined
) => void;

View File

@@ -1,5 +1,5 @@
import { mapInternalLinkToExplore } from './dataLinks';
import { FieldType } from '../types';
import { DataLink, FieldType } from '../types';
import { ArrayVector } from '../vector';
describe('mapInternalLinkToExplore', () => {
@@ -31,7 +31,52 @@ describe('mapInternalLinkToExplore', () => {
expect(link).toEqual(
expect.objectContaining({
title: 'dsName',
href: `/explore?left=${encodeURIComponent('{"datasource":"dsName","queries":[{"query":"12344"}]}')}`,
href: `/explore?left=${encodeURIComponent(
'{"datasource":"dsName","queries":[{"query":"12344"}],"panelsState":{}}'
)}`,
onClick: undefined,
})
);
});
it('includes panels state', () => {
const panelsState = {
trace: {
spanId: 'abcdef',
},
};
const dataLink: DataLink = {
url: '',
title: '',
internal: {
datasourceUid: 'uid',
datasourceName: 'dsName',
query: { query: '12344' },
panelsState,
},
};
const link = mapInternalLinkToExplore({
link: dataLink,
internalLink: dataLink.internal!,
scopedVars: {},
range: {} as any,
field: {
name: 'test',
type: FieldType.number,
config: {},
values: new ArrayVector([2]),
},
replaceVariables: (val) => val,
});
expect(link).toEqual(
expect.objectContaining({
title: 'dsName',
href: `/explore?left=${encodeURIComponent(
'{"datasource":"dsName","queries":[{"query":"12344"}],"panelsState":{"trace":{"spanId":"abcdef"}}}'
)}`,
onClick: undefined,
})
);

View File

@@ -1,11 +1,13 @@
import {
DataLink,
DataQuery,
ExplorePanelsState,
Field,
InternalDataLink,
InterpolateFunction,
LinkModel,
ScopedVars,
SplitOpen,
TimeRange,
} from '../types';
import { locationUtil } from './location';
@@ -33,26 +35,28 @@ export type LinkToExploreOptions = {
range: TimeRange;
field: Field;
internalLink: InternalDataLink;
onClickFn?: (options: { datasourceUid: string; query: any; range?: TimeRange }) => void;
onClickFn?: SplitOpen;
replaceVariables: InterpolateFunction;
};
export function mapInternalLinkToExplore(options: LinkToExploreOptions): LinkModel<Field> {
const { onClickFn, replaceVariables, link, scopedVars, range, field, internalLink } = options;
const interpolatedQuery = interpolateQuery(link, scopedVars, replaceVariables);
const interpolatedQuery = interpolateObject(link.internal?.query, scopedVars, replaceVariables);
const interpolatedPanelsState = interpolateObject(link.internal?.panelsState, scopedVars, replaceVariables);
const title = link.title ? link.title : internalLink.datasourceName;
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
// to explore but this way you can open it in new tab.
href: generateInternalHref(internalLink.datasourceName, interpolatedQuery, range),
href: generateInternalHref(internalLink.datasourceName, interpolatedQuery, range, interpolatedPanelsState),
onClick: onClickFn
? () => {
onClickFn({
datasourceUid: internalLink.datasourceUid,
query: interpolatedQuery,
panelsState: interpolatedPanelsState,
range,
});
}
@@ -65,26 +69,32 @@ export function mapInternalLinkToExplore(options: LinkToExploreOptions): LinkMod
/**
* Generates href for internal derived field link.
*/
function generateInternalHref<T extends DataQuery = any>(datasourceName: string, query: T, range: TimeRange): string {
function generateInternalHref<T extends DataQuery = any>(
datasourceName: string,
query: T,
range: TimeRange,
panelsState?: ExplorePanelsState
): string {
return locationUtil.assureBaseUrl(
`/explore?left=${encodeURIComponent(
serializeStateToUrlParam({
range: range.raw,
datasource: datasourceName,
queries: [query],
panelsState: panelsState,
})
)}`
);
}
function interpolateQuery<T extends DataQuery = any>(
link: DataLink,
function interpolateObject<T extends object>(
object: T | undefined,
scopedVars: ScopedVars,
replaceVariables: InterpolateFunction
): T {
let stringifiedQuery = '';
try {
stringifiedQuery = JSON.stringify(link.internal?.query || '');
stringifiedQuery = JSON.stringify(object || {});
} catch (err) {
// should not happen and not much to do about this, possibly something non stringifiable in the query
console.error(err);

View File

@@ -195,9 +195,23 @@ export const urlUtil = {
parseKeyValue,
};
/**
* Create an string that is used in URL to represent the Explore state. This is basically just a stringified json
* that is that used as a state of a single Explore pane so it does not represent full Explore URL.
*
* There are 2 versions of this, normal and compact. Normal is just the same object stringified while compact turns
* properties of the object into array where the order is significant.
* @param urlState
* @param compact
*/
export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
if (compact) {
return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]);
const compactState: unknown[] = [urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries];
// only serialize panel state if we have at least one non-default panel configuration
if (urlState.panelsState !== undefined) {
compactState.push({ __panelsState: urlState.panelsState });
}
return JSON.stringify(compactState);
}
return JSON.stringify(urlState);
}