', () => {
it('renders buttons', () => {
const buttons = wrapper.find('Button');
- expect(buttons.length).toBe(4);
+ expect(buttons.length).toBe(2);
buttons.forEach((button) => {
expect(button.prop('disabled')).toBe(true);
});
diff --git a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx
index 9e2df0446fe..4eef52b5ef0 100644
--- a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx
+++ b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx
@@ -14,7 +14,6 @@
import * as React from 'react';
import cx from 'classnames';
-import IoAndroidLocate from 'react-icons/lib/io/android-locate';
import { css } from '@emotion/css';
import { Button, useStyles2 } from '@grafana/ui';
@@ -24,11 +23,24 @@ import UiFindInput from '../common/UiFindInput';
import { ubFlexAuto, ubJustifyEnd } from '../uberUtilityStyles';
// eslint-disable-next-line no-duplicate-imports
import { memo } from 'react';
+import { GrafanaTheme2 } from '@grafana/data';
-export const getStyles = () => {
+export const getStyles = (theme: GrafanaTheme2) => {
return {
TracePageSearchBar: css`
label: TracePageSearchBar;
+ float: right;
+ position: sticky;
+ top: 8px;
+ right: 0;
+ z-index: ${theme.zIndex.navbarFixed};
+ background: ${theme.colors.background.primary};
+ margin-top: 8px;
+ margin-bottom: -48px;
+ padding: 8px;
+ margin-right: 2px;
+ border-radius: 4px;
+ box-shadow: ${theme.shadows.z2};
`,
TracePageSearchBarBar: css`
label: TracePageSearchBarBar;
@@ -38,14 +50,14 @@ export const getStyles = () => {
max-width: 100%;
}
`,
- TracePageSearchBarCount: css`
- label: TracePageSearchBarCount;
+ TracePageSearchBarSuffix: css`
+ label: TracePageSearchBarSuffix;
opacity: 0.6;
`,
TracePageSearchBarBtn: css`
label: TracePageSearchBarBtn;
- border-left: none;
transition: 0.2s;
+ margin-left: 8px;
`,
TracePageSearchBarBtnDisabled: css`
label: TracePageSearchBarBtnDisabled;
@@ -61,72 +73,62 @@ export const getStyles = () => {
type TracePageSearchBarProps = {
prevResult: () => void;
nextResult: () => void;
- clearSearch: () => void;
- focusUiFindMatches: () => void;
- resultCount: number;
navigable: boolean;
searchValue: string;
onSearchValueChange: (value: string) => void;
+ searchBarSuffix: string;
};
export default memo(function TracePageSearchBar(props: TracePageSearchBarProps) {
- const {
- clearSearch,
- focusUiFindMatches,
- navigable,
- nextResult,
- prevResult,
- resultCount,
- onSearchValueChange,
- searchValue,
- } = props;
+ const { navigable, nextResult, prevResult, onSearchValueChange, searchValue, searchBarSuffix } = props;
const styles = useStyles2(getStyles);
- const count = searchValue ? {resultCount} : null;
+ const suffix = searchValue ? (
+
+ {searchBarSuffix}
+
+ ) : null;
const btnClass = cx(styles.TracePageSearchBarBtn, { [styles.TracePageSearchBarBtnDisabled]: !searchValue });
const uiFindInputInputProps = {
'data-test': markers.IN_TRACE_SEARCH,
className: cx(styles.TracePageSearchBarBar, ubFlexAuto),
name: 'search',
- suffix: count,
+ suffix,
};
return (
-
+
<>
{navigable && (
<>
-
-
+
>
)}
-
>
diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx
index 0244e410252..10321578de1 100644
--- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx
+++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx
@@ -166,17 +166,17 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
`,
rowMatchingFilter: css`
label: rowMatchingFilter;
- background-color: ${autoColor(theme, '#fffce4')};
+ background-color: ${autoColor(theme, '#fffbde')};
&:hover .${nameWrapperClassName} {
background: linear-gradient(
90deg,
- ${autoColor(theme, '#fff5e1')},
- ${autoColor(theme, '#fff5e1')} 75%,
- ${autoColor(theme, '#ffe6c9')}
+ ${autoColor(theme, '#fffbde')},
+ ${autoColor(theme, '#fffbde')} 75%,
+ ${autoColor(theme, '#f7f1c6')}
);
}
&:hover .${viewClassName} {
- background-color: ${autoColor(theme, '#fff3d7')};
+ background-color: ${autoColor(theme, '#f7f1c6')};
outline: 1px solid ${autoColor(theme, '#ddd')};
}
`,
diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx
index d808eb07f5f..b365fcc9919 100644
--- a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx
+++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx
@@ -101,6 +101,7 @@ type TVirtualizedTraceViewOwnProps = {
createSpanLink?: SpanLinkFunc;
scrollElement?: Element;
focusedSpanId?: string;
+ focusedSpanIdForSearch: string;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
topOfExploreViewRef?: RefObject;
};
@@ -223,6 +224,7 @@ export class UnthemedVirtualizedTraceView extends React.Component LinkModel;
topOfExploreViewRef?: RefObject;
};
@@ -163,6 +164,7 @@ export class UnthemedTraceTimelineViewer extends React.PureComponent
diff --git a/public/app/features/explore/TraceView/TraceView.test.tsx b/public/app/features/explore/TraceView/TraceView.test.tsx
index 3ebd18bbad6..240d90bcb40 100644
--- a/public/app/features/explore/TraceView/TraceView.test.tsx
+++ b/public/app/features/explore/TraceView/TraceView.test.tsx
@@ -4,12 +4,13 @@ import { TraceView } from './TraceView';
import { setDataSourceSrv } from '@grafana/runtime';
import { ExploreId } from 'app/types';
import { TraceData, TraceSpanData } from '@jaegertracing/jaeger-ui-components/src/types/trace';
-import { getDefaultTimeRange, LoadingState, MutableDataFrame } from '@grafana/data';
+import { DataFrame, MutableDataFrame, getDefaultTimeRange, LoadingState } from '@grafana/data';
import { configureStore } from '../../../store/configureStore';
import { Provider } from 'react-redux';
+import { transformDataFrames } from './utils/transform';
import userEvent from '@testing-library/user-event';
-function renderTraceView(frames = [frameOld]) {
+function getTraceView(frames: DataFrame[]) {
const store = configureStore();
const mockPanelData = {
state: LoadingState.Done,
@@ -17,11 +18,31 @@ function renderTraceView(frames = [frameOld]) {
timeRange: getDefaultTimeRange(),
};
- const { container, baseElement } = render(
+ const traceView = (
- {}} queryResponse={mockPanelData} />
+ {}}
+ traceProp={transformDataFrames(frames[0])!}
+ search=""
+ focusedSpanIdForSearch=""
+ expandOne={() => {}}
+ expandAll={() => {}}
+ collapseOne={() => {}}
+ collapseAll={() => {}}
+ childrenToggle={() => {}}
+ childrenHiddenIDs={new Set()}
+ queryResponse={mockPanelData}
+ />
);
+ return traceView;
+}
+
+function renderTraceView(frames = [frameOld]) {
+ const { container, baseElement } = render(getTraceView(frames));
+
return {
header: container.children[0],
timeline: container.children[1],
@@ -79,42 +100,6 @@ describe('TraceView', () => {
expect(screen.queryByText(/Tags/)).toBeFalsy();
});
- it('toggles children visibility', () => {
- renderTraceViewNew();
- expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
- userEvent.click(screen.getAllByText('', { selector: 'span[data-test-id="SpanTreeOffset--indentGuide"]' })[0]);
- expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(1);
-
- userEvent.click(screen.getAllByText('', { selector: 'span[data-test-id="SpanTreeOffset--indentGuide"]' })[0]);
- expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
- });
-
- it('toggles collapses and expands one level of spans', () => {
- renderTraceViewNew();
- expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
- userEvent.click(screen.getByLabelText('Collapse +1'));
- expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(2);
- userEvent.click(screen.getByLabelText('Expand +1'));
- expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
- });
-
- it('toggles collapses and expands all levels', () => {
- renderTraceViewNew();
- expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
- userEvent.click(screen.getByLabelText('Collapse All'));
- expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(1);
- userEvent.click(screen.getByLabelText('Expand All'));
- expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
- });
-
- it('searches for spans', () => {
- renderTraceViewNew();
- userEvent.type(screen.getByPlaceholderText('Find...'), '1ed38015486087ca');
- expect(
- (screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' })[0].parentNode! as HTMLElement).className
- ).toContain('rowMatchingFilter');
- });
-
it('shows timeline ticks', () => {
renderTraceViewNew();
function ticks() {
@@ -151,38 +136,13 @@ describe('TraceView', () => {
});
it('resets detail view for new trace with the identical spanID', () => {
- const store = configureStore();
- const mockPanelData = {
- state: LoadingState.Done,
- series: [],
- timeRange: getDefaultTimeRange(),
- };
-
- const { rerender } = render(
-
- {}}
- queryResponse={mockPanelData}
- />
-
- );
+ const { rerender } = render(getTraceView([frameOld]));
const span = screen.getAllByText('', { selector: 'div[data-test-id="span-view"]' })[2];
userEvent.click(span);
//Process is in detail view
expect(screen.getByText(/Process/)).toBeInTheDocument();
- rerender(
-
- {}}
- queryResponse={mockPanelData}
- />
-
- );
+ rerender(getTraceView([frameNew]));
expect(screen.queryByText(/Process/)).not.toBeInTheDocument();
});
});
@@ -298,7 +258,7 @@ const response: TraceData & { spans: TraceSpanData[] } = {
warnings: null as any,
};
-const frameOld = new MutableDataFrame({
+export const frameOld = new MutableDataFrame({
fields: [
{
name: 'trace',
diff --git a/public/app/features/explore/TraceView/TraceView.tsx b/public/app/features/explore/TraceView/TraceView.tsx
index 697a36b2578..cfb3b61ccfa 100644
--- a/public/app/features/explore/TraceView/TraceView.tsx
+++ b/public/app/features/explore/TraceView/TraceView.tsx
@@ -1,6 +1,5 @@
import {
DataFrame,
- DataFrameView,
DataLink,
DataSourceApi,
Field,
@@ -9,16 +8,13 @@ import {
mapInternalLinkToExplore,
PanelData,
SplitOpen,
- TraceSpanRow,
} from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime';
import {
Trace,
TracePageHeader,
- TraceProcess,
- TraceResponse,
+ TraceSpan,
TraceTimelineViewer,
- transformTraceData,
TTraceTimeline,
} from '@jaegertracing/jaeger-ui-components';
import { TraceToLogsData } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
@@ -30,10 +26,8 @@ import React, { RefObject, useCallback, useEffect, useMemo, useState } from 'rea
import { useDispatch, useSelector } from 'react-redux';
import { changePanelState } from '../state/explorePane';
import { createSpanLinkFactory } from './createSpanLink';
-import { useChildrenState } from './useChildrenState';
import { useDetailState } from './useDetailState';
import { useHoverIndentGuide } from './useHoverIndentGuide';
-import { useSearch } from './useSearch';
import { useViewRange } from './useViewRange';
function noop(): {} {
@@ -46,14 +40,31 @@ type Props = {
exploreId: ExploreId;
scrollElement?: Element;
topOfExploreViewRef?: RefObject;
+ traceProp: Trace;
+ spanFindMatches?: Set;
+ search: string;
+ focusedSpanIdForSearch: string;
+ expandOne: (spans: TraceSpan[]) => void;
+ expandAll: () => void;
+ collapseOne: (spans: TraceSpan[]) => void;
+ collapseAll: (spans: TraceSpan[]) => void;
+ childrenToggle: (spanId: string) => void;
+ childrenHiddenIDs: Set;
queryResponse: PanelData;
};
export function TraceView(props: Props) {
- // At this point we only show single trace
- const frame = props.dataFrames[0];
+ const {
+ expandOne,
+ expandAll,
+ collapseOne,
+ collapseAll,
+ childrenToggle,
+ childrenHiddenIDs,
+ spanFindMatches,
+ traceProp,
+ } = props;
- const { expandOne, collapseOne, childrenToggle, collapseAll, childrenHiddenIDs, expandAll } = useChildrenState();
const {
detailStates,
toggleDetail,
@@ -65,7 +76,7 @@ export function TraceView(props: Props) {
detailTagsToggle,
detailWarningsToggle,
detailStackTracesToggle,
- } = useDetailState(frame);
+ } = useDetailState(props.dataFrames[0]);
const { removeHoverIndentGuideId, addHoverIndentGuideId, hoverIndentGuideIds } = useHoverIndentGuide();
const { viewRange, updateViewRangeTime, updateNextViewRangeTime } = useViewRange();
@@ -79,15 +90,12 @@ export function TraceView(props: Props) {
*/
const [slim, setSlim] = useState(false);
- const traceProp = useMemo(() => transformDataFrames(frame), [frame]);
- const { search, setSearch, spanFindMatches, clearSearch } = useSearch(traceProp?.spans);
-
const datasource = useSelector(
(state: StoreState) => state.explore[props.exploreId]?.datasourceInstance ?? undefined
);
const [focusedSpanId, createFocusSpanLink] = useFocusSpanLink({
- refId: frame?.refId,
+ refId: props.dataFrames[0]?.refId,
exploreId: props.exploreId,
datasource,
});
@@ -104,9 +112,9 @@ export function TraceView(props: Props) {
hoverIndentGuideIds,
shouldScrollToFirstUiFindMatch: false,
spanNameColumnWidth,
- traceID: traceProp?.traceID,
+ traceID: props.traceProp?.traceID,
}),
- [childrenHiddenIDs, detailStates, hoverIndentGuideIds, spanNameColumnWidth, traceProp?.traceID]
+ [childrenHiddenIDs, detailStates, hoverIndentGuideIds, spanNameColumnWidth, props.traceProp?.traceID]
);
useEffect(() => {
@@ -118,8 +126,8 @@ export function TraceView(props: Props) {
const traceToLogsOptions = (getDatasourceSrv().getInstanceSettings(datasource?.name)?.jsonData as TraceToLogsData)
?.tracesToLogs;
const createSpanLink = useMemo(
- () => createSpanLinkFactory({ splitOpenFn: props.splitOpenFn, traceToLogsOptions, dataFrame: frame }),
- [props.splitOpenFn, traceToLogsOptions, frame]
+ () => 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));
@@ -132,22 +140,15 @@ export function TraceView(props: Props) {
<>
@@ -192,56 +194,6 @@ export function TraceView(props: Props) {
);
}
-function transformDataFrames(frame?: DataFrame): Trace | null {
- if (!frame) {
- return null;
- }
- let data: TraceResponse =
- frame.fields.length === 1
- ? // For backward compatibility when we sent whole json response in a single field/value
- frame.fields[0].values.get(0)
- : transformTraceDataFrame(frame);
- return transformTraceData(data);
-}
-
-function transformTraceDataFrame(frame: DataFrame): TraceResponse {
- const view = new DataFrameView(frame);
- const processes: Record = {};
- for (let i = 0; i < view.length; i++) {
- const span = view.get(i);
- if (!processes[span.spanID]) {
- processes[span.spanID] = {
- serviceName: span.serviceName,
- tags: span.serviceTags,
- };
- }
- }
-
- return {
- traceID: view.get(0).traceID,
- processes,
- spans: view.toArray().map((s, index) => {
- const references = [];
- if (s.parentSpanID) {
- references.push({ refType: 'CHILD_OF' as const, spanID: s.parentSpanID, traceID: s.traceID });
- }
- if (s.references) {
- references.push(...s.references.map((reference) => ({ refType: 'FOLLOWS_FROM' as const, ...reference })));
- }
- return {
- ...s,
- duration: s.duration * 1000,
- startTime: s.startTime * 1000,
- processID: s.spanID,
- flags: 0,
- references,
- logs: s.logs?.map((l) => ({ ...l, timestamp: l.timestamp * 1000 })) || [],
- dataFrameRowIndex: index,
- };
- }),
- };
-}
-
/**
* 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.
diff --git a/public/app/features/explore/TraceView/TraceViewContainer.test.tsx b/public/app/features/explore/TraceView/TraceViewContainer.test.tsx
new file mode 100644
index 00000000000..68725bb429b
--- /dev/null
+++ b/public/app/features/explore/TraceView/TraceViewContainer.test.tsx
@@ -0,0 +1,112 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { TraceViewContainer } from './TraceViewContainer';
+import { frameOld } from './TraceView.test';
+import { ExploreId } from 'app/types';
+import { configureStore } from '../../../store/configureStore';
+import { getDefaultTimeRange, LoadingState } from '@grafana/data';
+import { Provider } from 'react-redux';
+import userEvent from '@testing-library/user-event';
+
+function renderTraceViewContainer(frames = [frameOld]) {
+ const store = configureStore();
+ const mockPanelData = {
+ state: LoadingState.Done,
+ series: [],
+ timeRange: getDefaultTimeRange(),
+ };
+
+ const { container, baseElement } = render(
+
+ {}}
+ queryResponse={mockPanelData}
+ />
+
+ );
+ return {
+ header: container.children[0],
+ timeline: container.children[1],
+ container,
+ baseElement,
+ };
+}
+
+describe('TraceViewContainer', () => {
+ it('toggles children visibility', () => {
+ renderTraceViewContainer();
+ expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
+ userEvent.click(screen.getAllByText('', { selector: 'span[data-test-id="SpanTreeOffset--indentGuide"]' })[0]);
+ expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(1);
+
+ userEvent.click(screen.getAllByText('', { selector: 'span[data-test-id="SpanTreeOffset--indentGuide"]' })[0]);
+ expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
+ });
+
+ it('toggles collapses and expands one level of spans', () => {
+ renderTraceViewContainer();
+ expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
+ userEvent.click(screen.getByLabelText('Collapse +1'));
+ expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(2);
+ userEvent.click(screen.getByLabelText('Expand +1'));
+ expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
+ });
+
+ it('toggles collapses and expands all levels', () => {
+ renderTraceViewContainer();
+ expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
+ userEvent.click(screen.getByLabelText('Collapse All'));
+ expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(1);
+ userEvent.click(screen.getByLabelText('Expand All'));
+ expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
+ });
+
+ it('searches for spans', () => {
+ renderTraceViewContainer();
+ userEvent.type(screen.getByPlaceholderText('Find...'), '1ed38015486087ca');
+ expect(
+ (screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' })[0].parentNode! as HTMLElement).className
+ ).toContain('rowMatchingFilter');
+ });
+
+ it('can select next/prev results', () => {
+ renderTraceViewContainer();
+ userEvent.type(screen.getByPlaceholderText('Find...'), 'logproto');
+ const nextResultButton = screen.getByTestId('trace-page-search-bar-next-result-button');
+ const prevResultButton = screen.getByTestId('trace-page-search-bar-prev-result-button');
+ const suffix = screen.getByTestId('trace-page-search-bar-suffix');
+
+ userEvent.click(nextResultButton);
+ expect(suffix.textContent).toBe('1 of 2');
+ expect(
+ (screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' })[1].parentNode! as HTMLElement).className
+ ).toContain('rowFocused');
+ userEvent.click(nextResultButton);
+ expect(suffix.textContent).toBe('2 of 2');
+ expect(
+ (screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' })[2].parentNode! as HTMLElement).className
+ ).toContain('rowFocused');
+ userEvent.click(nextResultButton);
+ expect(suffix.textContent).toBe('1 of 2');
+ expect(
+ (screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' })[1].parentNode! as HTMLElement).className
+ ).toContain('rowFocused');
+ userEvent.click(prevResultButton);
+ expect(suffix.textContent).toBe('2 of 2');
+ expect(
+ (screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' })[2].parentNode! as HTMLElement).className
+ ).toContain('rowFocused');
+ userEvent.click(prevResultButton);
+ expect(suffix.textContent).toBe('1 of 2');
+ expect(
+ (screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' })[1].parentNode! as HTMLElement).className
+ ).toContain('rowFocused');
+ userEvent.click(prevResultButton);
+ expect(suffix.textContent).toBe('2 of 2');
+ expect(
+ (screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' })[2].parentNode! as HTMLElement).className
+ ).toContain('rowFocused');
+ });
+});
diff --git a/public/app/features/explore/TraceView/TraceViewContainer.tsx b/public/app/features/explore/TraceView/TraceViewContainer.tsx
index 787be09db35..80abebf309c 100644
--- a/public/app/features/explore/TraceView/TraceViewContainer.tsx
+++ b/public/app/features/explore/TraceView/TraceViewContainer.tsx
@@ -1,9 +1,12 @@
-import React, { RefObject } from 'react';
+import React, { RefObject, useMemo, useState } from 'react';
import { Collapse } from '@grafana/ui';
-import { DataFrame, PanelData, SplitOpen } from '@grafana/data';
+import { DataFrame, SplitOpen, PanelData } from '@grafana/data';
import { TraceView } from './TraceView';
import { ExploreId } from 'app/types/explore';
-
+import TracePageSearchBar from '@jaegertracing/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar';
+import { useSearch } from './useSearch';
+import { transformDataFrames } from './utils/transform';
+import { useChildrenState } from './useChildrenState';
interface Props {
dataFrames: DataFrame[];
splitOpenFn: SplitOpen;
@@ -13,18 +16,103 @@ interface Props {
queryResponse: PanelData;
}
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 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 '';
+ };
+
+ if (!traceProp) {
+ return null;
+ }
return (
-
-
+
-
+
+
+
+
+ >
);
}
diff --git a/public/app/features/explore/TraceView/useSearch.ts b/public/app/features/explore/TraceView/useSearch.ts
index 2d0084edaac..dba2602a7f3 100644
--- a/public/app/features/explore/TraceView/useSearch.ts
+++ b/public/app/features/explore/TraceView/useSearch.ts
@@ -1,4 +1,4 @@
-import { useCallback, useMemo, useState } from 'react';
+import { useMemo, useState } from 'react';
import { filterSpans, TraceSpan } from '@jaegertracing/jaeger-ui-components';
/**
@@ -11,9 +11,5 @@ export function useSearch(spans?: TraceSpan[]) {
return search && spans ? filterSpans(search, spans) : undefined;
}, [search, spans]);
- const clearSearch = useCallback(() => {
- setSearch('');
- }, [setSearch]);
-
- return { search, setSearch, spanFindMatches, clearSearch };
+ return { search, setSearch, spanFindMatches };
}
diff --git a/public/app/features/explore/TraceView/utils/transform.ts b/public/app/features/explore/TraceView/utils/transform.ts
new file mode 100644
index 00000000000..7300566121f
--- /dev/null
+++ b/public/app/features/explore/TraceView/utils/transform.ts
@@ -0,0 +1,52 @@
+import { DataFrame, DataFrameView, TraceSpanRow } from '@grafana/data';
+import { Trace, TraceProcess, TraceResponse, transformTraceData } from '@jaegertracing/jaeger-ui-components';
+
+export function transformDataFrames(frame?: DataFrame): Trace | null {
+ if (!frame) {
+ return null;
+ }
+ let data: TraceResponse =
+ frame.fields.length === 1
+ ? // For backward compatibility when we sent whole json response in a single field/value
+ frame.fields[0].values.get(0)
+ : transformTraceDataFrame(frame);
+ return transformTraceData(data);
+}
+
+function transformTraceDataFrame(frame: DataFrame): TraceResponse {
+ const view = new DataFrameView(frame);
+ const processes: Record = {};
+ for (let i = 0; i < view.length; i++) {
+ const span = view.get(i);
+ if (!processes[span.spanID]) {
+ processes[span.spanID] = {
+ serviceName: span.serviceName,
+ tags: span.serviceTags,
+ };
+ }
+ }
+
+ return {
+ traceID: view.get(0).traceID,
+ processes,
+ spans: view.toArray().map((s, index) => {
+ const references = [];
+ if (s.parentSpanID) {
+ references.push({ refType: 'CHILD_OF' as const, spanID: s.parentSpanID, traceID: s.traceID });
+ }
+ if (s.references) {
+ references.push(...s.references.map((reference) => ({ refType: 'FOLLOWS_FROM' as const, ...reference })));
+ }
+ return {
+ ...s,
+ duration: s.duration * 1000,
+ startTime: s.startTime * 1000,
+ processID: s.spanID,
+ flags: 0,
+ references,
+ logs: s.logs?.map((l) => ({ ...l, timestamp: l.timestamp * 1000 })) || [],
+ dataFrameRowIndex: index,
+ };
+ }),
+ };
+}