diff --git a/.betterer.results b/.betterer.results index ee83f06d95e..4a297ad7ca0 100644 --- a/.betterer.results +++ b/.betterer.results @@ -92,7 +92,7 @@ exports[`no enzyme tests`] = { "packages/jaeger-ui-components/src/TracePageHeader/TracePageHeader.test.js:3669774385": [ [15, 26, 13, "RegExp match", "2409514259"] ], - "packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js:1298620967": [ + "packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js:2095199604": [ [15, 19, 13, "RegExp match", "2409514259"] ], "packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.test.js:793239444": [ diff --git a/packages/jaeger-ui-components/src/TracePageHeader/TracePageHeader.tsx b/packages/jaeger-ui-components/src/TracePageHeader/TracePageHeader.tsx index 938b6c5f5b6..2deb6921592 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/TracePageHeader.tsx +++ b/packages/jaeger-ui-components/src/TracePageHeader/TracePageHeader.tsx @@ -21,7 +21,6 @@ import { dateTimeFormat, GrafanaTheme2, TimeZone } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; import SpanGraph from './SpanGraph'; -import TracePageSearchBar from './TracePageSearchBar'; import { autoColor, TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate } from '..'; import LabeledList from '../common/LabeledList'; import TraceName from '../common/TraceName'; @@ -138,22 +137,15 @@ const getStyles = (theme: GrafanaTheme2) => { type TracePageHeaderEmbedProps = { canCollapse: boolean; - clearSearch: () => void; - focusUiFindMatches: () => void; hideMap: boolean; hideSummary: boolean; - nextResult: () => void; onSlimViewClicked: () => void; onTraceGraphViewClicked: () => void; - prevResult: () => void; - resultCount: number; slimView: boolean; trace: Trace; updateNextViewRangeTime: (update: ViewRangeTimeUpdate) => void; updateViewRangeTime: TUpdateViewRangeTimeFunction; viewRange: ViewRange; - searchValue: string; - onSearchValueChange: (value: string) => void; timeZone: TimeZone; }; @@ -200,21 +192,14 @@ export const HEADER_ITEMS = [ export default function TracePageHeader(props: TracePageHeaderEmbedProps) { const { canCollapse, - clearSearch, - focusUiFindMatches, hideMap, hideSummary, - nextResult, onSlimViewClicked, - prevResult, - resultCount, slimView, trace, updateNextViewRangeTime, updateViewRangeTime, viewRange, - searchValue, - onSearchValueChange, timeZone, } = props; @@ -267,17 +252,6 @@ export default function TracePageHeader(props: TracePageHeaderEmbedProps) { ) : ( title )} - {summaryItems && } {!hideMap && !slimView && ( diff --git a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js index 51b5eab3b57..47b4e916cde 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js +++ b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js @@ -14,6 +14,7 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { createTheme } from '@grafana/data'; import * as markers from './TracePageSearchBar.markers'; import TracePageSearchBar, { getStyles } from './TracePageSearchBar'; @@ -24,7 +25,7 @@ const defaultProps = { navigable: true, nextResult: () => {}, prevResult: () => {}, - resultCount: 0, + suffix: '', searchValue: 'something', }; @@ -45,26 +46,28 @@ describe('', () => { name: 'search', }) ); - expect(suffix.hasClass(getStyles().TracePageSearchBarCount)).toBe(true); - expect(suffix.text()).toBe(String(defaultProps.resultCount)); + const theme = createTheme(); + expect(suffix.hasClass(getStyles(theme).TracePageSearchBarSuffix)).toBe(true); + expect(suffix.text()).toBe(String(defaultProps.suffix)); }); 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(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); - expect(wrapper.find('Button[icon="times"]').prop('onClick')).toBe(defaultProps.clearSearch); }); - it('hides navigation buttons when not navigable', () => { + it('only shows navigable buttons when navigable is true', () => { wrapper.setProps({ navigable: false }); - const button = wrapper.find('Button'); - expect(button.length).toBe(1); - expect(button.prop('icon')).toBe('times'); + var buttons = wrapper.find('Button'); + expect(buttons.length).toBe(0); + wrapper.setProps({ navigable: true }); + buttons = wrapper.find('Button'); + expect(buttons.length).toBe(2); }); }); @@ -79,7 +82,7 @@ describe('', () => { 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, + }; + }), + }; +}