Traces Panel: Add new Traces Panel visualization (#47534)

* Panel

* Support tempo dash variables

* Support tempo explore variables

* Only show span links for explore

* Cleanup

* Added tests

* apply variables to search

* Tests for search variables

* Handling no data

* Interpolation and tests

* TracesPanel tests

* More tests

* Fix for backend test

* Manager integration test fix

* Traces doc and updated visualizations index doc

* Logs for this span

* Search, scrollToTop, other improvements

* Refactor to extract common code

* Removed TopOfViewRefType optional

* Remove topOfViewRef optional

* Removed another optional and fixed tests

* Test

* Only show search bar if trace

* Support traces panel in add to dashboard

* Self review

* Update betterer

* Linter fixes

* Updated traces doc

* Ahh, moved the for more info too

* Updated betterer.results

* Added new icon

* Updated expectedListResp.json
This commit is contained in:
Joey Tawadrous
2022-05-03 17:42:36 +01:00
committed by GitHub
parent 46e53cf42c
commit 09634b518c
30 changed files with 520 additions and 253 deletions
@@ -25,8 +25,6 @@ import * as markers from './TracePageSearchBar.markers';
const defaultProps = {
forwardedRef: React.createRef(),
navigable: true,
nextResult: () => {},
prevResult: () => {},
suffix: '',
searchValue: 'something',
};
@@ -59,8 +57,6 @@ describe('<TracePageSearchBar>', () => {
buttons.forEach((button) => {
expect(button.prop('disabled')).toBe(false);
});
expect(wrapper.find('Button[icon="arrow-up"]').prop('onClick')).toBe(defaultProps.prevResult);
expect(wrapper.find('Button[icon="arrow-down"]').prop('onClick')).toBe(defaultProps.nextResult);
});
it('only shows navigable buttons when navigable is true', () => {
@@ -14,8 +14,7 @@
import { css } from '@emotion/css';
import cx from 'classnames';
import * as React from 'react';
import { memo } from 'react';
import React, { memo, Dispatch, SetStateAction } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, useStyles2 } from '@grafana/ui';
@@ -72,16 +71,27 @@ export const getStyles = (theme: GrafanaTheme2) => {
};
type TracePageSearchBarProps = {
prevResult: () => void;
nextResult: () => void;
navigable: boolean;
searchValue: string;
onSearchValueChange: (value: string) => void;
setSearch: (value: string) => void;
searchBarSuffix: string;
spanFindMatches: Set<string> | undefined;
focusedSpanIdForSearch: string;
setSearchBarSuffix: Dispatch<SetStateAction<string>>;
setFocusedSpanIdForSearch: Dispatch<SetStateAction<string>>;
};
export default memo(function TracePageSearchBar(props: TracePageSearchBarProps) {
const { navigable, nextResult, prevResult, onSearchValueChange, searchValue, searchBarSuffix } = props;
const {
navigable,
setSearch,
searchValue,
searchBarSuffix,
spanFindMatches,
focusedSpanIdForSearch,
setSearchBarSuffix,
setFocusedSpanIdForSearch,
} = props;
const styles = useStyles2(getStyles);
const suffix = searchValue ? (
@@ -98,11 +108,60 @@ export default memo(function TracePageSearchBar(props: TracePageSearchBarProps)
suffix,
};
const setTraceSearch = (value: string) => {
setFocusedSpanIdForSearch('');
setSearchBarSuffix('');
setSearch(value);
};
const nextResult = () => {
const spanMatches = Array.from(spanFindMatches!);
const prevMatchedIndex = spanMatches.indexOf(focusedSpanIdForSearch)
? spanMatches.indexOf(focusedSpanIdForSearch)
: 0;
// new query || at end, go to start
if (prevMatchedIndex === -1 || prevMatchedIndex === spanMatches.length - 1) {
setFocusedSpanIdForSearch(spanMatches[0]);
setSearchBarSuffix(getSearchBarSuffix(1));
return;
}
// get next
setFocusedSpanIdForSearch(spanMatches[prevMatchedIndex + 1]);
setSearchBarSuffix(getSearchBarSuffix(prevMatchedIndex + 2));
};
const prevResult = () => {
const spanMatches = Array.from(spanFindMatches!);
const prevMatchedIndex = spanMatches.indexOf(focusedSpanIdForSearch)
? spanMatches.indexOf(focusedSpanIdForSearch)
: 0;
// new query || at start, go to end
if (prevMatchedIndex === -1 || prevMatchedIndex === 0) {
setFocusedSpanIdForSearch(spanMatches[spanMatches.length - 1]);
setSearchBarSuffix(getSearchBarSuffix(spanMatches.length));
return;
}
// get prev
setFocusedSpanIdForSearch(spanMatches[prevMatchedIndex - 1]);
setSearchBarSuffix(getSearchBarSuffix(prevMatchedIndex));
};
const getSearchBarSuffix = (index: number): string => {
if (spanFindMatches?.size && spanFindMatches?.size > 0) {
return index + ' of ' + spanFindMatches?.size;
}
return '';
};
return (
<div className={styles.TracePageSearchBar}>
<span className={ubJustifyEnd} style={{ display: 'flex' }}>
<UiFindInput
onChange={onSearchValueChange}
onChange={setTraceSearch}
value={searchValue}
inputProps={uiFindInputInputProps}
allowClear={true}
@@ -35,10 +35,12 @@ describe('<SpanDetail>', () => {
const span = transformTraceData(traceGenerator.trace({ numberOfSpans: 1 })).spans[0];
const detailState = new DetailState().toggleLogs().toggleProcess().toggleReferences().toggleTags();
const traceStartTime = 5;
const topOfExploreViewRef = jest.fn();
const props = {
detailState,
span,
traceStartTime,
topOfExploreViewRef,
logItemToggle: jest.fn(),
logsToggle: jest.fn(),
processToggle: jest.fn(),
@@ -46,6 +48,7 @@ describe('<SpanDetail>', () => {
warningsToggle: jest.fn(),
referencesToggle: jest.fn(),
createFocusSpanLink: jest.fn(),
topOfViewRefType: 'Explore',
};
span.logs = [
{
@@ -26,6 +26,7 @@ import LabeledList from '../../common/LabeledList';
import { SpanLinkFunc, TNil } from '../../types';
import { TraceKeyValuePair, TraceLink, TraceLog, TraceSpan, TraceSpanReference } from '../../types/trace';
import { uAlignIcon, ubM0, ubMb1, ubMy1, ubTxRightAlign } from '../../uberUtilityStyles';
import { TopOfViewRefType } from '../VirtualizedTraceView';
import { formatDuration } from '../utils';
import AccordianKeyValues from './AccordianKeyValues';
@@ -119,6 +120,7 @@ type SpanDetailProps = {
createSpanLink?: SpanLinkFunc;
focusedSpanId?: string;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
topOfViewRefType?: TopOfViewRefType;
};
export default function SpanDetail(props: SpanDetailProps) {
@@ -138,6 +140,7 @@ export default function SpanDetail(props: SpanDetailProps) {
focusSpan,
createSpanLink,
createFocusSpanLink,
topOfViewRefType,
} = props;
const {
isTagsOpen,
@@ -281,27 +284,29 @@ export default function SpanDetail(props: SpanDetailProps) {
focusSpan={focusSpan}
/>
)}
<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}
</small>
{topOfViewRefType === TopOfViewRefType.Explore && (
<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}
</small>
)}
</div>
</div>
);
@@ -26,6 +26,7 @@ import SpanDetail from './SpanDetail';
import DetailState from './SpanDetail/DetailState';
import SpanTreeOffset from './SpanTreeOffset';
import TimelineRow from './TimelineRow';
import { TopOfViewRefType } from './VirtualizedTraceView';
const getStyles = stylesFactory((theme: GrafanaTheme2) => {
return {
@@ -93,6 +94,7 @@ type SpanDetailRowProps = {
createSpanLink?: SpanLinkFunc;
focusedSpanId?: string;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
topOfViewRefType?: TopOfViewRefType;
};
export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProps> {
@@ -128,6 +130,7 @@ export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProp
createSpanLink,
focusedSpanId,
createFocusSpanLink,
topOfViewRefType,
} = this.props;
const styles = getStyles(theme);
return (
@@ -170,6 +173,7 @@ export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProp
createSpanLink={createSpanLink}
focusedSpanId={focusedSpanId}
createFocusSpanLink={createFocusSpanLink}
topOfViewRefType={topOfViewRefType}
/>
</div>
</TimelineRow.Cell>
@@ -31,6 +31,7 @@ describe('<VirtualizedTraceViewImpl>', () => {
let instance;
const trace = transformTraceData(traceGenerator.trace({ numberOfSpans: 10 }));
const topOfExploreViewRef = jest.fn();
const props = {
childrenHiddenIDs: new Set(),
childrenToggle: jest.fn(),
@@ -51,6 +52,7 @@ describe('<VirtualizedTraceViewImpl>', () => {
spanNameColumnWidth: 0.5,
trace,
uiFind: 'uiFind',
topOfExploreViewRef,
};
function expandRow(rowIndex) {
@@ -109,6 +111,10 @@ describe('<VirtualizedTraceViewImpl>', () => {
expect(wrapper.find(ListView)).toBeDefined();
});
it('renders scrollToTopButton', () => {
expect(wrapper.find({ title: 'Scroll to top' }).exists()).toBeTruthy();
});
it('sets the trace for global state.traceTimeline', () => {
expect(props.setTrace.mock.calls).toEqual([[trace, props.uiFind]]);
props.setTrace.mockReset();
@@ -45,7 +45,10 @@ type TExtractUiFindFromStateReturn = {
uiFind: string | undefined;
};
const getStyles = stylesFactory(() => {
const getStyles = stylesFactory((props: TVirtualizedTraceViewOwnProps) => {
const { topOfViewRefType } = props;
const position = topOfViewRefType === TopOfViewRefType.Explore ? 'fixed' : 'absolute';
return {
rowsWrapper: css`
width: 100%;
@@ -60,7 +63,7 @@ const getStyles = stylesFactory(() => {
align-items: center;
width: 40px;
height: 40px;
position: fixed;
position: ${position};
bottom: 30px;
right: 30px;
z-index: 1;
@@ -74,6 +77,11 @@ type RowState = {
spanIndex: number;
};
export enum TopOfViewRefType {
Explore = 'Explore',
Panel = 'Panel',
}
type TVirtualizedTraceViewOwnProps = {
currentViewRangeTime: [number, number];
findMatchesIDs: Set<string> | TNil;
@@ -104,7 +112,8 @@ type TVirtualizedTraceViewOwnProps = {
focusedSpanId?: string;
focusedSpanIdForSearch: string;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
topOfExploreViewRef?: RefObject<HTMLDivElement>;
topOfViewRef?: RefObject<HTMLDivElement>;
topOfViewRefType?: TopOfViewRefType;
};
type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & TExtractUiFindFromStateReturn & TTraceTimeline;
@@ -425,7 +434,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
};
}
const styles = getStyles();
const styles = getStyles(this.props);
return (
<div className={styles.row} key={key} style={style} {...attrs}>
<SpanBarRow
@@ -481,13 +490,14 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
createSpanLink,
focusedSpanId,
createFocusSpanLink,
topOfViewRefType,
} = this.props;
const detailState = detailStates.get(spanID);
if (!trace || !detailState) {
return null;
}
const color = getColorByKey(serviceName, theme);
const styles = getStyles();
const styles = getStyles(this.props);
return (
<div className={styles.row} key={key} style={{ ...style, zIndex: 1 }} {...attrs}>
<SpanDetailRow
@@ -513,18 +523,19 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
createSpanLink={createSpanLink}
focusedSpanId={focusedSpanId}
createFocusSpanLink={createFocusSpanLink}
topOfViewRefType={topOfViewRefType}
/>
</div>
);
}
scrollToTop = () => {
const { topOfExploreViewRef } = this.props;
topOfExploreViewRef?.current?.scrollIntoView({ behavior: 'smooth' });
const { topOfViewRef } = this.props;
topOfViewRef?.current?.scrollIntoView({ behavior: 'smooth' });
};
render() {
const styles = getStyles();
const styles = getStyles(this.props);
const { scrollElement } = this.props;
return (
<>
@@ -541,7 +552,6 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
windowScroller={false}
scrollElement={scrollElement}
/>
<ToolbarButton
className={styles.scrollToTopButton}
onClick={this.scrollToTop}
@@ -27,7 +27,7 @@ import { TraceSpan, Trace, TraceLog, TraceKeyValuePair, TraceLink, TraceSpanRefe
import ExternalLinkContext from '../url/externalLinkContext';
import TimelineHeaderRow from './TimelineHeaderRow';
import VirtualizedTraceView from './VirtualizedTraceView';
import VirtualizedTraceView, { TopOfViewRefType } from './VirtualizedTraceView';
import { TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate } from './types';
type TExtractUiFindFromStateReturn = {
@@ -109,7 +109,8 @@ type TProps = TExtractUiFindFromStateReturn & {
focusedSpanId?: string;
focusedSpanIdForSearch: string;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
topOfExploreViewRef?: RefObject<HTMLDivElement>;
topOfViewRef?: RefObject<HTMLDivElement>;
topOfViewRefType?: TopOfViewRefType;
};
type State = {
@@ -165,7 +166,7 @@ export class UnthemedTraceTimelineViewer extends React.PureComponent<TProps, Sta
createLinkToExternalSpan,
traceTimeline,
theme,
topOfExploreViewRef,
topOfViewRef,
focusedSpanIdForSearch,
...rest
} = this.props;
@@ -197,7 +198,7 @@ export class UnthemedTraceTimelineViewer extends React.PureComponent<TProps, Sta
{...traceTimeline}
setSpanNameColumnWidth={setSpanNameColumnWidth}
currentViewRangeTime={viewRange.time.current}
topOfExploreViewRef={topOfExploreViewRef}
topOfViewRef={topOfViewRef}
focusedSpanIdForSearch={focusedSpanIdForSearch}
/>
</div>