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

View File

@@ -80,7 +80,7 @@ exports[`no enzyme tests`] = {
"packages/jaeger-ui-components/src/TracePageHeader/TracePageHeader.test.js:3242042907": [
[14, 26, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js:2807329716": [
"packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js:1062402339": [
[14, 19, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.test.js:1734982398": [
@@ -113,7 +113,7 @@ exports[`no enzyme tests`] = {
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/TextList.test.js:3006381933": [
[14, 19, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.test.js:3097530078": [
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.test.js:2816619357": [
[16, 19, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.test.js:2623922632": [
@@ -137,7 +137,7 @@ exports[`no enzyme tests`] = {
"packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.js:1423129438": [
[15, 17, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.test.js:2326471104": [
"packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.test.js:551014442": [
[13, 26, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/TraceTimelineViewer/index.test.js:381298544": [

View File

@@ -27,6 +27,7 @@ Grafana offers a variety of visualizations to support different use cases. This
- [Table]({{< relref "./table/_index.md" >}}) is the main and only table visualization.
- [Logs]({{< relref "./logs-panel.md" >}}) is the main visualization for logs.
- [Node Graph]({{< relref "./node-graph.md" >}}) for directed graphs or networks.
- [Traces]({{< relref "./traces.md" >}}) is the main visualization for traces.
- Widgets
- [Dashboard list]({{< relref "./dashboard-list-panel.md" >}}) can list dashboards.
- [Alert list]({{< relref "./alert-list-panel.md" >}}) can list alerts.

View File

@@ -0,0 +1,19 @@
+++
title = "Traces"
keywords = ["grafana", "dashboard", "documentation", "panels", "traces"]
weight = 850
+++
# Traces panel
> **Note:** This panel is currently in beta. Expect changes in future releases.
_Traces_ are a visualization that enables you to track and log a request as it traverses the services in your infrastructure.
For more information about traces and how to use them, refer to the following documentation:
- [What are traces](https://grafana.com/docs/grafana-cloud/traces)
- [Tracing in expliore]({{< relref "../explore/trace-integration.md" >}})
- [Getting started with Tempo](https://grafana.com/docs/tempo/latest/getting-started)
{{< figure src="/static/img/docs/explore/explore-trace-view-full-8-0.png" class="docs-image--no-shadow" max-width= "900px" caption="Screenshot of the trace view" >}}

View File

@@ -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', () => {

View File

@@ -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}

View File

@@ -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 = [
{

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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();

View File

@@ -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}

View File

@@ -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>

View File

@@ -126,6 +126,7 @@ func verifyCorePluginCatalogue(t *testing.T, pm *PluginManager) {
"candlestick": {},
"news": {},
"nodeGraph": {},
"traces": {},
"piechart": {},
"stat": {},
"state-timeline": {},

File diff suppressed because one or more lines are too long

View File

@@ -90,12 +90,6 @@ describe('addPanelToDashboard', () => {
[{ refId: 'A', hide: true }],
{ ...createEmptyQueryResponse(), logsFrames: [new MutableDataFrame({ refId: 'A', fields: [] })] },
],
[
// trace view is not supported in dashboards, we expect to fallback to table panel
'If there are trace frames',
[{ refId: 'A' }],
{ ...createEmptyQueryResponse(), traceFrames: [new MutableDataFrame({ refId: 'A', fields: [] })] },
],
];
it.each(cases)('%s', async (_, queries, queryResponse) => {
@@ -115,15 +109,12 @@ describe('addPanelToDashboard', () => {
framesType: string;
expectedPanel: string;
};
// Note: traceFrames test is "duplicated" in "Defaults to table" tests.
// This is intentional as a way to enforce explicit tests for that case whenever in the future we'll
// add support for creating traceview panels
it.each`
framesType | expectedPanel
${'logsFrames'} | ${'logs'}
${'graphFrames'} | ${'timeseries'}
${'nodeGraphFrames'} | ${'nodeGraph'}
${'traceFrames'} | ${'table'}
${'traceFrames'} | ${'traces'}
`(
'Sets visualization to $expectedPanel if there are $frameType frames',
async ({ framesType, expectedPanel }: TestArgs) => {

View File

@@ -74,6 +74,9 @@ function getPanelType(queries: DataQuery[], queryResponse: ExplorePanelData) {
if (queryResponse.nodeGraphFrames.some(hasQueryRefId)) {
return 'nodeGraph';
}
if (queryResponse.traceFrames.some(hasQueryRefId)) {
return 'traces';
}
}
// falling back to table

View File

@@ -104,7 +104,7 @@ export type Props = ExploreProps & ConnectedProps<typeof connector>;
export class Explore extends React.PureComponent<Props, ExploreState> {
scrollElement: HTMLDivElement | undefined;
absoluteTimeUnsubsciber: Unsubscribable | undefined;
topOfExploreViewRef = createRef<HTMLDivElement>();
topOfViewRef = createRef<HTMLDivElement>();
constructor(props: Props) {
super(props);
@@ -313,8 +313,8 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
dataFrames={dataFrames}
splitOpenFn={splitOpen}
scrollElement={this.scrollElement}
topOfExploreViewRef={this.topOfExploreViewRef}
queryResponse={queryResponse}
topOfViewRef={this.topOfViewRef}
/>
)
);
@@ -357,11 +357,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
autoHeightMin={'100%'}
scrollRefCallback={(scrollElement) => (this.scrollElement = scrollElement || undefined)}
>
<ExploreToolbar
exploreId={exploreId}
onChangeTime={this.onChangeTime}
topOfExploreViewRef={this.topOfExploreViewRef}
/>
<ExploreToolbar exploreId={exploreId} onChangeTime={this.onChangeTime} topOfViewRef={this.topOfViewRef} />
{datasourceMissing ? this.renderEmptyState() : null}
{datasourceInstance && (
<div className="explore-container">

View File

@@ -36,7 +36,7 @@ const AddToDashboard = lazy(() =>
interface OwnProps {
exploreId: ExploreId;
onChangeTime: (range: RawTimeRange, changedByScanner?: boolean) => void;
topOfExploreViewRef?: RefObject<HTMLDivElement>;
topOfViewRef: RefObject<HTMLDivElement>;
}
type Props = OwnProps & ConnectedProps<typeof connector>;
@@ -114,14 +114,14 @@ class UnConnectedExploreToolbar extends PureComponent<Props> {
containerWidth,
onChangeTimeZone,
onChangeFiscalYearStartMonth,
topOfExploreViewRef,
topOfViewRef,
} = this.props;
const showSmallDataSourcePicker = (splitted ? containerWidth < 700 : containerWidth < 800) || false;
const showSmallTimePicker = splitted || containerWidth < 1210;
return (
<div ref={topOfExploreViewRef}>
<div ref={topOfViewRef}>
<PageToolbar
aria-label="Explore toolbar"
title={exploreId === ExploreId.left ? 'Explore' : undefined}

View File

@@ -1,7 +1,8 @@
import { TopOfViewRefType } from '@jaegertracing/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView';
import { TraceData, TraceSpanData } from '@jaegertracing/jaeger-ui-components/src/types/trace';
import { render, prettyDOM, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import React, { createRef } from 'react';
import { Provider } from 'react-redux';
import { DataFrame, MutableDataFrame, getDefaultTimeRange, LoadingState } from '@grafana/data';
@@ -20,6 +21,7 @@ function getTraceView(frames: DataFrame[]) {
series: [],
timeRange: getDefaultTimeRange(),
};
const topOfViewRef = createRef<HTMLDivElement>();
const traceView = (
<Provider store={store}>
@@ -30,13 +32,10 @@ function getTraceView(frames: DataFrame[]) {
traceProp={transformDataFrames(frames[0])!}
search=""
focusedSpanIdForSearch=""
expandOne={() => {}}
expandAll={() => {}}
collapseOne={() => {}}
collapseAll={() => {}}
childrenToggle={() => {}}
childrenHiddenIDs={new Set()}
queryResponse={mockPanelData}
datasource={undefined}
topOfViewRef={topOfViewRef}
topOfViewRefType={TopOfViewRefType.Explore}
/>
</Provider>
);
@@ -85,10 +84,10 @@ describe('TraceView', () => {
expect(prettyDOM(baseElement)).toEqual(prettyDOM(baseElementOld));
});
it('does not render anything on missing trace', () => {
it('only renders noDataMsg on missing trace', () => {
// Simulating Explore's access to empty response data
const { container } = renderTraceView([]);
expect(container.hasChildNodes()).toBeFalsy();
expect(container.childNodes.length === 1).toBeTruthy();
});
it('toggles detailState', async () => {

View File

@@ -1,25 +1,24 @@
import React, { RefObject, useCallback, useEffect, useMemo, useState } from 'react';
import { css } from '@emotion/css';
import { TopOfViewRefType } from '@jaegertracing/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView';
import React, { RefObject, useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
DataFrame,
DataLink,
DataQuery,
DataSourceApi,
DataSourceJsonData,
Field,
GrafanaTheme2,
LinkModel,
LoadingState,
mapInternalLinkToExplore,
PanelData,
SplitOpen,
} from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime';
import {
Trace,
TracePageHeader,
TraceSpan,
TraceTimelineViewer,
TTraceTimeline,
} from '@jaegertracing/jaeger-ui-components';
import { useStyles2 } from '@grafana/ui';
import { Trace, TracePageHeader, TraceTimelineViewer, TTraceTimeline } from '@jaegertracing/jaeger-ui-components';
import { TraceToLogsData } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { getTimeZone } from 'app/features/profile/state/selectors';
@@ -29,44 +28,43 @@ import { ExploreId } from 'app/types/explore';
import { changePanelState } from '../state/explorePane';
import { createSpanLinkFactory } from './createSpanLink';
import { useChildrenState } from './useChildrenState';
import { useDetailState } from './useDetailState';
import { useHoverIndentGuide } from './useHoverIndentGuide';
import { useViewRange } from './useViewRange';
const getStyles = (theme: GrafanaTheme2) => ({
noDataMsg: css`
height: 100%;
width: 100%;
display: grid;
place-items: center;
font-size: ${theme.typography.h4.fontSize};
color: ${theme.colors.text.secondary};
`,
});
function noop(): {} {
return {};
}
type Props = {
dataFrames: DataFrame[];
splitOpenFn: SplitOpen;
exploreId: ExploreId;
splitOpenFn?: SplitOpen;
exploreId?: ExploreId;
scrollElement?: Element;
topOfExploreViewRef?: RefObject<HTMLDivElement>;
traceProp: Trace;
spanFindMatches?: Set<string>;
search: string;
focusedSpanIdForSearch: string;
expandOne: (spans: TraceSpan[]) => void;
expandAll: () => void;
collapseOne: (spans: TraceSpan[]) => void;
collapseAll: (spans: TraceSpan[]) => void;
childrenToggle: (spanId: string) => void;
childrenHiddenIDs: Set<string>;
queryResponse: PanelData;
datasource: DataSourceApi<DataQuery, DataSourceJsonData, {}> | undefined;
topOfViewRef: RefObject<HTMLDivElement>;
topOfViewRefType: TopOfViewRefType;
};
export function TraceView(props: Props) {
const {
expandOne,
expandAll,
collapseOne,
collapseAll,
childrenToggle,
childrenHiddenIDs,
spanFindMatches,
traceProp,
} = props;
const { spanFindMatches, traceProp, datasource, topOfViewRef, topOfViewRefType } = props;
const {
detailStates,
@@ -83,6 +81,9 @@ export function TraceView(props: Props) {
const { removeHoverIndentGuideId, addHoverIndentGuideId, hoverIndentGuideIds } = useHoverIndentGuide();
const { viewRange, updateViewRangeTime, updateNextViewRangeTime } = useViewRange();
const { expandOne, collapseOne, childrenToggle, collapseAll, childrenHiddenIDs, expandAll } = useChildrenState();
const styles = useStyles2(getStyles);
/**
* Keeps state of resizable name column width
@@ -93,13 +94,9 @@ export function TraceView(props: Props) {
*/
const [slim, setSlim] = useState(false);
const datasource = useSelector(
(state: StoreState) => state.explore[props.exploreId]?.datasourceInstance ?? undefined
);
const [focusedSpanId, createFocusSpanLink] = useFocusSpanLink({
refId: props.dataFrames[0]?.refId,
exploreId: props.exploreId,
exploreId: props.exploreId!,
datasource,
});
@@ -120,79 +117,77 @@ export function TraceView(props: Props) {
[childrenHiddenIDs, detailStates, hoverIndentGuideIds, spanNameColumnWidth, props.traceProp?.traceID]
);
useEffect(() => {
if (props.queryResponse.state === LoadingState.Done) {
props.topOfExploreViewRef?.current?.scrollIntoView();
}
}, [props.queryResponse, props.topOfExploreViewRef]);
const traceToLogsOptions = (getDatasourceSrv().getInstanceSettings(datasource?.name)?.jsonData as TraceToLogsData)
?.tracesToLogs;
const createSpanLink = useMemo(
() => createSpanLinkFactory({ splitOpenFn: props.splitOpenFn, traceToLogsOptions, dataFrame: props.dataFrames[0] }),
() =>
createSpanLinkFactory({ splitOpenFn: props.splitOpenFn!, traceToLogsOptions, dataFrame: props.dataFrames[0] }),
[props.splitOpenFn, traceToLogsOptions, props.dataFrames]
);
const onSlimViewClicked = useCallback(() => setSlim(!slim), [slim]);
const timeZone = useSelector((state: StoreState) => getTimeZone(state.user));
if (!props.dataFrames?.length || !traceProp) {
return null;
}
return (
<>
<TracePageHeader
canCollapse={false}
hideMap={false}
hideSummary={false}
onSlimViewClicked={onSlimViewClicked}
onTraceGraphViewClicked={noop}
slimView={slim}
trace={traceProp}
updateNextViewRangeTime={updateNextViewRangeTime}
updateViewRangeTime={updateViewRangeTime}
viewRange={viewRange}
timeZone={timeZone}
/>
<TraceTimelineViewer
registerAccessors={noop}
scrollToFirstVisibleSpan={noop}
findMatchesIDs={spanFindMatches}
trace={traceProp}
traceTimeline={traceTimeline}
updateNextViewRangeTime={updateNextViewRangeTime}
updateViewRangeTime={updateViewRangeTime}
viewRange={viewRange}
focusSpan={noop}
createLinkToExternalSpan={createLinkToExternalSpan}
setSpanNameColumnWidth={setSpanNameColumnWidth}
collapseAll={collapseAll}
collapseOne={collapseOne}
expandAll={expandAll}
expandOne={expandOne}
childrenToggle={childrenToggle}
clearShouldScrollToFirstUiFindMatch={noop}
detailLogItemToggle={detailLogItemToggle}
detailLogsToggle={detailLogsToggle}
detailWarningsToggle={detailWarningsToggle}
detailStackTracesToggle={detailStackTracesToggle}
detailReferencesToggle={detailReferencesToggle}
detailReferenceItemToggle={detailReferenceItemToggle}
detailProcessToggle={detailProcessToggle}
detailTagsToggle={detailTagsToggle}
detailToggle={toggleDetail}
setTrace={noop}
addHoverIndentGuideId={addHoverIndentGuideId}
removeHoverIndentGuideId={removeHoverIndentGuideId}
linksGetter={noop as any}
uiFind={props.search}
createSpanLink={createSpanLink}
scrollElement={props.scrollElement}
focusedSpanId={focusedSpanId}
focusedSpanIdForSearch={props.focusedSpanIdForSearch}
createFocusSpanLink={createFocusSpanLink}
topOfExploreViewRef={props.topOfExploreViewRef}
/>
{props.dataFrames?.length && props.dataFrames[0]?.meta?.preferredVisualisationType === 'trace' && traceProp ? (
<>
<TracePageHeader
canCollapse={false}
hideMap={false}
hideSummary={false}
onSlimViewClicked={onSlimViewClicked}
onTraceGraphViewClicked={noop}
slimView={slim}
trace={traceProp}
updateNextViewRangeTime={updateNextViewRangeTime}
updateViewRangeTime={updateViewRangeTime}
viewRange={viewRange}
timeZone={timeZone}
/>
<TraceTimelineViewer
registerAccessors={noop}
scrollToFirstVisibleSpan={noop}
findMatchesIDs={spanFindMatches}
trace={traceProp}
traceTimeline={traceTimeline}
updateNextViewRangeTime={updateNextViewRangeTime}
updateViewRangeTime={updateViewRangeTime}
viewRange={viewRange}
focusSpan={noop}
createLinkToExternalSpan={createLinkToExternalSpan}
setSpanNameColumnWidth={setSpanNameColumnWidth}
collapseAll={collapseAll}
collapseOne={collapseOne}
expandAll={expandAll}
expandOne={expandOne}
childrenToggle={childrenToggle}
clearShouldScrollToFirstUiFindMatch={noop}
detailLogItemToggle={detailLogItemToggle}
detailLogsToggle={detailLogsToggle}
detailWarningsToggle={detailWarningsToggle}
detailStackTracesToggle={detailStackTracesToggle}
detailReferencesToggle={detailReferencesToggle}
detailReferenceItemToggle={detailReferenceItemToggle}
detailProcessToggle={detailProcessToggle}
detailTagsToggle={detailTagsToggle}
detailToggle={toggleDetail}
setTrace={noop}
addHoverIndentGuideId={addHoverIndentGuideId}
removeHoverIndentGuideId={removeHoverIndentGuideId}
linksGetter={noop as any}
uiFind={props.search}
createSpanLink={createSpanLink}
scrollElement={props.scrollElement}
focusedSpanId={focusedSpanId}
focusedSpanIdForSearch={props.focusedSpanIdForSearch!}
createFocusSpanLink={createFocusSpanLink}
topOfViewRef={topOfViewRef}
topOfViewRefType={topOfViewRefType}
/>
</>
) : (
<div className={styles.noDataMsg}>No data</div>
)}
</>
);
}

View File

@@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import React, { createRef } from 'react';
import { Provider } from 'react-redux';
import { getDefaultTimeRange, LoadingState } from '@grafana/data';
@@ -18,6 +18,7 @@ function renderTraceViewContainer(frames = [frameOld]) {
series: [],
timeRange: getDefaultTimeRange(),
};
const topOfViewRef = createRef<HTMLDivElement>();
const { container, baseElement } = render(
<Provider store={store}>
@@ -26,6 +27,7 @@ function renderTraceViewContainer(frames = [frameOld]) {
dataFrames={frames}
splitOpenFn={() => {}}
queryResponse={mockPanelData}
topOfViewRef={topOfViewRef}
/>
</Provider>
);

View File

@@ -1,12 +1,14 @@
import TracePageSearchBar from '@jaegertracing/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar';
import { TopOfViewRefType } from '@jaegertracing/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView';
import React, { RefObject, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { DataFrame, SplitOpen, PanelData } from '@grafana/data';
import { Collapse } from '@grafana/ui';
import { StoreState } from 'app/types';
import { ExploreId } from 'app/types/explore';
import { TraceView } from './TraceView';
import { useChildrenState } from './useChildrenState';
import { useSearch } from './useSearch';
import { transformDataFrames } from './utils/transform';
interface Props {
@@ -14,71 +16,20 @@ interface Props {
splitOpenFn: SplitOpen;
exploreId: ExploreId;
scrollElement?: Element;
topOfExploreViewRef?: RefObject<HTMLDivElement>;
queryResponse: PanelData;
topOfViewRef: RefObject<HTMLDivElement>;
}
export function TraceViewContainer(props: Props) {
// At this point we only show single trace
const frame = props.dataFrames[0];
const { dataFrames, splitOpenFn, exploreId, scrollElement, topOfExploreViewRef, queryResponse } = props;
const { dataFrames, splitOpenFn, exploreId, scrollElement, topOfViewRef, queryResponse } = props;
const traceProp = useMemo(() => transformDataFrames(frame), [frame]);
const { search, setSearch, spanFindMatches } = useSearch(traceProp?.spans);
const { expandOne, collapseOne, childrenToggle, collapseAll, childrenHiddenIDs, expandAll } = useChildrenState();
const [focusedSpanIdForSearch, setFocusedSpanIdForSearch] = useState('');
const [searchBarSuffix, setSearchBarSuffix] = useState('');
const setTraceSearch = (value: string) => {
setFocusedSpanIdForSearch('');
setSearchBarSuffix('');
setSearch(value);
};
const nextResult = () => {
expandAll();
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 = () => {
expandAll();
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 '';
};
const datasource = useSelector(
(state: StoreState) => state.explore[props.exploreId!]?.datasourceInstance ?? undefined
);
if (!traceProp) {
return null;
@@ -87,12 +38,14 @@ export function TraceViewContainer(props: Props) {
return (
<>
<TracePageSearchBar
nextResult={nextResult}
prevResult={prevResult}
navigable={true}
searchValue={search}
onSearchValueChange={setTraceSearch}
setSearch={setSearch}
spanFindMatches={spanFindMatches}
searchBarSuffix={searchBarSuffix}
setSearchBarSuffix={setSearchBarSuffix}
focusedSpanIdForSearch={focusedSpanIdForSearch}
setFocusedSpanIdForSearch={setFocusedSpanIdForSearch}
/>
<Collapse label="Trace View" isOpen>
@@ -101,18 +54,14 @@ export function TraceViewContainer(props: Props) {
dataFrames={dataFrames}
splitOpenFn={splitOpenFn}
scrollElement={scrollElement}
topOfExploreViewRef={topOfExploreViewRef}
traceProp={traceProp}
spanFindMatches={spanFindMatches}
search={search}
focusedSpanIdForSearch={focusedSpanIdForSearch}
expandOne={expandOne}
collapseOne={collapseOne}
collapseAll={collapseAll}
expandAll={expandAll}
childrenToggle={childrenToggle}
childrenHiddenIDs={childrenHiddenIDs}
queryResponse={queryResponse}
datasource={datasource}
topOfViewRef={topOfViewRef}
topOfViewRefType={TopOfViewRefType.Explore}
/>
</Collapse>
</>

View File

@@ -63,6 +63,7 @@ import * as statusHistoryPanel from 'app/plugins/panel/status-history/module';
import * as tablePanel from 'app/plugins/panel/table/module';
import * as textPanel from 'app/plugins/panel/text/module';
import * as timeseriesPanel from 'app/plugins/panel/timeseries/module';
import * as tracesPanel from 'app/plugins/panel/traces/module';
import * as welcomeBanner from 'app/plugins/panel/welcome/module';
import * as xyChartPanel from 'app/plugins/panel/xychart/module';
@@ -125,6 +126,7 @@ const builtInPlugins: any = {
'app/plugins/panel/bargauge/module': barGaugePanel,
'app/plugins/panel/barchart/module': barChartPanel,
'app/plugins/panel/logs/module': logsPanel,
'app/plugins/panel/traces/module': tracesPanel,
'app/plugins/panel/welcome/module': welcomeBanner,
'app/plugins/panel/nodeGraph/module': nodeGraph,
'app/plugins/panel/histogram/module': histogramPanel,

View File

@@ -5,6 +5,7 @@ import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { Node } from 'slate';
import { GrafanaTheme2, isValidGoDuration, SelectableValue } from '@grafana/data';
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
import {
InlineFieldRow,
InlineField,
@@ -152,6 +153,8 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props
}
};
const templateSrv: TemplateSrv = getTemplateSrv();
return (
<>
<div className={styles.container}>
@@ -235,7 +238,8 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props
value={query.minDuration || ''}
placeholder={durationPlaceholder}
onBlur={() => {
if (query.minDuration && !isValidGoDuration(query.minDuration)) {
const templatedMinDuration = templateSrv.replace(query.minDuration ?? '');
if (query.minDuration && !isValidGoDuration(templatedMinDuration)) {
setInputErrors({ ...inputErrors, minDuration: true });
} else {
setInputErrors({ ...inputErrors, minDuration: false });
@@ -258,7 +262,8 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props
value={query.maxDuration || ''}
placeholder={durationPlaceholder}
onBlur={() => {
if (query.maxDuration && !isValidGoDuration(query.maxDuration)) {
const templatedMaxDuration = templateSrv.replace(query.maxDuration ?? '');
if (query.maxDuration && !isValidGoDuration(templatedMaxDuration)) {
setInputErrors({ ...inputErrors, maxDuration: true });
} else {
setInputErrors({ ...inputErrors, maxDuration: false });

View File

@@ -32,6 +32,57 @@ describe('Tempo data source', () => {
expect(response).toBe('empty');
});
describe('Variables should be interpolated correctly', () => {
function getQuery(): TempoQuery {
return {
refId: 'x',
queryType: 'traceId',
linkedQuery: {
refId: 'linked',
expr: '{instance="$interpolationVar"}',
},
query: '$interpolationVar',
search: '$interpolationVar',
minDuration: '$interpolationVar',
maxDuration: '$interpolationVar',
};
}
it('when traceId query for dashboard->explore', async () => {
const templateSrv: any = { replace: jest.fn() };
const ds = new TempoDatasource(defaultSettings, templateSrv);
const text = 'interpolationText';
templateSrv.replace.mockReturnValue(text);
const queries = ds.interpolateVariablesInQueries([getQuery()], {
interpolationVar: { text: text, value: text },
});
expect(templateSrv.replace).toBeCalledTimes(5);
expect(queries[0].linkedQuery?.expr).toBe(text);
expect(queries[0].query).toBe(text);
expect(queries[0].search).toBe(text);
expect(queries[0].minDuration).toBe(text);
expect(queries[0].maxDuration).toBe(text);
});
it('when traceId query for template variable', async () => {
const templateSrv: any = { replace: jest.fn() };
const ds = new TempoDatasource(defaultSettings, templateSrv);
const text = 'interpolationText';
templateSrv.replace.mockReturnValue(text);
const resp = ds.applyTemplateVariables(getQuery(), {
interpolationVar: { text: text, value: text },
});
expect(templateSrv.replace).toBeCalledTimes(5);
expect(resp.linkedQuery?.expr).toBe(text);
expect(resp.query).toBe(text);
expect(resp.search).toBe(text);
expect(resp.minDuration).toBe(text);
expect(resp.maxDuration).toBe(text);
});
});
it('parses json fields from backend', async () => {
setupBackendSrv(
new MutableDataFrame({
@@ -49,7 +100,8 @@ describe('Tempo data source', () => {
],
})
);
const ds = new TempoDatasource(defaultSettings);
const templateSrv: any = { replace: jest.fn() };
const ds = new TempoDatasource(defaultSettings, templateSrv);
const response = await lastValueFrom(ds.query({ targets: [{ refId: 'refid1', query: '12345' }] } as any));
expect(
@@ -152,7 +204,10 @@ describe('Tempo data source', () => {
});
it('should build search query correctly', () => {
const ds = new TempoDatasource(defaultSettings);
const templateSrv: any = { replace: jest.fn() };
const ds = new TempoDatasource(defaultSettings, templateSrv);
const duration = '10ms';
templateSrv.replace.mockReturnValue(duration);
const tempoQuery: TempoQuery = {
queryType: 'search',
refId: 'A',
@@ -160,15 +215,15 @@ describe('Tempo data source', () => {
serviceName: 'frontend',
spanName: '/config',
search: 'root.http.status_code=500',
minDuration: '1ms',
maxDuration: '100s',
minDuration: '$interpolationVar',
maxDuration: '$interpolationVar',
limit: 10,
};
const builtQuery = ds.buildSearchQuery(tempoQuery);
expect(builtQuery).toStrictEqual({
tags: 'root.http.status_code=500 service.name="frontend" name="/config"',
minDuration: '1ms',
maxDuration: '100s',
minDuration: duration,
maxDuration: duration,
limit: 10,
});
});

View File

@@ -11,8 +11,16 @@ import {
DataSourceJsonData,
isValidGoDuration,
LoadingState,
ScopedVars,
} from '@grafana/data';
import { config, BackendSrvRequest, DataSourceWithBackend, getBackendSrv } from '@grafana/runtime';
import {
config,
BackendSrvRequest,
DataSourceWithBackend,
getBackendSrv,
TemplateSrv,
getTemplateSrv,
} from '@grafana/runtime';
import { NodeGraphOptions } from 'app/core/components/NodeGraphSettings';
import { TraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
import { serializeParams } from 'app/core/utils/fetch';
@@ -92,7 +100,10 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
};
uploadedJson?: string | ArrayBuffer | null = null;
constructor(private instanceSettings: DataSourceInstanceSettings<TempoJsonData>) {
constructor(
private instanceSettings: DataSourceInstanceSettings<TempoJsonData>,
private readonly templateSrv: TemplateSrv = getTemplateSrv()
) {
super(instanceSettings);
this.tracesToLogs = instanceSettings.jsonData.tracesToLogs;
this.serviceMap = instanceSettings.jsonData.serviceMap;
@@ -151,7 +162,8 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
const timeRange = config.featureToggles.tempoBackendSearch
? { startTime: options.range.from.unix(), endTime: options.range.to.unix() }
: undefined;
const searchQuery = this.buildSearchQuery(targets.nativeSearch[0], timeRange);
const query = this.applyVariables(targets.nativeSearch[0], options.scopedVars);
const searchQuery = this.buildSearchQuery(query, timeRange);
subQueries.push(
this._request('/api/search', searchQuery).pipe(
map((response) => {
@@ -193,6 +205,43 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
return merge(...subQueries);
}
applyTemplateVariables(query: TempoQuery, scopedVars: ScopedVars): Record<string, any> {
return this.applyVariables(query, scopedVars);
}
interpolateVariablesInQueries(queries: TempoQuery[], scopedVars: ScopedVars): TempoQuery[] {
if (!queries || queries.length === 0) {
return [];
}
return queries.map((query) => {
return {
...query,
datasource: this.getRef(),
...this.applyVariables(query, scopedVars),
};
});
}
applyVariables(query: TempoQuery, scopedVars: ScopedVars) {
const expandedQuery = { ...query };
if (query.linkedQuery) {
expandedQuery.linkedQuery = {
...query.linkedQuery,
expr: this.templateSrv.replace(query.linkedQuery?.expr ?? '', scopedVars),
};
}
return {
...expandedQuery,
query: this.templateSrv.replace(query.query ?? '', scopedVars),
search: this.templateSrv.replace(query.search ?? '', scopedVars),
minDuration: this.templateSrv.replace(query.minDuration ?? '', scopedVars),
maxDuration: this.templateSrv.replace(query.maxDuration ?? '', scopedVars),
};
}
/**
* Handles the simplest of the queries where we have just a trace id and return trace data for it.
* @param options
@@ -278,12 +327,14 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
// Validate query inputs and remove spaces if valid
if (tempoQuery.minDuration) {
tempoQuery.minDuration = this.templateSrv.replace(tempoQuery.minDuration ?? '');
if (!isValidGoDuration(tempoQuery.minDuration)) {
throw new Error('Please enter a valid min duration.');
}
tempoQuery.minDuration = tempoQuery.minDuration.replace(/\s/g, '');
}
if (tempoQuery.maxDuration) {
tempoQuery.maxDuration = this.templateSrv.replace(tempoQuery.maxDuration ?? '');
if (!isValidGoDuration(tempoQuery.maxDuration)) {
throw new Error('Please enter a valid max duration.');
}

View File

@@ -0,0 +1,22 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { LoadingState, PanelProps } from '@grafana/data';
import { TracesPanel } from './TracesPanel';
describe('TracesPanel', () => {
it('shows no data message when no data supplied', async () => {
const props = {
data: {
error: undefined,
series: [],
state: LoadingState.Done,
},
} as unknown as PanelProps;
render(<TracesPanel {...props} />);
await screen.findByText('No data found in response');
});
});

View File

@@ -0,0 +1,69 @@
import { css } from '@emotion/css';
import TracePageSearchBar from '@jaegertracing/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar';
import { TopOfViewRefType } from '@jaegertracing/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView';
import React, { useMemo, useState, createRef } from 'react';
import { useAsync } from 'react-use';
import { PanelProps } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { TraceView } from 'app/features/explore/TraceView/TraceView';
import { useSearch } from 'app/features/explore/TraceView/useSearch';
import { transformDataFrames } from 'app/features/explore/TraceView/utils/transform';
const styles = {
wrapper: css`
height: 100%;
overflow: scroll;
`,
};
export const TracesPanel: React.FunctionComponent<PanelProps> = ({ data }) => {
const topOfViewRef = createRef<HTMLDivElement>();
const traceProp = useMemo(() => transformDataFrames(data.series[0]), [data.series]);
const { search, setSearch, spanFindMatches } = useSearch(traceProp?.spans);
const [focusedSpanIdForSearch, setFocusedSpanIdForSearch] = useState('');
const [searchBarSuffix, setSearchBarSuffix] = useState('');
const dataSource = useAsync(async () => {
return await getDataSourceSrv().get(data.request?.targets[0].datasource?.uid);
});
const scrollElement = document.getElementsByClassName(styles.wrapper)[0];
if (!data || !data.series.length || !traceProp) {
return (
<div className="panel-empty">
<p>No data found in response</p>
</div>
);
}
return (
<div className={styles.wrapper}>
<div ref={topOfViewRef}></div>
{data.series[0]?.meta?.preferredVisualisationType === 'trace' ? (
<TracePageSearchBar
navigable={true}
searchValue={search}
setSearch={setSearch}
spanFindMatches={spanFindMatches}
searchBarSuffix={searchBarSuffix}
setSearchBarSuffix={setSearchBarSuffix}
focusedSpanIdForSearch={focusedSpanIdForSearch}
setFocusedSpanIdForSearch={setFocusedSpanIdForSearch}
/>
) : null}
<TraceView
dataFrames={data.series}
scrollElement={scrollElement}
traceProp={traceProp}
spanFindMatches={spanFindMatches}
search={search}
focusedSpanIdForSearch={focusedSpanIdForSearch}
queryResponse={data}
datasource={dataSource.value}
topOfViewRef={topOfViewRef}
topOfViewRefType={TopOfViewRefType.Panel}
/>
</div>
);
};

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 280.12 263.29"><defs><style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:#1f60c4;}</style><linearGradient id="linear-gradient" x1="181.23" y1="167.35" x2="280.12" y2="167.35" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f2cc0c"/><stop offset="1" stop-color="#ff9830"/></linearGradient></defs><g id="Layer_2" data-name="Layer 2"><g id="Icons"><rect class="cls-1" x="181.23" y="148.69" width="98.89" height="37.33"/><rect class="cls-2" x="153.84" y="142.83" width="12.44" height="49.04"/><rect class="cls-2" x="27.39" y="220.11" width="252.73" height="37.33"/><rect class="cls-2" y="214.25" width="12.44" height="49.04"/><rect class="cls-2" x="81.85" y="77.27" width="133.33" height="37.33"/><rect class="cls-2" x="8.08" y="71.42" width="12.44" height="49.03"/><rect class="cls-2" x="35.46" y="5.85" width="244.65" height="37.33"/><rect class="cls-2" x="8.08" width="12.44" height="49.03"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1016 B

View File

@@ -0,0 +1,5 @@
import { PanelPlugin } from '@grafana/data';
import { TracesPanel } from './TracesPanel';
export const plugin = new PanelPlugin(TracesPanel);

View File

@@ -0,0 +1,17 @@
{
"type": "panel",
"name": "Traces",
"id": "traces",
"state": "beta",
"info": {
"author": {
"name": "Grafana Labs",
"url": "https://grafana.com"
},
"logos": {
"small": "img/traces-panel.svg",
"large": "img/traces-panel.svg"
}
}
}