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
{{% 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)
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
{{% 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)
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
{{% 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)
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
{{% 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)
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 |
| `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) |
| `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 |
| `traceToMetrics` | Enable trace to metrics links |
| `prometheusWideSeries` | Enable wide series responses in the Prometheus datasource |

View File

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

View File

@ -79,13 +79,6 @@ var (
Stage: FeatureStageExperimental,
Owner: grafanaAppPlatformSquad,
},
{
Name: "newTraceViewHeader",
Description: "Shows the new trace view header",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaObservabilityTracesAndProfilingSquad,
},
{
Name: "correlations",
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
migrationLocking,preview,@grafana/backend-platform,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
datasourceQueryMultiStatus,experimental,@grafana/plugins-platform-backend,false,false,false,false
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
FlagStorage = "storage"
// FlagNewTraceViewHeader
// Shows the new trace view header
FlagNewTraceViewHeader = "newTraceViewHeader"
// FlagCorrelations
// Correlations page
FlagCorrelations = "correlations"

View File

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

View File

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

View File

@ -4,7 +4,6 @@ import React, { createRef } from 'react';
import { Provider } from 'react-redux';
import { getDefaultTimeRange, LoadingState } from '@grafana/data';
import { config } from '@grafana/runtime';
import { configureStore } from '../../../store/configureStore';
@ -86,55 +85,7 @@ describe('TraceViewContainer', () => {
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 () => {
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();
const spanFiltersButton = screen.getByRole('button', { name: 'Span Filters 3 spans Prev Next' });
await user.click(spanFiltersButton);
@ -184,7 +135,6 @@ describe('TraceViewContainer', () => {
});
it('show matches only works as expected', async () => {
config.featureToggles.newTraceViewHeader = true;
renderTraceViewContainer();
const spanFiltersButton = screen.getByRole('button', { name: 'Span Filters 3 spans Prev Next' });
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 { config } from '@grafana/runtime';
import { DataFrame, SplitOpen, PanelData } from '@grafana/data';
import { PanelChrome } from '@grafana/ui/src/components/PanelChrome/PanelChrome';
import { StoreState, useSelector } from 'app/types';
import { TraceView } from './TraceView';
import TracePageSearchBar from './components/TracePageHeader/SearchBar/TracePageSearchBar';
import { TopOfViewRefType } from './components/TraceTimelineViewer/VirtualizedTraceView';
import { useSearch } from './useSearch';
import { transformDataFrames } from './utils/transform';
interface Props {
@ -25,47 +22,22 @@ export function TraceViewContainer(props: Props) {
const frame = props.dataFrames[0];
const { dataFrames, splitOpenFn, exploreId, scrollElement, topOfViewRef, queryResponse } = props;
const traceProp = useMemo(() => transformDataFrames(frame), [frame]);
const { search, setSearch, spanFindMatches } = useSearch(traceProp?.spans);
const [focusedSpanIdForSearch, setFocusedSpanIdForSearch] = useState('');
const [searchBarSuffix, setSearchBarSuffix] = useState('');
const datasource = useSelector(
(state: StoreState) => state.explore.panes[props.exploreId]?.datasourceInstance ?? undefined
);
const datasourceType = datasource ? datasource?.type : 'unknown';
if (!traceProp) {
return null;
}
return (
<PanelChrome
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}
/>
)
}
>
<PanelChrome padding="none" title="Trace">
<TraceView
exploreId={exploreId}
dataFrames={dataFrames}
splitOpenFn={splitOpenFn}
scrollElement={scrollElement}
traceProp={traceProp}
spanFindMatches={spanFindMatches}
search={search}
focusedSpanIdForSearch={focusedSpanIdForSearch}
queryResponse={queryResponse}
datasource={datasource}
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) {
spanFilterMatches.forEach((spanID) => {
matchedServices.push(trace.processes[spanID].serviceName);
matchedDepth.push(trace.spans.find((span) => span.spanID === spanID)?.depth || 0);
if (trace.processes[spanID]) {
matchedServices.push(trace.processes[spanID].serviceName);
matchedDepth.push(trace.spans.find((span) => span.spanID === spanID)?.depth || 0);
}
});
if (spanFilterMatches.size === 0) {

View File

@ -15,65 +15,45 @@
import { render, screen } from '@testing-library/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';
const defaultProps = {
forwardedRef: React.createRef(),
navigable: true,
searchBarSuffix: 'suffix',
searchValue: 'value',
};
import TracePageSearchBar from './TracePageSearchBar';
describe('<TracePageSearchBar>', () => {
describe('truthy textFilter', () => {
it('renders SearchBarInput with correct props', () => {
render(<TracePageSearchBar {...(defaultProps as unknown as TracePageSearchBarProps)} />);
expect((screen.getByPlaceholderText('Find...') as HTMLInputElement)['value']).toEqual('value');
const suffix = screen.getByLabelText('Search bar suffix');
const theme = createTheme();
expect(suffix['className']).toBe(getStyles(theme).TracePageSearchBarSuffix);
expect(suffix.textContent).toBe('suffix');
});
const TracePageSearchBarWithProps = (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,
};
it('renders buttons', () => {
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);
});
return <TracePageSearchBar {...searchBarProps} />;
};
it('only shows navigable buttons when navigable is true', () => {
const props = {
...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();
});
it('should render', () => {
expect(() => render(<TracePageSearchBarWithProps matches={[]} />)).not.toThrow();
});
describe('falsy textFilter', () => {
beforeEach(() => {
const props = {
...defaultProps,
searchValue: '',
};
render(<TracePageSearchBar {...(props as unknown as TracePageSearchBarProps)} />);
});
it('renders clear filter button', () => {
render(<TracePageSearchBarWithProps matches={[]} />);
const clearFiltersButton = screen.getByRole('button', { name: 'Clear filters button' });
expect(clearFiltersButton).toBeInTheDocument();
expect((clearFiltersButton as HTMLButtonElement)['disabled']).toBe(true);
});
it('does not render suffix', () => {
expect(screen.queryByLabelText('Search bar suffix')).not.toBeInTheDocument();
});
it('renders buttons', () => {
expect(screen.getByRole('button', { name: 'Next results button' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Prev results button' })).toBeInTheDocument();
});
it('renders show span filter matches only switch', async () => {
render(<TracePageSearchBarWithProps matches={[]} />);
const matchesSwitch = screen.getByRole('checkbox', { name: 'Show matches only switch' });
expect(matchesSwitch).toBeInTheDocument();
});
});

View File

@ -13,181 +13,147 @@
// limitations under the License.
import { css } from '@emotion/css';
import cx from 'classnames';
import React, { memo, Dispatch, SetStateAction } from 'react';
import React, { memo, Dispatch, SetStateAction, useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime';
import { Button, useStyles2 } from '@grafana/ui';
import { Button, Switch, useStyles2 } from '@grafana/ui';
import { getButtonStyles } from '@grafana/ui/src/components/Button';
import SearchBarInput from '../../common/SearchBarInput';
import { ubFlexAuto, ubJustifyEnd } from '../../uberUtilityStyles';
import { SearchProps } from '../../../useSearch';
import { Trace } from '../../types';
import { convertTimeFilter } from '../../utils/filter-spans';
// eslint-disable-next-line no-duplicate-imports
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;
`,
};
};
import NextPrevResult from './NextPrevResult';
export type TracePageSearchBarProps = {
navigable: boolean;
searchValue: string;
setSearch: (value: string) => void;
searchBarSuffix: string;
spanFindMatches: Set<string> | undefined;
focusedSpanIdForSearch: string;
setSearchBarSuffix: Dispatch<SetStateAction<string>>;
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 TracePageSearchBar(props: TracePageSearchBarProps) {
const {
navigable,
setSearch,
searchValue,
searchBarSuffix,
spanFindMatches,
focusedSpanIdForSearch,
setSearchBarSuffix,
trace,
search,
spanFilterMatches,
showSpanFilterMatchesOnly,
setShowSpanFilterMatchesOnly,
focusedSpanIndexForSearch,
setFocusedSpanIndexForSearch,
setFocusedSpanIdForSearch,
datasourceType,
clear,
showSpanFilters,
} = props;
const styles = useStyles2(getStyles);
const suffix = searchValue ? (
<span className={styles.TracePageSearchBarSuffix} aria-label="Search bar suffix">
{searchBarSuffix}
</span>
) : null;
const SearchBarInputProps = {
className: cx(styles.TracePageSearchBarBar, ubFlexAuto),
name: 'search',
suffix,
};
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 '';
};
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.TracePageSearchBar}>
<span className={ubJustifyEnd} style={{ display: 'flex' }}>
<SearchBarInput
onChange={setTraceSearch}
value={searchValue}
inputProps={SearchBarInputProps}
allowClear={true}
/>
<div className={styles.container}>
<div className={styles.controls}>
<>
{navigable && (
<>
<Button
className={styles.TracePageSearchBarBtn}
variant="secondary"
disabled={!searchValue}
type="button"
icon="arrow-down"
aria-label="Next results button"
onClick={nextResult}
<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
className={styles.TracePageSearchBarBtn}
onClick={() => setShowSpanFilterMatchesOnly(!showSpanFilterMatchesOnly)}
className={styles.clearMatchesButton}
variant="secondary"
disabled={!searchValue}
type="button"
icon="arrow-up"
aria-label="Prev results button"
onClick={prevResult}
/>
</>
)}
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>
</>
</span>
</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 { KIND, LIBRARY_NAME, LIBRARY_VERSION, STATUS, STATUS_MESSAGE, TRACE_STATE, ID } from '../../constants/span';
import { Trace } from '../../types';
import NewTracePageSearchBar from '../SearchBar/NewTracePageSearchBar';
import NextPrevResult from '../SearchBar/NextPrevResult';
import TracePageSearchBar from '../SearchBar/TracePageSearchBar';
export type SpanFilterProps = {
trace: Trace;
@ -447,7 +447,7 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
</InlineField>
</InlineFieldRow>
<NewTracePageSearchBar
<TracePageSearchBar
trace={trace}
search={search}
spanFilterMatches={spanFilterMatches}

View File

@ -12,12 +12,54 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { render, screen } from '@testing-library/react';
import { getByText, render } from '@testing-library/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 = {
services: [{ name: 'serviceA', numberOfSpans: 1 }],
@ -144,72 +186,3 @@ export const trace = {
startTime: 1675605056289000,
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 cx from 'classnames';
import { get as _get, maxBy as _maxBy, values as _values } from 'lodash';
import React from 'react';
import React, { memo, useEffect, useMemo } from 'react';
import { dateTimeFormat, GrafanaTheme2, TimeZone } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { CoreApp, DataFrame, dateTimeFormat, 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 LabeledList from '../common/LabeledList';
import TraceName from '../common/TraceName';
import { autoColor, TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate } from '../index';
import { getTraceLinks } from '../model/link-patterns';
import { getTraceName } from '../model/trace-viewer';
import { getHeaderTags, getTraceName } from '../model/trace-viewer';
import { Trace } from '../types';
import { uTxMuted } from '../uberUtilityStyles';
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 {
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`
label: TracePageHeaderBack;
align-items: center;
@ -63,21 +183,6 @@ export const getStyles = (theme: GrafanaTheme2) => {
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(
css`
label: TracePageHeaderOverviewItemValueDetail;
@ -91,107 +196,57 @@ export const getStyles = (theme: GrafanaTheme2) => {
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`
label: TracePageHeaderTraceId;
white-space: nowrap;
`,
titleBorderBottom: css`
border-bottom: 1px solid ${autoColor(theme, '#e8e8e8')};
text-overflow: ellipsis;
max-width: 30%;
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
// limitations under the License.
export { default } from './TracePageHeader';
export { NewTracePageHeader } from './NewTracePageHeader';
export { TracePageHeader } from './TracePageHeader';

View File

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

View File

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

View File

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

View File

@ -1,10 +1,9 @@
export { default as TraceTimelineViewer } from './TraceTimelineViewer';
export { default as TracePageHeader } from './TracePageHeader';
export { NewTracePageHeader } from './TracePageHeader';
export { TracePageHeader } from './TracePageHeader';
export { default as SpanBarSettings } from './settings/SpanBarSettings';
export * from './types';
export * from './TraceTimelineViewer/types';
export { default as DetailState } from './TraceTimelineViewer/SpanDetail/DetailState';
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';

View File

@ -15,7 +15,7 @@
import { defaultFilters, defaultTagFilter } from '../../useSearch';
import { TraceSpan } from '../types';
import { filterSpans, filterSpansNewTraceViewHeader } from './filter-spans';
import { filterSpans } from './filter-spans';
describe('filterSpans', () => {
// span0 contains strings that end in 0 or 1
@ -122,125 +122,94 @@ describe('filterSpans', () => {
const spans = [span0, span2] as TraceSpan[];
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
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])
);
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', () => {
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])
);
expect(filterSpansNewTraceViewHeader({ ...defaultFilters, spanName: 'operationName2' }, spans)).toEqual(
new Set([spanID2])
);
expect(
filterSpansNewTraceViewHeader({ ...defaultFilters, spanName: 'operationName2', spanNameOperator: '!=' }, spans)
).toEqual(new Set([spanID0]));
});
// Durations
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])
);
expect(filterSpansNewTraceViewHeader({ ...defaultFilters, from: '2us' }, spans)).toEqual(
new Set([spanID0, spanID2])
);
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, from: '3.05ms', fromOperator: '>=', to: '4ms' }, spans)).toEqual(
new Set([spanID0])
);
expect(filterSpans({ ...defaultFilters, to: '4ms' }, spans)).toEqual(new Set([spanID0]));
expect(filterSpans({ ...defaultFilters, to: '5ms', toOperator: '<=' }, spans)).toEqual(new Set([spanID0, spanID2]));
});
// Tags
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(
filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1' }] }, 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
)
filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey2', operator: '!=' }] }, spans)
).toEqual(new Set([spanID0]));
});
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(
filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'kind' }] }, spans)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'kind', value: 'kind0' }] },
spans
)
filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'kind', value: 'kind0' }] }, spans)
).toEqual(new Set([spanID0]));
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'kind', operator: '!=', value: 'kind0' }] },
spans
)
).toEqual(new Set([spanID2]));
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect(
filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status' }] }, spans)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status', value: 'unset' }] },
spans
)
filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status', value: 'unset' }] }, spans)
).toEqual(new Set([spanID0]));
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status', operator: '!=', value: 'unset' }] },
spans
)
).toEqual(new Set([spanID2]));
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status.message' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status.message' }] },
spans
)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status.message', value: 'statusMessage0' }] },
spans
)
).toEqual(new Set([spanID0]));
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{
...defaultFilters,
tags: [{ ...defaultTagFilter, key: 'status.message', operator: '!=', value: 'statusMessage0' }],
@ -248,17 +217,17 @@ describe('filterSpans', () => {
spans
)
).toEqual(new Set([spanID2]));
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'library.name' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect(
filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'library.name' }] }, spans)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'library.name', value: 'libraryName' }] },
spans
)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{
...defaultFilters,
tags: [{ ...defaultTagFilter, key: 'library.name', operator: '!=', value: 'libraryName' }],
@ -266,20 +235,17 @@ describe('filterSpans', () => {
spans
)
).toEqual(new Set([]));
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'library.version' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'library.version' }] },
spans
)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'library.version', value: 'libraryVersion0' }] },
spans
)
).toEqual(new Set([spanID0]));
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{
...defaultFilters,
tags: [{ ...defaultTagFilter, key: 'library.version', operator: '!=', value: 'libraryVersion0' }],
@ -287,17 +253,17 @@ describe('filterSpans', () => {
spans
)
).toEqual(new Set([spanID2]));
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'trace.state' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect(
filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'trace.state' }] }, spans)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'trace.state', value: 'traceState0' }] },
spans
)
).toEqual(new Set([spanID0]));
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{
...defaultFilters,
tags: [{ ...defaultTagFilter, key: 'trace.state', operator: '!=', value: 'traceState0' }],
@ -305,17 +271,14 @@ describe('filterSpans', () => {
spans
)
).toEqual(new Set([spanID2]));
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'id' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect(
filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'id' }] }, spans)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'id', value: 'span-id-0' }] },
spans
)
filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'id', value: 'span-id-0' }] }, spans)
).toEqual(new Set([spanID0]));
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'id', operator: '!=', value: 'span-id-0' }] },
spans
)
@ -323,54 +286,39 @@ describe('filterSpans', () => {
});
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(
filterSpansNewTraceViewHeader(
{ ...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
)
filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'processTagKey2', operator: '!=' }] }, spans)
).toEqual(new Set([spanID0]));
});
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(
filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'logFieldKey1' }] }, 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
)
filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'logFieldKey2', operator: '!=' }] }, spans)
).toEqual(new Set([spanID0]));
});
it('should return no spans when logs is null', () => {
const nullSpan = { ...span0, logs: null };
expect(
filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'logFieldKey1' }] }, [
filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'logFieldKey1' }] }, [
nullSpan,
] as unknown as TraceSpan[])
).toEqual(new Set([]));
@ -378,13 +326,10 @@ describe('filterSpans', () => {
it("should return spans whose tags' kv.key and kv.value match a filter", () => {
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', value: 'tagValue1' }] },
spans
)
filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', value: 'tagValue1' }] }, spans)
).toEqual(new Set([spanID0]));
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', value: 'tagValue1', operator: '!=' }] },
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", () => {
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', operator: '!=' }] },
spans
)
filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', operator: '!=' }] }, spans)
).toEqual(new Set());
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey2', operator: '!=' }] },
spans
)
filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey2', operator: '!=' }] }, spans)
).toEqual(new Set([spanID0]));
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', value: 'tagValue1', operator: '!=' }] },
spans
)
@ -415,7 +354,7 @@ describe('filterSpans', () => {
it('should return spans with multiple tag filters', () => {
// tags in same span
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{
...defaultFilters,
tags: [
@ -427,7 +366,7 @@ describe('filterSpans', () => {
)
).toEqual(new Set([spanID0]));
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{
...defaultFilters,
tags: [
@ -439,7 +378,7 @@ describe('filterSpans', () => {
)
).toEqual(new Set([spanID0]));
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{
...defaultFilters,
tags: [
@ -453,7 +392,7 @@ describe('filterSpans', () => {
// tags in different spans
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{
...defaultFilters,
tags: [
@ -465,7 +404,7 @@ describe('filterSpans', () => {
)
).toEqual(new Set());
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{
...defaultFilters,
tags: [
@ -479,7 +418,7 @@ describe('filterSpans', () => {
// values in different spans
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{
...defaultFilters,
tags: [
@ -491,7 +430,7 @@ describe('filterSpans', () => {
)
).toEqual(new Set());
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{
...defaultFilters,
tags: [
@ -503,7 +442,7 @@ describe('filterSpans', () => {
)
).toEqual(new Set());
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{
...defaultFilters,
tags: [
@ -515,7 +454,7 @@ describe('filterSpans', () => {
)
).toEqual(new Set());
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{
...defaultFilters,
tags: [
@ -531,20 +470,14 @@ describe('filterSpans', () => {
// Multiple
it('should return spans with multiple filters', () => {
// 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(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, serviceName: 'serviceName0', spanName: 'operationName0' },
spans
)
).toEqual(new Set([spanID0]));
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, serviceName: 'serviceName0', spanName: 'operationName2' },
spans
)
).toEqual(new Set([]));
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{ ...defaultFilters, serviceName: 'serviceName0', spanName: 'operationName2', spanNameOperator: '!=' },
spans
)
@ -552,51 +485,42 @@ describe('filterSpans', () => {
// service name + span name + duration
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, serviceName: 'serviceName0', spanName: 'operationName0', from: '2ms' },
spans
)
filterSpans({ ...defaultFilters, serviceName: 'serviceName0', spanName: 'operationName0', from: '2ms' }, spans)
).toEqual(new Set([spanID0]));
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, serviceName: 'serviceName0', spanName: 'operationName0', to: '2ms' },
spans
)
filterSpans({ ...defaultFilters, serviceName: 'serviceName0', spanName: 'operationName0', to: '2ms' }, spans)
).toEqual(new Set([]));
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, serviceName: 'serviceName2', spanName: 'operationName2', to: '6ms' },
spans
)
filterSpans({ ...defaultFilters, serviceName: 'serviceName2', spanName: 'operationName2', to: '6ms' }, spans)
).toEqual(new Set([spanID2]));
// service name + tag key
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{ ...defaultFilters, serviceName: 'serviceName0', tags: [{ ...defaultTagFilter, key: 'tagKey0' }] },
spans
)
).toEqual(new Set([spanID0]));
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{ ...defaultFilters, serviceName: 'serviceName0', tags: [{ ...defaultTagFilter, key: 'tagKey1' }] },
spans
)
).toEqual(new Set([spanID0]));
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{ ...defaultFilters, serviceName: 'serviceName2', tags: [{ ...defaultTagFilter, key: 'tagKey1' }] },
spans
)
).toEqual(new Set([spanID2]));
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{ ...defaultFilters, serviceName: 'serviceName2', tags: [{ ...defaultTagFilter, key: 'tagKey2' }] },
spans
)
).toEqual(new Set([spanID2]));
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{
...defaultFilters,
serviceName: 'serviceName0',
@ -608,13 +532,10 @@ describe('filterSpans', () => {
// duration + tag
expect(
filterSpansNewTraceViewHeader(
{ ...defaultFilters, from: '2ms', tags: [{ ...defaultTagFilter, key: 'tagKey0' }] },
spans
)
filterSpans({ ...defaultFilters, from: '2ms', tags: [{ ...defaultTagFilter, key: 'tagKey0' }] }, spans)
).toEqual(new Set([spanID0]));
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{ ...defaultFilters, to: '5ms', toOperator: '<=', tags: [{ ...defaultTagFilter, key: 'tagKey2' }] },
spans
)
@ -622,7 +543,7 @@ describe('filterSpans', () => {
// all
expect(
filterSpansNewTraceViewHeader(
filterSpans(
{
...defaultFilters,
serviceName: 'serviceName0',
@ -637,96 +558,4 @@ describe('filterSpans', () => {
)
).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
// 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) {
return undefined;
}
@ -174,61 +174,3 @@ export const convertTimeFilter = (time: string) => {
}
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 { TraceSpan } from './components';
import { defaultFilters, useSearch, useSearchNewTraceViewHeader } from './useSearch';
import { defaultFilters, useSearch } from './useSearch';
describe('useSearch', () => {
const spans = [
@ -28,28 +28,15 @@ describe('useSearch', () => {
];
it('returns matching span IDs', async () => {
const { result } = renderHook(() => useSearchNewTraceViewHeader(spans));
act(() => result.current.setNewTraceViewHeaderSearch({ ...defaultFilters, serviceName: 'service1' }));
const { result } = renderHook(() => useSearch(spans));
act(() => result.current.setSearch({ ...defaultFilters, serviceName: 'service1' }));
expect(result.current.spanFilterMatches?.size).toBe(1);
expect(result.current.spanFilterMatches?.has('span1')).toBe(true);
});
it('works without spans', async () => {
const { result } = renderHook(() => useSearchNewTraceViewHeader());
act(() => result.current.setNewTraceViewHeaderSearch({ ...defaultFilters, serviceName: 'service1' }));
const { result } = renderHook(() => useSearch());
act(() => result.current.setSearch({ ...defaultFilters, serviceName: 'service1' }));
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 { v4 as uuidv4 } from 'uuid';
import { filterSpansNewTraceViewHeader, filterSpans, TraceSpan } from './components';
import { filterSpans, TraceSpan } from './components';
export interface SearchProps {
serviceName?: string;
@ -41,21 +41,11 @@ export const defaultFilters = {
* Controls the state of search input that highlights spans if they match the search string.
* @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[]) {
const [search, setSearch] = useState('');
const spanFindMatches: Set<string> | undefined = useMemo(() => {
return search && spans ? filterSpans(search, spans) : undefined;
const [search, setSearch] = useState<SearchProps>(defaultFilters);
const spanFilterMatches: Set<string> | undefined = useMemo(() => {
return spans && filterSpans(search, spans);
}, [search, spans]);
return { search, setSearch, spanFindMatches };
return { search, setSearch, spanFilterMatches };
}

View File

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