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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user