Traces: Only show filtered spans (#66986)

* Only show filtered spans

* Add & update tests
This commit is contained in:
Joey 2023-04-27 08:19:58 +01:00 committed by GitHub
parent 61e3bbb858
commit d949aa778b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 131 additions and 27 deletions

View File

@ -99,6 +99,7 @@ export function TraceView(props: Props) {
);
const [newTraceViewHeaderFocusedSpanIdForSearch, setNewTraceViewHeaderFocusedSpanIdForSearch] = useState('');
const [showSpanFilters, setShowSpanFilters] = useToggle(false);
const [showSpanFilterMatchesOnly, setShowSpanFilterMatchesOnly] = useState(false);
const [headerHeight, setHeaderHeight] = useState(0);
const styles = useStyles2(getStyles);
@ -163,6 +164,8 @@ export function TraceView(props: Props) {
setSearch={setNewTraceViewHeaderSearch}
showSpanFilters={showSpanFilters}
setShowSpanFilters={setShowSpanFilters}
showSpanFilterMatchesOnly={showSpanFilterMatchesOnly}
setShowSpanFilterMatchesOnly={setShowSpanFilterMatchesOnly}
focusedSpanIdForSearch={newTraceViewHeaderFocusedSpanIdForSearch}
setFocusedSpanIdForSearch={setNewTraceViewHeaderFocusedSpanIdForSearch}
spanFilterMatches={spanFilterMatches}
@ -226,6 +229,7 @@ export function TraceView(props: Props) {
? newTraceViewHeaderFocusedSpanIdForSearch
: props.focusedSpanIdForSearch!
}
showSpanFilterMatchesOnly={showSpanFilterMatchesOnly}
createFocusSpanLink={createFocusSpanLink}
topOfViewRef={topOfViewRef}
topOfViewRefType={topOfViewRefType}

View File

@ -1,9 +1,10 @@
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React, { createRef } from 'react';
import { Provider } from 'react-redux';
import { getDefaultTimeRange, LoadingState } from '@grafana/data';
import { config } from '@grafana/runtime';
import { ExploreId } from 'app/types';
import { configureStore } from '../../../store/configureStore';
@ -47,37 +48,48 @@ function renderTraceViewContainer(frames = [frameOld]) {
}
describe('TraceViewContainer', () => {
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
jest.useFakeTimers();
// Need to use delay: null here to work with fakeTimers
// see https://github.com/testing-library/user-event/issues/833
user = userEvent.setup({ delay: null });
});
afterEach(() => {
jest.useRealTimers();
});
it('toggles children visibility', async () => {
renderTraceViewContainer();
expect(screen.queryAllByText('', { selector: 'div[data-testid="span-view"]' }).length).toBe(3);
await userEvent.click(screen.getAllByText('', { selector: 'span[data-testid="SpanTreeOffset--indentGuide"]' })[0]);
await user.click(screen.getAllByText('', { selector: 'span[data-testid="SpanTreeOffset--indentGuide"]' })[0]);
expect(screen.queryAllByText('', { selector: 'div[data-testid="span-view"]' }).length).toBe(1);
await userEvent.click(screen.getAllByText('', { selector: 'span[data-testid="SpanTreeOffset--indentGuide"]' })[0]);
await user.click(screen.getAllByText('', { selector: 'span[data-testid="SpanTreeOffset--indentGuide"]' })[0]);
expect(screen.queryAllByText('', { selector: 'div[data-testid="span-view"]' }).length).toBe(3);
});
it('toggles collapses and expands one level of spans', async () => {
renderTraceViewContainer();
expect(screen.queryAllByText('', { selector: 'div[data-testid="span-view"]' }).length).toBe(3);
await userEvent.click(screen.getByLabelText('Collapse +1'));
await user.click(screen.getByLabelText('Collapse +1'));
expect(screen.queryAllByText('', { selector: 'div[data-testid="span-view"]' }).length).toBe(2);
await userEvent.click(screen.getByLabelText('Expand +1'));
await user.click(screen.getByLabelText('Expand +1'));
expect(screen.queryAllByText('', { selector: 'div[data-testid="span-view"]' }).length).toBe(3);
});
it('toggles collapses and expands all levels', async () => {
renderTraceViewContainer();
expect(screen.queryAllByText('', { selector: 'div[data-testid="span-view"]' }).length).toBe(3);
await userEvent.click(screen.getByLabelText('Collapse All'));
await user.click(screen.getByLabelText('Collapse All'));
expect(screen.queryAllByText('', { selector: 'div[data-testid="span-view"]' }).length).toBe(1);
await userEvent.click(screen.getByLabelText('Expand All'));
await user.click(screen.getByLabelText('Expand All'));
expect(screen.queryAllByText('', { selector: 'div[data-testid="span-view"]' }).length).toBe(3);
});
it('searches for spans', async () => {
renderTraceViewContainer();
await userEvent.type(screen.getByPlaceholderText('Find...'), '1ed38015486087ca');
await user.type(screen.getByPlaceholderText('Find...'), '1ed38015486087ca');
expect(
screen.queryAllByText('', { selector: 'div[data-testid="span-view"]' })[0].parentElement!.className
).toContain('rowMatchingFilter');
@ -85,40 +97,58 @@ describe('TraceViewContainer', () => {
it('can select next/prev results', async () => {
renderTraceViewContainer();
await userEvent.type(screen.getByPlaceholderText('Find...'), 'logproto');
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 userEvent.click(nextResultButton);
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 userEvent.click(nextResultButton);
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 userEvent.click(nextResultButton);
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 userEvent.click(prevResultButton);
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 userEvent.click(prevResultButton);
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 userEvent.click(prevResultButton);
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('show matches only works as expected', async () => {
config.featureToggles.newTraceViewHeader = true;
renderTraceViewContainer();
const spanFiltersButton = screen.getByRole('button', { name: 'Span Filters' });
await user.click(spanFiltersButton);
await user.click(screen.getByLabelText('Select tag key'));
const tagOption = screen.getByText('http.status_code');
await waitFor(() => expect(tagOption).toBeInTheDocument());
await user.click(tagOption);
expect(screen.queryAllByText('', { selector: 'div[data-testid="span-view"]' }).length).toBe(3);
const matchesSwitch = screen.getByRole('checkbox', { name: 'Show matches only switch' });
expect(matchesSwitch).toBeInTheDocument();
await user.click(matchesSwitch);
expect(screen.queryAllByText('', { selector: 'div[data-testid="span-view"]' }).length).toBe(1);
});
});

View File

@ -30,6 +30,8 @@ const setup = () => {
setSearch: jest.fn(),
showSpanFilters: true,
setShowSpanFilters: jest.fn(),
showSpanFilterMatchesOnly: false,
setShowSpanFilterMatchesOnly: jest.fn(),
spanFilterMatches: undefined,
focusedSpanIdForSearch: '',
setFocusedSpanIdForSearch: jest.fn(),

View File

@ -39,6 +39,8 @@ export type TracePageHeaderProps = {
setSearch: React.Dispatch<React.SetStateAction<SearchProps>>;
showSpanFilters: boolean;
setShowSpanFilters: (isOpen: boolean) => void;
showSpanFilterMatchesOnly: boolean;
setShowSpanFilterMatchesOnly: (showMatchesOnly: boolean) => void;
focusedSpanIdForSearch: string;
setFocusedSpanIdForSearch: React.Dispatch<React.SetStateAction<string>>;
spanFilterMatches: Set<string> | undefined;
@ -54,6 +56,8 @@ export const NewTracePageHeader = memo((props: TracePageHeaderProps) => {
setSearch,
showSpanFilters,
setShowSpanFilters,
showSpanFilterMatchesOnly,
setShowSpanFilterMatchesOnly,
focusedSpanIdForSearch,
setFocusedSpanIdForSearch,
spanFilterMatches,
@ -131,6 +135,8 @@ export const NewTracePageHeader = memo((props: TracePageHeaderProps) => {
trace={trace}
showSpanFilters={showSpanFilters}
setShowSpanFilters={setShowSpanFilters}
showSpanFilterMatchesOnly={showSpanFilterMatchesOnly}
setShowSpanFilterMatchesOnly={setShowSpanFilterMatchesOnly}
search={search}
setSearch={setSearch}
spanFilterMatches={spanFilterMatches}

View File

@ -22,6 +22,8 @@ import NewTracePageSearchBar, { TracePageSearchBarProps } from './NewTracePageSe
const defaultProps = {
search: defaultFilters,
setFocusedSpanIdForSearch: jest.fn(),
showSpanFilterMatchesOnly: false,
setShowSpanFilterMatchesOnly: jest.fn(),
};
describe('<NewTracePageSearchBar>', () => {
@ -51,4 +53,10 @@ describe('<NewTracePageSearchBar>', () => {
expect((nextResButton as HTMLButtonElement)['disabled']).toBe(false);
expect((prevResButton as HTMLButtonElement)['disabled']).toBe(false);
});
it('renders show span filter matches only switch', async () => {
render(<NewTracePageSearchBar {...(defaultProps as unknown as TracePageSearchBarProps)} />);
const matchesSwitch = screen.getByRole('checkbox', { name: 'Show matches only switch' });
expect(matchesSwitch).toBeInTheDocument();
});
});

View File

@ -16,7 +16,7 @@ import { css } from '@emotion/css';
import React, { memo, Dispatch, SetStateAction, useEffect, useMemo } from 'react';
import { config, reportInteraction } from '@grafana/runtime';
import { Button, useStyles2 } from '@grafana/ui';
import { Button, Switch, useStyles2 } from '@grafana/ui';
import { SearchProps } from '../../useSearch';
import { convertTimeFilter } from '../utils/filter-spans';
@ -25,6 +25,8 @@ export type TracePageSearchBarProps = {
search: SearchProps;
setSearch: React.Dispatch<React.SetStateAction<SearchProps>>;
spanFilterMatches: Set<string> | undefined;
showSpanFilterMatchesOnly: boolean;
setShowSpanFilterMatchesOnly: (showMatchesOnly: boolean) => void;
focusedSpanIdForSearch: string;
setFocusedSpanIdForSearch: Dispatch<SetStateAction<string>>;
datasourceType: string;
@ -32,7 +34,16 @@ export type TracePageSearchBarProps = {
};
export default memo(function NewTracePageSearchBar(props: TracePageSearchBarProps) {
const { search, spanFilterMatches, focusedSpanIdForSearch, setFocusedSpanIdForSearch, datasourceType, reset } = props;
const {
search,
spanFilterMatches,
focusedSpanIdForSearch,
setFocusedSpanIdForSearch,
datasourceType,
reset,
showSpanFilterMatchesOnly,
setShowSpanFilterMatchesOnly,
} = props;
const styles = useStyles2(getStyles);
useEffect(() => {
@ -108,6 +119,14 @@ export default memo(function NewTracePageSearchBar(props: TracePageSearchBarProp
>
Reset
</Button>
<div className={styles.matchesOnly}>
<Switch
value={showSpanFilterMatchesOnly}
onChange={(value) => setShowSpanFilterMatchesOnly(value.currentTarget.checked ?? false)}
label="Show matches only switch"
/>
<span onClick={() => setShowSpanFilterMatchesOnly(!showSpanFilterMatchesOnly)}>Show matches only</span>
</div>
</div>
<div className={styles.nextPrevButtons}>
<Button
@ -142,6 +161,16 @@ export const getStyles = () => {
searchBar: css`
display: inline;
`,
matchesOnly: css`
display: inline-flex;
margin: 0 0 0 10px;
vertical-align: middle;
span {
cursor: pointer;
margin: -3px 0 0 5px;
}
`,
buttons: css`
display: flex;
justify-content: flex-end;

View File

@ -43,6 +43,8 @@ describe('SpanFilters', () => {
trace: trace,
showSpanFilters: true,
setShowSpanFilters: jest.fn(),
showSpanFilterMatchesOnly: false,
setShowSpanFilterMatchesOnly: jest.fn(),
search: search,
setSearch: setSearch,
spanFilterMatches: undefined,

View File

@ -40,6 +40,8 @@ export type SpanFilterProps = {
setSearch: React.Dispatch<React.SetStateAction<SearchProps>>;
showSpanFilters: boolean;
setShowSpanFilters: (isOpen: boolean) => void;
showSpanFilterMatchesOnly: boolean;
setShowSpanFilterMatchesOnly: (showMatchesOnly: boolean) => void;
focusedSpanIdForSearch: string;
setFocusedSpanIdForSearch: React.Dispatch<React.SetStateAction<string>>;
spanFilterMatches: Set<string> | undefined;
@ -53,6 +55,8 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
setSearch,
showSpanFilters,
setShowSpanFilters,
showSpanFilterMatchesOnly,
setShowSpanFilterMatchesOnly,
focusedSpanIdForSearch,
setFocusedSpanIdForSearch,
spanFilterMatches,
@ -384,6 +388,8 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
search={search}
setSearch={setSearch}
spanFilterMatches={spanFilterMatches}
showSpanFilterMatchesOnly={showSpanFilterMatchesOnly}
setShowSpanFilterMatchesOnly={setShowSpanFilterMatchesOnly}
focusedSpanIdForSearch={focusedSpanIdForSearch}
setFocusedSpanIdForSearch={setFocusedSpanIdForSearch}
datasourceType={datasourceType}

View File

@ -461,7 +461,7 @@ export default class ListView extends React.Component<TListViewProps> {
end = dataLength - 1;
}
} else {
start = this._startIndexDrawn;
start = this._startIndexDrawn > dataLength - 1 ? 0 : this._startIndexDrawn;
end = this._endIndexDrawn > dataLength - 1 ? dataLength - 1 : this._endIndexDrawn;
}
}

View File

@ -38,7 +38,7 @@ const nameWrapperMatchingFilterClassName = 'nameWrapperMatchingFilter';
const viewClassName = 'jaegerView';
const nameColumnClassName = 'nameColumn';
const getStyles = stylesFactory((theme: GrafanaTheme2) => {
const getStyles = stylesFactory((theme: GrafanaTheme2, showSpanFilterMatchesOnly: boolean) => {
const animations = {
label: 'flash',
flash: keyframes`
@ -50,6 +50,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
}
`,
};
const backgroundColor = showSpanFilterMatchesOnly ? '' : autoColor(theme, '#fffce4');
return {
nameWrapper: css`
@ -60,7 +61,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
`,
nameWrapperMatchingFilter: css`
label: nameWrapperMatchingFilter;
background-color: ${autoColor(theme, '#fffce4')};
background-color: ${backgroundColor};
`,
nameColumn: css`
label: nameColumn;
@ -164,7 +165,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
`,
rowMatchingFilter: css`
label: rowMatchingFilter;
background-color: ${autoColor(theme, '#fffbde')};
// background-color: ${autoColor(theme, '#fffbde')};
&:hover .${nameWrapperClassName} {
background: linear-gradient(
90deg,
@ -297,6 +298,7 @@ export type SpanBarRowProps = {
isDetailExpanded: boolean;
isMatchingFilter: boolean;
isFocused: boolean;
showSpanFilterMatchesOnly: boolean;
onDetailToggled: (spanID: string) => void;
onChildrenToggled: (spanID: string) => void;
numTicks: number;
@ -360,6 +362,7 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
isChildrenExpanded,
isDetailExpanded,
isMatchingFilter,
showSpanFilterMatchesOnly,
isFocused,
numTicks,
rpc,
@ -388,7 +391,7 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
const viewBounds = getViewedBounds(span.startTime, span.startTime + span.duration);
const viewStart = viewBounds.start;
const viewEnd = viewBounds.end;
const styles = getStyles(theme);
const styles = getStyles(theme, showSpanFilterMatchesOnly);
const labelDetail = `${serviceName}::${operationName}`;
let longLabel;

View File

@ -113,6 +113,7 @@ type TVirtualizedTraceViewOwnProps = {
scrollElement?: Element;
focusedSpanId?: string;
focusedSpanIdForSearch: string;
showSpanFilterMatchesOnly: boolean;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
topOfViewRef?: RefObject<HTMLDivElement>;
topOfViewRefType?: TopOfViewRefType;
@ -134,11 +135,17 @@ const NUM_TICKS = 5;
function generateRowStates(
spans: TraceSpan[] | TNil,
childrenHiddenIDs: Set<string>,
detailStates: Map<string, DetailState | TNil>
detailStates: Map<string, DetailState | TNil>,
findMatchesIDs: Set<string> | TNil,
showSpanFilterMatchesOnly: boolean
): RowState[] {
if (!spans) {
return [];
}
if (showSpanFilterMatchesOnly && findMatchesIDs) {
spans = spans.filter((span) => findMatchesIDs.has(span.spanID));
}
let collapseDepth = null;
const rowStates = [];
for (let i = 0; i < spans.length; i++) {
@ -185,9 +192,13 @@ function getClipping(currentViewRange: [number, number]) {
function generateRowStatesFromTrace(
trace: Trace | TNil,
childrenHiddenIDs: Set<string>,
detailStates: Map<string, DetailState | TNil>
detailStates: Map<string, DetailState | TNil>,
findMatchesIDs: Set<string> | TNil,
showSpanFilterMatchesOnly: boolean
): RowState[] {
return trace ? generateRowStates(trace.spans, childrenHiddenIDs, detailStates) : [];
return trace
? generateRowStates(trace.spans, childrenHiddenIDs, detailStates, findMatchesIDs, showSpanFilterMatchesOnly)
: [];
}
const memoizedGenerateRowStates = memoizeOne(generateRowStatesFromTrace);
@ -263,8 +274,8 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
}
getRowStates(): RowState[] {
const { childrenHiddenIDs, detailStates, trace } = this.props;
return memoizedGenerateRowStates(trace, childrenHiddenIDs, detailStates);
const { childrenHiddenIDs, detailStates, trace, findMatchesIDs, showSpanFilterMatchesOnly } = this.props;
return memoizedGenerateRowStates(trace, childrenHiddenIDs, detailStates, findMatchesIDs, showSpanFilterMatchesOnly);
}
getClipping(): { left: boolean; right: boolean } {
@ -397,6 +408,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
createSpanLink,
focusedSpanId,
focusedSpanIdForSearch,
showSpanFilterMatchesOnly,
theme,
datasourceType,
} = this.props;
@ -451,6 +463,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
isDetailExpanded={isDetailExpanded}
isMatchingFilter={isMatchingFilter}
isFocused={isFocused}
showSpanFilterMatchesOnly={showSpanFilterMatchesOnly}
numTicks={NUM_TICKS}
onDetailToggled={detailToggle}
onChildrenToggled={childrenToggle}

View File

@ -110,6 +110,7 @@ export type TProps = TExtractUiFindFromStateReturn & {
scrollElement?: Element;
focusedSpanId?: string;
focusedSpanIdForSearch: string;
showSpanFilterMatchesOnly: boolean;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
topOfViewRef?: RefObject<HTMLDivElement>;
topOfViewRefType?: TopOfViewRefType;