mirror of https://github.com/grafana/grafana
Traces: Span filtering (#65725)
* Filters * Service/span/duration filters * Renames for focused span and matches * Tag filters and new component * Tag filtering * Multiple tags and enable next/prev appropriately * Enum, renames, fixes * Clean up unecessary props * setFocusedSearchMatch * Faster options * Perf enhancements and cleanup * General improvements to tags etc * Updates to filtering * Add datasourceType in next/prev * Integrate TracePageSearchBar with NewTracePageSearchBar * Design tweaks * Update sticky elem and header design * Fix tests * Self review * Enhancements * More enhancements * Update tests * tests * More tests * Add span filters to docs * Update image link * Update docs * Update buttonEnabled and text * PR review * Update sticky header * Doc updates * Set values for service/span name * Buffer and dash updatepull/66652/head
parent
9f0d44d176
commit
9391700d84
@ -0,0 +1,51 @@ |
||||
// 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, { TracePageSearchBarProps } from './NewTracePageSearchBar'; |
||||
|
||||
const defaultProps = { |
||||
search: defaultFilters, |
||||
setFocusedSpanIdForSearch: jest.fn(), |
||||
}; |
||||
|
||||
describe('<NewTracePageSearchBar>', () => { |
||||
it('renders buttons', () => { |
||||
render(<NewTracePageSearchBar {...(defaultProps as unknown as TracePageSearchBarProps)} />); |
||||
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(true); |
||||
expect((prevResButton as HTMLButtonElement)['disabled']).toBe(true); |
||||
}); |
||||
|
||||
it('renders buttons that can be used to search if filters added', () => { |
||||
const props = { |
||||
...defaultProps, |
||||
spanFilterMatches: new Set(['2ed38015486087ca']), |
||||
}; |
||||
render(<NewTracePageSearchBar {...(props as unknown as TracePageSearchBarProps)} />); |
||||
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); |
||||
}); |
||||
}); |
@ -0,0 +1,123 @@ |
||||
// 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 } from 'react'; |
||||
|
||||
import { config, reportInteraction } from '@grafana/runtime'; |
||||
import { Button, useStyles2 } from '@grafana/ui'; |
||||
|
||||
import { SearchProps } from '../../useSearch'; |
||||
|
||||
export type TracePageSearchBarProps = { |
||||
search: SearchProps; |
||||
setSearch: React.Dispatch<React.SetStateAction<SearchProps>>; |
||||
spanFilterMatches: Set<string> | undefined; |
||||
focusedSpanIdForSearch: string; |
||||
setFocusedSpanIdForSearch: Dispatch<SetStateAction<string>>; |
||||
datasourceType: string; |
||||
}; |
||||
|
||||
export default memo(function NewTracePageSearchBar(props: TracePageSearchBarProps) { |
||||
const { search, spanFilterMatches, focusedSpanIdForSearch, setFocusedSpanIdForSearch, datasourceType } = props; |
||||
const styles = useStyles2(getStyles); |
||||
|
||||
useEffect(() => { |
||||
setFocusedSpanIdForSearch(''); |
||||
}, [search, setFocusedSpanIdForSearch]); |
||||
|
||||
const nextResult = () => { |
||||
reportInteraction('grafana_traces_trace_view_find_next_prev_clicked', { |
||||
datasourceType: datasourceType, |
||||
grafana_version: config.buildInfo.version, |
||||
direction: 'next', |
||||
}); |
||||
|
||||
const spanMatches = Array.from(spanFilterMatches!); |
||||
const prevMatchedIndex = spanMatches.indexOf(focusedSpanIdForSearch); |
||||
|
||||
// new query || at end, go to start
|
||||
if (prevMatchedIndex === -1 || prevMatchedIndex === spanMatches.length - 1) { |
||||
setFocusedSpanIdForSearch(spanMatches[0]); |
||||
return; |
||||
} |
||||
|
||||
// get next
|
||||
setFocusedSpanIdForSearch(spanMatches[prevMatchedIndex + 1]); |
||||
}; |
||||
|
||||
const prevResult = () => { |
||||
reportInteraction('grafana_traces_trace_view_find_next_prev_clicked', { |
||||
datasourceType: datasourceType, |
||||
grafana_version: config.buildInfo.version, |
||||
direction: 'prev', |
||||
}); |
||||
|
||||
const spanMatches = Array.from(spanFilterMatches!); |
||||
const prevMatchedIndex = spanMatches.indexOf(focusedSpanIdForSearch); |
||||
|
||||
// new query || at start, go to end
|
||||
if (prevMatchedIndex === -1 || prevMatchedIndex === 0) { |
||||
setFocusedSpanIdForSearch(spanMatches[spanMatches.length - 1]); |
||||
return; |
||||
} |
||||
|
||||
// get prev
|
||||
setFocusedSpanIdForSearch(spanMatches[prevMatchedIndex - 1]); |
||||
}; |
||||
|
||||
const buttonEnabled = spanFilterMatches && spanFilterMatches?.size > 0; |
||||
|
||||
return ( |
||||
<div className={styles.searchBar}> |
||||
<> |
||||
<Button |
||||
className={styles.button} |
||||
variant="secondary" |
||||
disabled={!buttonEnabled} |
||||
type="button" |
||||
fill={'outline'} |
||||
aria-label="Prev result button" |
||||
onClick={prevResult} |
||||
> |
||||
Prev |
||||
</Button> |
||||
<Button |
||||
className={styles.button} |
||||
variant="secondary" |
||||
disabled={!buttonEnabled} |
||||
type="button" |
||||
fill={'outline'} |
||||
aria-label="Next result button" |
||||
onClick={nextResult} |
||||
> |
||||
Next |
||||
</Button> |
||||
</> |
||||
</div> |
||||
); |
||||
}); |
||||
|
||||
export const getStyles = () => { |
||||
return { |
||||
searchBar: css` |
||||
display: flex; |
||||
justify-content: flex-end; |
||||
margin-top: 5px; |
||||
`,
|
||||
button: css` |
||||
margin-left: 8px; |
||||
`,
|
||||
}; |
||||
}; |
@ -0,0 +1,179 @@ |
||||
import { render, screen, waitFor } from '@testing-library/react'; |
||||
import userEvent from '@testing-library/user-event'; |
||||
import React, { useState } from 'react'; |
||||
|
||||
import { defaultFilters } from '../../../useSearch'; |
||||
import { Trace } from '../../types/trace'; |
||||
|
||||
import { SpanFilters } from './SpanFilters'; |
||||
|
||||
const trace: Trace = { |
||||
traceID: '1ed38015486087ca', |
||||
spans: [ |
||||
{ |
||||
traceID: '1ed38015486087ca', |
||||
spanID: '1ed38015486087ca', |
||||
operationName: 'Span0', |
||||
tags: [{ key: 'TagKey0', type: 'string', value: 'TagValue0' }], |
||||
process: { |
||||
serviceName: 'Service0', |
||||
tags: [{ key: 'ProcessKey0', type: 'string', value: 'ProcessValue0' }], |
||||
}, |
||||
logs: [{ fields: [{ key: 'LogKey0', type: 'string', value: 'LogValue0' }] }], |
||||
}, |
||||
{ |
||||
traceID: '1ed38015486087ca', |
||||
spanID: '2ed38015486087ca', |
||||
operationName: 'Span1', |
||||
tags: [{ key: 'TagKey1', type: 'string', value: 'TagValue1' }], |
||||
process: { |
||||
serviceName: 'Service1', |
||||
tags: [{ key: 'ProcessKey1', type: 'string', value: 'ProcessValue1' }], |
||||
}, |
||||
logs: [{ fields: [{ key: 'LogKey1', type: 'string', value: 'LogValue1' }] }], |
||||
}, |
||||
], |
||||
} as unknown as Trace; |
||||
|
||||
describe('SpanFilters', () => { |
||||
let user: ReturnType<typeof userEvent.setup>; |
||||
const SpanFiltersWithProps = () => { |
||||
const [search, setSearch] = useState(defaultFilters); |
||||
const props = { |
||||
trace: trace, |
||||
showSpanFilters: true, |
||||
setShowSpanFilters: jest.fn(), |
||||
search: search, |
||||
setSearch: setSearch, |
||||
spanFilterMatches: undefined, |
||||
focusedSpanIdForSearch: '', |
||||
setFocusedSpanIdForSearch: jest.fn(), |
||||
datasourceType: 'tempo', |
||||
}; |
||||
|
||||
return <SpanFilters {...props} />; |
||||
}; |
||||
|
||||
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('should render', () => { |
||||
expect(() => render(<SpanFiltersWithProps />)).not.toThrow(); |
||||
}); |
||||
|
||||
it('should render filters', async () => { |
||||
render(<SpanFiltersWithProps />); |
||||
|
||||
const serviceOperator = screen.getByLabelText('Select service name operator'); |
||||
const serviceValue = screen.getByLabelText('Select service name'); |
||||
const spanOperator = screen.getByLabelText('Select span name operator'); |
||||
const spanValue = screen.getByLabelText('Select span name'); |
||||
const fromOperator = screen.getByLabelText('Select from operator'); |
||||
const fromValue = screen.getByLabelText('Select from value'); |
||||
const toOperator = screen.getByLabelText('Select to operator'); |
||||
const toValue = screen.getByLabelText('Select to value'); |
||||
const tagKey = screen.getByLabelText('Select tag key'); |
||||
const tagOperator = screen.getByLabelText('Select tag operator'); |
||||
const tagValue = screen.getByLabelText('Select tag value'); |
||||
const addTag = screen.getByLabelText('Add tag'); |
||||
const removeTag = screen.getByLabelText('Remove tag'); |
||||
|
||||
expect(serviceOperator).toBeInTheDocument(); |
||||
expect(getElemText(serviceOperator)).toBe('='); |
||||
expect(serviceValue).toBeInTheDocument(); |
||||
expect(spanOperator).toBeInTheDocument(); |
||||
expect(getElemText(spanOperator)).toBe('='); |
||||
expect(spanValue).toBeInTheDocument(); |
||||
expect(fromOperator).toBeInTheDocument(); |
||||
expect(getElemText(fromOperator)).toBe('>'); |
||||
expect(fromValue).toBeInTheDocument(); |
||||
expect(toOperator).toBeInTheDocument(); |
||||
expect(getElemText(toOperator)).toBe('<'); |
||||
expect(toValue).toBeInTheDocument(); |
||||
expect(tagKey).toBeInTheDocument(); |
||||
expect(tagOperator).toBeInTheDocument(); |
||||
expect(getElemText(tagOperator)).toBe('='); |
||||
expect(tagValue).toBeInTheDocument(); |
||||
expect(addTag).toBeInTheDocument(); |
||||
expect(removeTag).toBeInTheDocument(); |
||||
|
||||
await user.click(serviceValue); |
||||
jest.advanceTimersByTime(1000); |
||||
await waitFor(() => { |
||||
expect(screen.getByText('Service0')).toBeInTheDocument(); |
||||
expect(screen.getByText('Service1')).toBeInTheDocument(); |
||||
}); |
||||
await user.click(spanValue); |
||||
jest.advanceTimersByTime(1000); |
||||
await waitFor(() => { |
||||
expect(screen.getByText('Span0')).toBeInTheDocument(); |
||||
expect(screen.getByText('Span1')).toBeInTheDocument(); |
||||
}); |
||||
await user.click(tagKey); |
||||
jest.advanceTimersByTime(1000); |
||||
await waitFor(() => { |
||||
expect(screen.getByText('TagKey0')).toBeInTheDocument(); |
||||
expect(screen.getByText('TagKey1')).toBeInTheDocument(); |
||||
expect(screen.getByText('ProcessKey0')).toBeInTheDocument(); |
||||
expect(screen.getByText('ProcessKey1')).toBeInTheDocument(); |
||||
expect(screen.getByText('LogKey0')).toBeInTheDocument(); |
||||
expect(screen.getByText('LogKey1')).toBeInTheDocument(); |
||||
}); |
||||
}); |
||||
|
||||
it('should update filters', async () => { |
||||
render(<SpanFiltersWithProps />); |
||||
const serviceValue = screen.getByLabelText('Select service name'); |
||||
const spanValue = screen.getByLabelText('Select span name'); |
||||
const tagKey = screen.getByLabelText('Select tag key'); |
||||
const tagValue = screen.getByLabelText('Select tag value'); |
||||
|
||||
expect(getElemText(serviceValue)).toBe('All service names'); |
||||
await selectAndCheckValue(user, serviceValue, 'Service0'); |
||||
expect(getElemText(spanValue)).toBe('All span names'); |
||||
await selectAndCheckValue(user, spanValue, 'Span0'); |
||||
|
||||
await user.click(tagValue); |
||||
jest.advanceTimersByTime(1000); |
||||
await waitFor(() => expect(screen.getByText('No options found')).toBeInTheDocument()); |
||||
|
||||
expect(getElemText(tagKey)).toBe('Select tag'); |
||||
await selectAndCheckValue(user, tagKey, 'TagKey0'); |
||||
expect(getElemText(tagValue)).toBe('Select value'); |
||||
await selectAndCheckValue(user, tagValue, 'TagValue0'); |
||||
}); |
||||
|
||||
it('should allow adding/removing tags', async () => { |
||||
render(<SpanFiltersWithProps />); |
||||
expect(screen.getAllByLabelText('Select tag key').length).toBe(1); |
||||
await user.click(screen.getByLabelText('Add tag')); |
||||
jest.advanceTimersByTime(1000); |
||||
expect(screen.getAllByLabelText('Select tag key').length).toBe(2); |
||||
|
||||
await user.click(screen.getAllByLabelText('Remove tag')[0]); |
||||
jest.advanceTimersByTime(1000); |
||||
expect(screen.getAllByLabelText('Select tag key').length).toBe(1); |
||||
}); |
||||
}); |
||||
|
||||
const selectAndCheckValue = async (user: ReturnType<typeof userEvent.setup>, elem: HTMLElement, text: string) => { |
||||
await user.click(elem); |
||||
jest.advanceTimersByTime(1000); |
||||
await waitFor(() => expect(screen.getByText(text)).toBeInTheDocument()); |
||||
|
||||
await user.click(screen.getByText(text)); |
||||
jest.advanceTimersByTime(1000); |
||||
expect(screen.getByText(text)).toBeInTheDocument(); |
||||
}; |
||||
|
||||
const getElemText = (elem: HTMLElement) => { |
||||
return elem.parentElement?.previousSibling?.textContent; |
||||
}; |
@ -0,0 +1,407 @@ |
||||
// 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 { uniq } from 'lodash'; |
||||
import React, { useState, memo } from 'react'; |
||||
|
||||
import { SelectableValue, toOption } from '@grafana/data'; |
||||
import { AccessoryButton } from '@grafana/experimental'; |
||||
import { |
||||
Collapse, |
||||
HorizontalGroup, |
||||
Icon, |
||||
InlineField, |
||||
InlineFieldRow, |
||||
Input, |
||||
Select, |
||||
Tooltip, |
||||
useStyles2, |
||||
} from '@grafana/ui'; |
||||
|
||||
import { randomId, SearchProps, Tag } from '../../../useSearch'; |
||||
import { Trace } from '../../types'; |
||||
import NewTracePageSearchBar from '../NewTracePageSearchBar'; |
||||
|
||||
export type SpanFilterProps = { |
||||
trace: Trace; |
||||
search: SearchProps; |
||||
setSearch: React.Dispatch<React.SetStateAction<SearchProps>>; |
||||
showSpanFilters: boolean; |
||||
setShowSpanFilters: (isOpen: boolean) => void; |
||||
focusedSpanIdForSearch: string; |
||||
setFocusedSpanIdForSearch: React.Dispatch<React.SetStateAction<string>>; |
||||
spanFilterMatches: Set<string> | undefined; |
||||
datasourceType: string; |
||||
}; |
||||
|
||||
export const SpanFilters = memo((props: SpanFilterProps) => { |
||||
const { |
||||
trace, |
||||
search, |
||||
setSearch, |
||||
showSpanFilters, |
||||
setShowSpanFilters, |
||||
focusedSpanIdForSearch, |
||||
setFocusedSpanIdForSearch, |
||||
spanFilterMatches, |
||||
datasourceType, |
||||
} = props; |
||||
const styles = { ...useStyles2(getStyles) }; |
||||
const [serviceNames, setServiceNames] = useState<Array<SelectableValue<string>>>(); |
||||
const [spanNames, setSpanNames] = useState<Array<SelectableValue<string>>>(); |
||||
const [tagKeys, setTagKeys] = useState<Array<SelectableValue<string>>>(); |
||||
const [tagValues, setTagValues] = useState<{ [key: string]: Array<SelectableValue<string>> }>({}); |
||||
|
||||
if (!trace) { |
||||
return null; |
||||
} |
||||
|
||||
const getServiceNames = () => { |
||||
if (!serviceNames) { |
||||
const serviceNames = trace.spans.map((span) => { |
||||
return span.process.serviceName; |
||||
}); |
||||
setServiceNames( |
||||
uniq(serviceNames) |
||||
.sort() |
||||
.map((name) => { |
||||
return toOption(name); |
||||
}) |
||||
); |
||||
} |
||||
}; |
||||
|
||||
const getSpanNames = () => { |
||||
if (!spanNames) { |
||||
const spanNames = trace.spans.map((span) => { |
||||
return span.operationName; |
||||
}); |
||||
setSpanNames( |
||||
uniq(spanNames) |
||||
.sort() |
||||
.map((name) => { |
||||
return toOption(name); |
||||
}) |
||||
); |
||||
} |
||||
}; |
||||
|
||||
const getTagKeys = () => { |
||||
if (!tagKeys) { |
||||
const keys: string[] = []; |
||||
|
||||
trace.spans.forEach((span) => { |
||||
span.tags.forEach((tag) => { |
||||
keys.push(tag.key); |
||||
}); |
||||
span.process.tags.forEach((tag) => { |
||||
keys.push(tag.key); |
||||
}); |
||||
if (span.logs !== null) { |
||||
span.logs.forEach((log) => { |
||||
log.fields.forEach((field) => { |
||||
keys.push(field.key); |
||||
}); |
||||
}); |
||||
} |
||||
}); |
||||
|
||||
setTagKeys( |
||||
uniq(keys) |
||||
.sort() |
||||
.map((name) => { |
||||
return toOption(name); |
||||
}) |
||||
); |
||||
} |
||||
}; |
||||
|
||||
const getTagValues = async (key: string) => { |
||||
const values: string[] = []; |
||||
|
||||
trace.spans.forEach((span) => { |
||||
const tagValue = span.tags.find((t) => t.key === key)?.value; |
||||
if (tagValue) { |
||||
values.push(tagValue.toString()); |
||||
} |
||||
const processTagValue = span.process.tags.find((t) => t.key === key)?.value; |
||||
if (processTagValue) { |
||||
values.push(processTagValue.toString()); |
||||
} |
||||
if (span.logs !== null) { |
||||
span.logs.forEach((log) => { |
||||
const logsTagValue = log.fields.find((t) => t.key === key)?.value; |
||||
if (logsTagValue) { |
||||
values.push(logsTagValue.toString()); |
||||
} |
||||
}); |
||||
} |
||||
}); |
||||
|
||||
return uniq(values) |
||||
.sort() |
||||
.map((name) => { |
||||
return toOption(name); |
||||
}); |
||||
}; |
||||
|
||||
const onTagChange = (tag: Tag, v: SelectableValue<string>) => { |
||||
setSearch({ |
||||
...search, |
||||
tags: search.tags?.map((x) => { |
||||
return x.id === tag.id ? { ...x, key: v?.value || '', value: undefined } : x; |
||||
}), |
||||
}); |
||||
|
||||
const loadTagValues = async () => { |
||||
if (v?.value) { |
||||
setTagValues({ |
||||
...tagValues, |
||||
[tag.id]: await getTagValues(v.value), |
||||
}); |
||||
} else { |
||||
// removed value
|
||||
const updatedValues = { ...tagValues }; |
||||
if (updatedValues[tag.id]) { |
||||
delete updatedValues[tag.id]; |
||||
} |
||||
setTagValues(updatedValues); |
||||
} |
||||
}; |
||||
loadTagValues(); |
||||
}; |
||||
|
||||
const addTag = () => { |
||||
const tag = { |
||||
id: randomId(), |
||||
operator: '=', |
||||
}; |
||||
setSearch({ ...search, tags: [...search.tags, tag] }); |
||||
}; |
||||
|
||||
const removeTag = (id: string) => { |
||||
let tags = search.tags.filter((tag) => { |
||||
return tag.id !== id; |
||||
}); |
||||
if (tags.length === 0) { |
||||
tags = [ |
||||
{ |
||||
id: randomId(), |
||||
operator: '=', |
||||
}, |
||||
]; |
||||
} |
||||
setSearch({ ...search, tags: tags }); |
||||
}; |
||||
|
||||
const collapseLabel = ( |
||||
<Tooltip |
||||
content="Filter your spans below. The more filters, the more specific the filtered spans." |
||||
placement="right" |
||||
> |
||||
<span id="collapse-label"> |
||||
Span Filters |
||||
<Icon size="sm" name="info-circle" /> |
||||
</span> |
||||
</Tooltip> |
||||
); |
||||
|
||||
return ( |
||||
<div className={styles.container}> |
||||
<Collapse label={collapseLabel} collapsible={true} isOpen={showSpanFilters} onToggle={setShowSpanFilters}> |
||||
<InlineFieldRow> |
||||
<InlineField label="Service Name" labelWidth={16}> |
||||
<HorizontalGroup spacing={'xs'}> |
||||
<Select |
||||
aria-label="Select service name operator" |
||||
onChange={(v) => setSearch({ ...search, serviceNameOperator: v.value! })} |
||||
options={[toOption('='), toOption('!=')]} |
||||
value={search.serviceNameOperator} |
||||
/> |
||||
<Select |
||||
aria-label="Select service name" |
||||
isClearable |
||||
onChange={(v) => setSearch({ ...search, serviceName: v?.value || '' })} |
||||
onOpenMenu={getServiceNames} |
||||
options={serviceNames} |
||||
placeholder="All service names" |
||||
value={search.serviceName} |
||||
/> |
||||
</HorizontalGroup> |
||||
</InlineField> |
||||
</InlineFieldRow> |
||||
<InlineFieldRow> |
||||
<InlineField label="Span Name" labelWidth={16}> |
||||
<HorizontalGroup spacing={'xs'}> |
||||
<Select |
||||
aria-label="Select span name operator" |
||||
onChange={(v) => setSearch({ ...search, spanNameOperator: v.value! })} |
||||
options={[toOption('='), toOption('!=')]} |
||||
value={search.spanNameOperator} |
||||
/> |
||||
<Select |
||||
aria-label="Select span name" |
||||
isClearable |
||||
onChange={(v) => setSearch({ ...search, spanName: v?.value || '' })} |
||||
onOpenMenu={getSpanNames} |
||||
options={spanNames} |
||||
placeholder="All span names" |
||||
value={search.spanName} |
||||
/> |
||||
</HorizontalGroup> |
||||
</InlineField> |
||||
</InlineFieldRow> |
||||
<InlineFieldRow> |
||||
<InlineField label="Duration" labelWidth={16}> |
||||
<HorizontalGroup spacing={'xs'}> |
||||
<Select |
||||
aria-label="Select from operator" |
||||
onChange={(v) => setSearch({ ...search, fromOperator: v.value! })} |
||||
options={[toOption('>'), toOption('>=')]} |
||||
value={search.fromOperator} |
||||
/> |
||||
<Input |
||||
aria-label="Select from value" |
||||
onChange={(v) => setSearch({ ...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! })} |
||||
options={[toOption('<'), toOption('<=')]} |
||||
value={search.toOperator} |
||||
/> |
||||
<Input |
||||
aria-label="Select to value" |
||||
onChange={(v) => setSearch({ ...search, to: v.currentTarget.value })} |
||||
placeholder="e.g. 100ms, 1.2s" |
||||
value={search.to || ''} |
||||
width={18} |
||||
/> |
||||
</HorizontalGroup> |
||||
</InlineField> |
||||
</InlineFieldRow> |
||||
<InlineFieldRow> |
||||
<InlineField label="Tags" labelWidth={16} tooltip="Filter by tags, process tags or log fields in your spans."> |
||||
<div> |
||||
{search.tags.map((tag, i) => ( |
||||
<div key={i}> |
||||
<HorizontalGroup spacing={'xs'} width={'auto'}> |
||||
<Select |
||||
aria-label={`Select tag key`} |
||||
isClearable |
||||
key={tag.key} |
||||
onChange={(v) => onTagChange(tag, v)} |
||||
onOpenMenu={getTagKeys} |
||||
options={tagKeys} |
||||
placeholder="Select tag" |
||||
value={tag.key} |
||||
/> |
||||
<Select |
||||
aria-label={`Select tag operator`} |
||||
onChange={(v) => { |
||||
setSearch({ |
||||
...search, |
||||
tags: search.tags?.map((x) => { |
||||
return x.id === tag.id ? { ...x, operator: v.value! } : x; |
||||
}), |
||||
}); |
||||
}} |
||||
options={[toOption('='), toOption('!=')]} |
||||
value={tag.operator} |
||||
/> |
||||
<span className={styles.tagValues}> |
||||
<Select |
||||
aria-label={`Select tag value`} |
||||
isClearable |
||||
key={tag.value} |
||||
onChange={(v) => { |
||||
setSearch({ |
||||
...search, |
||||
tags: search.tags?.map((x) => { |
||||
return x.id === tag.id ? { ...x, value: v?.value || '' } : x; |
||||
}), |
||||
}); |
||||
}} |
||||
options={tagValues[tag.id] ? tagValues[tag.id] : []} |
||||
placeholder="Select value" |
||||
value={tag.value} |
||||
/> |
||||
</span> |
||||
<AccessoryButton |
||||
aria-label={`Remove tag`} |
||||
variant={'secondary'} |
||||
icon={'times'} |
||||
onClick={() => removeTag(tag.id)} |
||||
title={'Remove tag'} |
||||
/> |
||||
<span className={styles.addTag}> |
||||
{search?.tags?.length && i === search.tags.length - 1 && ( |
||||
<AccessoryButton |
||||
aria-label="Add tag" |
||||
variant={'secondary'} |
||||
icon={'plus'} |
||||
onClick={addTag} |
||||
title={'Add tag'} |
||||
/> |
||||
)} |
||||
</span> |
||||
</HorizontalGroup> |
||||
</div> |
||||
))} |
||||
</div> |
||||
</InlineField> |
||||
</InlineFieldRow> |
||||
|
||||
<NewTracePageSearchBar |
||||
search={search} |
||||
setSearch={setSearch} |
||||
spanFilterMatches={spanFilterMatches} |
||||
focusedSpanIdForSearch={focusedSpanIdForSearch} |
||||
setFocusedSpanIdForSearch={setFocusedSpanIdForSearch} |
||||
datasourceType={datasourceType} |
||||
/> |
||||
</Collapse> |
||||
</div> |
||||
); |
||||
}); |
||||
|
||||
SpanFilters.displayName = 'SpanFilters'; |
||||
|
||||
const getStyles = () => { |
||||
return { |
||||
container: css` |
||||
margin: 0.5em 0 -8px 0; |
||||
z-index: 5; |
||||
|
||||
& > div { |
||||
border-left: none; |
||||
border-right: none; |
||||
} |
||||
|
||||
#collapse-label svg { |
||||
margin: -1px 0 0 10px; |
||||
} |
||||
`,
|
||||
addTag: css` |
||||
margin: 0 0 0 10px; |
||||
`,
|
||||
tagValues: css` |
||||
max-width: 200px; |
||||
`,
|
||||
}; |
||||
}; |
Loading…
Reference in new issue