Tempo: Update status operators (#78730)

* Set keyword operators

* Tests for keyword, string, number option types
This commit is contained in:
Joey 2023-11-29 11:07:35 +00:00 committed by GitHub
parent f41cf40344
commit 5a6ac44902
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 120 additions and 38 deletions

View File

@ -1,40 +1,18 @@
import { render, screen } from '@testing-library/react'; import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import React from 'react'; import React from 'react';
import { initTemplateSrv } from 'test/helpers/initTemplateSrv'; import { initTemplateSrv } from 'test/helpers/initTemplateSrv';
import { LanguageProvider } from '@grafana/data';
import { FetchError, setTemplateSrv } from '@grafana/runtime'; import { FetchError, setTemplateSrv } from '@grafana/runtime';
import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen'; import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
import { TempoDatasource } from '../datasource'; import { TempoDatasource } from '../datasource';
import TempoLanguageProvider from '../language_provider';
import { keywordOperators, numberOperators, operators, stringOperators } from '../traceql/traceql';
import SearchField from './SearchField'; import SearchField from './SearchField';
const getOptionsV2 = jest.fn().mockImplementation(() => {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{
value: 'customer',
label: 'customer',
type: 'string',
},
{
value: 'driver',
label: 'driver',
type: 'string',
},
]);
}, 1000);
});
});
jest.mock('../language_provider', () => {
return jest.fn().mockImplementation(() => {
return { getOptionsV2 };
});
});
describe('SearchField', () => { describe('SearchField', () => {
let templateSrv = initTemplateSrv('key', [{ name: 'templateVariable1' }, { name: 'templateVariable2' }]); let templateSrv = initTemplateSrv('key', [{ name: 'templateVariable1' }, { name: 'templateVariable2' }]);
let user: ReturnType<typeof userEvent.setup>; let user: ReturnType<typeof userEvent.setup>;
@ -51,7 +29,7 @@ describe('SearchField', () => {
jest.useRealTimers(); jest.useRealTimers();
}); });
it('should not render tag if hideTag is true', () => { it('should not render tag if hideTag is true', async () => {
const updateFilter = jest.fn((val) => { const updateFilter = jest.fn((val) => {
return val; return val;
}); });
@ -59,10 +37,12 @@ describe('SearchField', () => {
const { container } = renderSearchField(updateFilter, filter, [], true); const { container } = renderSearchField(updateFilter, filter, [], true);
await waitFor(async () => {
expect(container.querySelector(`input[aria-label="select test1 tag"]`)).not.toBeInTheDocument(); expect(container.querySelector(`input[aria-label="select test1 tag"]`)).not.toBeInTheDocument();
expect(container.querySelector(`input[aria-label="select test1 operator"]`)).toBeInTheDocument(); expect(container.querySelector(`input[aria-label="select test1 operator"]`)).toBeInTheDocument();
expect(container.querySelector(`input[aria-label="select test1 value"]`)).toBeInTheDocument(); expect(container.querySelector(`input[aria-label="select test1 value"]`)).toBeInTheDocument();
}); });
});
it('should update operator when new value is selected in operator input', async () => { it('should update operator when new value is selected in operator input', async () => {
const updateFilter = jest.fn((val) => { const updateFilter = jest.fn((val) => {
@ -71,7 +51,7 @@ describe('SearchField', () => {
const filter: TraceqlFilter = { id: 'test1', operator: '=', valueType: 'string', tag: 'test-tag' }; const filter: TraceqlFilter = { id: 'test1', operator: '=', valueType: 'string', tag: 'test-tag' };
const { container } = renderSearchField(updateFilter, filter); const { container } = renderSearchField(updateFilter, filter);
const select = await container.querySelector(`input[aria-label="select test1 operator"]`); const select = container.querySelector(`input[aria-label="select test1 operator"]`);
expect(select).not.toBeNull(); expect(select).not.toBeNull();
expect(select).toBeInTheDocument(); expect(select).toBeInTheDocument();
if (select) { if (select) {
@ -95,7 +75,7 @@ describe('SearchField', () => {
}; };
const { container } = renderSearchField(updateFilter, filter); const { container } = renderSearchField(updateFilter, filter);
const select = await container.querySelector(`input[aria-label="select test1 value"]`); const select = container.querySelector(`input[aria-label="select test1 value"]`);
expect(select).not.toBeNull(); expect(select).not.toBeNull();
expect(select).toBeInTheDocument(); expect(select).toBeInTheDocument();
if (select) { if (select) {
@ -178,14 +158,112 @@ describe('SearchField', () => {
expect(await screen.findByText('$templateVariable2')).toBeInTheDocument(); expect(await screen.findByText('$templateVariable2')).toBeInTheDocument();
} }
}); });
it('should only show keyword operators if options tag type is keyword', async () => {
const filter: TraceqlFilter = { id: 'test1', operator: '=', valueType: 'string', tag: 'test-tag' };
const lp = {
getOptionsV2: jest.fn().mockReturnValue([
{
value: 'ok',
label: 'ok',
type: 'keyword',
},
]),
} as unknown as TempoLanguageProvider;
const { container } = renderSearchField(jest.fn(), filter, [], false, lp);
const select = container.querySelector(`input[aria-label="select test1 operator"]`);
if (select) {
await user.click(select);
await waitFor(async () => {
expect(screen.getByText('Equals')).toBeInTheDocument();
expect(screen.getByText('Not equals')).toBeInTheDocument();
operators
.filter((op) => !keywordOperators.includes(op))
.forEach((op) => {
expect(screen.queryByText(op)).not.toBeInTheDocument();
});
});
}
});
it('should only show string operators if options tag type is string', async () => {
const filter: TraceqlFilter = { id: 'test1', operator: '=', valueType: 'string', tag: 'test-tag' };
const { container } = renderSearchField(jest.fn(), filter);
const select = container.querySelector(`input[aria-label="select test1 operator"]`);
if (select) {
await user.click(select);
await waitFor(async () => {
expect(screen.getByText('Equals')).toBeInTheDocument();
expect(screen.getByText('Not equals')).toBeInTheDocument();
expect(screen.getByText('Matches regex')).toBeInTheDocument();
expect(screen.getByText('Does not match regex')).toBeInTheDocument();
operators
.filter((op) => !stringOperators.includes(op))
.forEach((op) => {
expect(screen.queryByText(op)).not.toBeInTheDocument();
});
});
}
});
it('should only show number operators if options tag type is number', async () => {
const filter: TraceqlFilter = { id: 'test1', operator: '=', valueType: 'string', tag: 'test-tag' };
const lp = {
getOptionsV2: jest.fn().mockReturnValue([
{
value: 200,
label: 200,
type: 'int',
},
]),
} as unknown as TempoLanguageProvider;
const { container } = renderSearchField(jest.fn(), filter, [], false, lp);
const select = container.querySelector(`input[aria-label="select test1 operator"]`);
if (select) {
await user.click(select);
await waitFor(async () => {
expect(screen.getByText('Equals')).toBeInTheDocument();
expect(screen.getByText('Not equals')).toBeInTheDocument();
expect(screen.getByText('Greater')).toBeInTheDocument();
expect(screen.getByText('Less')).toBeInTheDocument();
expect(screen.getByText('Greater or Equal')).toBeInTheDocument();
expect(screen.getByText('Less or Equal')).toBeInTheDocument();
operators
.filter((op) => !numberOperators.includes(op))
.forEach((op) => {
expect(screen.queryByText(op)).not.toBeInTheDocument();
});
});
}
});
}); });
const renderSearchField = ( const renderSearchField = (
updateFilter: (f: TraceqlFilter) => void, updateFilter: (f: TraceqlFilter) => void,
filter: TraceqlFilter, filter: TraceqlFilter,
tags?: string[], tags?: string[],
hideTag?: boolean hideTag?: boolean,
lp?: LanguageProvider
) => { ) => {
const languageProvider =
lp ||
({
getOptionsV2: jest.fn().mockReturnValue([
{
value: 'customer',
label: 'customer',
type: 'string',
},
{
value: 'driver',
label: 'driver',
type: 'string',
},
]),
} as unknown as TempoLanguageProvider);
const datasource: TempoDatasource = { const datasource: TempoDatasource = {
search: { search: {
filters: [ filters: [
@ -198,7 +276,9 @@ const renderSearchField = (
{ id: 'span-name', type: 'static', tag: 'name', operator: '=', scope: TraceqlSearchScope.Span }, { id: 'span-name', type: 'static', tag: 'name', operator: '=', scope: TraceqlSearchScope.Span },
], ],
}, },
languageProvider,
} as TempoDatasource; } as TempoDatasource;
return render( return render(
<SearchField <SearchField
datasource={datasource} datasource={datasource}

View File

@ -13,8 +13,7 @@ import { notifyApp } from '../../../../core/reducers/appNotification';
import { dispatch } from '../../../../store/store'; import { dispatch } from '../../../../store/store';
import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen'; import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
import { TempoDatasource } from '../datasource'; import { TempoDatasource } from '../datasource';
import TempoLanguageProvider from '../language_provider'; import { operators as allOperators, stringOperators, numberOperators, keywordOperators } from '../traceql/traceql';
import { operators as allOperators, stringOperators, numberOperators } from '../traceql/traceql';
import { filterScopedTag, operatorSelectableValue } from './utils'; import { filterScopedTag, operatorSelectableValue } from './utils';
@ -53,7 +52,6 @@ const SearchField = ({
query, query,
}: Props) => { }: Props) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const languageProvider = useMemo(() => new TempoLanguageProvider(datasource), [datasource]);
const scopedTag = useMemo(() => filterScopedTag(filter), [filter]); const scopedTag = useMemo(() => filterScopedTag(filter), [filter]);
// We automatically change the operator to the regex op when users select 2 or more values // 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 // However, they expect this to be automatically rolled back to the previous operator once
@ -63,7 +61,7 @@ const SearchField = ({
const updateOptions = async () => { const updateOptions = async () => {
try { try {
return filter.tag ? await languageProvider.getOptionsV2(scopedTag, query) : []; return filter.tag ? await datasource.languageProvider.getOptionsV2(scopedTag, query) : [];
} catch (error) { } catch (error) {
// Display message if Tempo is connected but search 404's // Display message if Tempo is connected but search 404's
if (isFetchError(error) && error?.status === 404) { if (isFetchError(error) && error?.status === 404) {
@ -77,7 +75,7 @@ const SearchField = ({
const { loading: isLoadingValues, value: options } = useAsync(updateOptions, [ const { loading: isLoadingValues, value: options } = useAsync(updateOptions, [
scopedTag, scopedTag,
languageProvider, datasource.languageProvider,
setError, setError,
query, query,
]); ]);
@ -115,6 +113,9 @@ const SearchField = ({
const uniqueOptionType = options?.length === optionsOfFirstType?.length ? options?.[0]?.type : undefined; const uniqueOptionType = options?.length === optionsOfFirstType?.length ? options?.[0]?.type : undefined;
let operatorList = allOperators; let operatorList = allOperators;
switch (uniqueOptionType) { switch (uniqueOptionType) {
case 'keyword':
operatorList = keywordOperators;
break;
case 'string': case 'string':
operatorList = stringOperators; operatorList = stringOperators;
break; break;

View File

@ -24,6 +24,7 @@ export const languageConfiguration: languages.LanguageConfiguration = {
}; };
export const operators = ['=', '!=', '>', '<', '>=', '<=', '=~', '!~']; export const operators = ['=', '!=', '>', '<', '>=', '<=', '=~', '!~'];
export const keywordOperators = ['=', '!='];
export const stringOperators = ['=', '!=', '=~', '!~']; export const stringOperators = ['=', '!=', '=~', '!~'];
export const numberOperators = ['=', '!=', '>', '<', '>=', '<=']; export const numberOperators = ['=', '!=', '>', '<', '>=', '<='];