mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Tempo / Trace Viewer: Implement deep linking to spans
This commit is contained in:
@@ -644,7 +644,7 @@ describe('getLinksSupplier', () => {
|
|||||||
expect(links[0]).toEqual(
|
expect(links[0]).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
title: 'testDS',
|
title: 'testDS',
|
||||||
href: `/explore?left=${encodeURIComponent('{"datasource":"testDS","queries":["12345"]}')}`,
|
href: `/explore?left=${encodeURIComponent('{"datasource":"testDS","queries":["12345"],"panelsState":{}}')}`,
|
||||||
onClick: undefined,
|
onClick: undefined,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { DataQuery } from './query';
|
import { DataQuery } from './query';
|
||||||
import { InterpolateFunction } from './panel';
|
import { InterpolateFunction } from './panel';
|
||||||
|
import { ExplorePanelsState } from './explore';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback info for DataLink click events
|
* Callback info for DataLink click events
|
||||||
@@ -44,6 +45,7 @@ export interface InternalDataLink<T extends DataQuery = any> {
|
|||||||
query: T;
|
query: T;
|
||||||
datasourceUid: string;
|
datasourceUid: string;
|
||||||
datasourceName: string;
|
datasourceName: string;
|
||||||
|
panelsState?: ExplorePanelsState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LinkTarget = '_blank' | '_self' | undefined;
|
export type LinkTarget = '_blank' | '_self' | undefined;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { PreferredVisualisationType } from './data';
|
||||||
import { DataQuery } from './query';
|
import { DataQuery } from './query';
|
||||||
import { RawTimeRange, TimeRange } from './time';
|
import { RawTimeRange, TimeRange } from './time';
|
||||||
|
|
||||||
@@ -10,11 +11,20 @@ export interface ExploreUrlState<T extends DataQuery = AnyQuery> {
|
|||||||
range: RawTimeRange;
|
range: RawTimeRange;
|
||||||
originPanelId?: number;
|
originPanelId?: number;
|
||||||
context?: string;
|
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.
|
* SplitOpen type is used in Explore and related components.
|
||||||
*/
|
*/
|
||||||
export type SplitOpen = <T extends DataQuery = any>(
|
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;
|
) => void;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { mapInternalLinkToExplore } from './dataLinks';
|
import { mapInternalLinkToExplore } from './dataLinks';
|
||||||
import { FieldType } from '../types';
|
import { DataLink, FieldType } from '../types';
|
||||||
import { ArrayVector } from '../vector';
|
import { ArrayVector } from '../vector';
|
||||||
|
|
||||||
describe('mapInternalLinkToExplore', () => {
|
describe('mapInternalLinkToExplore', () => {
|
||||||
@@ -31,7 +31,52 @@ describe('mapInternalLinkToExplore', () => {
|
|||||||
expect(link).toEqual(
|
expect(link).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
title: 'dsName',
|
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,
|
onClick: undefined,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
DataLink,
|
DataLink,
|
||||||
DataQuery,
|
DataQuery,
|
||||||
|
ExplorePanelsState,
|
||||||
Field,
|
Field,
|
||||||
InternalDataLink,
|
InternalDataLink,
|
||||||
InterpolateFunction,
|
InterpolateFunction,
|
||||||
LinkModel,
|
LinkModel,
|
||||||
ScopedVars,
|
ScopedVars,
|
||||||
|
SplitOpen,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { locationUtil } from './location';
|
import { locationUtil } from './location';
|
||||||
@@ -33,26 +35,28 @@ export type LinkToExploreOptions = {
|
|||||||
range: TimeRange;
|
range: TimeRange;
|
||||||
field: Field;
|
field: Field;
|
||||||
internalLink: InternalDataLink;
|
internalLink: InternalDataLink;
|
||||||
onClickFn?: (options: { datasourceUid: string; query: any; range?: TimeRange }) => void;
|
onClickFn?: SplitOpen;
|
||||||
replaceVariables: InterpolateFunction;
|
replaceVariables: InterpolateFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function mapInternalLinkToExplore(options: LinkToExploreOptions): LinkModel<Field> {
|
export function mapInternalLinkToExplore(options: LinkToExploreOptions): LinkModel<Field> {
|
||||||
const { onClickFn, replaceVariables, link, scopedVars, range, field, internalLink } = options;
|
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;
|
const title = link.title ? link.title : internalLink.datasourceName;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: replaceVariables(title, scopedVars),
|
title: replaceVariables(title, scopedVars),
|
||||||
// In this case this is meant to be internal link (opens split view by default) the href will also points
|
// 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.
|
// 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
|
onClick: onClickFn
|
||||||
? () => {
|
? () => {
|
||||||
onClickFn({
|
onClickFn({
|
||||||
datasourceUid: internalLink.datasourceUid,
|
datasourceUid: internalLink.datasourceUid,
|
||||||
query: interpolatedQuery,
|
query: interpolatedQuery,
|
||||||
|
panelsState: interpolatedPanelsState,
|
||||||
range,
|
range,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -65,26 +69,32 @@ export function mapInternalLinkToExplore(options: LinkToExploreOptions): LinkMod
|
|||||||
/**
|
/**
|
||||||
* Generates href for internal derived field link.
|
* 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(
|
return locationUtil.assureBaseUrl(
|
||||||
`/explore?left=${encodeURIComponent(
|
`/explore?left=${encodeURIComponent(
|
||||||
serializeStateToUrlParam({
|
serializeStateToUrlParam({
|
||||||
range: range.raw,
|
range: range.raw,
|
||||||
datasource: datasourceName,
|
datasource: datasourceName,
|
||||||
queries: [query],
|
queries: [query],
|
||||||
|
panelsState: panelsState,
|
||||||
})
|
})
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function interpolateQuery<T extends DataQuery = any>(
|
function interpolateObject<T extends object>(
|
||||||
link: DataLink,
|
object: T | undefined,
|
||||||
scopedVars: ScopedVars,
|
scopedVars: ScopedVars,
|
||||||
replaceVariables: InterpolateFunction
|
replaceVariables: InterpolateFunction
|
||||||
): T {
|
): T {
|
||||||
let stringifiedQuery = '';
|
let stringifiedQuery = '';
|
||||||
try {
|
try {
|
||||||
stringifiedQuery = JSON.stringify(link.internal?.query || '');
|
stringifiedQuery = JSON.stringify(object || {});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// should not happen and not much to do about this, possibly something non stringifiable in the query
|
// should not happen and not much to do about this, possibly something non stringifiable in the query
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
@@ -195,9 +195,23 @@ export const urlUtil = {
|
|||||||
parseKeyValue,
|
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 {
|
export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
|
||||||
if (compact) {
|
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);
|
return JSON.stringify(urlState);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -246,6 +246,20 @@ export default class ListView extends React.Component<TListViewProps> {
|
|||||||
getRowPosition = (index: number): { height: number; y: number } =>
|
getRowPosition = (index: number): { height: number; y: number } =>
|
||||||
this._yPositions.getRowPosition(index, this._getHeight);
|
this._yPositions.getRowPosition(index, this._getHeight);
|
||||||
|
|
||||||
|
scrollToIndex = (index: number) => {
|
||||||
|
// calculate the position of the list view relative to the scroll parent
|
||||||
|
const { scrollElement } = this.props;
|
||||||
|
const scrollElementTop = scrollElement?.getBoundingClientRect().top || 0;
|
||||||
|
const listViewTop = (scrollElement?.scrollTop || 0) + (this._itemHolderElm?.getBoundingClientRect().top || 0);
|
||||||
|
const listViewOffset = listViewTop - scrollElementTop;
|
||||||
|
|
||||||
|
const itemOffset = this.getRowPosition(index).y;
|
||||||
|
|
||||||
|
// hard code a small offset to leave a little bit of space above the focused span, so it is visually clear
|
||||||
|
// that there is content above
|
||||||
|
this.props.scrollElement?.scrollTo({ top: itemOffset + listViewOffset - 80 });
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scroll event listener that schedules a remeasuring of which items should be
|
* Scroll event listener that schedules a remeasuring of which items should be
|
||||||
* rendered.
|
* rendered.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import IoAlert from 'react-icons/lib/io/alert';
|
|||||||
import IoArrowRightA from 'react-icons/lib/io/arrow-right-a';
|
import IoArrowRightA from 'react-icons/lib/io/arrow-right-a';
|
||||||
import IoNetwork from 'react-icons/lib/io/network';
|
import IoNetwork from 'react-icons/lib/io/network';
|
||||||
import MdFileUpload from 'react-icons/lib/md/file-upload';
|
import MdFileUpload from 'react-icons/lib/md/file-upload';
|
||||||
import { css } from '@emotion/css';
|
import { css, keyframes } from '@emotion/css';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { stylesFactory, withTheme2 } from '@grafana/ui';
|
import { stylesFactory, withTheme2 } from '@grafana/ui';
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
@@ -41,6 +41,18 @@ const viewClassName = 'jaegerView';
|
|||||||
const nameColumnClassName = 'nameColumn';
|
const nameColumnClassName = 'nameColumn';
|
||||||
|
|
||||||
const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||||
|
const animations = {
|
||||||
|
flash: keyframes`
|
||||||
|
label: flash;
|
||||||
|
from {
|
||||||
|
background-color: ${autoColor(theme, '#68b9ff')};
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
background-color: default;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nameWrapper: css`
|
nameWrapper: css`
|
||||||
label: nameWrapper;
|
label: nameWrapper;
|
||||||
@@ -168,6 +180,25 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
|||||||
outline: 1px solid ${autoColor(theme, '#ddd')};
|
outline: 1px solid ${autoColor(theme, '#ddd')};
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
rowFocused: css`
|
||||||
|
label: rowFocused;
|
||||||
|
background-color: ${autoColor(theme, '#cbe7ff')};
|
||||||
|
animation: ${animations.flash} 1s cubic-bezier(0.12, 0, 0.39, 0);
|
||||||
|
& .${nameWrapperClassName}, .${viewClassName}, .${nameWrapperMatchingFilterClassName} {
|
||||||
|
background-color: ${autoColor(theme, '#cbe7ff')};
|
||||||
|
animation: ${animations.flash} 1s cubic-bezier(0.12, 0, 0.39, 0);
|
||||||
|
}
|
||||||
|
& .${spanBarClassName} {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
& .${spanBarLabelClassName} {
|
||||||
|
color: ${autoColor(theme, '#000')};
|
||||||
|
}
|
||||||
|
&:hover .${nameWrapperClassName}, :hover .${viewClassName} {
|
||||||
|
background: ${autoColor(theme, '#d5ebff')};
|
||||||
|
box-shadow: 0 1px 0 ${autoColor(theme, '#ddd')};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
|
||||||
rowExpandedAndMatchingFilter: css`
|
rowExpandedAndMatchingFilter: css`
|
||||||
label: rowExpandedAndMatchingFilter;
|
label: rowExpandedAndMatchingFilter;
|
||||||
@@ -268,6 +299,7 @@ type SpanBarRowProps = {
|
|||||||
isChildrenExpanded: boolean;
|
isChildrenExpanded: boolean;
|
||||||
isDetailExpanded: boolean;
|
isDetailExpanded: boolean;
|
||||||
isMatchingFilter: boolean;
|
isMatchingFilter: boolean;
|
||||||
|
isFocused: boolean;
|
||||||
onDetailToggled: (spanID: string) => void;
|
onDetailToggled: (spanID: string) => void;
|
||||||
onChildrenToggled: (spanID: string) => void;
|
onChildrenToggled: (spanID: string) => void;
|
||||||
numTicks: number;
|
numTicks: number;
|
||||||
@@ -330,6 +362,7 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
|
|||||||
isChildrenExpanded,
|
isChildrenExpanded,
|
||||||
isDetailExpanded,
|
isDetailExpanded,
|
||||||
isMatchingFilter,
|
isMatchingFilter,
|
||||||
|
isFocused,
|
||||||
numTicks,
|
numTicks,
|
||||||
rpc,
|
rpc,
|
||||||
noInstrumentedServer,
|
noInstrumentedServer,
|
||||||
@@ -377,6 +410,7 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
|
|||||||
[styles.rowExpanded]: isDetailExpanded,
|
[styles.rowExpanded]: isDetailExpanded,
|
||||||
[styles.rowMatchingFilter]: isMatchingFilter,
|
[styles.rowMatchingFilter]: isMatchingFilter,
|
||||||
[styles.rowExpandedAndMatchingFilter]: isMatchingFilter && isDetailExpanded,
|
[styles.rowExpandedAndMatchingFilter]: isMatchingFilter && isDetailExpanded,
|
||||||
|
[styles.rowFocused]: isFocused,
|
||||||
[styles.rowClippingLeft]: clippingLeft,
|
[styles.rowClippingLeft]: clippingLeft,
|
||||||
[styles.rowClippingRight]: clippingRight,
|
[styles.rowClippingRight]: clippingRight,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import AccordianLogs from './AccordianLogs';
|
|||||||
import DetailState from './DetailState';
|
import DetailState from './DetailState';
|
||||||
import SpanDetail from './index';
|
import SpanDetail from './index';
|
||||||
import { formatDuration } from '../utils';
|
import { formatDuration } from '../utils';
|
||||||
import CopyIcon from '../../common/CopyIcon';
|
|
||||||
import LabeledList from '../../common/LabeledList';
|
import LabeledList from '../../common/LabeledList';
|
||||||
import traceGenerator from '../../demo/trace-generators';
|
import traceGenerator from '../../demo/trace-generators';
|
||||||
import transformTraceData from '../../model/transform-trace-data';
|
import transformTraceData from '../../model/transform-trace-data';
|
||||||
@@ -44,6 +43,7 @@ describe('<SpanDetail>', () => {
|
|||||||
tagsToggle: jest.fn(),
|
tagsToggle: jest.fn(),
|
||||||
warningsToggle: jest.fn(),
|
warningsToggle: jest.fn(),
|
||||||
referencesToggle: jest.fn(),
|
referencesToggle: jest.fn(),
|
||||||
|
createFocusSpanLink: jest.fn(),
|
||||||
};
|
};
|
||||||
span.logs = [
|
span.logs = [
|
||||||
{
|
{
|
||||||
@@ -180,7 +180,7 @@ describe('<SpanDetail>', () => {
|
|||||||
expect(props.referencesToggle).toHaveBeenLastCalledWith(span.spanID);
|
expect(props.referencesToggle).toHaveBeenLastCalledWith(span.spanID);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders CopyIcon with deep link URL', () => {
|
it('renders deep link URL', () => {
|
||||||
expect(wrapper.find(CopyIcon).prop('copyText').includes(`?uiFind=${props.span.spanID}`)).toBe(true);
|
expect(wrapper.find('a').exists()).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,21 +16,30 @@ import React from 'react';
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { DataLinkButton, TextArea, useStyles2 } from '@grafana/ui';
|
import { DataLinkButton, TextArea, useStyles2 } from '@grafana/ui';
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2, LinkModel } from '@grafana/data';
|
||||||
|
import IoLink from 'react-icons/lib/io/link';
|
||||||
|
|
||||||
import AccordianKeyValues from './AccordianKeyValues';
|
import AccordianKeyValues from './AccordianKeyValues';
|
||||||
import AccordianLogs from './AccordianLogs';
|
import AccordianLogs from './AccordianLogs';
|
||||||
import AccordianText from './AccordianText';
|
import AccordianText from './AccordianText';
|
||||||
import DetailState from './DetailState';
|
import DetailState from './DetailState';
|
||||||
import { formatDuration } from '../utils';
|
import { formatDuration } from '../utils';
|
||||||
import CopyIcon from '../../common/CopyIcon';
|
|
||||||
import LabeledList from '../../common/LabeledList';
|
import LabeledList from '../../common/LabeledList';
|
||||||
import { SpanLinkFunc, TNil } from '../../types';
|
import { SpanLinkFunc, TNil } from '../../types';
|
||||||
import { TraceKeyValuePair, TraceLink, TraceLog, TraceSpan } from '../../types/trace';
|
import { TraceKeyValuePair, TraceLink, TraceLog, TraceSpan } from '../../types/trace';
|
||||||
import AccordianReferences from './AccordianReferences';
|
import AccordianReferences from './AccordianReferences';
|
||||||
import { autoColor } from '../../Theme';
|
import { autoColor } from '../../Theme';
|
||||||
import { UIDivider } from '../../uiElementsContext';
|
import { UIDivider } from '../../uiElementsContext';
|
||||||
import { ubFlex, ubFlexAuto, ubItemsCenter, ubM0, ubMb1, ubMy1, ubTxRightAlign } from '../../uberUtilityStyles';
|
import {
|
||||||
|
uAlignIcon,
|
||||||
|
ubFlex,
|
||||||
|
ubFlexAuto,
|
||||||
|
ubItemsCenter,
|
||||||
|
ubM0,
|
||||||
|
ubMb1,
|
||||||
|
ubMy1,
|
||||||
|
ubTxRightAlign,
|
||||||
|
} from '../../uberUtilityStyles';
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
return {
|
return {
|
||||||
@@ -99,6 +108,9 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
`,
|
`,
|
||||||
|
LinkIcon: css`
|
||||||
|
font-size: 1.5em;
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -116,6 +128,8 @@ type SpanDetailProps = {
|
|||||||
referencesToggle: (spanID: string) => void;
|
referencesToggle: (spanID: string) => void;
|
||||||
focusSpan: (uiFind: string) => void;
|
focusSpan: (uiFind: string) => void;
|
||||||
createSpanLink?: SpanLinkFunc;
|
createSpanLink?: SpanLinkFunc;
|
||||||
|
focusedSpanId?: string;
|
||||||
|
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SpanDetail(props: SpanDetailProps) {
|
export default function SpanDetail(props: SpanDetailProps) {
|
||||||
@@ -133,6 +147,7 @@ export default function SpanDetail(props: SpanDetailProps) {
|
|||||||
referencesToggle,
|
referencesToggle,
|
||||||
focusSpan,
|
focusSpan,
|
||||||
createSpanLink,
|
createSpanLink,
|
||||||
|
createFocusSpanLink,
|
||||||
} = props;
|
} = props;
|
||||||
const {
|
const {
|
||||||
isTagsOpen,
|
isTagsOpen,
|
||||||
@@ -147,6 +162,7 @@ export default function SpanDetail(props: SpanDetailProps) {
|
|||||||
process,
|
process,
|
||||||
duration,
|
duration,
|
||||||
relativeStartTime,
|
relativeStartTime,
|
||||||
|
traceID,
|
||||||
spanID,
|
spanID,
|
||||||
logs,
|
logs,
|
||||||
tags,
|
tags,
|
||||||
@@ -171,9 +187,9 @@ export default function SpanDetail(props: SpanDetailProps) {
|
|||||||
value: formatDuration(relativeStartTime),
|
value: formatDuration(relativeStartTime),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const deepLinkCopyText = `${window.location.origin}${window.location.pathname}?uiFind=${spanID}`;
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const link = createSpanLink?.(span);
|
const link = createSpanLink?.(span);
|
||||||
|
const focusSpanLink = createFocusSpanLink(traceID, spanID);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -263,13 +279,25 @@ export default function SpanDetail(props: SpanDetailProps) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<small className={styles.debugInfo}>
|
<small className={styles.debugInfo}>
|
||||||
|
<a
|
||||||
|
{...focusSpanLink}
|
||||||
|
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
|
||||||
|
if (
|
||||||
|
focusSpanLink.onClick &&
|
||||||
|
e.button === 0 && // Ignore everything but left clicks
|
||||||
|
(!e.currentTarget.target || e.currentTarget.target === '_self') && // Let browser handle "target=_blank" etc.
|
||||||
|
!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) // Ignore clicks with modifier keys
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
focusSpanLink.onClick(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IoLink className={cx(uAlignIcon, styles.LinkIcon)}></IoLink>
|
||||||
|
</a>
|
||||||
<span className={styles.debugLabel} data-label="SpanID:" /> {spanID}
|
<span className={styles.debugLabel} data-label="SpanID:" /> {spanID}
|
||||||
<CopyIcon
|
|
||||||
copyText={deepLinkCopyText}
|
|
||||||
icon="link"
|
|
||||||
placement="topRight"
|
|
||||||
tooltipTitle="Copy deep link to this span"
|
|
||||||
/>
|
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import SpanTreeOffset from './SpanTreeOffset';
|
|||||||
import TimelineRow from './TimelineRow';
|
import TimelineRow from './TimelineRow';
|
||||||
import { autoColor } from '../Theme';
|
import { autoColor } from '../Theme';
|
||||||
import { stylesFactory, withTheme2 } from '@grafana/ui';
|
import { stylesFactory, withTheme2 } from '@grafana/ui';
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2, LinkModel } from '@grafana/data';
|
||||||
|
|
||||||
import { TraceLog, TraceSpan, TraceKeyValuePair, TraceLink } from '../types/trace';
|
import { TraceLog, TraceSpan, TraceKeyValuePair, TraceLink } from '../types/trace';
|
||||||
import { SpanLinkFunc } from '../types';
|
import { SpanLinkFunc } from '../types';
|
||||||
@@ -89,6 +89,8 @@ type SpanDetailRowProps = {
|
|||||||
removeHoverIndentGuideId: (spanID: string) => void;
|
removeHoverIndentGuideId: (spanID: string) => void;
|
||||||
theme: GrafanaTheme2;
|
theme: GrafanaTheme2;
|
||||||
createSpanLink?: SpanLinkFunc;
|
createSpanLink?: SpanLinkFunc;
|
||||||
|
focusedSpanId?: string;
|
||||||
|
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProps> {
|
export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProps> {
|
||||||
@@ -121,6 +123,8 @@ export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProp
|
|||||||
removeHoverIndentGuideId,
|
removeHoverIndentGuideId,
|
||||||
theme,
|
theme,
|
||||||
createSpanLink,
|
createSpanLink,
|
||||||
|
focusedSpanId,
|
||||||
|
createFocusSpanLink,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
return (
|
return (
|
||||||
@@ -160,6 +164,8 @@ export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProp
|
|||||||
traceStartTime={traceStartTime}
|
traceStartTime={traceStartTime}
|
||||||
focusSpan={focusSpan}
|
focusSpan={focusSpan}
|
||||||
createSpanLink={createSpanLink}
|
createSpanLink={createSpanLink}
|
||||||
|
focusedSpanId={focusedSpanId}
|
||||||
|
createFocusSpanLink={createFocusSpanLink}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TimelineRow.Cell>
|
</TimelineRow.Cell>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { css } from '@emotion/css';
|
|||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import memoizeOne from 'memoize-one';
|
import memoizeOne from 'memoize-one';
|
||||||
import { stylesFactory, withTheme2 } from '@grafana/ui';
|
import { stylesFactory, withTheme2 } from '@grafana/ui';
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2, LinkModel } from '@grafana/data';
|
||||||
|
|
||||||
import ListView from './ListView';
|
import ListView from './ListView';
|
||||||
import SpanBarRow from './SpanBarRow';
|
import SpanBarRow from './SpanBarRow';
|
||||||
@@ -86,6 +86,8 @@ type TVirtualizedTraceViewOwnProps = {
|
|||||||
theme: GrafanaTheme2;
|
theme: GrafanaTheme2;
|
||||||
createSpanLink?: SpanLinkFunc;
|
createSpanLink?: SpanLinkFunc;
|
||||||
scrollElement?: Element;
|
scrollElement?: Element;
|
||||||
|
focusedSpanId?: string;
|
||||||
|
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
|
||||||
};
|
};
|
||||||
|
|
||||||
type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & TExtractUiFindFromStateReturn & TTraceTimeline;
|
type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & TExtractUiFindFromStateReturn & TTraceTimeline;
|
||||||
@@ -172,6 +174,10 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
|
|||||||
setTrace(trace, uiFind);
|
setTrace(trace, uiFind);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.scrollToSpan(this.props.focusedSpanId);
|
||||||
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps: VirtualizedTraceViewProps) {
|
shouldComponentUpdate(nextProps: VirtualizedTraceViewProps) {
|
||||||
// If any prop updates, VirtualizedTraceViewImpl should update.
|
// If any prop updates, VirtualizedTraceViewImpl should update.
|
||||||
const nextPropKeys = Object.keys(nextProps) as Array<keyof VirtualizedTraceViewProps>;
|
const nextPropKeys = Object.keys(nextProps) as Array<keyof VirtualizedTraceViewProps>;
|
||||||
@@ -200,6 +206,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
|
|||||||
setTrace,
|
setTrace,
|
||||||
trace: nextTrace,
|
trace: nextTrace,
|
||||||
uiFind,
|
uiFind,
|
||||||
|
focusedSpanId,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (trace !== nextTrace) {
|
if (trace !== nextTrace) {
|
||||||
@@ -214,6 +221,10 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
|
|||||||
scrollToFirstVisibleSpan();
|
scrollToFirstVisibleSpan();
|
||||||
clearShouldScrollToFirstUiFindMatch();
|
clearShouldScrollToFirstUiFindMatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (focusedSpanId !== prevProps.focusedSpanId) {
|
||||||
|
this.scrollToSpan(focusedSpanId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getRowStates(): RowState[] {
|
getRowStates(): RowState[] {
|
||||||
@@ -323,6 +334,16 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
|
|||||||
: this.renderSpanBarRow(span, spanIndex, key, style, attrs);
|
: this.renderSpanBarRow(span, spanIndex, key, style, attrs);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
scrollToSpan = (spanID?: string) => {
|
||||||
|
if (spanID == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const i = this.getRowStates().findIndex((row) => row.span.spanID === spanID);
|
||||||
|
if (i >= 0) {
|
||||||
|
this.listView?.scrollToIndex(i);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
renderSpanBarRow(span: TraceSpan, spanIndex: number, key: string, style: React.CSSProperties, attrs: {}) {
|
renderSpanBarRow(span: TraceSpan, spanIndex: number, key: string, style: React.CSSProperties, attrs: {}) {
|
||||||
const { spanID } = span;
|
const { spanID } = span;
|
||||||
const { serviceName } = span.process;
|
const { serviceName } = span.process;
|
||||||
@@ -340,6 +361,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
|
|||||||
removeHoverIndentGuideId,
|
removeHoverIndentGuideId,
|
||||||
theme,
|
theme,
|
||||||
createSpanLink,
|
createSpanLink,
|
||||||
|
focusedSpanId,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
// to avert flow error
|
// to avert flow error
|
||||||
if (!trace) {
|
if (!trace) {
|
||||||
@@ -349,6 +371,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
|
|||||||
const isCollapsed = childrenHiddenIDs.has(spanID);
|
const isCollapsed = childrenHiddenIDs.has(spanID);
|
||||||
const isDetailExpanded = detailStates.has(spanID);
|
const isDetailExpanded = detailStates.has(spanID);
|
||||||
const isMatchingFilter = findMatchesIDs ? findMatchesIDs.has(spanID) : false;
|
const isMatchingFilter = findMatchesIDs ? findMatchesIDs.has(spanID) : false;
|
||||||
|
const isFocused = spanID === focusedSpanId;
|
||||||
const showErrorIcon = isErrorSpan(span) || (isCollapsed && spanContainsErredSpan(trace.spans, spanIndex));
|
const showErrorIcon = isErrorSpan(span) || (isCollapsed && spanContainsErredSpan(trace.spans, spanIndex));
|
||||||
|
|
||||||
// Check for direct child "server" span if the span is a "client" span.
|
// Check for direct child "server" span if the span is a "client" span.
|
||||||
@@ -389,6 +412,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
|
|||||||
isChildrenExpanded={!isCollapsed}
|
isChildrenExpanded={!isCollapsed}
|
||||||
isDetailExpanded={isDetailExpanded}
|
isDetailExpanded={isDetailExpanded}
|
||||||
isMatchingFilter={isMatchingFilter}
|
isMatchingFilter={isMatchingFilter}
|
||||||
|
isFocused={isFocused}
|
||||||
numTicks={NUM_TICKS}
|
numTicks={NUM_TICKS}
|
||||||
onDetailToggled={detailToggle}
|
onDetailToggled={detailToggle}
|
||||||
onChildrenToggled={childrenToggle}
|
onChildrenToggled={childrenToggle}
|
||||||
@@ -430,6 +454,8 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
|
|||||||
linksGetter,
|
linksGetter,
|
||||||
theme,
|
theme,
|
||||||
createSpanLink,
|
createSpanLink,
|
||||||
|
focusedSpanId,
|
||||||
|
createFocusSpanLink,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const detailState = detailStates.get(spanID);
|
const detailState = detailStates.get(spanID);
|
||||||
if (!trace || !detailState) {
|
if (!trace || !detailState) {
|
||||||
@@ -459,6 +485,8 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
|
|||||||
addHoverIndentGuideId={addHoverIndentGuideId}
|
addHoverIndentGuideId={addHoverIndentGuideId}
|
||||||
removeHoverIndentGuideId={removeHoverIndentGuideId}
|
removeHoverIndentGuideId={removeHoverIndentGuideId}
|
||||||
createSpanLink={createSpanLink}
|
createSpanLink={createSpanLink}
|
||||||
|
focusedSpanId={focusedSpanId}
|
||||||
|
createFocusSpanLink={createFocusSpanLink}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2, LinkModel } from '@grafana/data';
|
||||||
import { stylesFactory, withTheme2 } from '@grafana/ui';
|
import { stylesFactory, withTheme2 } from '@grafana/ui';
|
||||||
|
|
||||||
import TimelineHeaderRow from './TimelineHeaderRow';
|
import TimelineHeaderRow from './TimelineHeaderRow';
|
||||||
@@ -103,6 +103,8 @@ type TProps = TExtractUiFindFromStateReturn & {
|
|||||||
theme: GrafanaTheme2;
|
theme: GrafanaTheme2;
|
||||||
createSpanLink?: SpanLinkFunc;
|
createSpanLink?: SpanLinkFunc;
|
||||||
scrollElement?: Element;
|
scrollElement?: Element;
|
||||||
|
focusedSpanId?: string;
|
||||||
|
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
|||||||
@@ -182,6 +182,36 @@ describe('state functions', () => {
|
|||||||
from: 'now - 5h',
|
from: 'now - 5h',
|
||||||
to: 'now',
|
to: 'now',
|
||||||
},
|
},
|
||||||
|
panelsState: undefined,
|
||||||
|
};
|
||||||
|
const serialized = serializeStateToUrlParam(state, true);
|
||||||
|
const parsed = parseUrlState(serialized);
|
||||||
|
expect(state).toMatchObject(parsed);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can parse serialized panelsState into the original state', () => {
|
||||||
|
const state = {
|
||||||
|
...DEFAULT_EXPLORE_STATE,
|
||||||
|
datasource: 'foo',
|
||||||
|
queries: [
|
||||||
|
{
|
||||||
|
expr: 'metric{test="a/b"}',
|
||||||
|
refId: 'A',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expr: 'super{foo="x/z"}',
|
||||||
|
refId: 'B',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
range: {
|
||||||
|
from: 'now - 5h',
|
||||||
|
to: 'now',
|
||||||
|
},
|
||||||
|
panelsState: {
|
||||||
|
trace: {
|
||||||
|
spanId: 'abcdef',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const serialized = serializeStateToUrlParam(state, true);
|
const serialized = serializeStateToUrlParam(state, true);
|
||||||
const parsed = parseUrlState(serialized);
|
const parsed = parseUrlState(serialized);
|
||||||
|
|||||||
@@ -245,10 +245,13 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
|||||||
};
|
};
|
||||||
const datasource = parsed[ParseUrlStateIndex.Datasource];
|
const datasource = parsed[ParseUrlStateIndex.Datasource];
|
||||||
const parsedSegments = parsed.slice(ParseUrlStateIndex.SegmentsStart);
|
const parsedSegments = parsed.slice(ParseUrlStateIndex.SegmentsStart);
|
||||||
const queries = parsedSegments.filter((segment) => !isSegment(segment, 'ui', 'originPanelId', 'mode'));
|
const queries = parsedSegments.filter(
|
||||||
|
(segment) => !isSegment(segment, 'ui', 'originPanelId', 'mode', '__panelsState')
|
||||||
|
);
|
||||||
|
|
||||||
const originPanelId = parsedSegments.filter((segment) => isSegment(segment, 'originPanelId'))[0];
|
const originPanelId = parsedSegments.find((segment) => isSegment(segment, 'originPanelId'));
|
||||||
return { datasource, queries, range, originPanelId };
|
const panelsState = parsedSegments.find((segment) => isSegment(segment, '__panelsState'))?.__panelsState;
|
||||||
|
return { datasource, queries, range, originPanelId, panelsState };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateKey(index = 0): string {
|
export function generateKey(index = 0): string {
|
||||||
|
|||||||
@@ -44,7 +44,15 @@ class ExplorePaneContainerUnconnected extends React.PureComponent<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { initialized, exploreId, initialDatasource, initialQueries, initialRange, originPanelId } = this.props;
|
const {
|
||||||
|
initialized,
|
||||||
|
exploreId,
|
||||||
|
initialDatasource,
|
||||||
|
initialQueries,
|
||||||
|
initialRange,
|
||||||
|
originPanelId,
|
||||||
|
panelsState,
|
||||||
|
} = this.props;
|
||||||
const width = this.el?.offsetWidth ?? 0;
|
const width = this.el?.offsetWidth ?? 0;
|
||||||
|
|
||||||
// initialize the whole explore first time we mount and if browser history contains a change in datasource
|
// initialize the whole explore first time we mount and if browser history contains a change in datasource
|
||||||
@@ -56,6 +64,7 @@ class ExplorePaneContainerUnconnected extends React.PureComponent<Props> {
|
|||||||
initialRange,
|
initialRange,
|
||||||
width,
|
width,
|
||||||
this.exploreEvents,
|
this.exploreEvents,
|
||||||
|
panelsState,
|
||||||
originPanelId
|
originPanelId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -101,7 +110,7 @@ function mapStateToProps(state: StoreState, props: OwnProps) {
|
|||||||
const timeZone = getTimeZone(state.user);
|
const timeZone = getTimeZone(state.user);
|
||||||
const fiscalYearStartMonth = getFiscalYearStartMonth(state.user);
|
const fiscalYearStartMonth = getFiscalYearStartMonth(state.user);
|
||||||
|
|
||||||
const { datasource, queries, range: urlRange, originPanelId } = (urlState || {}) as ExploreUrlState;
|
const { datasource, queries, range: urlRange, originPanelId, panelsState } = (urlState || {}) as ExploreUrlState;
|
||||||
const initialDatasource = datasource || store.get(lastUsedDatasourceKeyForOrgId(state.user.orgId));
|
const initialDatasource = datasource || store.get(lastUsedDatasourceKeyForOrgId(state.user.orgId));
|
||||||
const initialQueries: DataQuery[] = ensureQueriesMemoized(queries);
|
const initialQueries: DataQuery[] = ensureQueriesMemoized(queries);
|
||||||
const initialRange = urlRange
|
const initialRange = urlRange
|
||||||
@@ -114,6 +123,7 @@ function mapStateToProps(state: StoreState, props: OwnProps) {
|
|||||||
initialQueries,
|
initialQueries,
|
||||||
initialRange,
|
initialRange,
|
||||||
originPanelId,
|
originPanelId,
|
||||||
|
panelsState,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,13 @@ export class TableContainer extends PureComponent<Props> {
|
|||||||
// differently and sidestep this getLinks API on a dataframe
|
// differently and sidestep this getLinks API on a dataframe
|
||||||
for (const field of dataFrame.fields) {
|
for (const field of dataFrame.fields) {
|
||||||
field.getLinks = (config: ValueLinkConfig) => {
|
field.getLinks = (config: ValueLinkConfig) => {
|
||||||
return getFieldLinksForExplore({ field, rowIndex: config.valueRowIndex!, splitOpenFn: splitOpen, range });
|
return getFieldLinksForExplore({
|
||||||
|
field,
|
||||||
|
rowIndex: config.valueRowIndex!,
|
||||||
|
splitOpenFn: splitOpen,
|
||||||
|
range,
|
||||||
|
dataFrame: dataFrame!,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
import { DataFrame, DataFrameView, SplitOpen, TraceSpanRow } from '@grafana/data';
|
import {
|
||||||
|
DataFrame,
|
||||||
|
DataFrameView,
|
||||||
|
DataLink,
|
||||||
|
DataSourceApi,
|
||||||
|
Field,
|
||||||
|
LinkModel,
|
||||||
|
mapInternalLinkToExplore,
|
||||||
|
SplitOpen,
|
||||||
|
TraceSpanRow,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { getTemplateSrv } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
Trace,
|
Trace,
|
||||||
TracePageHeader,
|
TracePageHeader,
|
||||||
@@ -15,7 +26,8 @@ import { getTimeZone } from 'app/features/profile/state/selectors';
|
|||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import { ExploreId } from 'app/types/explore';
|
import { ExploreId } from 'app/types/explore';
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { changePanelState } from '../state/explorePane';
|
||||||
import { createSpanLinkFactory } from './createSpanLink';
|
import { createSpanLinkFactory } from './createSpanLink';
|
||||||
import { UIElements } from './uiElements';
|
import { UIElements } from './uiElements';
|
||||||
import { useChildrenState } from './useChildrenState';
|
import { useChildrenState } from './useChildrenState';
|
||||||
@@ -66,10 +78,16 @@ export function TraceView(props: Props) {
|
|||||||
|
|
||||||
const traceProp = useMemo(() => transformDataFrames(frame), [frame]);
|
const traceProp = useMemo(() => transformDataFrames(frame), [frame]);
|
||||||
const { search, setSearch, spanFindMatches } = useSearch(traceProp?.spans);
|
const { search, setSearch, spanFindMatches } = useSearch(traceProp?.spans);
|
||||||
const dataSourceName = useSelector((state: StoreState) => state.explore[props.exploreId]?.datasourceInstance?.name);
|
|
||||||
const traceToLogsOptions = (getDatasourceSrv().getInstanceSettings(dataSourceName)?.jsonData as TraceToLogsData)
|
const datasource = useSelector(
|
||||||
?.tracesToLogs;
|
(state: StoreState) => state.explore[props.exploreId]?.datasourceInstance ?? undefined
|
||||||
const timeZone = useSelector((state: StoreState) => getTimeZone(state.user));
|
);
|
||||||
|
|
||||||
|
const [focusedSpanId, createFocusSpanLink] = useFocusSpanLink({
|
||||||
|
refId: frame?.refId,
|
||||||
|
exploreId: props.exploreId,
|
||||||
|
datasource,
|
||||||
|
});
|
||||||
|
|
||||||
const traceTimeline: TTraceTimeline = useMemo(
|
const traceTimeline: TTraceTimeline = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -83,11 +101,14 @@ export function TraceView(props: Props) {
|
|||||||
[childrenHiddenIDs, detailStates, hoverIndentGuideIds, spanNameColumnWidth, traceProp?.traceID]
|
[childrenHiddenIDs, detailStates, hoverIndentGuideIds, spanNameColumnWidth, traceProp?.traceID]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const traceToLogsOptions = (getDatasourceSrv().getInstanceSettings(datasource?.name)?.jsonData as TraceToLogsData)
|
||||||
|
?.tracesToLogs;
|
||||||
const createSpanLink = useMemo(
|
const createSpanLink = useMemo(
|
||||||
() => createSpanLinkFactory({ splitOpenFn: props.splitOpenFn, traceToLogsOptions, dataFrame: frame }),
|
() => createSpanLinkFactory({ splitOpenFn: props.splitOpenFn, traceToLogsOptions, dataFrame: frame }),
|
||||||
[props.splitOpenFn, traceToLogsOptions, frame]
|
[props.splitOpenFn, traceToLogsOptions, frame]
|
||||||
);
|
);
|
||||||
const onSlimViewClicked = useCallback(() => setSlim(!slim), [slim]);
|
const onSlimViewClicked = useCallback(() => setSlim(!slim), [slim]);
|
||||||
|
const timeZone = useSelector((state: StoreState) => getTimeZone(state.user));
|
||||||
|
|
||||||
if (!props.dataFrames?.length || !traceProp) {
|
if (!props.dataFrames?.length || !traceProp) {
|
||||||
return null;
|
return null;
|
||||||
@@ -151,6 +172,8 @@ export function TraceView(props: Props) {
|
|||||||
uiFind={search}
|
uiFind={search}
|
||||||
createSpanLink={createSpanLink}
|
createSpanLink={createSpanLink}
|
||||||
scrollElement={props.scrollElement}
|
scrollElement={props.scrollElement}
|
||||||
|
focusedSpanId={focusedSpanId}
|
||||||
|
createFocusSpanLink={createFocusSpanLink}
|
||||||
/>
|
/>
|
||||||
</UIElementsContext.Provider>
|
</UIElementsContext.Provider>
|
||||||
);
|
);
|
||||||
@@ -198,3 +221,59 @@ function transformTraceDataFrame(frame: DataFrame): TraceResponse {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles focusing a span. Returns the span id to focus to based on what is in current explore state and also a
|
||||||
|
* function to change the focused span id.
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
function useFocusSpanLink(options: {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
refId?: string;
|
||||||
|
datasource?: DataSourceApi;
|
||||||
|
}): [string | undefined, (traceId: string, spanId: string) => LinkModel<Field>] {
|
||||||
|
const panelState = useSelector((state: StoreState) => state.explore[options.exploreId]?.panelsState.trace);
|
||||||
|
const focusedSpanId = panelState?.spanId;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const setFocusedSpanId = (spanId?: string) =>
|
||||||
|
dispatch(
|
||||||
|
changePanelState(options.exploreId, 'trace', {
|
||||||
|
...panelState,
|
||||||
|
spanId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const query = useSelector((state: StoreState) =>
|
||||||
|
state.explore[options.exploreId]?.queries.find((query) => query.refId === options.refId)
|
||||||
|
);
|
||||||
|
|
||||||
|
const createFocusSpanLink = (traceId: string, spanId: string) => {
|
||||||
|
const link: DataLink = {
|
||||||
|
title: 'Deep link to this span',
|
||||||
|
url: '',
|
||||||
|
internal: {
|
||||||
|
datasourceUid: options.datasource?.uid!,
|
||||||
|
datasourceName: options.datasource?.name!,
|
||||||
|
query: query,
|
||||||
|
panelsState: {
|
||||||
|
trace: {
|
||||||
|
spanId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return mapInternalLinkToExplore({
|
||||||
|
link,
|
||||||
|
internalLink: link.internal!,
|
||||||
|
scopedVars: {},
|
||||||
|
range: {} as any,
|
||||||
|
field: {} as Field,
|
||||||
|
onClickFn: () => setFocusedSpanId(focusedSpanId === spanId ? undefined : spanId),
|
||||||
|
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return [focusedSpanId, createFocusSpanLink];
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ describe('createSpanLinkFactory', () => {
|
|||||||
const linkDef = createLink!(createTraceSpan());
|
const linkDef = createLink!(createTraceSpan());
|
||||||
expect(linkDef!.href).toBe(
|
expect(linkDef!.href).toBe(
|
||||||
`/explore?left=${encodeURIComponent(
|
`/explore?left=${encodeURIComponent(
|
||||||
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{cluster=\\"cluster1\\", hostname=\\"hostname1\\"}","refId":""}]}'
|
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{cluster=\\"cluster1\\", hostname=\\"hostname1\\"}","refId":""}],"panelsState":{}}'
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -54,7 +54,7 @@ describe('createSpanLinkFactory', () => {
|
|||||||
);
|
);
|
||||||
expect(linkDef!.href).toBe(
|
expect(linkDef!.href).toBe(
|
||||||
`/explore?left=${encodeURIComponent(
|
`/explore?left=${encodeURIComponent(
|
||||||
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{ip=\\"192.168.0.1\\"}","refId":""}]}'
|
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{ip=\\"192.168.0.1\\"}","refId":""}],"panelsState":{}}'
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -77,7 +77,7 @@ describe('createSpanLinkFactory', () => {
|
|||||||
);
|
);
|
||||||
expect(linkDef!.href).toBe(
|
expect(linkDef!.href).toBe(
|
||||||
`/explore?left=${encodeURIComponent(
|
`/explore?left=${encodeURIComponent(
|
||||||
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{ip=\\"192.168.0.1\\", host=\\"host\\"}","refId":""}]}'
|
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{ip=\\"192.168.0.1\\", host=\\"host\\"}","refId":""}],"panelsState":{}}'
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -101,7 +101,7 @@ describe('createSpanLinkFactory', () => {
|
|||||||
);
|
);
|
||||||
expect(linkDef!.href).toBe(
|
expect(linkDef!.href).toBe(
|
||||||
`/explore?left=${encodeURIComponent(
|
`/explore?left=${encodeURIComponent(
|
||||||
'{"range":{"from":"2020-10-14T01:01:00.000Z","to":"2020-10-14T01:01:01.000Z"},"datasource":"loki1","queries":[{"expr":"{hostname=\\"hostname1\\"}","refId":""}]}'
|
'{"range":{"from":"2020-10-14T01:01:00.000Z","to":"2020-10-14T01:01:01.000Z"},"datasource":"loki1","queries":[{"expr":"{hostname=\\"hostname1\\"}","refId":""}],"panelsState":{}}'
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -116,7 +116,7 @@ describe('createSpanLinkFactory', () => {
|
|||||||
|
|
||||||
expect(linkDef!.href).toBe(
|
expect(linkDef!.href).toBe(
|
||||||
`/explore?left=${encodeURIComponent(
|
`/explore?left=${encodeURIComponent(
|
||||||
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{cluster=\\"cluster1\\", hostname=\\"hostname1\\"} |=\\"7946b05c2e2e4e5a\\" |=\\"6605c7b08e715d6c\\"","refId":""}]}'
|
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{cluster=\\"cluster1\\", hostname=\\"hostname1\\"} |=\\"7946b05c2e2e4e5a\\" |=\\"6605c7b08e715d6c\\"","refId":""}],"panelsState":{}}'
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,13 +22,22 @@ import {
|
|||||||
storeGraphStyle,
|
storeGraphStyle,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import { createAction, PayloadAction } from '@reduxjs/toolkit';
|
import { createAction, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { EventBusExtended, DataQuery, ExploreUrlState, TimeRange, HistoryItem, DataSourceApi } from '@grafana/data';
|
import {
|
||||||
|
EventBusExtended,
|
||||||
|
DataQuery,
|
||||||
|
ExploreUrlState,
|
||||||
|
TimeRange,
|
||||||
|
HistoryItem,
|
||||||
|
DataSourceApi,
|
||||||
|
ExplorePanelsState,
|
||||||
|
PreferredVisualisationType,
|
||||||
|
} from '@grafana/data';
|
||||||
// Types
|
// Types
|
||||||
import { ThunkResult } from 'app/types';
|
import { ThunkResult } from 'app/types';
|
||||||
import { getFiscalYearStartMonth, getTimeZone } from 'app/features/profile/state/selectors';
|
import { getFiscalYearStartMonth, getTimeZone } from 'app/features/profile/state/selectors';
|
||||||
import { getDataSourceSrv } from '@grafana/runtime';
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
import { getRichHistory } from '../../../core/utils/richHistory';
|
import { getRichHistory } from '../../../core/utils/richHistory';
|
||||||
import { richHistoryUpdatedAction } from './main';
|
import { richHistoryUpdatedAction, stateSave } from './main';
|
||||||
|
|
||||||
//
|
//
|
||||||
// Actions and Payloads
|
// Actions and Payloads
|
||||||
@@ -45,6 +54,38 @@ export interface ChangeSizePayload {
|
|||||||
}
|
}
|
||||||
export const changeSizeAction = createAction<ChangeSizePayload>('explore/changeSize');
|
export const changeSizeAction = createAction<ChangeSizePayload>('explore/changeSize');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks the state of explore panels that gets synced with the url.
|
||||||
|
*/
|
||||||
|
interface ChangePanelsState {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
panelsState: ExplorePanelsState;
|
||||||
|
}
|
||||||
|
const changePanelsStateAction = createAction<ChangePanelsState>('explore/changePanels');
|
||||||
|
export function changePanelState(
|
||||||
|
exploreId: ExploreId,
|
||||||
|
panel: PreferredVisualisationType,
|
||||||
|
panelState: ExplorePanelsState[PreferredVisualisationType]
|
||||||
|
): ThunkResult<void> {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
const exploreItem = getState().explore[exploreId];
|
||||||
|
if (exploreItem === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { panelsState } = exploreItem;
|
||||||
|
dispatch(
|
||||||
|
changePanelsStateAction({
|
||||||
|
exploreId,
|
||||||
|
panelsState: {
|
||||||
|
...panelsState,
|
||||||
|
[panel]: panelState,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
dispatch(stateSave());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize Explore state with state from the URL and the React component.
|
* Initialize Explore state with state from the URL and the React component.
|
||||||
* Call this only on components for with the Explore state has not been initialized.
|
* Call this only on components for with the Explore state has not been initialized.
|
||||||
@@ -102,6 +143,8 @@ export function initializeExplore(
|
|||||||
range: TimeRange,
|
range: TimeRange,
|
||||||
containerWidth: number,
|
containerWidth: number,
|
||||||
eventBridge: EventBusExtended,
|
eventBridge: EventBusExtended,
|
||||||
|
panelsState?: ExplorePanelsState,
|
||||||
|
|
||||||
originPanelId?: number | null
|
originPanelId?: number | null
|
||||||
): ThunkResult<void> {
|
): ThunkResult<void> {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
@@ -128,6 +171,9 @@ export function initializeExplore(
|
|||||||
history,
|
history,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
if (panelsState !== undefined) {
|
||||||
|
dispatch(changePanelsStateAction({ exploreId, panelsState }));
|
||||||
|
}
|
||||||
dispatch(updateTime({ exploreId }));
|
dispatch(updateTime({ exploreId }));
|
||||||
|
|
||||||
if (instance) {
|
if (instance) {
|
||||||
@@ -159,7 +205,7 @@ export function refreshExplore(exploreId: ExploreId, newUrlQuery: string): Thunk
|
|||||||
|
|
||||||
const { containerWidth, eventBridge } = itemState;
|
const { containerWidth, eventBridge } = itemState;
|
||||||
|
|
||||||
const { datasource, queries, range: urlRange, originPanelId } = newUrlState;
|
const { datasource, queries, range: urlRange, originPanelId, panelsState } = newUrlState;
|
||||||
const refreshQueries: DataQuery[] = [];
|
const refreshQueries: DataQuery[] = [];
|
||||||
|
|
||||||
for (let index = 0; index < queries.length; index++) {
|
for (let index = 0; index < queries.length; index++) {
|
||||||
@@ -176,7 +222,16 @@ export function refreshExplore(exploreId: ExploreId, newUrlQuery: string): Thunk
|
|||||||
if (update.datasource) {
|
if (update.datasource) {
|
||||||
const initialQueries = ensureQueries(queries);
|
const initialQueries = ensureQueries(queries);
|
||||||
await dispatch(
|
await dispatch(
|
||||||
initializeExplore(exploreId, datasource, initialQueries, range, containerWidth, eventBridge, originPanelId)
|
initializeExplore(
|
||||||
|
exploreId,
|
||||||
|
datasource,
|
||||||
|
initialQueries,
|
||||||
|
range,
|
||||||
|
containerWidth,
|
||||||
|
eventBridge,
|
||||||
|
panelsState,
|
||||||
|
originPanelId
|
||||||
|
)
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -189,6 +244,10 @@ export function refreshExplore(exploreId: ExploreId, newUrlQuery: string): Thunk
|
|||||||
dispatch(setQueriesAction({ exploreId, queries: refreshQueries }));
|
dispatch(setQueriesAction({ exploreId, queries: refreshQueries }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (update.panelsState && panelsState !== undefined) {
|
||||||
|
dispatch(changePanelsStateAction({ exploreId, panelsState }));
|
||||||
|
}
|
||||||
|
|
||||||
// always run queries when refresh is needed
|
// always run queries when refresh is needed
|
||||||
if (update.queries || update.range) {
|
if (update.queries || update.range) {
|
||||||
dispatch(runQueries(exploreId));
|
dispatch(runQueries(exploreId));
|
||||||
@@ -220,6 +279,11 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
|
|||||||
return { ...state, graphStyle };
|
return { ...state, graphStyle };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (changePanelsStateAction.match(action)) {
|
||||||
|
const { panelsState } = action.payload;
|
||||||
|
return { ...state, panelsState };
|
||||||
|
}
|
||||||
|
|
||||||
if (initializeExploreAction.match(action)) {
|
if (initializeExploreAction.match(action)) {
|
||||||
const { containerWidth, eventBridge, queries, range, originPanelId, datasourceInstance, history } = action.payload;
|
const { containerWidth, eventBridge, queries, range, originPanelId, datasourceInstance, history } = action.payload;
|
||||||
|
|
||||||
@@ -254,14 +318,17 @@ export const urlDiff = (
|
|||||||
datasource: boolean;
|
datasource: boolean;
|
||||||
queries: boolean;
|
queries: boolean;
|
||||||
range: boolean;
|
range: boolean;
|
||||||
|
panelsState: boolean;
|
||||||
} => {
|
} => {
|
||||||
const datasource = !isEqual(currentUrlState?.datasource, oldUrlState?.datasource);
|
const datasource = !isEqual(currentUrlState?.datasource, oldUrlState?.datasource);
|
||||||
const queries = !isEqual(currentUrlState?.queries, oldUrlState?.queries);
|
const queries = !isEqual(currentUrlState?.queries, oldUrlState?.queries);
|
||||||
const range = !isEqual(currentUrlState?.range || DEFAULT_RANGE, oldUrlState?.range || DEFAULT_RANGE);
|
const range = !isEqual(currentUrlState?.range || DEFAULT_RANGE, oldUrlState?.range || DEFAULT_RANGE);
|
||||||
|
const panelsState = !isEqual(currentUrlState?.panelsState, oldUrlState?.panelsState);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
datasource,
|
datasource,
|
||||||
queries,
|
queries,
|
||||||
range,
|
range,
|
||||||
|
panelsState,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AnyAction } from 'redux';
|
import { AnyAction } from 'redux';
|
||||||
import { DataSourceSrv, getDataSourceSrv, locationService } from '@grafana/runtime';
|
import { DataSourceSrv, getDataSourceSrv, locationService } from '@grafana/runtime';
|
||||||
import { DataQuery, ExploreUrlState, serializeStateToUrlParam, TimeRange, UrlQueryMap } from '@grafana/data';
|
import { ExploreUrlState, serializeStateToUrlParam, SplitOpen, UrlQueryMap } from '@grafana/data';
|
||||||
import { GetExploreUrlArguments, stopQueryState } from 'app/core/utils/explore';
|
import { GetExploreUrlArguments, stopQueryState } from 'app/core/utils/explore';
|
||||||
import { ExploreId, ExploreItemState, ExploreState } from 'app/types/explore';
|
import { ExploreId, ExploreItemState, ExploreState } from 'app/types/explore';
|
||||||
import { paneReducer } from './explorePane';
|
import { paneReducer } from './explorePane';
|
||||||
@@ -87,12 +87,7 @@ export const lastSavedUrl: UrlQueryMap = {};
|
|||||||
* or uses values from options arg. This does only navigation each pane is then responsible for initialization from
|
* or uses values from options arg. This does only navigation each pane is then responsible for initialization from
|
||||||
* the URL.
|
* the URL.
|
||||||
*/
|
*/
|
||||||
export function splitOpen<T extends DataQuery = any>(options?: {
|
export const splitOpen: SplitOpen = (options): ThunkResult<void> => {
|
||||||
datasourceUid: string;
|
|
||||||
query: T;
|
|
||||||
// Don't use right now. It's used for Traces to Logs interaction but is hacky in how the range is actually handled.
|
|
||||||
range?: TimeRange;
|
|
||||||
}): ThunkResult<void> {
|
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const leftState: ExploreItemState = getState().explore[ExploreId.left];
|
const leftState: ExploreItemState = getState().explore[ExploreId.left];
|
||||||
const leftUrlState = getUrlStateFromPaneState(leftState);
|
const leftUrlState = getUrlStateFromPaneState(leftState);
|
||||||
@@ -104,13 +99,14 @@ export function splitOpen<T extends DataQuery = any>(options?: {
|
|||||||
datasource: datasourceName,
|
datasource: datasourceName,
|
||||||
queries: [options.query],
|
queries: [options.query],
|
||||||
range: options.range || leftState.range,
|
range: options.range || leftState.range,
|
||||||
|
panelsState: options.panelsState,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlState = serializeStateToUrlParam(rightUrlState, true);
|
const urlState = serializeStateToUrlParam(rightUrlState, true);
|
||||||
locationService.partial({ right: urlState }, true);
|
locationService.partial({ right: urlState }, true);
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close the split view and save URL state. We need to update the state here because when closing we cannot just
|
* Close the split view and save URL state. We need to update the state here because when closing we cannot just
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { getDatasourceSrv } from '../../plugins/datasource_srv';
|
|||||||
import store from '../../../core/store';
|
import store from '../../../core/store';
|
||||||
import { clearQueryKeys, lastUsedDatasourceKeyForOrgId, toGraphStyle } from '../../../core/utils/explore';
|
import { clearQueryKeys, lastUsedDatasourceKeyForOrgId, toGraphStyle } from '../../../core/utils/explore';
|
||||||
import { toRawTimeRange } from '../utils/time';
|
import { toRawTimeRange } from '../utils/time';
|
||||||
|
import { isEmpty, isObject, mapValues, omitBy } from 'lodash';
|
||||||
|
|
||||||
export const DEFAULT_RANGE = {
|
export const DEFAULT_RANGE = {
|
||||||
from: 'now-6h',
|
from: 'now-6h',
|
||||||
@@ -63,6 +64,7 @@ export const makeExplorePaneState = (): ExploreItemState => ({
|
|||||||
logsVolumeDataProvider: undefined,
|
logsVolumeDataProvider: undefined,
|
||||||
logsVolumeData: undefined,
|
logsVolumeData: undefined,
|
||||||
graphStyle: loadGraphStyle(),
|
graphStyle: loadGraphStyle(),
|
||||||
|
panelsState: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createEmptyQueryResponse = (): PanelData => ({
|
export const createEmptyQueryResponse = (): PanelData => ({
|
||||||
@@ -101,6 +103,17 @@ export async function loadAndInitDatasource(
|
|||||||
return { history, instance };
|
return { history, instance };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recursively walks an object, removing keys where the value is undefined
|
||||||
|
// if the resulting object is empty, returns undefined
|
||||||
|
function pruneObject(obj: object): object | undefined {
|
||||||
|
let pruned = mapValues(obj, (value) => (isObject(value) ? pruneObject(value) : value));
|
||||||
|
pruned = omitBy<typeof pruned>(pruned, isEmpty);
|
||||||
|
if (isEmpty(pruned)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return pruned;
|
||||||
|
}
|
||||||
|
|
||||||
export function getUrlStateFromPaneState(pane: ExploreItemState): ExploreUrlState {
|
export function getUrlStateFromPaneState(pane: ExploreItemState): ExploreUrlState {
|
||||||
return {
|
return {
|
||||||
// datasourceInstance should not be undefined anymore here but in case there is some path for it to be undefined
|
// datasourceInstance should not be undefined anymore here but in case there is some path for it to be undefined
|
||||||
@@ -108,6 +121,8 @@ export function getUrlStateFromPaneState(pane: ExploreItemState): ExploreUrlStat
|
|||||||
datasource: pane.datasourceInstance?.name || '',
|
datasource: pane.datasourceInstance?.name || '',
|
||||||
queries: pane.queries.map(clearQueryKeys),
|
queries: pane.queries.map(clearQueryKeys),
|
||||||
range: toRawTimeRange(pane.range),
|
range: toRawTimeRange(pane.range),
|
||||||
|
// don't include panelsState in the url unless a piece of state is actually set
|
||||||
|
panelsState: pruneObject(pane.panelsState),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ describe('getFieldLinksForExplore', () => {
|
|||||||
query: { query: 'query_1' },
|
query: { query: 'query_1' },
|
||||||
datasourceUid: 'uid_1',
|
datasourceUid: 'uid_1',
|
||||||
datasourceName: 'test_ds',
|
datasourceName: 'test_ds',
|
||||||
|
panelsState: {
|
||||||
|
trace: {
|
||||||
|
spanId: 'abcdef',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const splitfn = jest.fn();
|
const splitfn = jest.fn();
|
||||||
@@ -62,7 +67,7 @@ describe('getFieldLinksForExplore', () => {
|
|||||||
|
|
||||||
expect(links[0].href).toBe(
|
expect(links[0].href).toBe(
|
||||||
`/explore?left=${encodeURIComponent(
|
`/explore?left=${encodeURIComponent(
|
||||||
'{"range":{"from":"now-1h","to":"now"},"datasource":"test_ds","queries":[{"query":"query_1"}]}'
|
'{"range":{"from":"now-1h","to":"now"},"datasource":"test_ds","queries":[{"query":"query_1"}],"panelsState":{"trace":{"spanId":"abcdef"}}}'
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
expect(links[0].title).toBe('test_ds');
|
expect(links[0].title).toBe('test_ds');
|
||||||
@@ -75,6 +80,11 @@ describe('getFieldLinksForExplore', () => {
|
|||||||
datasourceUid: 'uid_1',
|
datasourceUid: 'uid_1',
|
||||||
query: { query: 'query_1' },
|
query: { query: 'query_1' },
|
||||||
range,
|
range,
|
||||||
|
panelsState: {
|
||||||
|
trace: {
|
||||||
|
spanId: 'abcdef',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ import {
|
|||||||
DataFrame,
|
DataFrame,
|
||||||
DataQuery,
|
DataQuery,
|
||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
DataQueryResponse,
|
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
EventBusExtended,
|
|
||||||
HistoryItem,
|
HistoryItem,
|
||||||
LogsModel,
|
LogsModel,
|
||||||
PanelData,
|
PanelData,
|
||||||
QueryHint,
|
QueryHint,
|
||||||
RawTimeRange,
|
RawTimeRange,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
|
EventBusExtended,
|
||||||
|
DataQueryResponse,
|
||||||
|
ExplorePanelsState,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
export enum ExploreId {
|
export enum ExploreId {
|
||||||
@@ -173,6 +174,7 @@ export interface ExploreItemState {
|
|||||||
|
|
||||||
/* explore graph style */
|
/* explore graph style */
|
||||||
graphStyle: ExploreGraphStyle;
|
graphStyle: ExploreGraphStyle;
|
||||||
|
panelsState: ExplorePanelsState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExploreUpdateState {
|
export interface ExploreUpdateState {
|
||||||
|
|||||||
Reference in New Issue
Block a user