mirror of
https://github.com/grafana/grafana.git
synced 2025-01-08 23:23:45 -06:00
Tracing: Show next/prev buttons when span filters are collapsed (#71025)
* Show next/prev buttons when span filters are collapsed * Update test * Remove imports * Update lint * Prettier * Update test * Update styling
This commit is contained in:
parent
a2a890e85b
commit
6615418df8
@ -83,6 +83,8 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
font-weight: ${theme.typography.fontWeightMedium};
|
||||
margin-right: ${theme.spacing(1)};
|
||||
font-size: ${theme.typography.size.md};
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
`,
|
||||
icon: css`
|
||||
label: collapse__icon;
|
||||
|
@ -81,6 +81,7 @@ export class RawPrometheusContainer extends PureComponent<Props, PrometheusConta
|
||||
const spacing = css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
flex: '1',
|
||||
});
|
||||
const ALL_GRAPH_STYLE_OPTIONS: Array<SelectableValue<TableResultsStyle>> = TABLE_RESULTS_STYLES.map((style) => ({
|
||||
value: style,
|
||||
|
@ -136,13 +136,13 @@ describe('TraceViewContainer', () => {
|
||||
it('can select next/prev results', async () => {
|
||||
config.featureToggles.newTraceViewHeader = true;
|
||||
renderTraceViewContainer();
|
||||
const spanFiltersButton = screen.getByRole('button', { name: 'Span Filters' });
|
||||
const spanFiltersButton = screen.getByRole('button', { name: 'Span Filters 3 spans Prev Next' });
|
||||
await user.click(spanFiltersButton);
|
||||
|
||||
const nextResultButton = screen.getByRole('button', { name: 'Next result button' });
|
||||
const prevResultButton = screen.getByRole('button', { name: 'Prev result button' });
|
||||
expect((nextResultButton as HTMLButtonElement)['disabled']).toBe(true);
|
||||
expect((prevResultButton as HTMLButtonElement)['disabled']).toBe(true);
|
||||
expect(nextResultButton.getAttribute('tabindex')).toBe('-1');
|
||||
expect(prevResultButton.getAttribute('tabindex')).toBe('-1');
|
||||
|
||||
await user.click(screen.getByLabelText('Select tag key'));
|
||||
const tagOption = screen.getByText('component');
|
||||
@ -161,8 +161,8 @@ describe('TraceViewContainer', () => {
|
||||
).toContain('rowMatchingFilter');
|
||||
});
|
||||
|
||||
expect((nextResultButton as HTMLButtonElement)['disabled']).toBe(false);
|
||||
expect((prevResultButton as HTMLButtonElement)['disabled']).toBe(false);
|
||||
expect(nextResultButton.getAttribute('tabindex')).toBe('0');
|
||||
expect(prevResultButton.getAttribute('tabindex')).toBe('0');
|
||||
await user.click(nextResultButton);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
@ -186,7 +186,7 @@ describe('TraceViewContainer', () => {
|
||||
it('show matches only works as expected', async () => {
|
||||
config.featureToggles.newTraceViewHeader = true;
|
||||
renderTraceViewContainer();
|
||||
const spanFiltersButton = screen.getByRole('button', { name: 'Span Filters' });
|
||||
const spanFiltersButton = screen.getByRole('button', { name: 'Span Filters 3 spans Prev Next' });
|
||||
await user.click(spanFiltersButton);
|
||||
|
||||
await user.click(screen.getByLabelText('Select tag key'));
|
||||
|
@ -7,7 +7,7 @@ import { useStyles2 } from '@grafana/ui';
|
||||
import { StoreState, useSelector } from 'app/types';
|
||||
|
||||
import { TraceView } from './TraceView';
|
||||
import TracePageSearchBar from './components/TracePageHeader/TracePageSearchBar';
|
||||
import TracePageSearchBar from './components/TracePageHeader/SearchBar/TracePageSearchBar';
|
||||
import { TopOfViewRefType } from './components/TraceTimelineViewer/VirtualizedTraceView';
|
||||
import { useSearch } from './useSearch';
|
||||
import { transformDataFrames } from './utils/transform';
|
||||
|
@ -1,236 +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, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { Button, Icon, Switch, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { SearchProps } from '../../useSearch';
|
||||
import { convertTimeFilter } from '../utils/filter-spans';
|
||||
|
||||
export type TracePageSearchBarProps = {
|
||||
search: SearchProps;
|
||||
spanFilterMatches: Set<string> | undefined;
|
||||
showSpanFilterMatchesOnly: boolean;
|
||||
setShowSpanFilterMatchesOnly: (showMatchesOnly: boolean) => void;
|
||||
setFocusedSpanIdForSearch: Dispatch<SetStateAction<string>>;
|
||||
datasourceType: string;
|
||||
clear: () => void;
|
||||
totalSpans: number;
|
||||
};
|
||||
|
||||
export default memo(function NewTracePageSearchBar(props: TracePageSearchBarProps) {
|
||||
const {
|
||||
search,
|
||||
spanFilterMatches,
|
||||
showSpanFilterMatchesOnly,
|
||||
setShowSpanFilterMatchesOnly,
|
||||
setFocusedSpanIdForSearch,
|
||||
datasourceType,
|
||||
clear,
|
||||
totalSpans,
|
||||
} = props;
|
||||
const [currentSpanIndex, setCurrentSpanIndex] = useState(-1);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentSpanIndex(-1);
|
||||
setFocusedSpanIdForSearch('');
|
||||
}, [setFocusedSpanIdForSearch, spanFilterMatches]);
|
||||
|
||||
useEffect(() => {
|
||||
if (spanFilterMatches) {
|
||||
const spanMatches = Array.from(spanFilterMatches!);
|
||||
setFocusedSpanIdForSearch(spanMatches[currentSpanIndex]);
|
||||
}
|
||||
}, [currentSpanIndex, setFocusedSpanIdForSearch, spanFilterMatches]);
|
||||
|
||||
const nextResult = () => {
|
||||
reportInteraction('grafana_traces_trace_view_find_next_prev_clicked', {
|
||||
datasourceType: datasourceType,
|
||||
grafana_version: config.buildInfo.version,
|
||||
direction: 'next',
|
||||
});
|
||||
|
||||
// new query || at end, go to start
|
||||
if (currentSpanIndex === -1 || (spanFilterMatches && currentSpanIndex === spanFilterMatches.size - 1)) {
|
||||
setCurrentSpanIndex(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// get next
|
||||
setCurrentSpanIndex(currentSpanIndex + 1);
|
||||
};
|
||||
|
||||
const prevResult = () => {
|
||||
reportInteraction('grafana_traces_trace_view_find_next_prev_clicked', {
|
||||
datasourceType: datasourceType,
|
||||
grafana_version: config.buildInfo.version,
|
||||
direction: 'prev',
|
||||
});
|
||||
|
||||
// new query || at start, go to end
|
||||
if (spanFilterMatches && (currentSpanIndex === -1 || currentSpanIndex === 0)) {
|
||||
setCurrentSpanIndex(spanFilterMatches.size - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// get prev
|
||||
setCurrentSpanIndex(currentSpanIndex - 1);
|
||||
};
|
||||
|
||||
const buttonEnabled = spanFilterMatches && spanFilterMatches?.size > 0;
|
||||
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]);
|
||||
|
||||
const amountText = spanFilterMatches?.size === 1 ? 'match' : 'matches';
|
||||
const matches =
|
||||
spanFilterMatches?.size === 0 ? (
|
||||
<>
|
||||
<span>0 matches</span>
|
||||
<Tooltip
|
||||
content="There are 0 span matches for the filters selected. Please try removing some of the selected filters."
|
||||
placement="left"
|
||||
>
|
||||
<span className={styles.matchesTooltip}>
|
||||
<Icon name="info-circle" size="lg" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
) : currentSpanIndex !== -1 ? (
|
||||
`${currentSpanIndex + 1}/${spanFilterMatches?.size} ${amountText}`
|
||||
) : (
|
||||
`${spanFilterMatches?.size} ${amountText}`
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.searchBar}>
|
||||
<div className={styles.buttons}>
|
||||
<>
|
||||
<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.nextPrevButtons}>
|
||||
<span className={styles.matches}>{spanFilterMatches ? matches : `${totalSpans} spans`}</span>
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={!buttonEnabled}
|
||||
type="button"
|
||||
fill="outline"
|
||||
aria-label="Prev result button"
|
||||
onClick={prevResult}
|
||||
>
|
||||
Prev
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={!buttonEnabled}
|
||||
type="button"
|
||||
fill="outline"
|
||||
aria-label="Next result button"
|
||||
onClick={nextResult}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
searchBar: css`
|
||||
display: inline;
|
||||
`,
|
||||
matchesOnly: css`
|
||||
display: inline-flex;
|
||||
margin: 0 0 0 10px;
|
||||
vertical-align: middle;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
cursor: pointer;
|
||||
}
|
||||
`,
|
||||
buttons: css`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin: 5px 0 0 0;
|
||||
`,
|
||||
clearButton: css`
|
||||
order: 1;
|
||||
`,
|
||||
clearMatchesButton: css`
|
||||
color: ${theme.colors.text.primary};
|
||||
&:hover {
|
||||
background: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
`,
|
||||
nextPrevButtons: css`
|
||||
margin-left: auto;
|
||||
order: 2;
|
||||
|
||||
button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
`,
|
||||
matches: css`
|
||||
margin-right: 5px;
|
||||
`,
|
||||
matchesTooltip: css`
|
||||
color: #aaa;
|
||||
margin: -2px 0 0 10px;
|
||||
`,
|
||||
};
|
||||
};
|
@ -0,0 +1,57 @@
|
||||
// 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 NewTracePageSearchBar from './NewTracePageSearchBar';
|
||||
|
||||
describe('<NewTracePageSearchBar>', () => {
|
||||
const NewTracePageSearchBarWithProps = (props: { matches: string[] | undefined }) => {
|
||||
const searchBarProps = {
|
||||
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();
|
||||
});
|
||||
});
|
@ -0,0 +1,158 @@
|
||||
// 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 { convertTimeFilter } from '../../utils/filter-spans';
|
||||
|
||||
import NextPrevResult from './NextPrevResult';
|
||||
|
||||
export type TracePageSearchBarProps = {
|
||||
search: SearchProps;
|
||||
spanFilterMatches: Set<string> | undefined;
|
||||
showSpanFilterMatchesOnly: boolean;
|
||||
setShowSpanFilterMatchesOnly: (showMatchesOnly: boolean) => void;
|
||||
focusedSpanIndexForSearch: number;
|
||||
setFocusedSpanIndexForSearch: Dispatch<SetStateAction<number>>;
|
||||
setFocusedSpanIdForSearch: Dispatch<SetStateAction<string>>;
|
||||
datasourceType: string;
|
||||
totalSpans: number;
|
||||
clear: () => void;
|
||||
showSpanFilters: boolean;
|
||||
};
|
||||
|
||||
export default memo(function NewTracePageSearchBar(props: TracePageSearchBarProps) {
|
||||
const {
|
||||
search,
|
||||
spanFilterMatches,
|
||||
showSpanFilterMatchesOnly,
|
||||
setShowSpanFilterMatchesOnly,
|
||||
focusedSpanIndexForSearch,
|
||||
setFocusedSpanIndexForSearch,
|
||||
setFocusedSpanIdForSearch,
|
||||
datasourceType,
|
||||
totalSpans,
|
||||
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
|
||||
spanFilterMatches={spanFilterMatches}
|
||||
setFocusedSpanIdForSearch={setFocusedSpanIdForSearch}
|
||||
focusedSpanIndexForSearch={focusedSpanIndexForSearch}
|
||||
setFocusedSpanIndexForSearch={setFocusedSpanIndexForSearch}
|
||||
datasourceType={datasourceType}
|
||||
totalSpans={totalSpans}
|
||||
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;
|
||||
`,
|
||||
};
|
||||
};
|
@ -14,15 +14,15 @@
|
||||
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { createTheme } from '@grafana/data';
|
||||
|
||||
import { defaultFilters } from '../../useSearch';
|
||||
import { defaultFilters } from '../../../useSearch';
|
||||
|
||||
import NewTracePageSearchBar, { getStyles } from './NewTracePageSearchBar';
|
||||
import NextPrevResult, { getStyles } from './NextPrevResult';
|
||||
|
||||
describe('<NewTracePageSearchBar>', () => {
|
||||
describe('<NextPrevResult>', () => {
|
||||
let user: ReturnType<typeof userEvent.setup>;
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
@ -34,56 +34,58 @@ describe('<NewTracePageSearchBar>', () => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
const NewTracePageSearchBarWithProps = (props: { matches: string[] | undefined }) => {
|
||||
const NextPrevResultWithProps = (props: { matches: string[] | undefined }) => {
|
||||
const [focusedSpanIndexForSearch, setFocusedSpanIndexForSearch] = useState(-1);
|
||||
const searchBarProps = {
|
||||
search: defaultFilters,
|
||||
spanFilterMatches: props.matches ? new Set(props.matches) : undefined,
|
||||
showSpanFilterMatchesOnly: false,
|
||||
setShowSpanFilterMatchesOnly: jest.fn(),
|
||||
setFocusedSpanIdForSearch: jest.fn(),
|
||||
focusedSpanIndexForSearch: focusedSpanIndexForSearch,
|
||||
setFocusedSpanIndexForSearch: setFocusedSpanIndexForSearch,
|
||||
datasourceType: '',
|
||||
clear: jest.fn(),
|
||||
totalSpans: 100,
|
||||
showSpanFilters: true,
|
||||
};
|
||||
|
||||
return <NewTracePageSearchBar {...searchBarProps} />;
|
||||
return <NextPrevResult {...searchBarProps} />;
|
||||
};
|
||||
|
||||
it('should render', () => {
|
||||
expect(() => render(<NewTracePageSearchBarWithProps matches={[]} />)).not.toThrow();
|
||||
expect(() => render(<NextPrevResultWithProps matches={[]} />)).not.toThrow();
|
||||
});
|
||||
|
||||
it('renders buttons', () => {
|
||||
render(<NewTracePageSearchBarWithProps matches={[]} />);
|
||||
it('renders UI properly', () => {
|
||||
render(<NextPrevResultWithProps matches={[]} />);
|
||||
const nextResButton = screen.queryByRole('button', { name: 'Next result button' });
|
||||
const prevResButton = screen.queryByRole('button', { name: 'Prev result button' });
|
||||
const clearFiltersButton = screen.getByRole('button', { name: 'Clear filters button' });
|
||||
expect(nextResButton).toBeInTheDocument();
|
||||
expect(prevResButton).toBeInTheDocument();
|
||||
expect(clearFiltersButton).toBeInTheDocument();
|
||||
expect((nextResButton as HTMLButtonElement)['disabled']).toBe(true);
|
||||
expect((prevResButton as HTMLButtonElement)['disabled']).toBe(true);
|
||||
expect((clearFiltersButton as HTMLButtonElement)['disabled']).toBe(true);
|
||||
expect(nextResButton as HTMLDivElement).toHaveStyle('pointer-events: none');
|
||||
expect(prevResButton as HTMLDivElement).toHaveStyle('pointer-events: none');
|
||||
expect(screen.getByText('0 matches')).toBeDefined();
|
||||
});
|
||||
|
||||
it('renders total spans', async () => {
|
||||
render(<NewTracePageSearchBarWithProps matches={undefined} />);
|
||||
render(<NextPrevResultWithProps matches={undefined} />);
|
||||
expect(screen.getByText('100 spans')).toBeDefined();
|
||||
});
|
||||
|
||||
it('renders buttons that can be used to search if filters added', () => {
|
||||
render(<NewTracePageSearchBarWithProps matches={['2ed38015486087ca']} />);
|
||||
render(<NextPrevResultWithProps matches={['2ed38015486087ca']} />);
|
||||
const nextResButton = screen.queryByRole('button', { name: 'Next result button' });
|
||||
const prevResButton = screen.queryByRole('button', { name: 'Prev result button' });
|
||||
expect(nextResButton).toBeInTheDocument();
|
||||
expect(prevResButton).toBeInTheDocument();
|
||||
expect((nextResButton as HTMLButtonElement)['disabled']).toBe(false);
|
||||
expect((prevResButton as HTMLButtonElement)['disabled']).toBe(false);
|
||||
expect(nextResButton as HTMLDivElement).not.toHaveStyle('pointer-events: none');
|
||||
expect(prevResButton as HTMLDivElement).not.toHaveStyle('pointer-events: none');
|
||||
expect(screen.getByText('1 match')).toBeDefined();
|
||||
});
|
||||
|
||||
it('renders correctly when moving through matches', async () => {
|
||||
render(<NewTracePageSearchBarWithProps matches={['1ed38015486087ca', '2ed38015486087ca', '3ed38015486087ca']} />);
|
||||
render(<NextPrevResultWithProps matches={['1ed38015486087ca', '2ed38015486087ca', '3ed38015486087ca']} />);
|
||||
const nextResButton = screen.queryByRole('button', { name: 'Next result button' });
|
||||
const prevResButton = screen.queryByRole('button', { name: 'Prev result button' });
|
||||
expect(screen.getByText('3 matches')).toBeDefined();
|
||||
@ -102,9 +104,9 @@ describe('<NewTracePageSearchBar>', () => {
|
||||
});
|
||||
|
||||
it('renders correctly when there are no matches i.e. too many filters added', async () => {
|
||||
const { container } = render(<NewTracePageSearchBarWithProps matches={[]} />);
|
||||
const styles = getStyles(createTheme());
|
||||
const tooltip = container.querySelector('.' + styles.matchesTooltip);
|
||||
const { container } = render(<NextPrevResultWithProps matches={[]} />);
|
||||
const theme = createTheme();
|
||||
const tooltip = container.querySelector('.' + getStyles(theme, true).matchesTooltip);
|
||||
expect(screen.getByText('0 matches')).toBeDefined();
|
||||
userEvent.hover(tooltip!);
|
||||
jest.advanceTimersByTime(1000);
|
||||
@ -112,10 +114,4 @@ describe('<NewTracePageSearchBar>', () => {
|
||||
expect(screen.getByText(/0 span matches for the filters selected/)).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
@ -0,0 +1,194 @@
|
||||
// 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, cx } from '@emotion/css';
|
||||
import React, { memo, Dispatch, SetStateAction, useEffect } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { Icon, Tooltip, useTheme2 } from '@grafana/ui';
|
||||
import { getButtonStyles } from '@grafana/ui/src/components/Button';
|
||||
|
||||
export type NextPrevResultProps = {
|
||||
spanFilterMatches: Set<string> | undefined;
|
||||
setFocusedSpanIdForSearch: Dispatch<SetStateAction<string>>;
|
||||
focusedSpanIndexForSearch: number;
|
||||
setFocusedSpanIndexForSearch: Dispatch<SetStateAction<number>>;
|
||||
datasourceType: string;
|
||||
totalSpans: number;
|
||||
showSpanFilters: boolean;
|
||||
};
|
||||
|
||||
export default memo(function NextPrevResult(props: NextPrevResultProps) {
|
||||
const {
|
||||
spanFilterMatches,
|
||||
setFocusedSpanIdForSearch,
|
||||
focusedSpanIndexForSearch,
|
||||
setFocusedSpanIndexForSearch,
|
||||
datasourceType,
|
||||
totalSpans,
|
||||
showSpanFilters,
|
||||
} = props;
|
||||
const styles = getStyles(useTheme2(), showSpanFilters);
|
||||
|
||||
useEffect(() => {
|
||||
if (spanFilterMatches && focusedSpanIndexForSearch !== -1) {
|
||||
const spanMatches = Array.from(spanFilterMatches!);
|
||||
setFocusedSpanIdForSearch(spanMatches[focusedSpanIndexForSearch]);
|
||||
}
|
||||
}, [focusedSpanIndexForSearch, setFocusedSpanIdForSearch, spanFilterMatches]);
|
||||
|
||||
const nextResult = (event: React.UIEvent, buttonEnabled: boolean) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (buttonEnabled) {
|
||||
reportInteraction('grafana_traces_trace_view_find_next_prev_clicked', {
|
||||
datasourceType: datasourceType,
|
||||
grafana_version: config.buildInfo.version,
|
||||
direction: 'next',
|
||||
});
|
||||
|
||||
// new query || at end, go to start
|
||||
if (
|
||||
focusedSpanIndexForSearch === -1 ||
|
||||
(spanFilterMatches && focusedSpanIndexForSearch === spanFilterMatches.size - 1)
|
||||
) {
|
||||
setFocusedSpanIndexForSearch(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// get next
|
||||
setFocusedSpanIndexForSearch(focusedSpanIndexForSearch + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const prevResult = (event: React.UIEvent, buttonEnabled: boolean) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (buttonEnabled) {
|
||||
reportInteraction('grafana_traces_trace_view_find_next_prev_clicked', {
|
||||
datasourceType: datasourceType,
|
||||
grafana_version: config.buildInfo.version,
|
||||
direction: 'prev',
|
||||
});
|
||||
|
||||
// new query || at start, go to end
|
||||
if (spanFilterMatches && (focusedSpanIndexForSearch === -1 || focusedSpanIndexForSearch === 0)) {
|
||||
setFocusedSpanIndexForSearch(spanFilterMatches.size - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// get prev
|
||||
setFocusedSpanIndexForSearch(focusedSpanIndexForSearch - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const nextResultOnKeyDown = (event: React.KeyboardEvent, buttonEnabled: boolean) => {
|
||||
if (event.key === 'Enter') {
|
||||
nextResult(event, buttonEnabled);
|
||||
}
|
||||
};
|
||||
|
||||
const prevResultOnKeyDown = (event: React.KeyboardEvent, buttonEnabled: boolean) => {
|
||||
if (event.key === 'Enter') {
|
||||
prevResult(event, buttonEnabled);
|
||||
}
|
||||
};
|
||||
|
||||
const buttonEnabled = (spanFilterMatches && spanFilterMatches?.size > 0) ?? false;
|
||||
const amountText = spanFilterMatches?.size === 1 ? 'match' : 'matches';
|
||||
const matches =
|
||||
spanFilterMatches?.size === 0 ? (
|
||||
<>
|
||||
<span>0 matches</span>
|
||||
<Tooltip
|
||||
content="There are 0 span matches for the filters selected. Please try removing some of the selected filters."
|
||||
placement="left"
|
||||
>
|
||||
<span className={styles.matchesTooltip}>
|
||||
<Icon name="info-circle" size="lg" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
) : focusedSpanIndexForSearch !== -1 ? (
|
||||
`${focusedSpanIndexForSearch + 1}/${spanFilterMatches?.size} ${amountText}`
|
||||
) : (
|
||||
`${spanFilterMatches?.size} ${amountText}`
|
||||
);
|
||||
const buttonClass = buttonEnabled ? styles.button : cx(styles.button, styles.buttonDisabled);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={styles.matches}>{spanFilterMatches ? matches : `${totalSpans} spans`}</span>
|
||||
<div className={buttonEnabled ? styles.buttons : cx(styles.buttons, styles.buttonsDisabled)}>
|
||||
<div
|
||||
aria-label="Prev result button"
|
||||
className={buttonClass}
|
||||
onClick={(event) => prevResult(event, buttonEnabled)}
|
||||
onKeyDown={(event) => prevResultOnKeyDown(event, buttonEnabled)}
|
||||
role="button"
|
||||
tabIndex={buttonEnabled ? 0 : -1}
|
||||
>
|
||||
Prev
|
||||
</div>
|
||||
<div
|
||||
aria-label="Next result button"
|
||||
className={buttonClass}
|
||||
onClick={(event) => nextResult(event, buttonEnabled)}
|
||||
onKeyDown={(event) => nextResultOnKeyDown(event, buttonEnabled)}
|
||||
role="button"
|
||||
tabIndex={buttonEnabled ? 0 : -1}
|
||||
>
|
||||
Next
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2, showSpanFilters: boolean) => {
|
||||
const buttonStyles = getButtonStyles({
|
||||
theme,
|
||||
variant: 'secondary',
|
||||
size: showSpanFilters ? 'md' : 'sm',
|
||||
iconOnly: false,
|
||||
fill: 'outline',
|
||||
});
|
||||
|
||||
return {
|
||||
buttons: css`
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
`,
|
||||
buttonsDisabled: css`
|
||||
cursor: not-allowed;
|
||||
`,
|
||||
button: css`
|
||||
${buttonStyles.button};
|
||||
`,
|
||||
buttonDisabled: css`
|
||||
${buttonStyles.disabled};
|
||||
pointer-events: none;
|
||||
`,
|
||||
matches: css`
|
||||
margin-right: ${theme.spacing(2)};
|
||||
`,
|
||||
matchesTooltip: css`
|
||||
color: #aaa;
|
||||
margin: -2px 0 0 10px;
|
||||
`,
|
||||
};
|
||||
};
|
@ -20,8 +20,8 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { Button, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import SearchBarInput from '../common/SearchBarInput';
|
||||
import { ubFlexAuto, ubJustifyEnd } from '../uberUtilityStyles';
|
||||
import SearchBarInput from '../../common/SearchBarInput';
|
||||
import { ubFlexAuto, ubJustifyEnd } from '../../uberUtilityStyles';
|
||||
|
||||
// eslint-disable-next-line no-duplicate-imports
|
||||
|
@ -43,11 +43,11 @@ const trace: Trace = {
|
||||
|
||||
describe('SpanFilters', () => {
|
||||
let user: ReturnType<typeof userEvent.setup>;
|
||||
const SpanFiltersWithProps = () => {
|
||||
const SpanFiltersWithProps = ({ showFilters = true }) => {
|
||||
const [search, setSearch] = useState(defaultFilters);
|
||||
const props = {
|
||||
trace: trace,
|
||||
showSpanFilters: true,
|
||||
showSpanFilters: showFilters,
|
||||
setShowSpanFilters: jest.fn(),
|
||||
showSpanFilterMatchesOnly: false,
|
||||
setShowSpanFilterMatchesOnly: jest.fn(),
|
||||
@ -217,6 +217,12 @@ describe('SpanFilters', () => {
|
||||
expect(screen.queryByText('TagKey0')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('TagValue0')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders buttons when span filters is collapsed', async () => {
|
||||
render(<SpanFiltersWithProps showFilters={false} />);
|
||||
expect(screen.queryByRole('button', { name: 'Next result button' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Prev result button' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
const selectAndCheckValue = async (user: ReturnType<typeof userEvent.setup>, elem: HTMLElement, text: string) => {
|
||||
|
@ -17,7 +17,7 @@ import { SpanStatusCode } from '@opentelemetry/api';
|
||||
import { uniq } from 'lodash';
|
||||
import React, { useState, useEffect, memo, useCallback } from 'react';
|
||||
|
||||
import { SelectableValue, toOption } from '@grafana/data';
|
||||
import { GrafanaTheme2, SelectableValue, toOption } from '@grafana/data';
|
||||
import { AccessoryButton } from '@grafana/experimental';
|
||||
import {
|
||||
Collapse,
|
||||
@ -34,7 +34,8 @@ import {
|
||||
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 '../NewTracePageSearchBar';
|
||||
import NewTracePageSearchBar from '../SearchBar/NewTracePageSearchBar';
|
||||
import NextPrevResult from '../SearchBar/NextPrevResult';
|
||||
|
||||
export type SpanFilterProps = {
|
||||
trace: Trace;
|
||||
@ -67,6 +68,7 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
|
||||
const [spanNames, setSpanNames] = useState<Array<SelectableValue<string>>>();
|
||||
const [tagKeys, setTagKeys] = useState<Array<SelectableValue<string>>>();
|
||||
const [tagValues, setTagValues] = useState<{ [key: string]: Array<SelectableValue<string>> }>({});
|
||||
const [focusedSpanIndexForSearch, setFocusedSpanIndexForSearch] = useState(-1);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
setServiceNames(undefined);
|
||||
@ -84,6 +86,12 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const setSpanFiltersSearch = (spanSearch: SearchProps) => {
|
||||
setFocusedSpanIndexForSearch(-1);
|
||||
setFocusedSpanIdForSearch('');
|
||||
setSearch(spanSearch);
|
||||
};
|
||||
|
||||
const getServiceNames = () => {
|
||||
if (!serviceNames) {
|
||||
const serviceNames = trace.spans.map((span) => {
|
||||
@ -262,15 +270,31 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
|
||||
};
|
||||
|
||||
const collapseLabel = (
|
||||
<Tooltip
|
||||
content="Filter your spans below. The more filters, the more specific the filtered spans."
|
||||
placement="right"
|
||||
>
|
||||
<span className={styles.collapseLabel}>
|
||||
Span Filters
|
||||
<Icon size="md" name="info-circle" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<>
|
||||
<Tooltip
|
||||
content="Filter your spans below. You can continue to apply filters until you have narrowed down your resulting spans to the select few you are most interested in."
|
||||
placement="right"
|
||||
>
|
||||
<span className={styles.collapseLabel}>
|
||||
Span Filters
|
||||
<Icon size="md" name="info-circle" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
{!showSpanFilters && (
|
||||
<div className={styles.nextPrevResult}>
|
||||
<NextPrevResult
|
||||
spanFilterMatches={spanFilterMatches}
|
||||
setFocusedSpanIdForSearch={setFocusedSpanIdForSearch}
|
||||
focusedSpanIndexForSearch={focusedSpanIndexForSearch}
|
||||
setFocusedSpanIndexForSearch={setFocusedSpanIndexForSearch}
|
||||
datasourceType={datasourceType}
|
||||
totalSpans={trace.spans.length}
|
||||
showSpanFilters={showSpanFilters}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
@ -281,14 +305,14 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Select
|
||||
aria-label="Select service name operator"
|
||||
onChange={(v) => setSearch({ ...search, serviceNameOperator: v.value! })}
|
||||
onChange={(v) => setSpanFiltersSearch({ ...search, serviceNameOperator: v.value! })}
|
||||
options={[toOption('='), toOption('!=')]}
|
||||
value={search.serviceNameOperator}
|
||||
/>
|
||||
<Select
|
||||
aria-label="Select service name"
|
||||
isClearable
|
||||
onChange={(v) => setSearch({ ...search, serviceName: v?.value || '' })}
|
||||
onChange={(v) => setSpanFiltersSearch({ ...search, serviceName: v?.value || '' })}
|
||||
onOpenMenu={getServiceNames}
|
||||
options={serviceNames}
|
||||
placeholder="All service names"
|
||||
@ -302,14 +326,14 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Select
|
||||
aria-label="Select span name operator"
|
||||
onChange={(v) => setSearch({ ...search, spanNameOperator: v.value! })}
|
||||
onChange={(v) => setSpanFiltersSearch({ ...search, spanNameOperator: v.value! })}
|
||||
options={[toOption('='), toOption('!=')]}
|
||||
value={search.spanNameOperator}
|
||||
/>
|
||||
<Select
|
||||
aria-label="Select span name"
|
||||
isClearable
|
||||
onChange={(v) => setSearch({ ...search, spanName: v?.value || '' })}
|
||||
onChange={(v) => setSpanFiltersSearch({ ...search, spanName: v?.value || '' })}
|
||||
onOpenMenu={getSpanNames}
|
||||
options={spanNames}
|
||||
placeholder="All span names"
|
||||
@ -323,26 +347,26 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Select
|
||||
aria-label="Select from operator"
|
||||
onChange={(v) => setSearch({ ...search, fromOperator: v.value! })}
|
||||
onChange={(v) => setSpanFiltersSearch({ ...search, fromOperator: v.value! })}
|
||||
options={[toOption('>'), toOption('>=')]}
|
||||
value={search.fromOperator}
|
||||
/>
|
||||
<Input
|
||||
aria-label="Select from value"
|
||||
onChange={(v) => setSearch({ ...search, from: v.currentTarget.value })}
|
||||
onChange={(v) => setSpanFiltersSearch({ ...search, from: v.currentTarget.value })}
|
||||
placeholder="e.g. 100ms, 1.2s"
|
||||
value={search.from || ''}
|
||||
width={18}
|
||||
/>
|
||||
<Select
|
||||
aria-label="Select to operator"
|
||||
onChange={(v) => setSearch({ ...search, toOperator: v.value! })}
|
||||
onChange={(v) => setSpanFiltersSearch({ ...search, toOperator: v.value! })}
|
||||
options={[toOption('<'), toOption('<=')]}
|
||||
value={search.toOperator}
|
||||
/>
|
||||
<Input
|
||||
aria-label="Select to value"
|
||||
onChange={(v) => setSearch({ ...search, to: v.currentTarget.value })}
|
||||
onChange={(v) => setSpanFiltersSearch({ ...search, to: v.currentTarget.value })}
|
||||
placeholder="e.g. 100ms, 1.2s"
|
||||
value={search.to || ''}
|
||||
width={18}
|
||||
@ -369,7 +393,7 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
|
||||
<Select
|
||||
aria-label="Select tag operator"
|
||||
onChange={(v) => {
|
||||
setSearch({
|
||||
setSpanFiltersSearch({
|
||||
...search,
|
||||
tags: search.tags?.map((x) => {
|
||||
return x.id === tag.id ? { ...x, operator: v.value! } : x;
|
||||
@ -385,7 +409,7 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
|
||||
isClearable
|
||||
key={tag.value}
|
||||
onChange={(v) => {
|
||||
setSearch({
|
||||
setSpanFiltersSearch({
|
||||
...search,
|
||||
tags: search.tags?.map((x) => {
|
||||
return x.id === tag.id ? { ...x, value: v?.value || '' } : x;
|
||||
@ -423,14 +447,17 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
|
||||
</InlineFieldRow>
|
||||
|
||||
<NewTracePageSearchBar
|
||||
totalSpans={trace.spans.length}
|
||||
search={search}
|
||||
spanFilterMatches={spanFilterMatches}
|
||||
showSpanFilterMatchesOnly={showSpanFilterMatchesOnly}
|
||||
setShowSpanFilterMatchesOnly={setShowSpanFilterMatchesOnly}
|
||||
setFocusedSpanIdForSearch={setFocusedSpanIdForSearch}
|
||||
focusedSpanIndexForSearch={focusedSpanIndexForSearch}
|
||||
setFocusedSpanIndexForSearch={setFocusedSpanIndexForSearch}
|
||||
datasourceType={datasourceType}
|
||||
clear={clear}
|
||||
totalSpans={trace.spans.length}
|
||||
showSpanFilters={showSpanFilters}
|
||||
/>
|
||||
</Collapse>
|
||||
</div>
|
||||
@ -439,10 +466,10 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
|
||||
|
||||
SpanFilters.displayName = 'SpanFilters';
|
||||
|
||||
const getStyles = () => {
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
container: css`
|
||||
margin: 0.5em 0 -8px 0;
|
||||
margin: 0.5em 0 -${theme.spacing(1)} 0;
|
||||
z-index: 5;
|
||||
|
||||
& > div {
|
||||
@ -462,5 +489,12 @@ const getStyles = () => {
|
||||
tagValues: css`
|
||||
max-width: 200px;
|
||||
`,
|
||||
nextPrevResult: css`
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-right: ${theme.spacing(1)};
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
@ -74,7 +74,7 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
text-align: start;
|
||||
line-break: anywhere;
|
||||
margin-top: -${theme.spacing(0.25)};
|
||||
margin-right: ${theme.spacing(4)};
|
||||
margin-right: ${theme.spacing(6)};
|
||||
min-height: ${theme.spacing(4)};
|
||||
`,
|
||||
ui: css`
|
||||
|
@ -5,7 +5,7 @@ import { useAsync } from 'react-use';
|
||||
import { PanelProps } from '@grafana/data';
|
||||
import { config, getDataSourceSrv } from '@grafana/runtime';
|
||||
import { TraceView } from 'app/features/explore/TraceView/TraceView';
|
||||
import TracePageSearchBar from 'app/features/explore/TraceView/components/TracePageHeader/TracePageSearchBar';
|
||||
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';
|
||||
|
Loading…
Reference in New Issue
Block a user