mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 18:30:41 -06:00
Tempo: TraceQL Configurable static fields (#65284)
* TraceQL - configurable static fields for new UI * TraceQL - filter out static fields from Tags section. Added tooltip to static fields * Add more units to duration validation. Improve duration field tooltip with accepted units * Better control of delete button on SearchField * Move new config behind feature toggle * Special title for intrinsic "name" * Fix tests * Move static fields not in the datasource to the Tags section * Start using the useAsync hook in the Tempo TraceQL configuration page to retrieve the tags and datasource * Fix tests * Fix test. Use useAsync to retrieve options in SearchField * Remove ability to set a default value in filter configuration. Removed type from filter, dynamic filters are now any filters not present in the datasource config * Updated the static filters tooltip * Replace useState + useEffect with useMemo for scopedTag
This commit is contained in:
parent
fb83414b6a
commit
541a03f33b
@ -4994,6 +4994,9 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/plugins/datasource/tempo/configuration/TraceQLSearchSettings.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/tempo/datasource.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
|
@ -16,12 +16,6 @@ const (
|
||||
TempoQueryFiltersScopeUnscoped TempoQueryFiltersScope = "unscoped"
|
||||
)
|
||||
|
||||
// Defines values for TempoQueryFiltersType.
|
||||
const (
|
||||
TempoQueryFiltersTypeDynamic TempoQueryFiltersType = "dynamic"
|
||||
TempoQueryFiltersTypeStatic TempoQueryFiltersType = "static"
|
||||
)
|
||||
|
||||
// Defines values for TempoQueryType.
|
||||
const (
|
||||
TempoQueryTypeClear TempoQueryType = "clear"
|
||||
@ -40,18 +34,6 @@ const (
|
||||
TraceqlFilterScopeUnscoped TraceqlFilterScope = "unscoped"
|
||||
)
|
||||
|
||||
// Defines values for TraceqlFilterType.
|
||||
const (
|
||||
TraceqlFilterTypeDynamic TraceqlFilterType = "dynamic"
|
||||
TraceqlFilterTypeStatic TraceqlFilterType = "static"
|
||||
)
|
||||
|
||||
// Defines values for TraceqlSearchFilterType.
|
||||
const (
|
||||
TraceqlSearchFilterTypeDynamic TraceqlSearchFilterType = "dynamic"
|
||||
TraceqlSearchFilterTypeStatic TraceqlSearchFilterType = "static"
|
||||
)
|
||||
|
||||
// Defines values for TraceqlSearchScope.
|
||||
const (
|
||||
TraceqlSearchScopeResource TraceqlSearchScope = "resource"
|
||||
@ -82,9 +64,6 @@ type TempoQuery struct {
|
||||
// The tag for the search filter, for example: .http.status_code, .service.name, status
|
||||
Tag *string `json:"tag,omitempty"`
|
||||
|
||||
// The type of the filter, can either be static (pre defined in the UI) or dynamic
|
||||
Type TempoQueryFiltersType `json:"type"`
|
||||
|
||||
// The value for the search filter
|
||||
Value *interface{} `json:"value,omitempty"`
|
||||
|
||||
@ -134,9 +113,6 @@ type TempoQuery struct {
|
||||
// The scope of the filter, can either be unscoped/all scopes, resource or span
|
||||
type TempoQueryFiltersScope string
|
||||
|
||||
// The type of the filter, can either be static (pre defined in the UI) or dynamic
|
||||
type TempoQueryFiltersType string
|
||||
|
||||
// TempoQueryType search = Loki search, nativeSearch = Tempo search for backwards compatibility
|
||||
type TempoQueryType string
|
||||
|
||||
@ -154,9 +130,6 @@ type TraceqlFilter struct {
|
||||
// The tag for the search filter, for example: .http.status_code, .service.name, status
|
||||
Tag *string `json:"tag,omitempty"`
|
||||
|
||||
// The type of the filter, can either be static (pre defined in the UI) or dynamic
|
||||
Type TraceqlFilterType `json:"type"`
|
||||
|
||||
// The value for the search filter
|
||||
Value *interface{} `json:"value,omitempty"`
|
||||
|
||||
@ -167,11 +140,5 @@ type TraceqlFilter struct {
|
||||
// The scope of the filter, can either be unscoped/all scopes, resource or span
|
||||
type TraceqlFilterScope string
|
||||
|
||||
// The type of the filter, can either be static (pre defined in the UI) or dynamic
|
||||
type TraceqlFilterType string
|
||||
|
||||
// TraceqlSearchFilterType static fields are pre-set in the UI, dynamic fields are added by the user
|
||||
type TraceqlSearchFilterType string
|
||||
|
||||
// TraceqlSearchScope defines model for TraceqlSearchScope.
|
||||
// TraceqlSearchScope static fields are pre-set in the UI, dynamic fields are added by the user
|
||||
type TraceqlSearchScope string
|
||||
|
@ -13,7 +13,7 @@ interface Props {
|
||||
operators: string[];
|
||||
}
|
||||
|
||||
const validationRegex = /^\d+(?:\.\d)?\d*(?:ms|s|ns)$/;
|
||||
const validationRegex = /^\d+(?:\.\d)?\d*(?:us|µs|ns|ms|s|m|h)$/;
|
||||
|
||||
const DurationInput = ({ filter, operators, updateFilter }: Props) => {
|
||||
let invalid = false;
|
||||
|
@ -10,7 +10,7 @@ interface Props {
|
||||
const SearchField = ({ label, tooltip, children }: Props) => {
|
||||
return (
|
||||
<InlineFieldRow>
|
||||
<InlineField label={label} labelWidth={16} grow tooltip={tooltip}>
|
||||
<InlineField label={label} labelWidth={28} grow tooltip={tooltip}>
|
||||
{children}
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
|
@ -4,7 +4,7 @@ import React from 'react';
|
||||
|
||||
import { FetchError } from '@grafana/runtime';
|
||||
|
||||
import { TraceqlFilter } from '../dataquery.gen';
|
||||
import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
|
||||
import { TempoDatasource } from '../datasource';
|
||||
|
||||
import SearchField from './SearchField';
|
||||
@ -48,12 +48,13 @@ describe('SearchField', () => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should not render tag if tag is present in field', () => {
|
||||
it('should not render tag if hideTag is true', () => {
|
||||
const updateFilter = jest.fn((val) => {
|
||||
return val;
|
||||
});
|
||||
const filter: TraceqlFilter = { id: 'test1', type: 'static', valueType: 'string', tag: 'test-tag' };
|
||||
const { container } = renderSearchField(updateFilter, filter);
|
||||
const filter: TraceqlFilter = { id: 'test1', valueType: 'string', tag: 'test-tag' };
|
||||
|
||||
const { container } = renderSearchField(updateFilter, filter, [], true);
|
||||
|
||||
expect(container.querySelector(`input[aria-label="select test1 tag"]`)).not.toBeInTheDocument();
|
||||
expect(container.querySelector(`input[aria-label="select test1 operator"]`)).toBeInTheDocument();
|
||||
@ -64,7 +65,7 @@ describe('SearchField', () => {
|
||||
const updateFilter = jest.fn((val) => {
|
||||
return val;
|
||||
});
|
||||
const filter: TraceqlFilter = { id: 'test1', operator: '=', type: 'static', valueType: 'string', tag: 'test-tag' };
|
||||
const filter: TraceqlFilter = { id: 'test1', operator: '=', valueType: 'string', tag: 'test-tag' };
|
||||
const { container } = renderSearchField(updateFilter, filter);
|
||||
|
||||
const select = await container.querySelector(`input[aria-label="select test1 operator"]`);
|
||||
@ -87,7 +88,6 @@ describe('SearchField', () => {
|
||||
const filter: TraceqlFilter = {
|
||||
id: 'test1',
|
||||
value: 'old',
|
||||
type: 'static',
|
||||
valueType: 'string',
|
||||
tag: 'test-tag',
|
||||
};
|
||||
@ -124,7 +124,6 @@ describe('SearchField', () => {
|
||||
});
|
||||
const filter: TraceqlFilter = {
|
||||
id: 'test1',
|
||||
type: 'dynamic',
|
||||
valueType: 'string',
|
||||
};
|
||||
const { container } = renderSearchField(updateFilter, filter, ['tag1', 'tag22', 'tag33']);
|
||||
@ -155,16 +154,35 @@ describe('SearchField', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const renderSearchField = (updateFilter: (f: TraceqlFilter) => void, filter: TraceqlFilter, tags?: string[]) => {
|
||||
const renderSearchField = (
|
||||
updateFilter: (f: TraceqlFilter) => void,
|
||||
filter: TraceqlFilter,
|
||||
tags?: string[],
|
||||
hideTag?: boolean
|
||||
) => {
|
||||
const datasource: TempoDatasource = {
|
||||
search: {
|
||||
filters: [
|
||||
{
|
||||
id: 'service-name',
|
||||
tag: 'service.name',
|
||||
operator: '=',
|
||||
scope: TraceqlSearchScope.Resource,
|
||||
},
|
||||
{ id: 'span-name', type: 'static', tag: 'name', operator: '=', scope: TraceqlSearchScope.Span },
|
||||
],
|
||||
},
|
||||
} as TempoDatasource;
|
||||
return render(
|
||||
<SearchField
|
||||
datasource={{} as TempoDatasource}
|
||||
datasource={datasource}
|
||||
updateFilter={updateFilter}
|
||||
filter={filter}
|
||||
setError={function (error: FetchError): void {
|
||||
throw error;
|
||||
}}
|
||||
tags={tags || []}
|
||||
hideTag={hideTag}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { uniq } from 'lodash';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { AccessoryButton } from '@grafana/experimental';
|
||||
import { FetchError, isFetchError } from '@grafana/runtime';
|
||||
import { Select, HorizontalGroup, useStyles2 } from '@grafana/ui';
|
||||
@ -14,7 +15,7 @@ import { TempoDatasource } from '../datasource';
|
||||
import TempoLanguageProvider from '../language_provider';
|
||||
import { operators as allOperators, stringOperators, numberOperators } from '../traceql/traceql';
|
||||
|
||||
import { operatorSelectableValue, scopeHelper } from './utils';
|
||||
import { filterScopedTag, operatorSelectableValue } from './utils';
|
||||
|
||||
const getStyles = () => ({
|
||||
dropdown: css`
|
||||
@ -30,19 +31,49 @@ interface Props {
|
||||
setError: (error: FetchError) => void;
|
||||
isTagsLoading?: boolean;
|
||||
tags: string[];
|
||||
hideScope?: boolean;
|
||||
hideTag?: boolean;
|
||||
hideValue?: boolean;
|
||||
allowDelete?: boolean;
|
||||
}
|
||||
const SearchField = ({ filter, datasource, updateFilter, deleteFilter, isTagsLoading, tags, setError }: Props) => {
|
||||
const SearchField = ({
|
||||
filter,
|
||||
datasource,
|
||||
updateFilter,
|
||||
deleteFilter,
|
||||
isTagsLoading,
|
||||
tags,
|
||||
setError,
|
||||
hideScope,
|
||||
hideTag,
|
||||
hideValue,
|
||||
allowDelete,
|
||||
}: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const languageProvider = useMemo(() => new TempoLanguageProvider(datasource), [datasource]);
|
||||
const [isLoadingValues, setIsLoadingValues] = useState(false);
|
||||
const [options, setOptions] = useState<Array<SelectableValue<string>>>([]);
|
||||
const [scopedTag, setScopedTag] = useState(scopeHelper(filter) + filter.tag);
|
||||
const scopedTag = useMemo(() => filterScopedTag(filter), [filter]);
|
||||
// We automatically change the operator to the regex op when users select 2 or more values
|
||||
// However, they expect this to be automatically rolled back to the previous operator once
|
||||
// there's only one value selected, so we store the previous operator and value
|
||||
const [prevOperator, setPrevOperator] = useState(filter.operator);
|
||||
const [prevValue, setPrevValue] = useState(filter.value);
|
||||
|
||||
const updateOptions = async () => {
|
||||
try {
|
||||
return await languageProvider.getOptionsV2(scopedTag);
|
||||
} catch (error) {
|
||||
// Display message if Tempo is connected but search 404's
|
||||
if (isFetchError(error) && error?.status === 404) {
|
||||
setError(error);
|
||||
} else if (error instanceof Error) {
|
||||
dispatch(notifyApp(createErrorNotification('Error', error)));
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const { loading: isLoadingValues, value: options } = useAsync(updateOptions, [scopedTag, languageProvider, setError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (Array.isArray(filter.value) && filter.value.length > 1 && filter.operator !== '=~') {
|
||||
setPrevOperator(filter.operator);
|
||||
@ -57,38 +88,11 @@ const SearchField = ({ filter, datasource, updateFilter, deleteFilter, isTagsLoa
|
||||
setPrevValue(filter.value);
|
||||
}, [filter.value]);
|
||||
|
||||
useEffect(() => {
|
||||
const newScopedTag = scopeHelper(filter) + filter.tag;
|
||||
if (newScopedTag !== scopedTag) {
|
||||
setScopedTag(newScopedTag);
|
||||
}
|
||||
}, [filter, scopedTag]);
|
||||
|
||||
const updateOptions = useCallback(async () => {
|
||||
try {
|
||||
setIsLoadingValues(true);
|
||||
setOptions(await languageProvider.getOptionsV2(scopedTag));
|
||||
} catch (error) {
|
||||
// Display message if Tempo is connected but search 404's
|
||||
if (isFetchError(error) && error?.status === 404) {
|
||||
setError(error);
|
||||
} else if (error instanceof Error) {
|
||||
dispatch(notifyApp(createErrorNotification('Error', error)));
|
||||
}
|
||||
} finally {
|
||||
setIsLoadingValues(false);
|
||||
}
|
||||
}, [scopedTag, languageProvider, setError]);
|
||||
|
||||
useEffect(() => {
|
||||
updateOptions();
|
||||
}, [updateOptions]);
|
||||
|
||||
const scopeOptions = Object.values(TraceqlSearchScope).map((t) => ({ label: t, value: t }));
|
||||
|
||||
// If all values have type string or int/float use a focused list of operators instead of all operators
|
||||
const optionsOfFirstType = options.filter((o) => o.type === options[0]?.type);
|
||||
const uniqueOptionType = options.length === optionsOfFirstType.length ? options[0]?.type : undefined;
|
||||
const optionsOfFirstType = options?.filter((o) => o.type === options[0]?.type);
|
||||
const uniqueOptionType = options?.length === optionsOfFirstType?.length ? options?.[0]?.type : undefined;
|
||||
let operatorList = allOperators;
|
||||
switch (uniqueOptionType) {
|
||||
case 'string':
|
||||
@ -101,7 +105,7 @@ const SearchField = ({ filter, datasource, updateFilter, deleteFilter, isTagsLoa
|
||||
|
||||
return (
|
||||
<HorizontalGroup spacing={'none'} width={'auto'}>
|
||||
{filter.type === 'dynamic' && (
|
||||
{!hideScope && (
|
||||
<Select
|
||||
className={styles.dropdown}
|
||||
inputId={`${filter.id}-scope`}
|
||||
@ -114,12 +118,16 @@ const SearchField = ({ filter, datasource, updateFilter, deleteFilter, isTagsLoa
|
||||
aria-label={`select ${filter.id} scope`}
|
||||
/>
|
||||
)}
|
||||
{filter.type === 'dynamic' && (
|
||||
{!hideTag && (
|
||||
<Select
|
||||
className={styles.dropdown}
|
||||
inputId={`${filter.id}-tag`}
|
||||
isLoading={isTagsLoading}
|
||||
options={tags.map((t) => ({ label: t, value: t }))}
|
||||
// Add the current tag to the list if it doesn't exist in the tags prop, otherwise the field will be empty even though the state has a value
|
||||
options={(filter.tag !== undefined ? uniq([filter.tag, ...tags]) : tags).map((t) => ({
|
||||
label: t,
|
||||
value: t,
|
||||
}))}
|
||||
value={filter.tag}
|
||||
onChange={(v) => {
|
||||
updateFilter({ ...filter, tag: v?.value });
|
||||
@ -143,26 +151,28 @@ const SearchField = ({ filter, datasource, updateFilter, deleteFilter, isTagsLoa
|
||||
allowCustomValue={true}
|
||||
width={8}
|
||||
/>
|
||||
<Select
|
||||
className={styles.dropdown}
|
||||
inputId={`${filter.id}-value`}
|
||||
isLoading={isLoadingValues}
|
||||
options={options}
|
||||
value={filter.value}
|
||||
onChange={(val) => {
|
||||
if (Array.isArray(val)) {
|
||||
updateFilter({ ...filter, value: val.map((v) => v.value), valueType: val[0]?.type });
|
||||
} else {
|
||||
updateFilter({ ...filter, value: val?.value, valueType: val?.type });
|
||||
}
|
||||
}}
|
||||
placeholder="Select value"
|
||||
isClearable={false}
|
||||
aria-label={`select ${filter.id} value`}
|
||||
allowCustomValue={true}
|
||||
isMulti
|
||||
/>
|
||||
{filter.type === 'dynamic' && (
|
||||
{!hideValue && (
|
||||
<Select
|
||||
className={styles.dropdown}
|
||||
inputId={`${filter.id}-value`}
|
||||
isLoading={isLoadingValues}
|
||||
options={options}
|
||||
value={filter.value}
|
||||
onChange={(val) => {
|
||||
if (Array.isArray(val)) {
|
||||
updateFilter({ ...filter, value: val.map((v) => v.value), valueType: val[0]?.type });
|
||||
} else {
|
||||
updateFilter({ ...filter, value: val?.value, valueType: val?.type });
|
||||
}
|
||||
}}
|
||||
placeholder="Select value"
|
||||
isClearable={false}
|
||||
aria-label={`select ${filter.id} value`}
|
||||
allowCustomValue={true}
|
||||
isMulti
|
||||
/>
|
||||
)}
|
||||
{allowDelete && (
|
||||
<AccessoryButton
|
||||
variant={'secondary'}
|
||||
icon={'times'}
|
||||
|
@ -32,26 +32,34 @@ interface Props {
|
||||
setError: (error: FetchError) => void;
|
||||
tags: string[];
|
||||
isTagsLoading: boolean;
|
||||
hideValues?: boolean;
|
||||
}
|
||||
const TagsInput = ({ updateFilter, deleteFilter, filters, datasource, setError, tags, isTagsLoading }: Props) => {
|
||||
const TagsInput = ({
|
||||
updateFilter,
|
||||
deleteFilter,
|
||||
filters,
|
||||
datasource,
|
||||
setError,
|
||||
tags,
|
||||
isTagsLoading,
|
||||
hideValues,
|
||||
}: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const generateId = () => uuidv4().slice(0, 8);
|
||||
const handleOnAdd = useCallback(
|
||||
() => updateFilter({ id: generateId(), type: 'dynamic', operator: '=', scope: TraceqlSearchScope.Span }),
|
||||
() => updateFilter({ id: generateId(), operator: '=', scope: TraceqlSearchScope.Span }),
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!filters?.find((f) => f.type === 'dynamic')) {
|
||||
if (!filters?.length) {
|
||||
handleOnAdd();
|
||||
}
|
||||
}, [filters, handleOnAdd]);
|
||||
|
||||
const dynamicFilters = filters?.filter((f) => f.type === 'dynamic');
|
||||
|
||||
return (
|
||||
<div className={styles.vertical}>
|
||||
{dynamicFilters?.map((f, i) => (
|
||||
{filters?.map((f, i) => (
|
||||
<div className={styles.horizontal} key={f.id}>
|
||||
<SearchField
|
||||
filter={f}
|
||||
@ -61,8 +69,10 @@ const TagsInput = ({ updateFilter, deleteFilter, filters, datasource, setError,
|
||||
tags={tags}
|
||||
isTagsLoading={isTagsLoading}
|
||||
deleteFilter={deleteFilter}
|
||||
allowDelete={true}
|
||||
hideValue={hideValues}
|
||||
/>
|
||||
{i === dynamicFilters.length - 1 && (
|
||||
{i === filters.length - 1 && (
|
||||
<AccessoryButton variant={'secondary'} icon={'plus'} onClick={handleOnAdd} title={'Add tag'} />
|
||||
)}
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
@ -40,12 +40,25 @@ jest.mock('../language_provider', () => {
|
||||
describe('TraceQLSearch', () => {
|
||||
let user: ReturnType<typeof userEvent.setup>;
|
||||
|
||||
const datasource: TempoDatasource = {
|
||||
search: {
|
||||
filters: [
|
||||
{
|
||||
id: 'service-name',
|
||||
tag: 'service.name',
|
||||
operator: '=',
|
||||
scope: TraceqlSearchScope.Resource,
|
||||
},
|
||||
],
|
||||
},
|
||||
} as TempoDatasource;
|
||||
|
||||
let query: TempoQuery = {
|
||||
refId: 'A',
|
||||
queryType: 'traceqlSearch',
|
||||
key: 'Q-595a9bbc-2a25-49a7-9249-a52a0a475d83-0',
|
||||
query: '',
|
||||
filters: [{ id: 'min-duration', operator: '>', type: 'static', valueType: 'duration', tag: 'duration' }],
|
||||
filters: [{ id: 'min-duration', operator: '>', valueType: 'duration', tag: 'duration' }],
|
||||
};
|
||||
const onChange = (q: TempoQuery) => {
|
||||
query = q;
|
||||
@ -63,14 +76,11 @@ describe('TraceQLSearch', () => {
|
||||
});
|
||||
|
||||
it('should update operator when new value is selected in operator input', async () => {
|
||||
const { container } = render(
|
||||
<TraceQLSearch datasource={{} as TempoDatasource} query={query} onChange={onChange} />
|
||||
);
|
||||
const { container } = render(<TraceQLSearch datasource={datasource} query={query} onChange={onChange} />);
|
||||
|
||||
const minDurationOperator = container.querySelector(`input[aria-label="select min-duration operator"]`);
|
||||
expect(minDurationOperator).not.toBeNull();
|
||||
expect(minDurationOperator).toBeInTheDocument();
|
||||
expect(await screen.findByText('>')).toBeInTheDocument();
|
||||
|
||||
if (minDurationOperator) {
|
||||
await user.click(minDurationOperator);
|
||||
@ -84,9 +94,7 @@ describe('TraceQLSearch', () => {
|
||||
});
|
||||
|
||||
it('should add new filter when new value is selected in the service name section', async () => {
|
||||
const { container } = render(
|
||||
<TraceQLSearch datasource={{} as TempoDatasource} query={query} onChange={onChange} />
|
||||
);
|
||||
const { container } = render(<TraceQLSearch datasource={datasource} query={query} onChange={onChange} />);
|
||||
const serviceNameValue = container.querySelector(`input[aria-label="select service-name value"]`);
|
||||
expect(serviceNameValue).not.toBeNull();
|
||||
expect(serviceNameValue).toBeInTheDocument();
|
||||
@ -106,28 +114,4 @@ describe('TraceQLSearch', () => {
|
||||
expect(nameFilter?.scope).toBe(TraceqlSearchScope.Resource);
|
||||
}
|
||||
});
|
||||
|
||||
it('should add new filter when new filter button is clicked and remove filter when remove button is clicked', async () => {
|
||||
render(<TraceQLSearch datasource={{} as TempoDatasource} query={query} onChange={onChange} />);
|
||||
|
||||
const dynamicFilters = query.filters.filter((f) => f.type === 'dynamic');
|
||||
expect(dynamicFilters.length).toBe(1);
|
||||
const addButton = await screen.findByTitle('Add tag');
|
||||
await user.click(addButton);
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
// We have to rerender here so it picks up the new dynamic field
|
||||
render(<TraceQLSearch datasource={{} as TempoDatasource} query={query} onChange={onChange} />);
|
||||
|
||||
const newDynamicFilters = query.filters.filter((f) => f.type === 'dynamic');
|
||||
expect(newDynamicFilters.length).toBe(2);
|
||||
|
||||
const notInitialDynamic = newDynamicFilters.find((f) => f.id !== dynamicFilters[0].id);
|
||||
const secondDynamicRemoveButton = await screen.findByLabelText(`remove tag with ID ${notInitialDynamic?.id}`);
|
||||
await waitFor(() => expect(secondDynamicRemoveButton).toBeInTheDocument());
|
||||
if (secondDynamicRemoveButton) {
|
||||
await user.click(secondDynamicRemoveButton);
|
||||
expect(query.filters.filter((f) => f.type === 'dynamic')).toStrictEqual(dynamicFilters);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { EditorRow } from '@grafana/experimental';
|
||||
@ -10,7 +10,7 @@ import { createErrorNotification } from '../../../../core/copy/appNotification';
|
||||
import { notifyApp } from '../../../../core/reducers/appNotification';
|
||||
import { dispatch } from '../../../../store/store';
|
||||
import { RawQuery } from '../../prometheus/querybuilder/shared/RawQuery';
|
||||
import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
|
||||
import { TraceqlFilter } from '../dataquery.gen';
|
||||
import { TempoDatasource } from '../datasource';
|
||||
import { TempoQueryBuilderOptions } from '../traceql/TempoQueryBuilderOptions';
|
||||
import { CompletionProvider } from '../traceql/autocomplete';
|
||||
@ -21,7 +21,7 @@ import DurationInput from './DurationInput';
|
||||
import InlineSearchField from './InlineSearchField';
|
||||
import SearchField from './SearchField';
|
||||
import TagsInput from './TagsInput';
|
||||
import { generateQueryFromFilters, replaceAt } from './utils';
|
||||
import { filterScopedTag, filterTitle, generateQueryFromFilters, replaceAt } from './utils';
|
||||
|
||||
interface Props {
|
||||
datasource: TempoDatasource;
|
||||
@ -38,18 +38,21 @@ const TraceQLSearch = ({ datasource, query, onChange }: Props) => {
|
||||
const [isTagsLoading, setIsTagsLoading] = useState(true);
|
||||
const [traceQlQuery, setTraceQlQuery] = useState<string>('');
|
||||
|
||||
const updateFilter = (s: TraceqlFilter) => {
|
||||
const copy = { ...query };
|
||||
copy.filters ||= [];
|
||||
const indexOfFilter = copy.filters.findIndex((f) => f.id === s.id);
|
||||
if (indexOfFilter >= 0) {
|
||||
// update in place if the filter already exists, for consistency and to avoid UI bugs
|
||||
copy.filters = replaceAt(copy.filters, indexOfFilter, s);
|
||||
} else {
|
||||
copy.filters.push(s);
|
||||
}
|
||||
onChange(copy);
|
||||
};
|
||||
const updateFilter = useCallback(
|
||||
(s: TraceqlFilter) => {
|
||||
const copy = { ...query };
|
||||
copy.filters ||= [];
|
||||
const indexOfFilter = copy.filters.findIndex((f) => f.id === s.id);
|
||||
if (indexOfFilter >= 0) {
|
||||
// update in place if the filter already exists, for consistency and to avoid UI bugs
|
||||
copy.filters = replaceAt(copy.filters, indexOfFilter, s);
|
||||
} else {
|
||||
copy.filters.push(s);
|
||||
}
|
||||
onChange(copy);
|
||||
},
|
||||
[onChange, query]
|
||||
);
|
||||
|
||||
const deleteFilter = (s: TraceqlFilter) => {
|
||||
onChange({ ...query, filters: query.filters.filter((f) => f.id !== s.id) });
|
||||
@ -59,7 +62,7 @@ const TraceQLSearch = ({ datasource, query, onChange }: Props) => {
|
||||
setTraceQlQuery(generateQueryFromFilters(query.filters || []));
|
||||
}, [query]);
|
||||
|
||||
const findFilter = (id: string) => query.filters?.find((f) => f.id === id);
|
||||
const findFilter = useCallback((id: string) => query.filters?.find((f) => f.id === id), [query.filters]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTags = async () => {
|
||||
@ -85,43 +88,60 @@ const TraceQLSearch = ({ datasource, query, onChange }: Props) => {
|
||||
fetchTags();
|
||||
}, [datasource]);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize state with configured static filters that already have a value from the config
|
||||
datasource.search?.filters
|
||||
?.filter((f) => f.value)
|
||||
.forEach((f) => {
|
||||
if (!findFilter(f.id)) {
|
||||
updateFilter(f);
|
||||
}
|
||||
});
|
||||
}, [datasource.search?.filters, findFilter, updateFilter]);
|
||||
|
||||
// filter out tags that already exist in the static fields
|
||||
const staticTags = datasource.search?.filters?.map((f) => f.tag) || [];
|
||||
staticTags.push('duration');
|
||||
const filteredTags = [...CompletionProvider.intrinsics, ...tags].filter((t) => !staticTags.includes(t));
|
||||
|
||||
// Dynamic filters are all filters that don't match the ID of a filter in the datasource configuration
|
||||
// The duration tag is a special case since its selector is hard-coded
|
||||
const dynamicFilters = (query.filters || []).filter(
|
||||
(f) => f.tag !== 'duration' && (datasource.search?.filters?.findIndex((sf) => sf.id === f.id) || 0) === -1
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.container}>
|
||||
<div>
|
||||
<InlineSearchField label={'Service Name'}>
|
||||
<SearchField
|
||||
filter={
|
||||
findFilter('service-name') || {
|
||||
id: 'service-name',
|
||||
type: 'static',
|
||||
tag: 'service.name',
|
||||
operator: '=',
|
||||
scope: TraceqlSearchScope.Resource,
|
||||
}
|
||||
}
|
||||
datasource={datasource}
|
||||
setError={setError}
|
||||
updateFilter={updateFilter}
|
||||
tags={[]}
|
||||
/>
|
||||
</InlineSearchField>
|
||||
<InlineSearchField label={'Span Name'}>
|
||||
<SearchField
|
||||
filter={findFilter('span-name') || { id: 'span-name', type: 'static', tag: 'name', operator: '=' }}
|
||||
datasource={datasource}
|
||||
setError={setError}
|
||||
updateFilter={updateFilter}
|
||||
tags={[]}
|
||||
/>
|
||||
</InlineSearchField>
|
||||
<InlineSearchField label={'Duration'} tooltip="The span duration, i.e. end - start time of the span">
|
||||
{datasource.search?.filters?.map((f) => (
|
||||
<InlineSearchField
|
||||
key={f.id}
|
||||
label={filterTitle(f)}
|
||||
tooltip={`Filter your search by ${filterScopedTag(
|
||||
f
|
||||
)}. To modify the default filters shown for search visit the Tempo datasource configuration page.`}
|
||||
>
|
||||
<SearchField
|
||||
filter={findFilter(f.id) || f}
|
||||
datasource={datasource}
|
||||
setError={setError}
|
||||
updateFilter={updateFilter}
|
||||
tags={[]}
|
||||
hideScope={true}
|
||||
hideTag={true}
|
||||
/>
|
||||
</InlineSearchField>
|
||||
))}
|
||||
<InlineSearchField
|
||||
label={'Duration'}
|
||||
tooltip="The span duration, i.e. end - start time of the span. Accepted units are ns, ms, s, m, h"
|
||||
>
|
||||
<HorizontalGroup spacing={'sm'}>
|
||||
<DurationInput
|
||||
filter={
|
||||
findFilter('min-duration') || {
|
||||
id: 'min-duration',
|
||||
type: 'static',
|
||||
tag: 'duration',
|
||||
operator: '>',
|
||||
valueType: 'duration',
|
||||
@ -134,7 +154,6 @@ const TraceQLSearch = ({ datasource, query, onChange }: Props) => {
|
||||
filter={
|
||||
findFilter('max-duration') || {
|
||||
id: 'max-duration',
|
||||
type: 'static',
|
||||
tag: 'duration',
|
||||
operator: '<',
|
||||
valueType: 'duration',
|
||||
@ -147,12 +166,12 @@ const TraceQLSearch = ({ datasource, query, onChange }: Props) => {
|
||||
</InlineSearchField>
|
||||
<InlineSearchField label={'Tags'}>
|
||||
<TagsInput
|
||||
filters={query.filters}
|
||||
filters={dynamicFilters}
|
||||
datasource={datasource}
|
||||
setError={setError}
|
||||
updateFilter={updateFilter}
|
||||
deleteFilter={deleteFilter}
|
||||
tags={[...CompletionProvider.intrinsics, ...tags]}
|
||||
tags={filteredTags}
|
||||
isTagsLoading={isTagsLoading}
|
||||
/>
|
||||
</InlineSearchField>
|
||||
|
@ -8,51 +8,49 @@ describe('generateQueryFromFilters generates the correct query for', () => {
|
||||
});
|
||||
|
||||
it('a field without value', () => {
|
||||
expect(generateQueryFromFilters([{ id: 'foo', type: 'static', tag: 'footag', operator: '=' }])).toBe('{}');
|
||||
expect(generateQueryFromFilters([{ id: 'foo', tag: 'footag', operator: '=' }])).toBe('{}');
|
||||
});
|
||||
|
||||
it('a field with value but without tag', () => {
|
||||
expect(generateQueryFromFilters([{ id: 'foo', type: 'static', value: 'foovalue', operator: '=' }])).toBe('{}');
|
||||
expect(generateQueryFromFilters([{ id: 'foo', value: 'foovalue', operator: '=' }])).toBe('{}');
|
||||
});
|
||||
|
||||
it('a field with value and tag but without operator', () => {
|
||||
expect(generateQueryFromFilters([{ id: 'foo', type: 'static', tag: 'footag', value: 'foovalue' }])).toBe('{}');
|
||||
expect(generateQueryFromFilters([{ id: 'foo', tag: 'footag', value: 'foovalue' }])).toBe('{}');
|
||||
});
|
||||
|
||||
it('a field with tag, operator and tag', () => {
|
||||
expect(
|
||||
generateQueryFromFilters([{ id: 'foo', type: 'static', tag: 'footag', value: 'foovalue', operator: '=' }])
|
||||
).toBe('{.footag="foovalue"}');
|
||||
expect(generateQueryFromFilters([{ id: 'foo', tag: 'footag', value: 'foovalue', operator: '=' }])).toBe(
|
||||
'{.footag="foovalue"}'
|
||||
);
|
||||
});
|
||||
|
||||
it('a field with valueType as integer', () => {
|
||||
expect(
|
||||
generateQueryFromFilters([
|
||||
{ id: 'foo', type: 'static', tag: 'footag', value: '1234', operator: '>', valueType: 'integer' },
|
||||
])
|
||||
generateQueryFromFilters([{ id: 'foo', tag: 'footag', value: '1234', operator: '>', valueType: 'integer' }])
|
||||
).toBe('{.footag>1234}');
|
||||
});
|
||||
it('two fields with everything filled in', () => {
|
||||
expect(
|
||||
generateQueryFromFilters([
|
||||
{ id: 'foo', type: 'static', tag: 'footag', value: '1234', operator: '>=', valueType: 'integer' },
|
||||
{ id: 'bar', type: 'dynamic', tag: 'bartag', value: 'barvalue', operator: '=', valueType: 'string' },
|
||||
{ id: 'foo', tag: 'footag', value: '1234', operator: '>=', valueType: 'integer' },
|
||||
{ id: 'bar', tag: 'bartag', value: 'barvalue', operator: '=', valueType: 'string' },
|
||||
])
|
||||
).toBe('{.footag>=1234 && .bartag="barvalue"}');
|
||||
});
|
||||
it('two fields but one is missing a value', () => {
|
||||
expect(
|
||||
generateQueryFromFilters([
|
||||
{ id: 'foo', type: 'static', tag: 'footag', value: '1234', operator: '>=', valueType: 'integer' },
|
||||
{ id: 'bar', type: 'dynamic', tag: 'bartag', operator: '=', valueType: 'string' },
|
||||
{ id: 'foo', tag: 'footag', value: '1234', operator: '>=', valueType: 'integer' },
|
||||
{ id: 'bar', tag: 'bartag', operator: '=', valueType: 'string' },
|
||||
])
|
||||
).toBe('{.footag>=1234}');
|
||||
});
|
||||
it('two fields but one is missing a value and the other a tag', () => {
|
||||
expect(
|
||||
generateQueryFromFilters([
|
||||
{ id: 'foo', type: 'static', value: '1234', operator: '>=', valueType: 'integer' },
|
||||
{ id: 'bar', type: 'dynamic', tag: 'bartag', operator: '=', valueType: 'string' },
|
||||
{ id: 'foo', value: '1234', operator: '>=', valueType: 'integer' },
|
||||
{ id: 'bar', tag: 'bartag', operator: '=', valueType: 'string' },
|
||||
])
|
||||
).toBe('{}');
|
||||
});
|
||||
@ -61,7 +59,6 @@ describe('generateQueryFromFilters generates the correct query for', () => {
|
||||
generateQueryFromFilters([
|
||||
{
|
||||
id: 'foo',
|
||||
type: 'static',
|
||||
tag: 'footag',
|
||||
value: '1234',
|
||||
operator: '>=',
|
||||
@ -76,7 +73,6 @@ describe('generateQueryFromFilters generates the correct query for', () => {
|
||||
generateQueryFromFilters([
|
||||
{
|
||||
id: 'foo',
|
||||
type: 'static',
|
||||
tag: 'footag',
|
||||
value: '1234',
|
||||
operator: '>=',
|
||||
@ -91,7 +87,6 @@ describe('generateQueryFromFilters generates the correct query for', () => {
|
||||
generateQueryFromFilters([
|
||||
{
|
||||
id: 'foo',
|
||||
type: 'static',
|
||||
tag: 'footag',
|
||||
value: '1234',
|
||||
operator: '>=',
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { startCase } from 'lodash';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
|
||||
@ -19,7 +21,7 @@ const valueHelper = (f: TraceqlFilter) => {
|
||||
}
|
||||
return f.value;
|
||||
};
|
||||
export const scopeHelper = (f: TraceqlFilter) => {
|
||||
const scopeHelper = (f: TraceqlFilter) => {
|
||||
// Intrinsic fields don't have a scope
|
||||
if (CompletionProvider.intrinsics.find((t) => t === f.tag)) {
|
||||
return '';
|
||||
@ -29,6 +31,18 @@ export const scopeHelper = (f: TraceqlFilter) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const filterScopedTag = (f: TraceqlFilter) => {
|
||||
return scopeHelper(f) + f.tag;
|
||||
};
|
||||
|
||||
export const filterTitle = (f: TraceqlFilter) => {
|
||||
// Special case for the intrinsic "name" since a label called "Name" isn't explicit
|
||||
if (f.tag === 'name') {
|
||||
return 'Span Name';
|
||||
}
|
||||
return startCase(filterScopedTag(f));
|
||||
};
|
||||
|
||||
export function replaceAt<T>(array: T[], index: number, value: T) {
|
||||
const ret = array.slice(0);
|
||||
ret[index] = value;
|
||||
|
@ -12,6 +12,7 @@ import { LokiSearchSettings } from './LokiSearchSettings';
|
||||
import { QuerySettings } from './QuerySettings';
|
||||
import { SearchSettings } from './SearchSettings';
|
||||
import { ServiceGraphSettings } from './ServiceGraphSettings';
|
||||
import { TraceQLSearchSettings } from './TraceQLSearchSettings';
|
||||
|
||||
export type Props = DataSourcePluginOptionsEditorProps;
|
||||
|
||||
@ -48,7 +49,11 @@ export const ConfigEditor = ({ options, onOptionsChange }: Props) => {
|
||||
</div>
|
||||
|
||||
<div className="gf-form-group">
|
||||
<SearchSettings options={options} onOptionsChange={onOptionsChange} />
|
||||
{config.featureToggles.traceqlSearch ? (
|
||||
<TraceQLSearchSettings options={options} onOptionsChange={onOptionsChange} />
|
||||
) : (
|
||||
<SearchSettings options={options} onOptionsChange={onOptionsChange} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="gf-form-group">
|
||||
|
@ -0,0 +1,59 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
|
||||
import { DataSourcePluginOptionsEditorProps, updateDatasourcePluginJsonDataOption } from '@grafana/data';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { InlineField, InlineFieldRow, InlineSwitch } from '@grafana/ui';
|
||||
|
||||
import { TempoDatasource } from '../datasource';
|
||||
import { TempoJsonData } from '../types';
|
||||
|
||||
import { TraceQLSearchTags } from './TraceQLSearchTags';
|
||||
|
||||
interface Props extends DataSourcePluginOptionsEditorProps<TempoJsonData> {}
|
||||
|
||||
export function TraceQLSearchSettings({ options, onOptionsChange }: Props) {
|
||||
const dataSourceSrv = getDataSourceSrv();
|
||||
const fetchDatasource = async () => {
|
||||
return (await dataSourceSrv.get({ type: options.type, uid: options.uid })) as TempoDatasource;
|
||||
};
|
||||
|
||||
const { value: datasource } = useAsync(fetchDatasource, [dataSourceSrv, options]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<h3 className="page-heading">Tempo search</h3>
|
||||
<InlineFieldRow className={styles.row}>
|
||||
<InlineField tooltip="Removes the search tab from the query editor" label="Hide search" labelWidth={26}>
|
||||
<InlineSwitch
|
||||
id="hideSearch"
|
||||
value={options.jsonData.search?.hide}
|
||||
onChange={(event: React.SyntheticEvent<HTMLInputElement>) =>
|
||||
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'search', {
|
||||
...options.jsonData.search,
|
||||
hide: event.currentTarget.checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow className={styles.row}>
|
||||
<InlineField tooltip="Configures which fields are available in the UI" label="Static filters" labelWidth={26}>
|
||||
<TraceQLSearchTags datasource={datasource} options={options} onOptionsChange={onOptionsChange} />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = {
|
||||
container: css`
|
||||
label: container;
|
||||
width: 100%;
|
||||
`,
|
||||
row: css`
|
||||
label: row;
|
||||
align-items: baseline;
|
||||
`,
|
||||
};
|
@ -0,0 +1,111 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
|
||||
import { DataSourcePluginOptionsEditorProps, updateDatasourcePluginJsonDataOption } from '@grafana/data';
|
||||
import { Alert } from '@grafana/ui';
|
||||
|
||||
import TagsInput from '../SearchTraceQLEditor/TagsInput';
|
||||
import { replaceAt } from '../SearchTraceQLEditor/utils';
|
||||
import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
|
||||
import { TempoDatasource } from '../datasource';
|
||||
import { CompletionProvider } from '../traceql/autocomplete';
|
||||
import { TempoJsonData } from '../types';
|
||||
|
||||
interface Props extends DataSourcePluginOptionsEditorProps<TempoJsonData> {
|
||||
datasource?: TempoDatasource;
|
||||
}
|
||||
|
||||
export function TraceQLSearchTags({ options, onOptionsChange, datasource }: Props) {
|
||||
const fetchTags = async () => {
|
||||
if (!datasource) {
|
||||
throw new Error('Unable to retrieve datasource');
|
||||
}
|
||||
|
||||
try {
|
||||
await datasource.languageProvider.start();
|
||||
const tags = datasource.languageProvider.getTags();
|
||||
|
||||
if (tags) {
|
||||
// This is needed because the /api/v2/search/tag/${tag}/values API expects "status" and the v1 API expects "status.code"
|
||||
// so Tempo doesn't send anything and we inject it here for the autocomplete
|
||||
if (!tags.find((t) => t === 'status')) {
|
||||
tags.push('status');
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
} catch (e) {
|
||||
// @ts-ignore
|
||||
throw new Error(`${e.statusText}: ${e.data.error}`);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const { error, loading, value: tags } = useAsync(fetchTags, [datasource, options]);
|
||||
|
||||
const updateFilter = useCallback(
|
||||
(s: TraceqlFilter) => {
|
||||
let copy = options.jsonData.search?.filters;
|
||||
copy ||= [];
|
||||
const indexOfFilter = copy.findIndex((f) => f.id === s.id);
|
||||
if (indexOfFilter >= 0) {
|
||||
// update in place if the filter already exists, for consistency and to avoid UI bugs
|
||||
copy = replaceAt(copy, indexOfFilter, s);
|
||||
} else {
|
||||
copy.push(s);
|
||||
}
|
||||
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'search', {
|
||||
...options.jsonData.search,
|
||||
filters: copy,
|
||||
});
|
||||
},
|
||||
[onOptionsChange, options]
|
||||
);
|
||||
|
||||
const deleteFilter = (s: TraceqlFilter) => {
|
||||
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'search', {
|
||||
...options.jsonData.search,
|
||||
filters: options.jsonData.search?.filters?.filter((f) => f.id !== s.id),
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!options.jsonData.search?.filters) {
|
||||
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'search', {
|
||||
...options.jsonData.search,
|
||||
filters: [
|
||||
{
|
||||
id: 'service-name',
|
||||
tag: 'service.name',
|
||||
operator: '=',
|
||||
scope: TraceqlSearchScope.Resource,
|
||||
},
|
||||
{ id: 'span-name', tag: 'name', operator: '=', scope: TraceqlSearchScope.Span },
|
||||
],
|
||||
});
|
||||
}
|
||||
}, [onOptionsChange, options]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{datasource ? (
|
||||
<TagsInput
|
||||
updateFilter={updateFilter}
|
||||
deleteFilter={deleteFilter}
|
||||
filters={options.jsonData.search?.filters || []}
|
||||
datasource={datasource}
|
||||
setError={() => {}}
|
||||
tags={[...CompletionProvider.intrinsics, ...(tags || [])]}
|
||||
isTagsLoading={loading}
|
||||
hideValues={true}
|
||||
/>
|
||||
) : (
|
||||
<div>Invalid data source, please create a valid data source and try again</div>
|
||||
)}
|
||||
{error && (
|
||||
<Alert title={'Unable to fetch TraceQL tags'} severity={'error'} topSpacing={1}>
|
||||
{error.message}
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -54,13 +54,10 @@ composableKinds: DataQuery: {
|
||||
#TempoQueryType: "traceql" | "traceqlSearch" | "search" | "serviceMap" | "upload" | "nativeSearch" | "clear" @cuetsy(kind="type")
|
||||
|
||||
// static fields are pre-set in the UI, dynamic fields are added by the user
|
||||
#TraceqlSearchFilterType: "static" | "dynamic" @cuetsy(kind="type")
|
||||
#TraceqlSearchScope: "unscoped" | "resource" | "span" @cuetsy(kind="enum")
|
||||
#TraceqlSearchScope: "unscoped" | "resource" | "span" @cuetsy(kind="enum")
|
||||
#TraceqlFilter: {
|
||||
// Uniquely identify the filter, will not be used in the query generation
|
||||
id: string
|
||||
// The type of the filter, can either be static (pre defined in the UI) or dynamic
|
||||
type: #TraceqlSearchFilterType
|
||||
// The tag for the search filter, for example: .http.status_code, .service.name, status
|
||||
tag?: string
|
||||
// The operator that connects the tag to the value, for example: =, >, !=, =~
|
||||
|
@ -60,8 +60,6 @@ export type TempoQueryType = ('traceql' | 'traceqlSearch' | 'search' | 'serviceM
|
||||
/**
|
||||
* static fields are pre-set in the UI, dynamic fields are added by the user
|
||||
*/
|
||||
export type TraceqlSearchFilterType = ('static' | 'dynamic');
|
||||
|
||||
export enum TraceqlSearchScope {
|
||||
Resource = 'resource',
|
||||
Span = 'span',
|
||||
@ -85,10 +83,6 @@ export interface TraceqlFilter {
|
||||
* The tag for the search filter, for example: .http.status_code, .service.name, status
|
||||
*/
|
||||
tag?: string;
|
||||
/**
|
||||
* The type of the filter, can either be static (pre defined in the UI) or dynamic
|
||||
*/
|
||||
type: TraceqlSearchFilterType;
|
||||
/**
|
||||
* The value for the search filter
|
||||
*/
|
||||
|
@ -36,6 +36,7 @@ import { PrometheusDatasource } from '../prometheus/datasource';
|
||||
import { PromQuery } from '../prometheus/types';
|
||||
|
||||
import { generateQueryFromFilters } from './SearchTraceQLEditor/utils';
|
||||
import { TraceqlFilter, TraceqlSearchScope } from './dataquery.gen';
|
||||
import {
|
||||
failedMetric,
|
||||
histogramMetric,
|
||||
@ -66,6 +67,7 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
|
||||
};
|
||||
search?: {
|
||||
hide?: boolean;
|
||||
filters?: TraceqlFilter[];
|
||||
};
|
||||
nodeGraph?: NodeGraphOptions;
|
||||
lokiSearch?: {
|
||||
@ -92,6 +94,20 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
|
||||
this.lokiSearch = instanceSettings.jsonData.lokiSearch;
|
||||
this.traceQuery = instanceSettings.jsonData.traceQuery;
|
||||
this.languageProvider = new TempoLanguageProvider(this);
|
||||
if (!this.search?.filters) {
|
||||
this.search = {
|
||||
...this.search,
|
||||
filters: [
|
||||
{
|
||||
id: 'service-name',
|
||||
tag: 'service.name',
|
||||
operator: '=',
|
||||
scope: TraceqlSearchScope.Resource,
|
||||
},
|
||||
{ id: 'span-name', tag: 'name', operator: '=', scope: TraceqlSearchScope.Span },
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
query(options: DataQueryRequest<TempoQuery>): Observable<DataQueryResponse> {
|
||||
|
@ -4,7 +4,7 @@ import { TraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsS
|
||||
|
||||
import { LokiQuery } from '../loki/types';
|
||||
|
||||
import { TempoQuery as TempoBase, TempoQueryType } from './dataquery.gen';
|
||||
import { TempoQuery as TempoBase, TempoQueryType, TraceqlFilter } from './dataquery.gen';
|
||||
|
||||
export interface SearchQueryParams {
|
||||
minDuration?: string;
|
||||
@ -22,6 +22,7 @@ export interface TempoJsonData extends DataSourceJsonData {
|
||||
};
|
||||
search?: {
|
||||
hide?: boolean;
|
||||
filters?: TraceqlFilter[];
|
||||
};
|
||||
nodeGraph?: NodeGraphOptions;
|
||||
lokiSearch?: {
|
||||
|
Loading…
Reference in New Issue
Block a user