mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
27107225ec
commit
090b8d61e2
@ -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 %}}
|
||||
|
||||

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

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

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

|
||||
|
||||
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.
|
||||
|
@ -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 |
|
||||
|
@ -29,7 +29,6 @@ export interface FeatureToggles {
|
||||
featureHighlights?: boolean;
|
||||
migrationLocking?: boolean;
|
||||
storage?: boolean;
|
||||
newTraceViewHeader?: boolean;
|
||||
correlations?: boolean;
|
||||
datasourceQueryMultiStatus?: boolean;
|
||||
traceToMetrics?: boolean;
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
@ -29,8 +29,6 @@ function getTraceView(frames: DataFrame[]) {
|
||||
dataFrames={frames}
|
||||
splitOpenFn={() => {}}
|
||||
traceProp={transformDataFrames(frames[0])!}
|
||||
search=""
|
||||
focusedSpanIdForSearch=""
|
||||
queryResponse={mockPanelData}
|
||||
datasource={undefined}
|
||||
topOfViewRef={topOfViewRef}
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
|
@ -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}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
@ -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();
|
||||
});
|
||||
});
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
@ -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) {
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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]]);
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -51,7 +51,6 @@ describe('<TraceTimelineViewer>', () => {
|
||||
expandOne: jest.fn(),
|
||||
registerAccessors: jest.fn(),
|
||||
collapseOne: jest.fn(),
|
||||
setTrace: jest.fn(),
|
||||
theme: createTheme(),
|
||||
history: {
|
||||
replace: () => {},
|
||||
|
@ -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[];
|
||||
|
@ -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';
|
||||
|
@ -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([]));
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user