From d949aa778b2696a3ebcdd5ee06d6e75b7a10a7e1 Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Thu, 27 Apr 2023 08:19:58 +0100 Subject: [PATCH] Traces: Only show filtered spans (#66986) * Only show filtered spans * Add & update tests --- .../features/explore/TraceView/TraceView.tsx | 4 ++ .../TraceView/TraceViewContainer.test.tsx | 60 ++++++++++++++----- .../NewTracePageHeader.test.tsx | 2 + .../TracePageHeader/NewTracePageHeader.tsx | 6 ++ .../NewTracePageSearchBar.test.tsx | 8 +++ .../TracePageHeader/NewTracePageSearchBar.tsx | 33 +++++++++- .../SpanFilters/SpanFilters.test.tsx | 2 + .../SpanFilters/SpanFilters.tsx | 6 ++ .../TraceTimelineViewer/ListView/index.tsx | 2 +- .../TraceTimelineViewer/SpanBarRow.tsx | 11 ++-- .../VirtualizedTraceView.tsx | 23 +++++-- .../components/TraceTimelineViewer/index.tsx | 1 + 12 files changed, 131 insertions(+), 27 deletions(-) diff --git a/public/app/features/explore/TraceView/TraceView.tsx b/public/app/features/explore/TraceView/TraceView.tsx index 1d2c655f79a..2c2049d0974 100644 --- a/public/app/features/explore/TraceView/TraceView.tsx +++ b/public/app/features/explore/TraceView/TraceView.tsx @@ -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} diff --git a/public/app/features/explore/TraceView/TraceViewContainer.test.tsx b/public/app/features/explore/TraceView/TraceViewContainer.test.tsx index d061560861d..86677a298e6 100644 --- a/public/app/features/explore/TraceView/TraceViewContainer.test.tsx +++ b/public/app/features/explore/TraceView/TraceViewContainer.test.tsx @@ -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; + 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); + }); }); diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.test.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.test.tsx index 64db99647b1..eec1a45a7b2 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.test.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.test.tsx @@ -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(), diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.tsx index 835ff170d6b..1e0ffdcbb9f 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.tsx @@ -39,6 +39,8 @@ export type TracePageHeaderProps = { setSearch: React.Dispatch>; showSpanFilters: boolean; setShowSpanFilters: (isOpen: boolean) => void; + showSpanFilterMatchesOnly: boolean; + setShowSpanFilterMatchesOnly: (showMatchesOnly: boolean) => void; focusedSpanIdForSearch: string; setFocusedSpanIdForSearch: React.Dispatch>; spanFilterMatches: Set | 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} diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageSearchBar.test.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageSearchBar.test.tsx index fdc550211d9..4515b0bbbeb 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageSearchBar.test.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageSearchBar.test.tsx @@ -22,6 +22,8 @@ import NewTracePageSearchBar, { TracePageSearchBarProps } from './NewTracePageSe const defaultProps = { search: defaultFilters, setFocusedSpanIdForSearch: jest.fn(), + showSpanFilterMatchesOnly: false, + setShowSpanFilterMatchesOnly: jest.fn(), }; describe('', () => { @@ -51,4 +53,10 @@ describe('', () => { expect((nextResButton as HTMLButtonElement)['disabled']).toBe(false); expect((prevResButton as HTMLButtonElement)['disabled']).toBe(false); }); + + it('renders show span filter matches only switch', async () => { + render(); + const matchesSwitch = screen.getByRole('checkbox', { name: 'Show matches only switch' }); + expect(matchesSwitch).toBeInTheDocument(); + }); }); diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageSearchBar.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageSearchBar.tsx index c0ffd8460d3..bf0ddd18a8b 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageSearchBar.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageSearchBar.tsx @@ -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>; spanFilterMatches: Set | undefined; + showSpanFilterMatchesOnly: boolean; + setShowSpanFilterMatchesOnly: (showMatchesOnly: boolean) => void; focusedSpanIdForSearch: string; setFocusedSpanIdForSearch: Dispatch>; 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 +
+ setShowSpanFilterMatchesOnly(value.currentTarget.checked ?? false)} + label="Show matches only switch" + /> + setShowSpanFilterMatchesOnly(!showSpanFilterMatchesOnly)}>Show matches only +