Tracing: Remove newTraceViewHeader feature toggle (#71818)

* Remove TracePageHeader, uiFind, setTrace and many spanFindMatches

* Removed TracePageSearchBar

* Update useSearch

* Update filterSpans

* Update docs

* Updated tests

* Add trace to tests

* Remove feature toggle

* Renames in useSearch

* Renames in filter-spans

* Cleanup fixes

* Rename TracePageHeader

* Rename TracePageSearchBar

* Update test

* Update style for long urls

* Style and check
This commit is contained in:
Joey 2023-07-19 14:31:58 +01:00 committed by GitHub
parent 27107225ec
commit 090b8d61e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 565 additions and 1522 deletions

View File

@ -304,11 +304,6 @@ If the file has multiple traces, Grafana visualizes its first trace.
## Span Filters ## Span Filters
{{% admonition type="note" %}}
This feature is behind the `newTraceViewHeader` [feature toggle]({{< relref "../../setup-grafana/configure-grafana#feature_toggles" >}}).
If you use Grafana Cloud, open a [support ticket in the Cloud Portal](/profile/org#support) to access this feature.
{{% /admonition %}}
![Screenshot of span filtering](/media/docs/tempo/screenshot-grafana-tempo-span-filters.png) ![Screenshot of span filtering](/media/docs/tempo/screenshot-grafana-tempo-span-filters.png)
Using span filters, you can filter your spans in the trace timeline viewer. The more filters you add, the more specific are the filtered spans. Using span filters, you can filter your spans in the trace timeline viewer. The more filters you add, the more specific are the filtered spans.

View File

@ -370,11 +370,6 @@ To open a query in Tempo with the span name of that row automatically set in the
## Span Filters ## Span Filters
{{% admonition type="note" %}}
This feature is behind the `newTraceViewHeader` [feature toggle]({{< relref "../../setup-grafana/configure-grafana#feature_toggles" >}}).
If you use Grafana Cloud, open a [support ticket in the Cloud Portal](/profile/org#support) to access this feature.
{{% /admonition %}}
![Screenshot of span filtering](/media/docs/tempo/screenshot-grafana-tempo-span-filters.png) ![Screenshot of span filtering](/media/docs/tempo/screenshot-grafana-tempo-span-filters.png)
Using span filters, you can filter your spans in the trace timeline viewer. The more filters you add, the more specific are the filtered spans. Using span filters, you can filter your spans in the trace timeline viewer. The more filters you add, the more specific are the filtered spans.

View File

@ -273,11 +273,6 @@ If the file has multiple traces, Grafana visualizes its first trace.
## Span Filters ## Span Filters
{{% admonition type="note" %}}
This feature is behind the `newTraceViewHeader` [feature toggle]({{< relref "../../setup-grafana/configure-grafana#feature_toggles" >}}).
If you use Grafana Cloud, open a [support ticket in the Cloud Portal](/profile/org#support) to access this feature.
{{% /admonition %}}
![Screenshot of span filtering](/media/docs/tempo/screenshot-grafana-tempo-span-filters.png) ![Screenshot of span filtering](/media/docs/tempo/screenshot-grafana-tempo-span-filters.png)
Using span filters, you can filter your spans in the trace timeline viewer. The more filters you add, the more specific are the filtered spans. Using span filters, you can filter your spans in the trace timeline viewer. The more filters you add, the more specific are the filtered spans.

View File

@ -59,11 +59,6 @@ Shows condensed view or the trace timeline. Drag your mouse over the minimap to
### Span Filters ### Span Filters
{{% admonition type="note" %}}
This feature is behind the `newTraceViewHeader` [feature toggle]({{< relref "../setup-grafana/configure-grafana/feature-toggles" >}}).
If you use Grafana Cloud, open a [support ticket in the Cloud Portal](/profile/org#support) to access this feature.
{{% /admonition %}}
![Screenshot of span filtering](/media/docs/tempo/screenshot-grafana-tempo-span-filters.png) ![Screenshot of span filtering](/media/docs/tempo/screenshot-grafana-tempo-span-filters.png)
Using span filters, you can filter your spans in the trace timeline viewer. The more filters you add, the more specific are the filtered spans. Using span filters, you can filter your spans in the trace timeline viewer. The more filters you add, the more specific are the filtered spans.

View File

@ -78,7 +78,6 @@ Experimental features might be changed or removed without prior notice.
| `queryOverLive` | Use Grafana Live WebSocket to execute backend queries | | `queryOverLive` | Use Grafana Live WebSocket to execute backend queries |
| `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) | | `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) |
| `storage` | Configurable storage for dashboards, datasources, and resources | | `storage` | Configurable storage for dashboards, datasources, and resources |
| `newTraceViewHeader` | Shows the new trace view header |
| `datasourceQueryMultiStatus` | Introduce HTTP 207 Multi Status for api/ds/query | | `datasourceQueryMultiStatus` | Introduce HTTP 207 Multi Status for api/ds/query |
| `traceToMetrics` | Enable trace to metrics links | | `traceToMetrics` | Enable trace to metrics links |
| `prometheusWideSeries` | Enable wide series responses in the Prometheus datasource | | `prometheusWideSeries` | Enable wide series responses in the Prometheus datasource |

View File

@ -29,7 +29,6 @@ export interface FeatureToggles {
featureHighlights?: boolean; featureHighlights?: boolean;
migrationLocking?: boolean; migrationLocking?: boolean;
storage?: boolean; storage?: boolean;
newTraceViewHeader?: boolean;
correlations?: boolean; correlations?: boolean;
datasourceQueryMultiStatus?: boolean; datasourceQueryMultiStatus?: boolean;
traceToMetrics?: boolean; traceToMetrics?: boolean;

View File

@ -79,13 +79,6 @@ var (
Stage: FeatureStageExperimental, Stage: FeatureStageExperimental,
Owner: grafanaAppPlatformSquad, Owner: grafanaAppPlatformSquad,
}, },
{
Name: "newTraceViewHeader",
Description: "Shows the new trace view header",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaObservabilityTracesAndProfilingSquad,
},
{ {
Name: "correlations", Name: "correlations",
Description: "Correlations page", Description: "Correlations page",

View File

@ -10,7 +10,6 @@ lokiExperimentalStreaming,experimental,@grafana/observability-logs,false,false,f
featureHighlights,GA,@grafana/grafana-as-code,false,false,false,false featureHighlights,GA,@grafana/grafana-as-code,false,false,false,false
migrationLocking,preview,@grafana/backend-platform,false,false,false,false migrationLocking,preview,@grafana/backend-platform,false,false,false,false
storage,experimental,@grafana/grafana-app-platform-squad,false,false,false,false storage,experimental,@grafana/grafana-app-platform-squad,false,false,false,false
newTraceViewHeader,experimental,@grafana/observability-traces-and-profiling,false,false,false,true
correlations,preview,@grafana/explore-squad,false,false,false,false correlations,preview,@grafana/explore-squad,false,false,false,false
datasourceQueryMultiStatus,experimental,@grafana/plugins-platform-backend,false,false,false,false datasourceQueryMultiStatus,experimental,@grafana/plugins-platform-backend,false,false,false,false
traceToMetrics,experimental,@grafana/observability-traces-and-profiling,false,false,false,true traceToMetrics,experimental,@grafana/observability-traces-and-profiling,false,false,false,true

1 Name Stage Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
10 featureHighlights GA @grafana/grafana-as-code false false false false
11 migrationLocking preview @grafana/backend-platform false false false false
12 storage experimental @grafana/grafana-app-platform-squad false false false false
newTraceViewHeader experimental @grafana/observability-traces-and-profiling false false false true
13 correlations preview @grafana/explore-squad false false false false
14 datasourceQueryMultiStatus experimental @grafana/plugins-platform-backend false false false false
15 traceToMetrics experimental @grafana/observability-traces-and-profiling false false false true

View File

@ -51,10 +51,6 @@ const (
// Configurable storage for dashboards, datasources, and resources // Configurable storage for dashboards, datasources, and resources
FlagStorage = "storage" FlagStorage = "storage"
// FlagNewTraceViewHeader
// Shows the new trace view header
FlagNewTraceViewHeader = "newTraceViewHeader"
// FlagCorrelations // FlagCorrelations
// Correlations page // Correlations page
FlagCorrelations = "correlations" FlagCorrelations = "correlations"

View File

@ -29,8 +29,6 @@ function getTraceView(frames: DataFrame[]) {
dataFrames={frames} dataFrames={frames}
splitOpenFn={() => {}} splitOpenFn={() => {}}
traceProp={transformDataFrames(frames[0])!} traceProp={transformDataFrames(frames[0])!}
search=""
focusedSpanIdForSearch=""
queryResponse={mockPanelData} queryResponse={mockPanelData}
datasource={undefined} datasource={undefined}
topOfViewRef={topOfViewRef} topOfViewRef={topOfViewRef}

View File

@ -15,7 +15,7 @@ import {
PanelData, PanelData,
SplitOpen, SplitOpen,
} from '@grafana/data'; } from '@grafana/data';
import { config, getTemplateSrv } from '@grafana/runtime'; import { getTemplateSrv } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema'; import { DataQuery } from '@grafana/schema';
import { useStyles2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import { getTraceToLogsOptions, TraceToLogsData } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; import { getTraceToLogsOptions, TraceToLogsData } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
@ -27,21 +27,14 @@ import { useDispatch, useSelector } from 'app/types';
import { changePanelState } from '../state/explorePane'; import { changePanelState } from '../state/explorePane';
import { import { SpanBarOptionsData, Trace, TracePageHeader, TraceTimelineViewer, TTraceTimeline } from './components';
SpanBarOptionsData,
Trace,
TracePageHeader,
NewTracePageHeader,
TraceTimelineViewer,
TTraceTimeline,
} from './components';
import SpanGraph from './components/TracePageHeader/SpanGraph'; import SpanGraph from './components/TracePageHeader/SpanGraph';
import { TopOfViewRefType } from './components/TraceTimelineViewer/VirtualizedTraceView'; import { TopOfViewRefType } from './components/TraceTimelineViewer/VirtualizedTraceView';
import { createSpanLinkFactory } from './createSpanLink'; import { createSpanLinkFactory } from './createSpanLink';
import { useChildrenState } from './useChildrenState'; import { useChildrenState } from './useChildrenState';
import { useDetailState } from './useDetailState'; import { useDetailState } from './useDetailState';
import { useHoverIndentGuide } from './useHoverIndentGuide'; import { useHoverIndentGuide } from './useHoverIndentGuide';
import { useSearchNewTraceViewHeader } from './useSearch'; import { useSearch } from './useSearch';
import { useViewRange } from './useViewRange'; import { useViewRange } from './useViewRange';
const getStyles = (theme: GrafanaTheme2) => ({ const getStyles = (theme: GrafanaTheme2) => ({
@ -66,9 +59,6 @@ type Props = {
scrollElement?: Element; scrollElement?: Element;
scrollElementClass?: string; scrollElementClass?: string;
traceProp: Trace; traceProp: Trace;
spanFindMatches?: Set<string>;
search: string;
focusedSpanIdForSearch: string;
queryResponse: PanelData; queryResponse: PanelData;
datasource: DataSourceApi<DataQuery, DataSourceJsonData, {}> | undefined; datasource: DataSourceApi<DataQuery, DataSourceJsonData, {}> | undefined;
topOfViewRef: RefObject<HTMLDivElement>; topOfViewRef: RefObject<HTMLDivElement>;
@ -76,7 +66,7 @@ type Props = {
}; };
export function TraceView(props: Props) { export function TraceView(props: Props) {
const { spanFindMatches, traceProp, datasource, topOfViewRef, topOfViewRefType, exploreId } = props; const { traceProp, datasource, topOfViewRef, topOfViewRefType, exploreId } = props;
const { const {
detailStates, detailStates,
@ -94,13 +84,11 @@ export function TraceView(props: Props) {
const { removeHoverIndentGuideId, addHoverIndentGuideId, hoverIndentGuideIds } = useHoverIndentGuide(); const { removeHoverIndentGuideId, addHoverIndentGuideId, hoverIndentGuideIds } = useHoverIndentGuide();
const { viewRange, updateViewRangeTime, updateNextViewRangeTime } = useViewRange(); const { viewRange, updateViewRangeTime, updateNextViewRangeTime } = useViewRange();
const { expandOne, collapseOne, childrenToggle, collapseAll, childrenHiddenIDs, expandAll } = useChildrenState(); const { expandOne, collapseOne, childrenToggle, collapseAll, childrenHiddenIDs, expandAll } = useChildrenState();
const { newTraceViewHeaderSearch, setNewTraceViewHeaderSearch, spanFilterMatches } = useSearchNewTraceViewHeader( const { search, setSearch, spanFilterMatches } = useSearch(traceProp?.spans);
traceProp?.spans const [focusedSpanIdForSearch, setFocusedSpanIdForSearch] = useState('');
);
const [newTraceViewHeaderFocusedSpanIdForSearch, setNewTraceViewHeaderFocusedSpanIdForSearch] = useState('');
const [showSpanFilters, setShowSpanFilters] = useToggle(false); const [showSpanFilters, setShowSpanFilters] = useToggle(false);
const [showSpanFilterMatchesOnly, setShowSpanFilterMatchesOnly] = useState(false); const [showSpanFilterMatchesOnly, setShowSpanFilterMatchesOnly] = useState(false);
const [headerHeight, setHeaderHeight] = useState(0); const [headerHeight, setHeaderHeight] = useState(100);
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
@ -154,43 +142,31 @@ export function TraceView(props: Props) {
<> <>
{props.dataFrames?.length && traceProp ? ( {props.dataFrames?.length && traceProp ? (
<> <>
{config.featureToggles.newTraceViewHeader ? ( <TracePageHeader
<> trace={traceProp}
<NewTracePageHeader data={props.dataFrames[0]}
trace={traceProp} timeZone={timeZone}
data={props.dataFrames[0]} search={search}
timeZone={timeZone} setSearch={setSearch}
search={newTraceViewHeaderSearch} showSpanFilters={showSpanFilters}
setSearch={setNewTraceViewHeaderSearch} setShowSpanFilters={setShowSpanFilters}
showSpanFilters={showSpanFilters} showSpanFilterMatchesOnly={showSpanFilterMatchesOnly}
setShowSpanFilters={setShowSpanFilters} setShowSpanFilterMatchesOnly={setShowSpanFilterMatchesOnly}
showSpanFilterMatchesOnly={showSpanFilterMatchesOnly} setFocusedSpanIdForSearch={setFocusedSpanIdForSearch}
setShowSpanFilterMatchesOnly={setShowSpanFilterMatchesOnly} spanFilterMatches={spanFilterMatches}
setFocusedSpanIdForSearch={setNewTraceViewHeaderFocusedSpanIdForSearch} datasourceType={datasourceType}
spanFilterMatches={spanFilterMatches} setHeaderHeight={setHeaderHeight}
datasourceType={datasourceType} app={exploreId ? CoreApp.Explore : CoreApp.Unknown}
setHeaderHeight={setHeaderHeight} />
app={exploreId ? CoreApp.Explore : CoreApp.Unknown} <SpanGraph
/> trace={traceProp}
<SpanGraph viewRange={viewRange}
trace={traceProp} updateNextViewRangeTime={updateNextViewRangeTime}
viewRange={viewRange} updateViewRangeTime={updateViewRangeTime}
updateNextViewRangeTime={updateNextViewRangeTime} />
updateViewRangeTime={updateViewRangeTime}
/>
</>
) : (
<TracePageHeader
trace={traceProp}
updateNextViewRangeTime={updateNextViewRangeTime}
updateViewRangeTime={updateViewRangeTime}
viewRange={viewRange}
timeZone={timeZone}
/>
)}
<TraceTimelineViewer <TraceTimelineViewer
registerAccessors={noop} registerAccessors={noop}
findMatchesIDs={config.featureToggles.newTraceViewHeader ? spanFilterMatches : spanFindMatches} findMatchesIDs={spanFilterMatches}
trace={traceProp} trace={traceProp}
datasourceType={datasourceType} datasourceType={datasourceType}
spanBarOptions={spanBarOptions?.spanBar} spanBarOptions={spanBarOptions?.spanBar}
@ -214,19 +190,13 @@ export function TraceView(props: Props) {
detailProcessToggle={detailProcessToggle} detailProcessToggle={detailProcessToggle}
detailTagsToggle={detailTagsToggle} detailTagsToggle={detailTagsToggle}
detailToggle={toggleDetail} detailToggle={toggleDetail}
setTrace={noop}
addHoverIndentGuideId={addHoverIndentGuideId} addHoverIndentGuideId={addHoverIndentGuideId}
removeHoverIndentGuideId={removeHoverIndentGuideId} removeHoverIndentGuideId={removeHoverIndentGuideId}
linksGetter={() => []} linksGetter={() => []}
uiFind={props.search}
createSpanLink={createSpanLink} createSpanLink={createSpanLink}
scrollElement={scrollElement} scrollElement={scrollElement}
focusedSpanId={focusedSpanId} focusedSpanId={focusedSpanId}
focusedSpanIdForSearch={ focusedSpanIdForSearch={focusedSpanIdForSearch}
config.featureToggles.newTraceViewHeader
? newTraceViewHeaderFocusedSpanIdForSearch
: props.focusedSpanIdForSearch!
}
showSpanFilterMatchesOnly={showSpanFilterMatchesOnly} showSpanFilterMatchesOnly={showSpanFilterMatchesOnly}
createFocusSpanLink={createFocusSpanLink} createFocusSpanLink={createFocusSpanLink}
topOfViewRef={topOfViewRef} topOfViewRef={topOfViewRef}

View File

@ -4,7 +4,6 @@ import React, { createRef } from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { getDefaultTimeRange, LoadingState } from '@grafana/data'; import { getDefaultTimeRange, LoadingState } from '@grafana/data';
import { config } from '@grafana/runtime';
import { configureStore } from '../../../store/configureStore'; import { configureStore } from '../../../store/configureStore';
@ -86,55 +85,7 @@ describe('TraceViewContainer', () => {
expect(screen.queryAllByText('', { selector: 'div[data-testid="span-view"]' }).length).toBe(3); expect(screen.queryAllByText('', { selector: 'div[data-testid="span-view"]' }).length).toBe(3);
}); });
it('searches for spans', async () => {
renderTraceViewContainer();
await user.type(screen.getByPlaceholderText('Find...'), '1ed38015486087ca');
expect(
screen.queryAllByText('', { selector: 'div[data-testid="span-view"]' })[0].parentElement!.className
).toContain('rowMatchingFilter');
});
it('can select next/prev results', async () => { it('can select next/prev results', async () => {
renderTraceViewContainer();
await user.type(screen.getByPlaceholderText('Find...'), 'logproto');
const nextResultButton = screen.getByRole('button', { name: 'Next results button' });
const prevResultButton = screen.getByRole('button', { name: 'Prev results button' });
const suffix = screen.getByLabelText('Search bar suffix');
await user.click(nextResultButton);
expect(suffix.textContent).toBe('1 of 2');
expect(
screen.queryAllByText('', { selector: 'div[data-testid="span-view"]' })[1].parentElement!.className
).toContain('rowFocused');
await user.click(nextResultButton);
expect(suffix.textContent).toBe('2 of 2');
expect(
screen.queryAllByText('', { selector: 'div[data-testid="span-view"]' })[2].parentElement!.className
).toContain('rowFocused');
await user.click(nextResultButton);
expect(suffix.textContent).toBe('1 of 2');
expect(
screen.queryAllByText('', { selector: 'div[data-testid="span-view"]' })[1].parentElement!.className
).toContain('rowFocused');
await user.click(prevResultButton);
expect(suffix.textContent).toBe('2 of 2');
expect(
screen.queryAllByText('', { selector: 'div[data-testid="span-view"]' })[2].parentElement!.className
).toContain('rowFocused');
await user.click(prevResultButton);
expect(suffix.textContent).toBe('1 of 2');
expect(
screen.queryAllByText('', { selector: 'div[data-testid="span-view"]' })[1].parentElement!.className
).toContain('rowFocused');
await user.click(prevResultButton);
expect(suffix.textContent).toBe('2 of 2');
expect(
screen.queryAllByText('', { selector: 'div[data-testid="span-view"]' })[2].parentElement!.className
).toContain('rowFocused');
});
it('can select next/prev results', async () => {
config.featureToggles.newTraceViewHeader = true;
renderTraceViewContainer(); renderTraceViewContainer();
const spanFiltersButton = screen.getByRole('button', { name: 'Span Filters 3 spans Prev Next' }); const spanFiltersButton = screen.getByRole('button', { name: 'Span Filters 3 spans Prev Next' });
await user.click(spanFiltersButton); await user.click(spanFiltersButton);
@ -184,7 +135,6 @@ describe('TraceViewContainer', () => {
}); });
it('show matches only works as expected', async () => { it('show matches only works as expected', async () => {
config.featureToggles.newTraceViewHeader = true;
renderTraceViewContainer(); renderTraceViewContainer();
const spanFiltersButton = screen.getByRole('button', { name: 'Span Filters 3 spans Prev Next' }); const spanFiltersButton = screen.getByRole('button', { name: 'Span Filters 3 spans Prev Next' });
await user.click(spanFiltersButton); await user.click(spanFiltersButton);

View File

@ -1,14 +1,11 @@
import React, { RefObject, useMemo, useState } from 'react'; import React, { RefObject, useMemo } from 'react';
import { DataFrame, PanelData, SplitOpen } from '@grafana/data'; import { DataFrame, SplitOpen, PanelData } from '@grafana/data';
import { config } from '@grafana/runtime';
import { PanelChrome } from '@grafana/ui/src/components/PanelChrome/PanelChrome'; import { PanelChrome } from '@grafana/ui/src/components/PanelChrome/PanelChrome';
import { StoreState, useSelector } from 'app/types'; import { StoreState, useSelector } from 'app/types';
import { TraceView } from './TraceView'; import { TraceView } from './TraceView';
import TracePageSearchBar from './components/TracePageHeader/SearchBar/TracePageSearchBar';
import { TopOfViewRefType } from './components/TraceTimelineViewer/VirtualizedTraceView'; import { TopOfViewRefType } from './components/TraceTimelineViewer/VirtualizedTraceView';
import { useSearch } from './useSearch';
import { transformDataFrames } from './utils/transform'; import { transformDataFrames } from './utils/transform';
interface Props { interface Props {
@ -25,47 +22,22 @@ export function TraceViewContainer(props: Props) {
const frame = props.dataFrames[0]; const frame = props.dataFrames[0];
const { dataFrames, splitOpenFn, exploreId, scrollElement, topOfViewRef, queryResponse } = props; const { dataFrames, splitOpenFn, exploreId, scrollElement, topOfViewRef, queryResponse } = props;
const traceProp = useMemo(() => transformDataFrames(frame), [frame]); const traceProp = useMemo(() => transformDataFrames(frame), [frame]);
const { search, setSearch, spanFindMatches } = useSearch(traceProp?.spans);
const [focusedSpanIdForSearch, setFocusedSpanIdForSearch] = useState('');
const [searchBarSuffix, setSearchBarSuffix] = useState('');
const datasource = useSelector( const datasource = useSelector(
(state: StoreState) => state.explore.panes[props.exploreId]?.datasourceInstance ?? undefined (state: StoreState) => state.explore.panes[props.exploreId]?.datasourceInstance ?? undefined
); );
const datasourceType = datasource ? datasource?.type : 'unknown';
if (!traceProp) { if (!traceProp) {
return null; return null;
} }
return ( return (
<PanelChrome <PanelChrome padding="none" title="Trace">
padding="none"
title="Trace"
actions={
!config.featureToggles.newTraceViewHeader && (
<TracePageSearchBar
navigable={true}
searchValue={search}
setSearch={setSearch}
spanFindMatches={spanFindMatches}
searchBarSuffix={searchBarSuffix}
setSearchBarSuffix={setSearchBarSuffix}
focusedSpanIdForSearch={focusedSpanIdForSearch}
setFocusedSpanIdForSearch={setFocusedSpanIdForSearch}
datasourceType={datasourceType}
/>
)
}
>
<TraceView <TraceView
exploreId={exploreId} exploreId={exploreId}
dataFrames={dataFrames} dataFrames={dataFrames}
splitOpenFn={splitOpenFn} splitOpenFn={splitOpenFn}
scrollElement={scrollElement} scrollElement={scrollElement}
traceProp={traceProp} traceProp={traceProp}
spanFindMatches={spanFindMatches}
search={search}
focusedSpanIdForSearch={focusedSpanIdForSearch}
queryResponse={queryResponse} queryResponse={queryResponse}
datasource={datasource} datasource={datasource}
topOfViewRef={topOfViewRef} topOfViewRef={topOfViewRef}

View File

@ -1,65 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { getByText, render } from '@testing-library/react';
import React from 'react';
import { MutableDataFrame } from '@grafana/data';
import config from 'app/core/config';
import { defaultFilters } from '../../useSearch';
import { NewTracePageHeader } from './NewTracePageHeader';
import { trace } from './TracePageHeader.test';
const setup = () => {
const defaultProps = {
trace,
timeZone: '',
search: defaultFilters,
setSearch: jest.fn(),
showSpanFilters: true,
setShowSpanFilters: jest.fn(),
showSpanFilterMatchesOnly: false,
setShowSpanFilterMatchesOnly: jest.fn(),
spanFilterMatches: undefined,
setFocusedSpanIdForSearch: jest.fn(),
datasourceType: 'tempo',
setHeaderHeight: jest.fn(),
data: new MutableDataFrame(),
};
return render(<NewTracePageHeader {...defaultProps} />);
};
describe('NewTracePageHeader test', () => {
it('should render the new trace header', () => {
config.featureToggles.newTraceViewHeader = true;
setup();
const header = document.querySelector('header');
const method = getByText(header!, 'POST');
const status = getByText(header!, '200');
const url = getByText(header!, '/v2/gamma/792edh2w897y2huehd2h89');
const duration = getByText(header!, '2.36s');
const timestampPart1 = getByText(header!, '2023-02-05 08:50');
const timestampPart2 = getByText(header!, ':56.289');
expect(method).toBeInTheDocument();
expect(status).toBeInTheDocument();
expect(url).toBeInTheDocument();
expect(duration).toBeInTheDocument();
expect(timestampPart1).toBeInTheDocument();
expect(timestampPart2).toBeInTheDocument();
});
});

View File

@ -1,203 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { css } from '@emotion/css';
import cx from 'classnames';
import React, { memo, useEffect, useMemo } from 'react';
import { CoreApp, DataFrame, GrafanaTheme2 } from '@grafana/data';
import { TimeZone } from '@grafana/schema';
import { Badge, BadgeColor, Tooltip, useStyles2 } from '@grafana/ui';
import { SearchProps } from '../../useSearch';
import ExternalLinks from '../common/ExternalLinks';
import TraceName from '../common/TraceName';
import { getTraceLinks } from '../model/link-patterns';
import { getHeaderTags, getTraceName } from '../model/trace-viewer';
import { Trace } from '../types';
import { formatDuration } from '../utils/date';
import TracePageActions from './Actions/TracePageActions';
import { SpanFilters } from './SpanFilters/SpanFilters';
import { timestamp, getStyles } from './TracePageHeader';
export type TracePageHeaderProps = {
trace: Trace | null;
data: DataFrame;
app?: CoreApp;
timeZone: TimeZone;
search: SearchProps;
setSearch: React.Dispatch<React.SetStateAction<SearchProps>>;
showSpanFilters: boolean;
setShowSpanFilters: (isOpen: boolean) => void;
showSpanFilterMatchesOnly: boolean;
setShowSpanFilterMatchesOnly: (showMatchesOnly: boolean) => void;
setFocusedSpanIdForSearch: React.Dispatch<React.SetStateAction<string>>;
spanFilterMatches: Set<string> | undefined;
datasourceType: string;
setHeaderHeight: (height: number) => void;
};
export const NewTracePageHeader = memo((props: TracePageHeaderProps) => {
const {
trace,
data,
app,
timeZone,
search,
setSearch,
showSpanFilters,
setShowSpanFilters,
showSpanFilterMatchesOnly,
setShowSpanFilterMatchesOnly,
setFocusedSpanIdForSearch,
spanFilterMatches,
datasourceType,
setHeaderHeight,
} = props;
const styles = { ...useStyles2(getStyles), ...useStyles2(getNewStyles) };
useEffect(() => {
setHeaderHeight(document.querySelector('.' + styles.header)?.scrollHeight ?? 0);
}, [setHeaderHeight, showSpanFilters, styles.header]);
const links = useMemo(() => {
if (!trace) {
return [];
}
return getTraceLinks(trace);
}, [trace]);
if (!trace) {
return null;
}
const title = (
<h1 className={cx(styles.title)}>
<TraceName traceName={getTraceName(trace.spans)} />
<small className={styles.duration}>{formatDuration(trace.duration)}</small>
</h1>
);
const { method, status, url } = getHeaderTags(trace.spans);
let statusColor: BadgeColor = 'green';
if (status && status.length > 0) {
if (status[0].value.toString().charAt(0) === '4') {
statusColor = 'orange';
} else if (status[0].value.toString().charAt(0) === '5') {
statusColor = 'red';
}
}
return (
<header className={styles.header}>
<div className={styles.titleRow}>
{links && links.length > 0 && <ExternalLinks links={links} className={styles.TracePageHeaderBack} />}
{title}
<TracePageActions traceId={trace.traceID} data={data} app={app} />
</div>
<div className={styles.subtitle}>
<span className={styles.timestamp}>{timestamp(trace, timeZone, styles)}</span>
<span className={styles.tagMeta}>
{method && method.length > 0 && (
<Tooltip content={'http.method'} interactive={true}>
<span className={styles.tag}>
<Badge text={method[0].value} color="blue" />
</span>
</Tooltip>
)}
{status && status.length > 0 && (
<Tooltip content={'http.status_code'} interactive={true}>
<span className={styles.tag}>
<Badge text={status[0].value} color={statusColor} />
</span>
</Tooltip>
)}
{url && url.length > 0 && (
<Tooltip content={'http.url or http.target or http.path'} interactive={true}>
<span className={styles.url}>{url[0].value}</span>
</Tooltip>
)}
</span>
</div>
<SpanFilters
trace={trace}
showSpanFilters={showSpanFilters}
setShowSpanFilters={setShowSpanFilters}
showSpanFilterMatchesOnly={showSpanFilterMatchesOnly}
setShowSpanFilterMatchesOnly={setShowSpanFilterMatchesOnly}
search={search}
setSearch={setSearch}
spanFilterMatches={spanFilterMatches}
setFocusedSpanIdForSearch={setFocusedSpanIdForSearch}
datasourceType={datasourceType}
/>
</header>
);
});
NewTracePageHeader.displayName = 'NewTracePageHeader';
const getNewStyles = (theme: GrafanaTheme2) => {
return {
header: css`
label: TracePageHeader;
background-color: ${theme.colors.background.primary};
position: sticky;
top: 0;
z-index: 5;
`,
titleRow: css`
align-items: flex-start;
display: flex;
padding: 0 8px;
`,
title: css`
color: inherit;
flex: 1;
font-size: 1.7em;
line-height: 1em;
`,
subtitle: css`
flex: 1;
line-height: 1em;
margin: -0.5em 0.5em 0.75em 0.5em;
`,
tag: css`
margin: 0 0.5em 0 0;
`,
duration: css`
color: #aaa;
margin: 0 0.75em;
`,
timestamp: css`
vertical-align: middle;
`,
tagMeta: css`
margin: 0 0.75em;
vertical-align: text-top;
`,
url: css`
margin: -2.5px 0.3em;
height: 15px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 30%;
display: inline-block;
`,
};
};

View File

@ -1,59 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { render, screen } from '@testing-library/react';
import React from 'react';
import { defaultFilters } from '../../../useSearch';
import { trace } from '../TracePageHeader.test';
import NewTracePageSearchBar from './NewTracePageSearchBar';
describe('<NewTracePageSearchBar>', () => {
const NewTracePageSearchBarWithProps = (props: { matches: string[] | undefined }) => {
const searchBarProps = {
trace: trace,
search: defaultFilters,
spanFilterMatches: props.matches ? new Set(props.matches) : undefined,
showSpanFilterMatchesOnly: false,
setShowSpanFilterMatchesOnly: jest.fn(),
setFocusedSpanIdForSearch: jest.fn(),
focusedSpanIndexForSearch: -1,
setFocusedSpanIndexForSearch: jest.fn(),
datasourceType: '',
clear: jest.fn(),
totalSpans: 100,
showSpanFilters: true,
};
return <NewTracePageSearchBar {...searchBarProps} />;
};
it('should render', () => {
expect(() => render(<NewTracePageSearchBarWithProps matches={[]} />)).not.toThrow();
});
it('renders clear filter button', () => {
render(<NewTracePageSearchBarWithProps matches={[]} />);
const clearFiltersButton = screen.getByRole('button', { name: 'Clear filters button' });
expect(clearFiltersButton).toBeInTheDocument();
expect((clearFiltersButton as HTMLButtonElement)['disabled']).toBe(true);
});
it('renders show span filter matches only switch', async () => {
render(<NewTracePageSearchBarWithProps matches={[]} />);
const matchesSwitch = screen.getByRole('checkbox', { name: 'Show matches only switch' });
expect(matchesSwitch).toBeInTheDocument();
});
});

View File

@ -1,159 +0,0 @@
// Copyright (c) 2018 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { css } from '@emotion/css';
import React, { memo, Dispatch, SetStateAction, useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, Switch, useStyles2 } from '@grafana/ui';
import { getButtonStyles } from '@grafana/ui/src/components/Button';
import { SearchProps } from '../../../useSearch';
import { Trace } from '../../types';
import { convertTimeFilter } from '../../utils/filter-spans';
import NextPrevResult from './NextPrevResult';
export type TracePageSearchBarProps = {
trace: Trace;
search: SearchProps;
spanFilterMatches: Set<string> | undefined;
showSpanFilterMatchesOnly: boolean;
setShowSpanFilterMatchesOnly: (showMatchesOnly: boolean) => void;
focusedSpanIndexForSearch: number;
setFocusedSpanIndexForSearch: Dispatch<SetStateAction<number>>;
setFocusedSpanIdForSearch: Dispatch<SetStateAction<string>>;
datasourceType: string;
clear: () => void;
showSpanFilters: boolean;
};
export default memo(function NewTracePageSearchBar(props: TracePageSearchBarProps) {
const {
trace,
search,
spanFilterMatches,
showSpanFilterMatchesOnly,
setShowSpanFilterMatchesOnly,
focusedSpanIndexForSearch,
setFocusedSpanIndexForSearch,
setFocusedSpanIdForSearch,
datasourceType,
clear,
showSpanFilters,
} = props;
const styles = useStyles2(getStyles);
const clearEnabled = useMemo(() => {
return (
(search.serviceName && search.serviceName !== '') ||
(search.spanName && search.spanName !== '') ||
convertTimeFilter(search.from || '') ||
convertTimeFilter(search.to || '') ||
search.tags.length > 1 ||
search.tags.some((tag) => {
return tag.key;
})
);
}, [search.serviceName, search.spanName, search.from, search.to, search.tags]);
return (
<div className={styles.container}>
<div className={styles.controls}>
<>
<div className={styles.clearButton}>
<Button
variant="destructive"
disabled={!clearEnabled}
type="button"
fill="outline"
aria-label="Clear filters button"
onClick={clear}
>
Clear
</Button>
<div className={styles.matchesOnly}>
<Switch
value={showSpanFilterMatchesOnly}
onChange={(value) => setShowSpanFilterMatchesOnly(value.currentTarget.checked ?? false)}
label="Show matches only switch"
/>
<Button
onClick={() => setShowSpanFilterMatchesOnly(!showSpanFilterMatchesOnly)}
className={styles.clearMatchesButton}
variant="secondary"
fill="text"
>
Show matches only
</Button>
</div>
</div>
<div className={styles.nextPrevResult}>
<NextPrevResult
trace={trace}
spanFilterMatches={spanFilterMatches}
setFocusedSpanIdForSearch={setFocusedSpanIdForSearch}
focusedSpanIndexForSearch={focusedSpanIndexForSearch}
setFocusedSpanIndexForSearch={setFocusedSpanIndexForSearch}
datasourceType={datasourceType}
showSpanFilters={showSpanFilters}
/>
</div>
</>
</div>
</div>
);
});
export const getStyles = (theme: GrafanaTheme2) => {
const buttonStyles = getButtonStyles({ theme, variant: 'secondary', size: 'md', iconOnly: false, fill: 'outline' });
return {
button: css(buttonStyles.button),
buttonDisabled: css(buttonStyles.disabled, { pointerEvents: 'none', cursor: 'not-allowed' }),
container: css`
display: inline;
`,
controls: css`
display: flex;
justify-content: flex-end;
margin: 5px 0 0 0;
`,
clearButton: css`
order: 1;
`,
matchesOnly: css`
display: inline-flex;
margin: 0 0 0 10px;
vertical-align: middle;
align-items: center;
span {
cursor: pointer;
margin: 0 0 0 5px;
}
`,
clearMatchesButton: css`
color: ${theme.colors.text.primary};
&:hover {
background: inherit;
color: inherit;
}
`,
nextPrevResult: css`
margin-left: auto;
order: 2;
`,
};
};

View File

@ -145,8 +145,10 @@ export default memo(function NextPrevResult(props: NextPrevResultProps) {
if (spanFilterMatches) { if (spanFilterMatches) {
spanFilterMatches.forEach((spanID) => { spanFilterMatches.forEach((spanID) => {
matchedServices.push(trace.processes[spanID].serviceName); if (trace.processes[spanID]) {
matchedDepth.push(trace.spans.find((span) => span.spanID === spanID)?.depth || 0); matchedServices.push(trace.processes[spanID].serviceName);
matchedDepth.push(trace.spans.find((span) => span.spanID === spanID)?.depth || 0);
}
}); });
if (spanFilterMatches.size === 0) { if (spanFilterMatches.size === 0) {

View File

@ -15,65 +15,45 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import React from 'react'; import React from 'react';
import { createTheme } from '@grafana/data'; import { defaultFilters } from '../../../useSearch';
import { trace } from '../TracePageHeader.test';
import TracePageSearchBar, { getStyles, TracePageSearchBarProps } from './TracePageSearchBar'; import TracePageSearchBar from './TracePageSearchBar';
const defaultProps = {
forwardedRef: React.createRef(),
navigable: true,
searchBarSuffix: 'suffix',
searchValue: 'value',
};
describe('<TracePageSearchBar>', () => { describe('<TracePageSearchBar>', () => {
describe('truthy textFilter', () => { const TracePageSearchBarWithProps = (props: { matches: string[] | undefined }) => {
it('renders SearchBarInput with correct props', () => { const searchBarProps = {
render(<TracePageSearchBar {...(defaultProps as unknown as TracePageSearchBarProps)} />); trace: trace,
expect((screen.getByPlaceholderText('Find...') as HTMLInputElement)['value']).toEqual('value'); search: defaultFilters,
const suffix = screen.getByLabelText('Search bar suffix'); spanFilterMatches: props.matches ? new Set(props.matches) : undefined,
const theme = createTheme(); showSpanFilterMatchesOnly: false,
expect(suffix['className']).toBe(getStyles(theme).TracePageSearchBarSuffix); setShowSpanFilterMatchesOnly: jest.fn(),
expect(suffix.textContent).toBe('suffix'); setFocusedSpanIdForSearch: jest.fn(),
}); focusedSpanIndexForSearch: -1,
setFocusedSpanIndexForSearch: jest.fn(),
datasourceType: '',
clear: jest.fn(),
totalSpans: 100,
showSpanFilters: true,
};
it('renders buttons', () => { return <TracePageSearchBar {...searchBarProps} />;
render(<TracePageSearchBar {...(defaultProps as unknown as TracePageSearchBarProps)} />); };
const nextResButton = screen.queryByRole('button', { name: 'Next results button' });
const prevResButton = screen.queryByRole('button', { name: 'Prev results button' });
expect(nextResButton).toBeInTheDocument();
expect(prevResButton).toBeInTheDocument();
expect((nextResButton as HTMLButtonElement)['disabled']).toBe(false);
expect((prevResButton as HTMLButtonElement)['disabled']).toBe(false);
});
it('only shows navigable buttons when navigable is true', () => { it('should render', () => {
const props = { expect(() => render(<TracePageSearchBarWithProps matches={[]} />)).not.toThrow();
...defaultProps,
navigable: false,
};
render(<TracePageSearchBar {...(props as unknown as TracePageSearchBarProps)} />);
expect(screen.queryByRole('button', { name: 'Next results button' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Prev results button' })).not.toBeInTheDocument();
});
}); });
describe('falsy textFilter', () => { it('renders clear filter button', () => {
beforeEach(() => { render(<TracePageSearchBarWithProps matches={[]} />);
const props = { const clearFiltersButton = screen.getByRole('button', { name: 'Clear filters button' });
...defaultProps, expect(clearFiltersButton).toBeInTheDocument();
searchValue: '', expect((clearFiltersButton as HTMLButtonElement)['disabled']).toBe(true);
}; });
render(<TracePageSearchBar {...(props as unknown as TracePageSearchBarProps)} />);
});
it('does not render suffix', () => { it('renders show span filter matches only switch', async () => {
expect(screen.queryByLabelText('Search bar suffix')).not.toBeInTheDocument(); render(<TracePageSearchBarWithProps matches={[]} />);
}); const matchesSwitch = screen.getByRole('checkbox', { name: 'Show matches only switch' });
expect(matchesSwitch).toBeInTheDocument();
it('renders buttons', () => {
expect(screen.getByRole('button', { name: 'Next results button' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Prev results button' })).toBeInTheDocument();
});
}); });
}); });

View File

@ -13,181 +13,147 @@
// limitations under the License. // limitations under the License.
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import cx from 'classnames'; import React, { memo, Dispatch, SetStateAction, useMemo } from 'react';
import React, { memo, Dispatch, SetStateAction } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime'; import { Button, Switch, useStyles2 } from '@grafana/ui';
import { Button, useStyles2 } from '@grafana/ui'; import { getButtonStyles } from '@grafana/ui/src/components/Button';
import SearchBarInput from '../../common/SearchBarInput'; import { SearchProps } from '../../../useSearch';
import { ubFlexAuto, ubJustifyEnd } from '../../uberUtilityStyles'; import { Trace } from '../../types';
import { convertTimeFilter } from '../../utils/filter-spans';
// eslint-disable-next-line no-duplicate-imports import NextPrevResult from './NextPrevResult';
export const getStyles = (theme: GrafanaTheme2) => {
return {
TracePageSearchBar: css`
label: TracePageSearchBar;
float: right;
position: absolute;
top: 0;
right: 0;
padding: 8px;
margin-right: 2px;
`,
TracePageSearchBarBar: css`
label: TracePageSearchBarBar;
max-width: 20rem;
transition: max-width 0.5s;
&:focus-within {
max-width: 100%;
}
`,
TracePageSearchBarSuffix: css`
label: TracePageSearchBarSuffix;
opacity: 0.6;
`,
TracePageSearchBarBtn: css`
label: TracePageSearchBarBtn;
margin-left: 8px;
`,
};
};
export type TracePageSearchBarProps = { export type TracePageSearchBarProps = {
navigable: boolean; trace: Trace;
searchValue: string; search: SearchProps;
setSearch: (value: string) => void; spanFilterMatches: Set<string> | undefined;
searchBarSuffix: string; showSpanFilterMatchesOnly: boolean;
spanFindMatches: Set<string> | undefined; setShowSpanFilterMatchesOnly: (showMatchesOnly: boolean) => void;
focusedSpanIdForSearch: string; focusedSpanIndexForSearch: number;
setSearchBarSuffix: Dispatch<SetStateAction<string>>; setFocusedSpanIndexForSearch: Dispatch<SetStateAction<number>>;
setFocusedSpanIdForSearch: Dispatch<SetStateAction<string>>; setFocusedSpanIdForSearch: Dispatch<SetStateAction<string>>;
datasourceType: string; datasourceType: string;
clear: () => void;
showSpanFilters: boolean;
}; };
export default memo(function TracePageSearchBar(props: TracePageSearchBarProps) { export default memo(function TracePageSearchBar(props: TracePageSearchBarProps) {
const { const {
navigable, trace,
setSearch, search,
searchValue, spanFilterMatches,
searchBarSuffix, showSpanFilterMatchesOnly,
spanFindMatches, setShowSpanFilterMatchesOnly,
focusedSpanIdForSearch, focusedSpanIndexForSearch,
setSearchBarSuffix, setFocusedSpanIndexForSearch,
setFocusedSpanIdForSearch, setFocusedSpanIdForSearch,
datasourceType, datasourceType,
clear,
showSpanFilters,
} = props; } = props;
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const suffix = searchValue ? ( const clearEnabled = useMemo(() => {
<span className={styles.TracePageSearchBarSuffix} aria-label="Search bar suffix"> return (
{searchBarSuffix} (search.serviceName && search.serviceName !== '') ||
</span> (search.spanName && search.spanName !== '') ||
) : null; convertTimeFilter(search.from || '') ||
convertTimeFilter(search.to || '') ||
const SearchBarInputProps = { search.tags.length > 1 ||
className: cx(styles.TracePageSearchBarBar, ubFlexAuto), search.tags.some((tag) => {
name: 'search', return tag.key;
suffix, })
}; );
}, [search.serviceName, search.spanName, search.from, search.to, search.tags]);
const setTraceSearch = (value: string) => {
setFocusedSpanIdForSearch('');
setSearchBarSuffix('');
setSearch(value);
};
const nextResult = () => {
reportInteraction('grafana_traces_trace_view_find_next_prev_clicked', {
datasourceType: datasourceType,
grafana_version: config.buildInfo.version,
direction: 'next',
});
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 = () => {
reportInteraction('grafana_traces_trace_view_find_next_prev_clicked', {
datasourceType: datasourceType,
grafana_version: config.buildInfo.version,
direction: 'prev',
});
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 ( return (
<div className={styles.TracePageSearchBar}> <div className={styles.container}>
<span className={ubJustifyEnd} style={{ display: 'flex' }}> <div className={styles.controls}>
<SearchBarInput
onChange={setTraceSearch}
value={searchValue}
inputProps={SearchBarInputProps}
allowClear={true}
/>
<> <>
{navigable && ( <div className={styles.clearButton}>
<> <Button
<Button variant="destructive"
className={styles.TracePageSearchBarBtn} disabled={!clearEnabled}
variant="secondary" type="button"
disabled={!searchValue} fill="outline"
type="button" aria-label="Clear filters button"
icon="arrow-down" onClick={clear}
aria-label="Next results button" >
onClick={nextResult} Clear
</Button>
<div className={styles.matchesOnly}>
<Switch
value={showSpanFilterMatchesOnly}
onChange={(value) => setShowSpanFilterMatchesOnly(value.currentTarget.checked ?? false)}
label="Show matches only switch"
/> />
<Button <Button
className={styles.TracePageSearchBarBtn} onClick={() => setShowSpanFilterMatchesOnly(!showSpanFilterMatchesOnly)}
className={styles.clearMatchesButton}
variant="secondary" variant="secondary"
disabled={!searchValue} fill="text"
type="button" >
icon="arrow-up" Show matches only
aria-label="Prev results button" </Button>
onClick={prevResult} </div>
/> </div>
</> <div className={styles.nextPrevResult}>
)} <NextPrevResult
trace={trace}
spanFilterMatches={spanFilterMatches}
setFocusedSpanIdForSearch={setFocusedSpanIdForSearch}
focusedSpanIndexForSearch={focusedSpanIndexForSearch}
setFocusedSpanIndexForSearch={setFocusedSpanIndexForSearch}
datasourceType={datasourceType}
showSpanFilters={showSpanFilters}
/>
</div>
</> </>
</span> </div>
</div> </div>
); );
}); });
export const getStyles = (theme: GrafanaTheme2) => {
const buttonStyles = getButtonStyles({ theme, variant: 'secondary', size: 'md', iconOnly: false, fill: 'outline' });
return {
button: css(buttonStyles.button),
buttonDisabled: css(buttonStyles.disabled, { pointerEvents: 'none', cursor: 'not-allowed' }),
container: css`
display: inline;
`,
controls: css`
display: flex;
justify-content: flex-end;
margin: 5px 0 0 0;
`,
clearButton: css`
order: 1;
`,
matchesOnly: css`
display: inline-flex;
margin: 0 0 0 10px;
vertical-align: middle;
align-items: center;
span {
cursor: pointer;
margin: 0 0 0 5px;
}
`,
clearMatchesButton: css`
color: ${theme.colors.text.primary};
&:hover {
background: inherit;
color: inherit;
}
`,
nextPrevResult: css`
margin-left: auto;
order: 2;
`,
};
};

View File

@ -25,8 +25,8 @@ import { IntervalInput } from 'app/core/components/IntervalInput/IntervalInput';
import { defaultFilters, randomId, SearchProps, Tag } from '../../../useSearch'; import { defaultFilters, randomId, SearchProps, Tag } from '../../../useSearch';
import { KIND, LIBRARY_NAME, LIBRARY_VERSION, STATUS, STATUS_MESSAGE, TRACE_STATE, ID } from '../../constants/span'; import { KIND, LIBRARY_NAME, LIBRARY_VERSION, STATUS, STATUS_MESSAGE, TRACE_STATE, ID } from '../../constants/span';
import { Trace } from '../../types'; import { Trace } from '../../types';
import NewTracePageSearchBar from '../SearchBar/NewTracePageSearchBar';
import NextPrevResult from '../SearchBar/NextPrevResult'; import NextPrevResult from '../SearchBar/NextPrevResult';
import TracePageSearchBar from '../SearchBar/TracePageSearchBar';
export type SpanFilterProps = { export type SpanFilterProps = {
trace: Trace; trace: Trace;
@ -447,7 +447,7 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
</InlineField> </InlineField>
</InlineFieldRow> </InlineFieldRow>
<NewTracePageSearchBar <TracePageSearchBar
trace={trace} trace={trace}
search={search} search={search}
spanFilterMatches={spanFilterMatches} spanFilterMatches={spanFilterMatches}

View File

@ -12,12 +12,54 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { render, screen } from '@testing-library/react'; import { getByText, render } from '@testing-library/react';
import React from 'react'; import React from 'react';
import { getTraceName } from '../model/trace-viewer'; import { MutableDataFrame } from '@grafana/data';
import TracePageHeader, { TracePageHeaderProps } from './TracePageHeader'; import { defaultFilters } from '../../useSearch';
import { TracePageHeader } from './TracePageHeader';
const setup = () => {
const defaultProps = {
trace,
timeZone: '',
search: defaultFilters,
setSearch: jest.fn(),
showSpanFilters: true,
setShowSpanFilters: jest.fn(),
showSpanFilterMatchesOnly: false,
setShowSpanFilterMatchesOnly: jest.fn(),
spanFilterMatches: undefined,
setFocusedSpanIdForSearch: jest.fn(),
datasourceType: 'tempo',
setHeaderHeight: jest.fn(),
data: new MutableDataFrame(),
};
return render(<TracePageHeader {...defaultProps} />);
};
describe('TracePageHeader test', () => {
it('should render the new trace header', () => {
setup();
const header = document.querySelector('header');
const method = getByText(header!, 'POST');
const status = getByText(header!, '200');
const url = getByText(header!, '/v2/gamma/792edh2w897y2huehd2h89');
const duration = getByText(header!, '2.36s');
const timestampPart1 = getByText(header!, '2023-02-05 08:50');
const timestampPart2 = getByText(header!, ':56.289');
expect(method).toBeInTheDocument();
expect(status).toBeInTheDocument();
expect(url).toBeInTheDocument();
expect(duration).toBeInTheDocument();
expect(timestampPart1).toBeInTheDocument();
expect(timestampPart2).toBeInTheDocument();
});
});
export const trace = { export const trace = {
services: [{ name: 'serviceA', numberOfSpans: 1 }], services: [{ name: 'serviceA', numberOfSpans: 1 }],
@ -144,72 +186,3 @@ export const trace = {
startTime: 1675605056289000, startTime: 1675605056289000,
endTime: 1675605058644515, endTime: 1675605058644515,
}; };
const setup = (propOverrides?: TracePageHeaderProps) => {
const defaultProps = {
trace,
timeZone: '',
viewRange: { time: { current: [10, 20] as [number, number] } },
updateNextViewRangeTime: () => {},
updateViewRangeTime: () => {},
...propOverrides,
};
return render(<TracePageHeader {...defaultProps} />);
};
describe('TracePageHeader test', () => {
it('should render a header ', () => {
setup();
expect(screen.getByRole('banner')).toBeInTheDocument();
});
it('should render nothing if a trace is not present', () => {
setup({ trace: null } as TracePageHeaderProps);
expect(screen.queryByRole('banner')).not.toBeInTheDocument();
expect(screen.queryAllByRole('listitem')).toHaveLength(0);
expect(screen.queryByText(/Reset Selection/)).not.toBeInTheDocument();
});
it('should render the trace title', () => {
setup();
expect(
screen.getByRole('heading', {
name: (content) => content.replace(/ /g, '').startsWith(getTraceName(trace!.spans).replace(/ /g, '')),
})
).toBeInTheDocument();
});
it('should render the header items', () => {
setup();
const headerItems = screen.queryAllByRole('listitem');
expect(headerItems).toHaveLength(5);
// Year-month-day hour-minute-second
expect(headerItems[0].textContent?.match(/Trace Start:\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\.\d{3}/g)).toBeTruthy();
expect(headerItems[1].textContent?.match(/Duration:[\d|\.][\.|\d|s][\.|\d|s]?[\d]?/)).toBeTruthy();
expect(headerItems[2].textContent?.match(/Services:\d\d?/g)).toBeTruthy();
expect(headerItems[3].textContent?.match(/Depth:\d\d?/)).toBeTruthy();
expect(headerItems[4].textContent?.match(/Total Spans:\d\d?\d?\d?/)).toBeTruthy();
});
it('should render a <SpanGraph>', () => {
setup();
expect(screen.getByText(/Reset Selection/)).toBeInTheDocument();
});
it('shows the summary', () => {
const { rerender } = setup();
rerender(
<TracePageHeader
{...({
trace: trace,
viewRange: { time: { current: [10, 20] } },
} as unknown as TracePageHeaderProps)}
/>
);
expect(screen.queryAllByRole('listitem')).toHaveLength(5);
});
});

View File

@ -14,38 +14,158 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import cx from 'classnames'; import cx from 'classnames';
import { get as _get, maxBy as _maxBy, values as _values } from 'lodash'; import React, { memo, useEffect, useMemo } from 'react';
import React from 'react';
import { dateTimeFormat, GrafanaTheme2, TimeZone } from '@grafana/data'; import { CoreApp, DataFrame, dateTimeFormat, GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui'; import { TimeZone } from '@grafana/schema';
import { Badge, BadgeColor, Tooltip, useStyles2 } from '@grafana/ui';
import { SearchProps } from '../../useSearch';
import ExternalLinks from '../common/ExternalLinks'; import ExternalLinks from '../common/ExternalLinks';
import LabeledList from '../common/LabeledList';
import TraceName from '../common/TraceName'; import TraceName from '../common/TraceName';
import { autoColor, TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate } from '../index';
import { getTraceLinks } from '../model/link-patterns'; import { getTraceLinks } from '../model/link-patterns';
import { getTraceName } from '../model/trace-viewer'; import { getHeaderTags, getTraceName } from '../model/trace-viewer';
import { Trace } from '../types'; import { Trace } from '../types';
import { uTxMuted } from '../uberUtilityStyles';
import { formatDuration } from '../utils/date'; import { formatDuration } from '../utils/date';
import SpanGraph from './SpanGraph'; import TracePageActions from './Actions/TracePageActions';
import { SpanFilters } from './SpanFilters/SpanFilters';
export const getStyles = (theme: GrafanaTheme2) => { export type TracePageHeaderProps = {
trace: Trace | null;
data: DataFrame;
app?: CoreApp;
timeZone: TimeZone;
search: SearchProps;
setSearch: React.Dispatch<React.SetStateAction<SearchProps>>;
showSpanFilters: boolean;
setShowSpanFilters: (isOpen: boolean) => void;
showSpanFilterMatchesOnly: boolean;
setShowSpanFilterMatchesOnly: (showMatchesOnly: boolean) => void;
setFocusedSpanIdForSearch: React.Dispatch<React.SetStateAction<string>>;
spanFilterMatches: Set<string> | undefined;
datasourceType: string;
setHeaderHeight: (height: number) => void;
};
export const TracePageHeader = memo((props: TracePageHeaderProps) => {
const {
trace,
data,
app,
timeZone,
search,
setSearch,
showSpanFilters,
setShowSpanFilters,
showSpanFilterMatchesOnly,
setShowSpanFilterMatchesOnly,
setFocusedSpanIdForSearch,
spanFilterMatches,
datasourceType,
setHeaderHeight,
} = props;
const styles = useStyles2(getNewStyles);
useEffect(() => {
setHeaderHeight(document.querySelector('.' + styles.header)?.scrollHeight ?? 0);
}, [setHeaderHeight, showSpanFilters, styles.header]);
const links = useMemo(() => {
if (!trace) {
return [];
}
return getTraceLinks(trace);
}, [trace]);
if (!trace) {
return null;
}
const timestamp = (trace: Trace, timeZone: TimeZone) => {
// Convert date from micro to milli seconds
const dateStr = dateTimeFormat(trace.startTime / 1000, { timeZone, defaultWithMS: true });
const match = dateStr.match(/^(.+)(:\d\d\.\d+)$/);
return match ? (
<span className={styles.TracePageHeaderOverviewItemValue}>
{match[1]}
<span className={styles.TracePageHeaderOverviewItemValueDetail}>{match[2]}</span>
</span>
) : (
dateStr
);
};
const title = (
<h1 className={cx(styles.title)}>
<TraceName traceName={getTraceName(trace.spans)} />
<small className={styles.duration}>{formatDuration(trace.duration)}</small>
</h1>
);
const { method, status, url } = getHeaderTags(trace.spans);
let statusColor: BadgeColor = 'green';
if (status && status.length > 0) {
if (status[0].value.toString().charAt(0) === '4') {
statusColor = 'orange';
} else if (status[0].value.toString().charAt(0) === '5') {
statusColor = 'red';
}
}
return (
<header className={styles.header}>
<div className={styles.titleRow}>
{links && links.length > 0 && <ExternalLinks links={links} className={styles.TracePageHeaderBack} />}
{title}
<TracePageActions traceId={trace.traceID} data={data} app={app} />
</div>
<div className={styles.subtitle}>
<span className={styles.timestamp}>{timestamp(trace, timeZone)}</span>
<span className={styles.tagMeta}>
{method && method.length > 0 && (
<Tooltip content={'http.method'} interactive={true}>
<span className={styles.tag}>
<Badge text={method[0].value} color="blue" />
</span>
</Tooltip>
)}
{status && status.length > 0 && (
<Tooltip content={'http.status_code'} interactive={true}>
<span className={styles.tag}>
<Badge text={status[0].value} color={statusColor} />
</span>
</Tooltip>
)}
{url && url.length > 0 && (
<Tooltip content={'http.url or http.target or http.path'} interactive={true}>
<span className={styles.url}>{url[0].value}</span>
</Tooltip>
)}
</span>
</div>
<SpanFilters
trace={trace}
showSpanFilters={showSpanFilters}
setShowSpanFilters={setShowSpanFilters}
showSpanFilterMatchesOnly={showSpanFilterMatchesOnly}
setShowSpanFilterMatchesOnly={setShowSpanFilterMatchesOnly}
search={search}
setSearch={setSearch}
spanFilterMatches={spanFilterMatches}
setFocusedSpanIdForSearch={setFocusedSpanIdForSearch}
datasourceType={datasourceType}
/>
</header>
);
});
TracePageHeader.displayName = 'TracePageHeader';
const getNewStyles = (theme: GrafanaTheme2) => {
return { return {
theme,
TracePageHeader: css`
label: TracePageHeader;
& > :last-child {
border-bottom: 1px solid ${autoColor(theme, '#ccc')};
}
`,
TracePageHeaderTitleRow: css`
label: TracePageHeaderTitleRow;
align-items: center;
display: flex;
`,
TracePageHeaderBack: css` TracePageHeaderBack: css`
label: TracePageHeaderBack; label: TracePageHeaderBack;
align-items: center; align-items: center;
@ -63,21 +183,6 @@ export const getStyles = (theme: GrafanaTheme2) => {
border-color: #ccc; border-color: #ccc;
} }
`, `,
TracePageHeaderTitle: css`
label: TracePageHeaderTitle;
color: inherit;
flex: 1;
font-size: 1.7em;
line-height: 1em;
margin: 0 0 0 0.3em;
padding-bottom: 0.5em;
`,
TracePageHeaderOverviewItems: css`
label: TracePageHeaderOverviewItems;
background-color: ${autoColor(theme, '#eee')};
border-bottom: 1px solid ${autoColor(theme, '#e4e4e4')};
padding: 0.25rem 0.5rem !important;
`,
TracePageHeaderOverviewItemValueDetail: cx( TracePageHeaderOverviewItemValueDetail: cx(
css` css`
label: TracePageHeaderOverviewItemValueDetail; label: TracePageHeaderOverviewItemValueDetail;
@ -91,107 +196,57 @@ export const getStyles = (theme: GrafanaTheme2) => {
color: unset; color: unset;
} }
`, `,
header: css`
label: TracePageHeader;
background-color: ${theme.colors.background.primary};
padding: 0.5em 0 0 0;
position: sticky;
top: 0;
z-index: 5;
`,
titleRow: css`
align-items: flex-start;
display: flex;
padding: 0 8px;
`,
title: css`
color: inherit;
flex: 1;
font-size: 1.7em;
line-height: 1em;
`,
subtitle: css`
flex: 1;
line-height: 1em;
margin: -0.5em 0.5em 0.75em 0.5em;
`,
tag: css`
margin: 0 0.5em 0 0;
`,
duration: css`
color: #aaa;
margin: 0 0.75em;
`,
timestamp: css`
vertical-align: middle;
`,
tagMeta: css`
margin: 0 0.75em;
vertical-align: text-top;
`,
url: css`
margin: -2.5px 0.3em;
height: 15px;
overflow: hidden;
word-break: break-all;
line-height: 20px;
`,
TracePageHeaderTraceId: css` TracePageHeaderTraceId: css`
label: TracePageHeaderTraceId; label: TracePageHeaderTraceId;
white-space: nowrap; white-space: nowrap;
`, text-overflow: ellipsis;
titleBorderBottom: css` max-width: 30%;
border-bottom: 1px solid ${autoColor(theme, '#e8e8e8')}; display: inline-block;
`, `,
}; };
}; };
export type TracePageHeaderProps = {
trace: Trace | null;
updateNextViewRangeTime: (update: ViewRangeTimeUpdate) => void;
updateViewRangeTime: TUpdateViewRangeTimeFunction;
viewRange: ViewRange;
timeZone: TimeZone;
};
export const timestamp = (trace: Trace, timeZone: TimeZone, styles: ReturnType<typeof getStyles>) => {
// Convert date from micro to milli seconds
const dateStr = dateTimeFormat(trace.startTime / 1000, { timeZone, defaultWithMS: true });
const match = dateStr.match(/^(.+)(:\d\d\.\d+)$/);
return match ? (
<span className={styles.TracePageHeaderOverviewItemValue}>
{match[1]}
<span className={styles.TracePageHeaderOverviewItemValueDetail}>{match[2]}</span>
</span>
) : (
dateStr
);
};
export const HEADER_ITEMS = [
{
key: 'timestamp',
label: 'Trace Start:',
renderer: timestamp,
},
{
key: 'duration',
label: 'Duration:',
renderer: (trace: Trace) => formatDuration(trace.duration),
},
{
key: 'service-count',
label: 'Services:',
renderer: (trace: Trace) => new Set(_values(trace.processes).map((p) => p.serviceName)).size,
},
{
key: 'depth',
label: 'Depth:',
renderer: (trace: Trace) => _get(_maxBy(trace.spans, 'depth'), 'depth', 0) + 1,
},
{
key: 'span-count',
label: 'Total Spans:',
renderer: (trace: Trace) => trace.spans.length,
},
];
export default function TracePageHeader(props: TracePageHeaderProps) {
const { trace, updateNextViewRangeTime, updateViewRangeTime, viewRange, timeZone } = props;
const styles = useStyles2(getStyles);
const links = React.useMemo(() => {
if (!trace) {
return [];
}
return getTraceLinks(trace);
}, [trace]);
if (!trace) {
return null;
}
const summaryItems = HEADER_ITEMS.map((item) => {
const { renderer, ...rest } = item;
return { ...rest, value: renderer(trace, timeZone, styles) };
});
const title = (
<h1 className={styles.TracePageHeaderTitle}>
<TraceName traceName={getTraceName(trace.spans)} />{' '}
<small className={cx(styles.TracePageHeaderTraceId, uTxMuted)}>{trace.traceID}</small>
</h1>
);
return (
<header className={styles.TracePageHeader}>
<div className={cx(styles.TracePageHeaderTitleRow, styles.titleBorderBottom)}>
{links && links.length > 0 && <ExternalLinks links={links} className={styles.TracePageHeaderBack} />}
{title}
</div>
{summaryItems && <LabeledList className={styles.TracePageHeaderOverviewItems} items={summaryItems} />}
<SpanGraph
trace={trace}
viewRange={viewRange}
updateNextViewRangeTime={updateNextViewRangeTime}
updateViewRangeTime={updateViewRangeTime}
/>
</header>
);
}

View File

@ -12,5 +12,4 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
export { default } from './TracePageHeader'; export { TracePageHeader } from './TracePageHeader';
export { NewTracePageHeader } from './NewTracePageHeader';

View File

@ -39,7 +39,6 @@ let props = {
findMatchesIDs: null, findMatchesIDs: null,
registerAccessors: jest.fn(), registerAccessors: jest.fn(),
setSpanNameColumnWidth: jest.fn(), setSpanNameColumnWidth: jest.fn(),
setTrace: jest.fn(),
spanNameColumnWidth: 0.5, spanNameColumnWidth: 0.5,
trace, trace,
uiFind: 'uiFind', uiFind: 'uiFind',
@ -97,12 +96,4 @@ describe('<VirtualizedTraceViewImpl>', () => {
}) })
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
it('sets the trace for global state.traceTimeline', () => {
const traceID = 'some-other-id';
const _trace = { ...trace, traceID };
props = { ...props, trace: _trace };
render(<VirtualizedTraceView {...props} />);
expect(jest.mocked(props.setTrace).mock.calls).toEqual([[_trace, props.uiFind]]);
});
}); });

View File

@ -42,10 +42,6 @@ import {
ViewedBoundsFunctionType, ViewedBoundsFunctionType,
} from './utils'; } from './utils';
type TExtractUiFindFromStateReturn = {
uiFind: string | undefined;
};
const getStyles = stylesFactory((props: TVirtualizedTraceViewOwnProps) => { const getStyles = stylesFactory((props: TVirtualizedTraceViewOwnProps) => {
const { topOfViewRefType } = props; const { topOfViewRefType } = props;
const position = topOfViewRefType === TopOfViewRefType.Explore ? 'fixed' : 'absolute'; const position = topOfViewRefType === TopOfViewRefType.Explore ? 'fixed' : 'absolute';
@ -102,7 +98,6 @@ type TVirtualizedTraceViewOwnProps = {
detailTagsToggle: (spanID: string) => void; detailTagsToggle: (spanID: string) => void;
detailToggle: (spanID: string) => void; detailToggle: (spanID: string) => void;
setSpanNameColumnWidth: (width: number) => void; setSpanNameColumnWidth: (width: number) => void;
setTrace: (trace: Trace | TNil, uiFind: string | TNil) => void;
hoverIndentGuideIds: Set<string>; hoverIndentGuideIds: Set<string>;
addHoverIndentGuideId: (spanID: string) => void; addHoverIndentGuideId: (spanID: string) => void;
removeHoverIndentGuideId: (spanID: string) => void; removeHoverIndentGuideId: (spanID: string) => void;
@ -119,7 +114,7 @@ type TVirtualizedTraceViewOwnProps = {
headerHeight: number; headerHeight: number;
}; };
export type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & TExtractUiFindFromStateReturn & TTraceTimeline; export type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & TTraceTimeline;
// export for tests // export for tests
export const DEFAULT_HEIGHTS = { export const DEFAULT_HEIGHTS = {
@ -206,12 +201,7 @@ const memoizedGetClipping = memoizeOne(getClipping, isEqual);
// export from tests // export from tests
export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTraceViewProps> { export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTraceViewProps> {
listView: ListView | TNil; listView: ListView | TNil;
hasScrolledToSpan = false;
constructor(props: VirtualizedTraceViewProps) {
super(props);
const { setTrace, trace, uiFind } = props;
setTrace(trace, uiFind);
}
componentDidMount() { componentDidMount() {
this.scrollToSpan(this.props.headerHeight, this.props.focusedSpanId); this.scrollToSpan(this.props.headerHeight, this.props.focusedSpanId);
@ -229,24 +219,23 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
} }
componentDidUpdate(prevProps: Readonly<VirtualizedTraceViewProps>) { componentDidUpdate(prevProps: Readonly<VirtualizedTraceViewProps>) {
const { registerAccessors, trace, headerHeight } = prevProps; const { registerAccessors } = prevProps;
const { const {
registerAccessors: nextRegisterAccessors, registerAccessors: nextRegisterAccessors,
setTrace, headerHeight,
trace: nextTrace,
uiFind,
focusedSpanId, focusedSpanId,
focusedSpanIdForSearch, focusedSpanIdForSearch,
} = this.props; } = this.props;
if (trace !== nextTrace) {
setTrace(nextTrace, uiFind);
}
if (this.listView && registerAccessors !== nextRegisterAccessors) { if (this.listView && registerAccessors !== nextRegisterAccessors) {
nextRegisterAccessors(this.getAccessors()); nextRegisterAccessors(this.getAccessors());
} }
if (!this.hasScrolledToSpan) {
this.scrollToSpan(headerHeight, focusedSpanId);
this.hasScrolledToSpan = true;
}
if (focusedSpanId !== prevProps.focusedSpanId) { if (focusedSpanId !== prevProps.focusedSpanId) {
this.scrollToSpan(headerHeight, focusedSpanId); this.scrollToSpan(headerHeight, focusedSpanId);
} }

View File

@ -51,7 +51,6 @@ describe('<TraceTimelineViewer>', () => {
expandOne: jest.fn(), expandOne: jest.fn(),
registerAccessors: jest.fn(), registerAccessors: jest.fn(),
collapseOne: jest.fn(), collapseOne: jest.fn(),
setTrace: jest.fn(),
theme: createTheme(), theme: createTheme(),
history: { history: {
replace: () => {}, replace: () => {},

View File

@ -31,10 +31,6 @@ import TimelineHeaderRow from './TimelineHeaderRow';
import VirtualizedTraceView, { TopOfViewRefType } from './VirtualizedTraceView'; import VirtualizedTraceView, { TopOfViewRefType } from './VirtualizedTraceView';
import { TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate } from './types'; import { TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate } from './types';
type TExtractUiFindFromStateReturn = {
uiFind: string | undefined;
};
const getStyles = stylesFactory((theme: GrafanaTheme2) => { const getStyles = stylesFactory((theme: GrafanaTheme2) => {
return { return {
TraceTimelineViewer: css` TraceTimelineViewer: css`
@ -71,7 +67,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
}; };
}); });
export type TProps = TExtractUiFindFromStateReturn & { export type TProps = {
registerAccessors: (accessors: Accessors) => void; registerAccessors: (accessors: Accessors) => void;
findMatchesIDs: Set<string> | TNil; findMatchesIDs: Set<string> | TNil;
traceTimeline: TTraceTimeline; traceTimeline: TTraceTimeline;
@ -99,7 +95,6 @@ export type TProps = TExtractUiFindFromStateReturn & {
detailProcessToggle: (spanID: string) => void; detailProcessToggle: (spanID: string) => void;
detailTagsToggle: (spanID: string) => void; detailTagsToggle: (spanID: string) => void;
detailToggle: (spanID: string) => void; detailToggle: (spanID: string) => void;
setTrace: (trace: Trace | TNil, uiFind: string | TNil) => void;
addHoverIndentGuideId: (spanID: string) => void; addHoverIndentGuideId: (spanID: string) => void;
removeHoverIndentGuideId: (spanID: string) => void; removeHoverIndentGuideId: (spanID: string) => void;
linksGetter: (span: TraceSpan, items: TraceKeyValuePair[], itemIndex: number) => TraceLink[]; linksGetter: (span: TraceSpan, items: TraceKeyValuePair[], itemIndex: number) => TraceLink[];

View File

@ -1,10 +1,9 @@
export { default as TraceTimelineViewer } from './TraceTimelineViewer'; export { default as TraceTimelineViewer } from './TraceTimelineViewer';
export { default as TracePageHeader } from './TracePageHeader'; export { TracePageHeader } from './TracePageHeader';
export { NewTracePageHeader } from './TracePageHeader';
export { default as SpanBarSettings } from './settings/SpanBarSettings'; export { default as SpanBarSettings } from './settings/SpanBarSettings';
export * from './types'; export * from './types';
export * from './TraceTimelineViewer/types'; export * from './TraceTimelineViewer/types';
export { default as DetailState } from './TraceTimelineViewer/SpanDetail/DetailState'; export { default as DetailState } from './TraceTimelineViewer/SpanDetail/DetailState';
export { default as transformTraceData } from './model/transform-trace-data'; export { default as transformTraceData } from './model/transform-trace-data';
export { filterSpansNewTraceViewHeader, filterSpans } from './utils/filter-spans'; export { filterSpans } from './utils/filter-spans';
export * from './Theme'; export * from './Theme';

View File

@ -15,7 +15,7 @@
import { defaultFilters, defaultTagFilter } from '../../useSearch'; import { defaultFilters, defaultTagFilter } from '../../useSearch';
import { TraceSpan } from '../types'; import { TraceSpan } from '../types';
import { filterSpans, filterSpansNewTraceViewHeader } from './filter-spans'; import { filterSpans } from './filter-spans';
describe('filterSpans', () => { describe('filterSpans', () => {
// span0 contains strings that end in 0 or 1 // span0 contains strings that end in 0 or 1
@ -122,125 +122,94 @@ describe('filterSpans', () => {
const spans = [span0, span2] as TraceSpan[]; const spans = [span0, span2] as TraceSpan[];
it('should return `undefined` if spans is falsy', () => { it('should return `undefined` if spans is falsy', () => {
expect(filterSpansNewTraceViewHeader({ ...defaultFilters, spanName: 'operationName' }, null)).toBe(undefined); expect(filterSpans({ ...defaultFilters, spanName: 'operationName' }, null)).toBe(undefined);
}); });
// Service / span name // Service / span name
it('should return spans whose serviceName match a filter', () => { it('should return spans whose serviceName match a filter', () => {
expect(filterSpansNewTraceViewHeader({ ...defaultFilters, serviceName: 'serviceName0' }, spans)).toEqual( expect(filterSpans({ ...defaultFilters, serviceName: 'serviceName0' }, spans)).toEqual(new Set([spanID0]));
expect(filterSpans({ ...defaultFilters, serviceName: 'serviceName2' }, spans)).toEqual(new Set([spanID2]));
expect(filterSpans({ ...defaultFilters, serviceName: 'serviceName2', serviceNameOperator: '!=' }, spans)).toEqual(
new Set([spanID0]) new Set([spanID0])
); );
expect(filterSpansNewTraceViewHeader({ ...defaultFilters, serviceName: 'serviceName2' }, spans)).toEqual(
new Set([spanID2])
);
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, serviceName: 'serviceName2', serviceNameOperator: '!=' },
spans
)
).toEqual(new Set([spanID0]));
}); });
it('should return spans whose operationName match a filter', () => { it('should return spans whose operationName match a filter', () => {
expect(filterSpansNewTraceViewHeader({ ...defaultFilters, spanName: 'operationName0' }, spans)).toEqual( expect(filterSpans({ ...defaultFilters, spanName: 'operationName0' }, spans)).toEqual(new Set([spanID0]));
expect(filterSpans({ ...defaultFilters, spanName: 'operationName2' }, spans)).toEqual(new Set([spanID2]));
expect(filterSpans({ ...defaultFilters, spanName: 'operationName2', spanNameOperator: '!=' }, spans)).toEqual(
new Set([spanID0]) new Set([spanID0])
); );
expect(filterSpansNewTraceViewHeader({ ...defaultFilters, spanName: 'operationName2' }, spans)).toEqual(
new Set([spanID2])
);
expect(
filterSpansNewTraceViewHeader({ ...defaultFilters, spanName: 'operationName2', spanNameOperator: '!=' }, spans)
).toEqual(new Set([spanID0]));
}); });
// Durations // Durations
it('should return spans whose duration match a filter', () => { it('should return spans whose duration match a filter', () => {
expect(filterSpansNewTraceViewHeader({ ...defaultFilters, from: '2ns' }, spans)).toEqual( expect(filterSpans({ ...defaultFilters, from: '2ns' }, spans)).toEqual(new Set([spanID0, spanID2]));
expect(filterSpans({ ...defaultFilters, from: '2us' }, spans)).toEqual(new Set([spanID0, spanID2]));
expect(filterSpans({ ...defaultFilters, from: '2ms' }, spans)).toEqual(new Set([spanID0, spanID2]));
expect(filterSpans({ ...defaultFilters, from: '3.05ms' }, spans)).toEqual(new Set([spanID2]));
expect(filterSpans({ ...defaultFilters, from: '3.05ms', fromOperator: '>=' }, spans)).toEqual(
new Set([spanID0, spanID2]) new Set([spanID0, spanID2])
); );
expect(filterSpansNewTraceViewHeader({ ...defaultFilters, from: '2us' }, spans)).toEqual( expect(filterSpans({ ...defaultFilters, from: '3.05ms', fromOperator: '>=', to: '4ms' }, spans)).toEqual(
new Set([spanID0, spanID2]) new Set([spanID0])
);
expect(filterSpansNewTraceViewHeader({ ...defaultFilters, from: '2ms' }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect(filterSpansNewTraceViewHeader({ ...defaultFilters, from: '3.05ms' }, spans)).toEqual(new Set([spanID2]));
expect(filterSpansNewTraceViewHeader({ ...defaultFilters, from: '3.05ms', fromOperator: '>=' }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect(
filterSpansNewTraceViewHeader({ ...defaultFilters, from: '3.05ms', fromOperator: '>=', to: '4ms' }, spans)
).toEqual(new Set([spanID0]));
expect(filterSpansNewTraceViewHeader({ ...defaultFilters, to: '4ms' }, spans)).toEqual(new Set([spanID0]));
expect(filterSpansNewTraceViewHeader({ ...defaultFilters, to: '5ms', toOperator: '<=' }, spans)).toEqual(
new Set([spanID0, spanID2])
); );
expect(filterSpans({ ...defaultFilters, to: '4ms' }, spans)).toEqual(new Set([spanID0]));
expect(filterSpans({ ...defaultFilters, to: '5ms', toOperator: '<=' }, spans)).toEqual(new Set([spanID0, spanID2]));
}); });
// Tags // Tags
it('should return spans whose tags kv.key match a filter', () => { it('should return spans whose tags kv.key match a filter', () => {
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey0' }] }, spans)).toEqual(
new Set([spanID0])
);
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey2' }] }, spans)).toEqual(
new Set([spanID2])
);
expect( expect(
filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1' }] }, spans) filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey2', operator: '!=' }] }, spans)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey0' }] }, spans)
).toEqual(new Set([spanID0]));
expect(
filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey2' }] }, spans)
).toEqual(new Set([spanID2]));
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey2', operator: '!=' }] },
spans
)
).toEqual(new Set([spanID0])); ).toEqual(new Set([spanID0]));
}); });
it('should return spans whose kind, statusCode, statusMessage, libraryName, libraryVersion, traceState, or id match a filter', () => { it('should return spans whose kind, statusCode, statusMessage, libraryName, libraryVersion, traceState, or id match a filter', () => {
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'kind' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect( expect(
filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'kind' }] }, spans) filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'kind', value: 'kind0' }] }, spans)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'kind', value: 'kind0' }] },
spans
)
).toEqual(new Set([spanID0])); ).toEqual(new Set([spanID0]));
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'kind', operator: '!=', value: 'kind0' }] }, { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'kind', operator: '!=', value: 'kind0' }] },
spans spans
) )
).toEqual(new Set([spanID2])); ).toEqual(new Set([spanID2]));
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect( expect(
filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status' }] }, spans) filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status', value: 'unset' }] }, spans)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status', value: 'unset' }] },
spans
)
).toEqual(new Set([spanID0])); ).toEqual(new Set([spanID0]));
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status', operator: '!=', value: 'unset' }] }, { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status', operator: '!=', value: 'unset' }] },
spans spans
) )
).toEqual(new Set([spanID2])); ).toEqual(new Set([spanID2]));
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status.message' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status.message' }] },
spans
)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status.message', value: 'statusMessage0' }] }, { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status.message', value: 'statusMessage0' }] },
spans spans
) )
).toEqual(new Set([spanID0])); ).toEqual(new Set([spanID0]));
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ {
...defaultFilters, ...defaultFilters,
tags: [{ ...defaultTagFilter, key: 'status.message', operator: '!=', value: 'statusMessage0' }], tags: [{ ...defaultTagFilter, key: 'status.message', operator: '!=', value: 'statusMessage0' }],
@ -248,17 +217,17 @@ describe('filterSpans', () => {
spans spans
) )
).toEqual(new Set([spanID2])); ).toEqual(new Set([spanID2]));
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'library.name' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect( expect(
filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'library.name' }] }, spans) filterSpans(
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'library.name', value: 'libraryName' }] }, { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'library.name', value: 'libraryName' }] },
spans spans
) )
).toEqual(new Set([spanID0, spanID2])); ).toEqual(new Set([spanID0, spanID2]));
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ {
...defaultFilters, ...defaultFilters,
tags: [{ ...defaultTagFilter, key: 'library.name', operator: '!=', value: 'libraryName' }], tags: [{ ...defaultTagFilter, key: 'library.name', operator: '!=', value: 'libraryName' }],
@ -266,20 +235,17 @@ describe('filterSpans', () => {
spans spans
) )
).toEqual(new Set([])); ).toEqual(new Set([]));
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'library.version' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'library.version' }] },
spans
)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'library.version', value: 'libraryVersion0' }] }, { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'library.version', value: 'libraryVersion0' }] },
spans spans
) )
).toEqual(new Set([spanID0])); ).toEqual(new Set([spanID0]));
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ {
...defaultFilters, ...defaultFilters,
tags: [{ ...defaultTagFilter, key: 'library.version', operator: '!=', value: 'libraryVersion0' }], tags: [{ ...defaultTagFilter, key: 'library.version', operator: '!=', value: 'libraryVersion0' }],
@ -287,17 +253,17 @@ describe('filterSpans', () => {
spans spans
) )
).toEqual(new Set([spanID2])); ).toEqual(new Set([spanID2]));
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'trace.state' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect( expect(
filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'trace.state' }] }, spans) filterSpans(
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'trace.state', value: 'traceState0' }] }, { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'trace.state', value: 'traceState0' }] },
spans spans
) )
).toEqual(new Set([spanID0])); ).toEqual(new Set([spanID0]));
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ {
...defaultFilters, ...defaultFilters,
tags: [{ ...defaultTagFilter, key: 'trace.state', operator: '!=', value: 'traceState0' }], tags: [{ ...defaultTagFilter, key: 'trace.state', operator: '!=', value: 'traceState0' }],
@ -305,17 +271,14 @@ describe('filterSpans', () => {
spans spans
) )
).toEqual(new Set([spanID2])); ).toEqual(new Set([spanID2]));
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'id' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect( expect(
filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'id' }] }, spans) filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'id', value: 'span-id-0' }] }, spans)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'id', value: 'span-id-0' }] },
spans
)
).toEqual(new Set([spanID0])); ).toEqual(new Set([spanID0]));
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'id', operator: '!=', value: 'span-id-0' }] }, { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'id', operator: '!=', value: 'span-id-0' }] },
spans spans
) )
@ -323,54 +286,39 @@ describe('filterSpans', () => {
}); });
it('should return spans whose process.tags kv.key match a filter', () => { it('should return spans whose process.tags kv.key match a filter', () => {
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'processTagKey1' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'processTagKey0' }] }, spans)).toEqual(
new Set([spanID0])
);
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'processTagKey2' }] }, spans)).toEqual(
new Set([spanID2])
);
expect( expect(
filterSpansNewTraceViewHeader( filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'processTagKey2', operator: '!=' }] }, spans)
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'processTagKey1' }] },
spans
)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'processTagKey0' }] },
spans
)
).toEqual(new Set([spanID0]));
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'processTagKey2' }] },
spans
)
).toEqual(new Set([spanID2]));
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'processTagKey2', operator: '!=' }] },
spans
)
).toEqual(new Set([spanID0])); ).toEqual(new Set([spanID0]));
}); });
it('should return spans whose logs have a field whose kv.key match a filter', () => { it('should return spans whose logs have a field whose kv.key match a filter', () => {
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'logFieldKey1' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'logFieldKey0' }] }, spans)).toEqual(
new Set([spanID0])
);
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'logFieldKey2' }] }, spans)).toEqual(
new Set([spanID2])
);
expect( expect(
filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'logFieldKey1' }] }, spans) filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'logFieldKey2', operator: '!=' }] }, spans)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'logFieldKey0' }] }, spans)
).toEqual(new Set([spanID0]));
expect(
filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'logFieldKey2' }] }, spans)
).toEqual(new Set([spanID2]));
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'logFieldKey2', operator: '!=' }] },
spans
)
).toEqual(new Set([spanID0])); ).toEqual(new Set([spanID0]));
}); });
it('should return no spans when logs is null', () => { it('should return no spans when logs is null', () => {
const nullSpan = { ...span0, logs: null }; const nullSpan = { ...span0, logs: null };
expect( expect(
filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'logFieldKey1' }] }, [ filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'logFieldKey1' }] }, [
nullSpan, nullSpan,
] as unknown as TraceSpan[]) ] as unknown as TraceSpan[])
).toEqual(new Set([])); ).toEqual(new Set([]));
@ -378,13 +326,10 @@ describe('filterSpans', () => {
it("should return spans whose tags' kv.key and kv.value match a filter", () => { it("should return spans whose tags' kv.key and kv.value match a filter", () => {
expect( expect(
filterSpansNewTraceViewHeader( filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', value: 'tagValue1' }] }, spans)
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', value: 'tagValue1' }] },
spans
)
).toEqual(new Set([spanID0])); ).toEqual(new Set([spanID0]));
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', value: 'tagValue1', operator: '!=' }] }, { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', value: 'tagValue1', operator: '!=' }] },
spans spans
) )
@ -393,19 +338,13 @@ describe('filterSpans', () => {
it("should not return spans whose tags' kv.key match a filter but kv.value/operator does not match", () => { it("should not return spans whose tags' kv.key match a filter but kv.value/operator does not match", () => {
expect( expect(
filterSpansNewTraceViewHeader( filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', operator: '!=' }] }, spans)
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', operator: '!=' }] },
spans
)
).toEqual(new Set()); ).toEqual(new Set());
expect( expect(
filterSpansNewTraceViewHeader( filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey2', operator: '!=' }] }, spans)
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey2', operator: '!=' }] },
spans
)
).toEqual(new Set([spanID0])); ).toEqual(new Set([spanID0]));
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', value: 'tagValue1', operator: '!=' }] }, { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', value: 'tagValue1', operator: '!=' }] },
spans spans
) )
@ -415,7 +354,7 @@ describe('filterSpans', () => {
it('should return spans with multiple tag filters', () => { it('should return spans with multiple tag filters', () => {
// tags in same span // tags in same span
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ {
...defaultFilters, ...defaultFilters,
tags: [ tags: [
@ -427,7 +366,7 @@ describe('filterSpans', () => {
) )
).toEqual(new Set([spanID0])); ).toEqual(new Set([spanID0]));
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ {
...defaultFilters, ...defaultFilters,
tags: [ tags: [
@ -439,7 +378,7 @@ describe('filterSpans', () => {
) )
).toEqual(new Set([spanID0])); ).toEqual(new Set([spanID0]));
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ {
...defaultFilters, ...defaultFilters,
tags: [ tags: [
@ -453,7 +392,7 @@ describe('filterSpans', () => {
// tags in different spans // tags in different spans
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ {
...defaultFilters, ...defaultFilters,
tags: [ tags: [
@ -465,7 +404,7 @@ describe('filterSpans', () => {
) )
).toEqual(new Set()); ).toEqual(new Set());
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ {
...defaultFilters, ...defaultFilters,
tags: [ tags: [
@ -479,7 +418,7 @@ describe('filterSpans', () => {
// values in different spans // values in different spans
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ {
...defaultFilters, ...defaultFilters,
tags: [ tags: [
@ -491,7 +430,7 @@ describe('filterSpans', () => {
) )
).toEqual(new Set()); ).toEqual(new Set());
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ {
...defaultFilters, ...defaultFilters,
tags: [ tags: [
@ -503,7 +442,7 @@ describe('filterSpans', () => {
) )
).toEqual(new Set()); ).toEqual(new Set());
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ {
...defaultFilters, ...defaultFilters,
tags: [ tags: [
@ -515,7 +454,7 @@ describe('filterSpans', () => {
) )
).toEqual(new Set()); ).toEqual(new Set());
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ {
...defaultFilters, ...defaultFilters,
tags: [ tags: [
@ -531,20 +470,14 @@ describe('filterSpans', () => {
// Multiple // Multiple
it('should return spans with multiple filters', () => { it('should return spans with multiple filters', () => {
// service name + span name // service name + span name
expect(filterSpans({ ...defaultFilters, serviceName: 'serviceName0', spanName: 'operationName0' }, spans)).toEqual(
new Set([spanID0])
);
expect(filterSpans({ ...defaultFilters, serviceName: 'serviceName0', spanName: 'operationName2' }, spans)).toEqual(
new Set([])
);
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ ...defaultFilters, serviceName: 'serviceName0', spanName: 'operationName0' },
spans
)
).toEqual(new Set([spanID0]));
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, serviceName: 'serviceName0', spanName: 'operationName2' },
spans
)
).toEqual(new Set([]));
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, serviceName: 'serviceName0', spanName: 'operationName2', spanNameOperator: '!=' }, { ...defaultFilters, serviceName: 'serviceName0', spanName: 'operationName2', spanNameOperator: '!=' },
spans spans
) )
@ -552,51 +485,42 @@ describe('filterSpans', () => {
// service name + span name + duration // service name + span name + duration
expect( expect(
filterSpansNewTraceViewHeader( filterSpans({ ...defaultFilters, serviceName: 'serviceName0', spanName: 'operationName0', from: '2ms' }, spans)
{ ...defaultFilters, serviceName: 'serviceName0', spanName: 'operationName0', from: '2ms' },
spans
)
).toEqual(new Set([spanID0])); ).toEqual(new Set([spanID0]));
expect( expect(
filterSpansNewTraceViewHeader( filterSpans({ ...defaultFilters, serviceName: 'serviceName0', spanName: 'operationName0', to: '2ms' }, spans)
{ ...defaultFilters, serviceName: 'serviceName0', spanName: 'operationName0', to: '2ms' },
spans
)
).toEqual(new Set([])); ).toEqual(new Set([]));
expect( expect(
filterSpansNewTraceViewHeader( filterSpans({ ...defaultFilters, serviceName: 'serviceName2', spanName: 'operationName2', to: '6ms' }, spans)
{ ...defaultFilters, serviceName: 'serviceName2', spanName: 'operationName2', to: '6ms' },
spans
)
).toEqual(new Set([spanID2])); ).toEqual(new Set([spanID2]));
// service name + tag key // service name + tag key
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ ...defaultFilters, serviceName: 'serviceName0', tags: [{ ...defaultTagFilter, key: 'tagKey0' }] }, { ...defaultFilters, serviceName: 'serviceName0', tags: [{ ...defaultTagFilter, key: 'tagKey0' }] },
spans spans
) )
).toEqual(new Set([spanID0])); ).toEqual(new Set([spanID0]));
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ ...defaultFilters, serviceName: 'serviceName0', tags: [{ ...defaultTagFilter, key: 'tagKey1' }] }, { ...defaultFilters, serviceName: 'serviceName0', tags: [{ ...defaultTagFilter, key: 'tagKey1' }] },
spans spans
) )
).toEqual(new Set([spanID0])); ).toEqual(new Set([spanID0]));
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ ...defaultFilters, serviceName: 'serviceName2', tags: [{ ...defaultTagFilter, key: 'tagKey1' }] }, { ...defaultFilters, serviceName: 'serviceName2', tags: [{ ...defaultTagFilter, key: 'tagKey1' }] },
spans spans
) )
).toEqual(new Set([spanID2])); ).toEqual(new Set([spanID2]));
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ ...defaultFilters, serviceName: 'serviceName2', tags: [{ ...defaultTagFilter, key: 'tagKey2' }] }, { ...defaultFilters, serviceName: 'serviceName2', tags: [{ ...defaultTagFilter, key: 'tagKey2' }] },
spans spans
) )
).toEqual(new Set([spanID2])); ).toEqual(new Set([spanID2]));
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ {
...defaultFilters, ...defaultFilters,
serviceName: 'serviceName0', serviceName: 'serviceName0',
@ -608,13 +532,10 @@ describe('filterSpans', () => {
// duration + tag // duration + tag
expect( expect(
filterSpansNewTraceViewHeader( filterSpans({ ...defaultFilters, from: '2ms', tags: [{ ...defaultTagFilter, key: 'tagKey0' }] }, spans)
{ ...defaultFilters, from: '2ms', tags: [{ ...defaultTagFilter, key: 'tagKey0' }] },
spans
)
).toEqual(new Set([spanID0])); ).toEqual(new Set([spanID0]));
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ ...defaultFilters, to: '5ms', toOperator: '<=', tags: [{ ...defaultTagFilter, key: 'tagKey2' }] }, { ...defaultFilters, to: '5ms', toOperator: '<=', tags: [{ ...defaultTagFilter, key: 'tagKey2' }] },
spans spans
) )
@ -622,7 +543,7 @@ describe('filterSpans', () => {
// all // all
expect( expect(
filterSpansNewTraceViewHeader( filterSpans(
{ {
...defaultFilters, ...defaultFilters,
serviceName: 'serviceName0', serviceName: 'serviceName0',
@ -637,96 +558,4 @@ describe('filterSpans', () => {
) )
).toEqual(new Set([spanID0])); ).toEqual(new Set([spanID0]));
}); });
it('should return `undefined` if spans is falsy', () => {
expect(filterSpans('operationName', null)).toBe(undefined);
});
it('should return spans whose spanID exactly match a filter', () => {
expect(filterSpans('spanID', spans)).toEqual(new Set([]));
expect(filterSpans(spanID0, spans)).toEqual(new Set([spanID0]));
expect(filterSpans(spanID2, spans)).toEqual(new Set([spanID2]));
});
it('should return spans whose operationName match a filter', () => {
expect(filterSpans('operationName', spans)).toEqual(new Set([spanID0, spanID2]));
expect(filterSpans('operationName0', spans)).toEqual(new Set([spanID0]));
expect(filterSpans('operationName2', spans)).toEqual(new Set([spanID2]));
});
it('should return spans whose serviceName match a filter', () => {
expect(filterSpans('serviceName', spans)).toEqual(new Set([spanID0, spanID2]));
expect(filterSpans('serviceName0', spans)).toEqual(new Set([spanID0]));
expect(filterSpans('serviceName2', spans)).toEqual(new Set([spanID2]));
});
it("should return spans whose tags' kv.key match a filter", () => {
expect(filterSpans('tagKey1', spans)).toEqual(new Set([spanID0, spanID2]));
expect(filterSpans('tagKey0', spans)).toEqual(new Set([spanID0]));
expect(filterSpans('tagKey2', spans)).toEqual(new Set([spanID2]));
});
it("should return spans whose tags' kv.value match a filter", () => {
expect(filterSpans('tagValue1', spans)).toEqual(new Set([spanID0, spanID2]));
expect(filterSpans('tagValue0', spans)).toEqual(new Set([spanID0]));
expect(filterSpans('tagValue2', spans)).toEqual(new Set([spanID2]));
});
it("should exclude span whose tags' kv.value or kv.key match a filter if the key matches an excludeKey", () => {
expect(filterSpans('tagValue1 -tagKey2', spans)).toEqual(new Set([spanID0]));
expect(filterSpans('tagValue1 -tagKey1', spans)).toEqual(new Set([spanID2]));
});
it('should return spans whose kind, statusCode, statusMessage, libraryName, libraryVersion or traceState value match a filter', () => {
expect(filterSpans('kind0', spans)).toEqual(new Set([spanID0]));
expect(filterSpans('error', spans)).toEqual(new Set([spanID2]));
expect(filterSpans('statusMessage0', spans)).toEqual(new Set([spanID0]));
expect(filterSpans('libraryName', spans)).toEqual(new Set([spanID0, spanID2]));
expect(filterSpans('libraryVersion2', spans)).toEqual(new Set([spanID2]));
expect(filterSpans('traceState0', spans)).toEqual(new Set([spanID0]));
});
it('should return spans whose logs have a field whose kv.key match a filter', () => {
expect(filterSpans('logFieldKey1', spans)).toEqual(new Set([spanID0, spanID2]));
expect(filterSpans('logFieldKey0', spans)).toEqual(new Set([spanID0]));
expect(filterSpans('logFieldKey2', spans)).toEqual(new Set([spanID2]));
});
it('should return spans whose logs have a field whose kv.value match a filter', () => {
expect(filterSpans('logFieldValue1', spans)).toEqual(new Set([spanID0, spanID2]));
expect(filterSpans('logFieldValue0', spans)).toEqual(new Set([spanID0]));
expect(filterSpans('logFieldValue2', spans)).toEqual(new Set([spanID2]));
});
it('should exclude span whose logs have a field whose kv.value or kv.key match a filter if the key matches an excludeKey', () => {
expect(filterSpans('logFieldValue1 -logFieldKey2', spans)).toEqual(new Set([spanID0]));
expect(filterSpans('logFieldValue1 -logFieldKey1', spans)).toEqual(new Set([spanID2]));
});
it("should return spans whose process.tags' kv.key match a filter", () => {
expect(filterSpans('processTagKey1', spans)).toEqual(new Set([spanID0, spanID2]));
expect(filterSpans('processTagKey0', spans)).toEqual(new Set([spanID0]));
expect(filterSpans('processTagKey2', spans)).toEqual(new Set([spanID2]));
});
it("should return spans whose process.processTags' kv.value match a filter", () => {
expect(filterSpans('processTagValue1', spans)).toEqual(new Set([spanID0, spanID2]));
expect(filterSpans('processTagValue0', spans)).toEqual(new Set([spanID0]));
expect(filterSpans('processTagValue2', spans)).toEqual(new Set([spanID2]));
});
it("should exclude span whose process.processTags' kv.value or kv.key match a filter if the key matches an excludeKey", () => {
expect(filterSpans('processTagValue1 -processTagKey2', spans)).toEqual(new Set([spanID0]));
expect(filterSpans('processTagValue1 -processTagKey1', spans)).toEqual(new Set([spanID2]));
});
// This test may false positive if other tests are failing
it('should return an empty set if no spans match the filter', () => {
expect(filterSpans('-processTagKey1', spans)).toEqual(new Set());
});
it('should return no spans when logs is null', () => {
const nullSpan = { ...span0, logs: null };
expect(filterSpans('logFieldKey1', [nullSpan] as unknown as TraceSpan[])).toEqual(new Set([]));
});
}); });

View File

@ -20,7 +20,7 @@ import { TNil, TraceKeyValuePair, TraceSpan } from '../types';
// filter spans where all filters added need to be true for each individual span that is returned // filter spans where all filters added need to be true for each individual span that is returned
// i.e. the more filters added -> the more specific that the returned results are // i.e. the more filters added -> the more specific that the returned results are
export function filterSpansNewTraceViewHeader(searchProps: SearchProps, spans: TraceSpan[] | TNil) { export function filterSpans(searchProps: SearchProps, spans: TraceSpan[] | TNil) {
if (!spans) { if (!spans) {
return undefined; return undefined;
} }
@ -174,61 +174,3 @@ export const convertTimeFilter = (time: string) => {
} }
return undefined; return undefined;
}; };
// legacy code that will be removed when the Header feature flag is removed
export function filterSpans(textFilter: string, spans: TraceSpan[] | TNil) {
if (!spans) {
return undefined;
}
// if a span field includes at least one filter in includeFilters, the span is a match
const includeFilters: string[] = [];
// values with keys that include text in any one of the excludeKeys will be ignored
const excludeKeys: string[] = [];
// split textFilter by whitespace, remove empty strings, and extract includeFilters and excludeKeys
textFilter
.split(/\s+/)
.filter(Boolean)
.forEach((w) => {
if (w[0] === '-') {
excludeKeys.push(w.slice(1).toLowerCase());
} else {
includeFilters.push(w.toLowerCase());
}
});
const isTextInFilters = (filters: string[], text: string) =>
filters.some((filter) => text.toLowerCase().includes(filter));
const isTextInKeyValues = (kvs: TraceKeyValuePair[]) =>
kvs
? kvs.some((kv) => {
// ignore checking key and value for a match if key is in excludeKeys
if (isTextInFilters(excludeKeys, kv.key)) {
return false;
}
// match if key or value matches an item in includeFilters
return isTextInFilters(includeFilters, kv.key) || isTextInFilters(includeFilters, kv.value.toString());
})
: false;
const isSpanAMatch = (span: TraceSpan) =>
isTextInFilters(includeFilters, span.operationName) ||
isTextInFilters(includeFilters, span.process.serviceName) ||
isTextInKeyValues(span.tags) ||
(span.kind && isTextInFilters(includeFilters, span.kind)) ||
(span.statusCode !== undefined && isTextInFilters(includeFilters, SpanStatusCode[span.statusCode])) ||
(span.statusMessage && isTextInFilters(includeFilters, span.statusMessage)) ||
(span.instrumentationLibraryName && isTextInFilters(includeFilters, span.instrumentationLibraryName)) ||
(span.instrumentationLibraryVersion && isTextInFilters(includeFilters, span.instrumentationLibraryVersion)) ||
(span.traceState && isTextInFilters(includeFilters, span.traceState)) ||
(span.logs !== null && span.logs.some((log) => isTextInKeyValues(log.fields))) ||
isTextInKeyValues(span.process.tags) ||
includeFilters.some((filter) => filter === span.spanID);
// declare as const because need to disambiguate the type
const rv: Set<string> = new Set(spans.filter(isSpanAMatch).map((span: TraceSpan) => span.spanID));
return rv;
}

View File

@ -1,7 +1,7 @@
import { act, renderHook } from '@testing-library/react'; import { act, renderHook } from '@testing-library/react';
import { TraceSpan } from './components'; import { TraceSpan } from './components';
import { defaultFilters, useSearch, useSearchNewTraceViewHeader } from './useSearch'; import { defaultFilters, useSearch } from './useSearch';
describe('useSearch', () => { describe('useSearch', () => {
const spans = [ const spans = [
@ -28,28 +28,15 @@ describe('useSearch', () => {
]; ];
it('returns matching span IDs', async () => { it('returns matching span IDs', async () => {
const { result } = renderHook(() => useSearchNewTraceViewHeader(spans)); const { result } = renderHook(() => useSearch(spans));
act(() => result.current.setNewTraceViewHeaderSearch({ ...defaultFilters, serviceName: 'service1' })); act(() => result.current.setSearch({ ...defaultFilters, serviceName: 'service1' }));
expect(result.current.spanFilterMatches?.size).toBe(1); expect(result.current.spanFilterMatches?.size).toBe(1);
expect(result.current.spanFilterMatches?.has('span1')).toBe(true); expect(result.current.spanFilterMatches?.has('span1')).toBe(true);
}); });
it('works without spans', async () => { it('works without spans', async () => {
const { result } = renderHook(() => useSearchNewTraceViewHeader()); const { result } = renderHook(() => useSearch());
act(() => result.current.setNewTraceViewHeaderSearch({ ...defaultFilters, serviceName: 'service1' })); act(() => result.current.setSearch({ ...defaultFilters, serviceName: 'service1' }));
expect(result.current.spanFilterMatches).toBe(undefined); expect(result.current.spanFilterMatches).toBe(undefined);
}); });
it('returns matching span IDs', async () => {
const { result } = renderHook(() => useSearch(spans));
act(() => result.current.setSearch('service1'));
expect(result.current.spanFindMatches?.size).toBe(1);
expect(result.current.spanFindMatches?.has('span1')).toBe(true);
});
it('works without spans', async () => {
const { result } = renderHook(() => useSearch());
act(() => result.current.setSearch('service1'));
expect(result.current.spanFindMatches).toBe(undefined);
});
}); });

View File

@ -1,7 +1,7 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { filterSpansNewTraceViewHeader, filterSpans, TraceSpan } from './components'; import { filterSpans, TraceSpan } from './components';
export interface SearchProps { export interface SearchProps {
serviceName?: string; serviceName?: string;
@ -41,21 +41,11 @@ export const defaultFilters = {
* Controls the state of search input that highlights spans if they match the search string. * Controls the state of search input that highlights spans if they match the search string.
* @param spans * @param spans
*/ */
export function useSearchNewTraceViewHeader(spans?: TraceSpan[]) {
const [newTraceViewHeaderSearch, setNewTraceViewHeaderSearch] = useState<SearchProps>(defaultFilters);
const spanFilterMatches: Set<string> | undefined = useMemo(() => {
return spans && filterSpansNewTraceViewHeader(newTraceViewHeaderSearch, spans);
}, [newTraceViewHeaderSearch, spans]);
return { newTraceViewHeaderSearch, setNewTraceViewHeaderSearch, spanFilterMatches };
}
// legacy code that will be removed when the newTraceViewHeader feature flag is removed
export function useSearch(spans?: TraceSpan[]) { export function useSearch(spans?: TraceSpan[]) {
const [search, setSearch] = useState(''); const [search, setSearch] = useState<SearchProps>(defaultFilters);
const spanFindMatches: Set<string> | undefined = useMemo(() => { const spanFilterMatches: Set<string> | undefined = useMemo(() => {
return search && spans ? filterSpans(search, spans) : undefined; return spans && filterSpans(search, spans);
}, [search, spans]); }, [search, spans]);
return { search, setSearch, spanFindMatches }; return { search, setSearch, spanFilterMatches };
} }

View File

@ -1,13 +1,11 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React, { useMemo, useState, createRef } from 'react'; import React, { useMemo, createRef } from 'react';
import { useAsync } from 'react-use'; import { useAsync } from 'react-use';
import { PanelProps } from '@grafana/data'; import { PanelProps } from '@grafana/data';
import { config, getDataSourceSrv } from '@grafana/runtime'; import { getDataSourceSrv } from '@grafana/runtime';
import { TraceView } from 'app/features/explore/TraceView/TraceView'; import { TraceView } from 'app/features/explore/TraceView/TraceView';
import TracePageSearchBar from 'app/features/explore/TraceView/components/TracePageHeader/SearchBar/TracePageSearchBar';
import { TopOfViewRefType } from 'app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView'; import { TopOfViewRefType } from 'app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView';
import { useSearch } from 'app/features/explore/TraceView/useSearch';
import { transformDataFrames } from 'app/features/explore/TraceView/utils/transform'; import { transformDataFrames } from 'app/features/explore/TraceView/utils/transform';
const styles = { const styles = {
@ -20,13 +18,9 @@ const styles = {
export const TracesPanel = ({ data }: PanelProps) => { export const TracesPanel = ({ data }: PanelProps) => {
const topOfViewRef = createRef<HTMLDivElement>(); const topOfViewRef = createRef<HTMLDivElement>();
const traceProp = useMemo(() => transformDataFrames(data.series[0]), [data.series]); 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 () => { const dataSource = useAsync(async () => {
return await getDataSourceSrv().get(data.request?.targets[0].datasource?.uid); return await getDataSourceSrv().get(data.request?.targets[0].datasource?.uid);
}); });
const datasourceType = dataSource && dataSource.value ? dataSource.value.type : 'unknown';
if (!data || !data.series.length || !traceProp) { if (!data || !data.series.length || !traceProp) {
return ( return (
@ -39,27 +33,10 @@ export const TracesPanel = ({ data }: PanelProps) => {
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
<div ref={topOfViewRef}></div> <div ref={topOfViewRef}></div>
{!config.featureToggles.newTraceViewHeader ? (
<TracePageSearchBar
navigable={true}
searchValue={search}
setSearch={setSearch}
spanFindMatches={spanFindMatches}
searchBarSuffix={searchBarSuffix}
setSearchBarSuffix={setSearchBarSuffix}
focusedSpanIdForSearch={focusedSpanIdForSearch}
setFocusedSpanIdForSearch={setFocusedSpanIdForSearch}
datasourceType={datasourceType}
/>
) : null}
<TraceView <TraceView
dataFrames={data.series} dataFrames={data.series}
scrollElementClass={styles.wrapper} scrollElementClass={styles.wrapper}
traceProp={traceProp} traceProp={traceProp}
spanFindMatches={spanFindMatches}
search={search}
focusedSpanIdForSearch={focusedSpanIdForSearch}
queryResponse={data} queryResponse={data}
datasource={dataSource.value} datasource={dataSource.value}
topOfViewRef={topOfViewRef} topOfViewRef={topOfViewRef}