mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Traces: Filter by service/span name and operation in Tempo and Jaeger (#48209)
* Filter search options in Tempo * Tests for filtering search options in Tempo * Filter search options in Jaeger * Tests for filtering search options in Jaeger * Self review * Fuzzy search
This commit is contained in:
parent
9e84e20ade
commit
412dbc21e3
@ -20,12 +20,6 @@ describe('SearchForm', () => {
|
||||
const handleOnChange = jest.fn(() => promise);
|
||||
const query = {
|
||||
...defaultQuery,
|
||||
targets: [
|
||||
{
|
||||
query: 'a/b',
|
||||
refId: '1',
|
||||
},
|
||||
],
|
||||
refId: '121314',
|
||||
};
|
||||
const ds = {
|
||||
@ -53,12 +47,6 @@ describe('SearchForm', () => {
|
||||
const handleOnChange = jest.fn(() => promise);
|
||||
const query2 = {
|
||||
...defaultQuery,
|
||||
targets: [
|
||||
{
|
||||
query: 'a/b',
|
||||
refId: '1',
|
||||
},
|
||||
],
|
||||
refId: '121314',
|
||||
service: 'jaeger-query',
|
||||
};
|
||||
@ -73,35 +61,23 @@ describe('SearchForm', () => {
|
||||
|
||||
describe('SearchForm', () => {
|
||||
let user: UserEvent;
|
||||
let query: JaegerQuery;
|
||||
let ds: JaegerDatasource;
|
||||
|
||||
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 show loader if there is a delay fetching options', async () => {
|
||||
const handleOnChange = jest.fn();
|
||||
const query = {
|
||||
query = {
|
||||
...defaultQuery,
|
||||
targets: [
|
||||
{
|
||||
query: 'a/b',
|
||||
refId: '1',
|
||||
},
|
||||
],
|
||||
refId: '121314',
|
||||
service: 'jaeger-query',
|
||||
};
|
||||
const ds = new JaegerDatasource(defaultSettings);
|
||||
setupFetchMock({ data: [testResponse] });
|
||||
|
||||
render(<SearchForm datasource={ds} query={query} onChange={handleOnChange} />);
|
||||
ds = new JaegerDatasource(defaultSettings);
|
||||
setupFetchMock({ data: [testResponse] });
|
||||
|
||||
jest.spyOn(ds, 'metadataRequest').mockImplementation(() => {
|
||||
return new Promise((resolve) => {
|
||||
@ -110,6 +86,16 @@ describe('SearchForm', () => {
|
||||
}, 3000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should show loader if there is a delay fetching options', async () => {
|
||||
const handleOnChange = jest.fn();
|
||||
render(<SearchForm datasource={ds} query={query} onChange={handleOnChange} />);
|
||||
|
||||
const asyncServiceSelect = screen.getByRole('combobox', { name: 'select-service-name' });
|
||||
await user.click(asyncServiceSelect);
|
||||
expect(screen.getByText('Loading options...')).toBeInTheDocument();
|
||||
@ -117,6 +103,25 @@ describe('SearchForm', () => {
|
||||
jest.advanceTimersByTime(3000);
|
||||
await waitFor(() => expect(screen.queryByText('Loading options...')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should filter the span dropdown when user types a search value', async () => {
|
||||
render(<SearchForm datasource={ds} query={query} onChange={() => {}} />);
|
||||
|
||||
const asyncServiceSelect = screen.getByRole('combobox', { name: 'select-service-name' });
|
||||
await user.click(asyncServiceSelect);
|
||||
jest.advanceTimersByTime(3000);
|
||||
expect(asyncServiceSelect).toBeInTheDocument();
|
||||
|
||||
await user.type(asyncServiceSelect, 'j');
|
||||
jest.advanceTimersByTime(3000);
|
||||
var option = await screen.findByText('jaeger-query');
|
||||
expect(option).toBeDefined();
|
||||
|
||||
await user.type(asyncServiceSelect, 'c');
|
||||
jest.advanceTimersByTime(3000);
|
||||
option = await screen.findByText('No options found');
|
||||
expect(option).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
function setupFetchMock(response: any, mock?: any) {
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { debounce } from 'lodash';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { AsyncSelect, InlineField, InlineFieldRow, Input } from '@grafana/ui';
|
||||
import { AsyncSelect, fuzzyMatch, InlineField, InlineFieldRow, Input } from '@grafana/ui';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
import { dispatch } from 'app/store/store';
|
||||
@ -37,7 +38,7 @@ export function SearchForm({ datasource, query, onChange }: Props) {
|
||||
});
|
||||
|
||||
const loadServices = useCallback(
|
||||
async (url: string, loaderOfType: string): Promise<Array<SelectableValue<string>>> => {
|
||||
async (url: string, loaderOfType: string, query = ''): Promise<Array<SelectableValue<string>>> => {
|
||||
setIsLoading((prevValue) => ({ ...prevValue, [loaderOfType]: true }));
|
||||
|
||||
try {
|
||||
@ -50,7 +51,11 @@ export function SearchForm({ datasource, query, onChange }: Props) {
|
||||
label: service,
|
||||
value: service,
|
||||
}));
|
||||
return serviceOptions;
|
||||
|
||||
const filteredOptions = serviceOptions.filter((item) =>
|
||||
item.value ? fuzzyMatch(item.value, query).found : false
|
||||
);
|
||||
return filteredOptions;
|
||||
} catch (error) {
|
||||
dispatch(notifyApp(createErrorNotification('Error', error)));
|
||||
return [];
|
||||
@ -61,6 +66,17 @@ export function SearchForm({ datasource, query, onChange }: Props) {
|
||||
[datasource]
|
||||
);
|
||||
|
||||
const getServiceOptions = (userQuery: string) => {
|
||||
return loadServices('/api/services', 'services', userQuery);
|
||||
};
|
||||
|
||||
const getOperationOptions = (userQuery: string) => {
|
||||
return loadServices(`/api/services/${encodeURIComponent(query.service!)}/operations`, 'operations', userQuery);
|
||||
};
|
||||
|
||||
const serviceSearch = debounce(getServiceOptions, 500, { leading: true, trailing: true });
|
||||
const operationSearch = debounce(getOperationOptions, 500, { leading: true, trailing: true });
|
||||
|
||||
useEffect(() => {
|
||||
const getServices = async () => {
|
||||
const services = await loadServices('/api/services', 'services');
|
||||
@ -90,7 +106,7 @@ export function SearchForm({ datasource, query, onChange }: Props) {
|
||||
inputId="service"
|
||||
menuShouldPortal
|
||||
cacheOptions={false}
|
||||
loadOptions={() => loadServices('/api/services', 'services')}
|
||||
loadOptions={serviceSearch}
|
||||
onOpenMenu={() => loadServices('/api/services', 'services')}
|
||||
isLoading={isLoading.services}
|
||||
value={serviceOptions?.find((v) => v?.value === query.service) || undefined}
|
||||
@ -114,9 +130,7 @@ export function SearchForm({ datasource, query, onChange }: Props) {
|
||||
inputId="operation"
|
||||
menuShouldPortal
|
||||
cacheOptions={false}
|
||||
loadOptions={() =>
|
||||
loadServices(`/api/services/${encodeURIComponent(query.service!)}/operations`, 'operations')
|
||||
}
|
||||
loadOptions={operationSearch}
|
||||
onOpenMenu={() =>
|
||||
loadServices(`/api/services/${encodeURIComponent(query.service!)}/operations`, 'operations')
|
||||
}
|
||||
|
@ -99,4 +99,26 @@ describe('NativeSearch', () => {
|
||||
|
||||
expect(handleOnChange).toHaveBeenCalledWith(fakeOptionChoice);
|
||||
});
|
||||
|
||||
it('should filter the span dropdown when user types a search value', async () => {
|
||||
render(
|
||||
<NativeSearch datasource={{} as TempoDatasource} query={mockQuery} onChange={() => {}} onRunQuery={() => {}} />
|
||||
);
|
||||
|
||||
const asyncServiceSelect = await screen.findByRole('combobox', { name: 'select-span-name' });
|
||||
|
||||
expect(asyncServiceSelect).toBeInTheDocument();
|
||||
await user.click(asyncServiceSelect);
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
await user.type(asyncServiceSelect, 'd');
|
||||
jest.advanceTimersByTime(1000);
|
||||
var option = await screen.findByText('driver');
|
||||
expect(option).toBeDefined();
|
||||
|
||||
await user.type(asyncServiceSelect, 'a');
|
||||
jest.advanceTimersByTime(1000);
|
||||
option = await screen.findByText('No options found');
|
||||
expect(option).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
AsyncSelect,
|
||||
Alert,
|
||||
useStyles2,
|
||||
fuzzyMatch,
|
||||
} from '@grafana/ui';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
@ -66,17 +67,18 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props
|
||||
spanName: false,
|
||||
});
|
||||
|
||||
async function fetchOptionsCallback(nameType: string, lp: TempoLanguageProvider) {
|
||||
async function fetchOptionsCallback(name: string, lp: TempoLanguageProvider, query = '') {
|
||||
try {
|
||||
const res = await lp.getOptions(nameType === 'serviceName' ? 'service.name' : 'name');
|
||||
setIsLoading((prevValue) => ({ ...prevValue, [nameType]: false }));
|
||||
return res;
|
||||
setIsLoading((prevValue) => ({ ...prevValue, [name]: false }));
|
||||
const options = await lp.getOptions(name);
|
||||
const filteredOptions = options.filter((item) => (item.value ? fuzzyMatch(item.value, query).found : false));
|
||||
return filteredOptions;
|
||||
} catch (error) {
|
||||
if (error?.status === 404) {
|
||||
setIsLoading((prevValue) => ({ ...prevValue, [nameType]: false }));
|
||||
setIsLoading((prevValue) => ({ ...prevValue, [name]: false }));
|
||||
} else {
|
||||
dispatch(notifyApp(createErrorNotification('Error', error)));
|
||||
setIsLoading((prevValue) => ({ ...prevValue, [nameType]: false }));
|
||||
setIsLoading((prevValue) => ({ ...prevValue, [name]: false }));
|
||||
}
|
||||
setError(error);
|
||||
return [];
|
||||
@ -84,24 +86,40 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props
|
||||
}
|
||||
|
||||
const loadOptionsOfType = useCallback(
|
||||
(nameType: string) => {
|
||||
setIsLoading((prevValue) => ({ ...prevValue, [nameType]: true }));
|
||||
return fetchOptionsCallback(nameType, languageProvider);
|
||||
(name: string) => {
|
||||
setIsLoading((prevValue) => ({ ...prevValue, [name]: true }));
|
||||
return fetchOptionsCallback(name, languageProvider);
|
||||
},
|
||||
[languageProvider]
|
||||
);
|
||||
|
||||
const fetchOptionsOfType = useCallback(
|
||||
(nameType: string) => debounce(() => loadOptionsOfType(nameType), 500, { leading: true, trailing: true }),
|
||||
(name: string) => debounce(() => loadOptionsOfType(name), 500, { leading: true, trailing: true }),
|
||||
[loadOptionsOfType]
|
||||
);
|
||||
|
||||
const getNameOptions = (query: string, name: string) => {
|
||||
setIsLoading((prevValue) => ({ ...prevValue, [name]: true }));
|
||||
return fetchOptionsCallback(name, languageProvider, query);
|
||||
};
|
||||
|
||||
const getServiceNameOptions = (query: string) => {
|
||||
return getNameOptions(query, 'service.name');
|
||||
};
|
||||
|
||||
const getSpanNameOptions = (query: string) => {
|
||||
return getNameOptions(query, 'name');
|
||||
};
|
||||
|
||||
const serviceNameSearch = debounce(getServiceNameOptions, 500, { leading: true, trailing: true });
|
||||
const spanNameSearch = debounce(getSpanNameOptions, 500, { leading: true, trailing: true });
|
||||
|
||||
useEffect(() => {
|
||||
const fetchOptions = async () => {
|
||||
try {
|
||||
await languageProvider.start();
|
||||
fetchOptionsCallback('serviceName', languageProvider);
|
||||
fetchOptionsCallback('spanName', languageProvider);
|
||||
fetchOptionsCallback('service.name', languageProvider);
|
||||
fetchOptionsCallback('name', languageProvider);
|
||||
setHasSyntaxLoaded(true);
|
||||
} catch (error) {
|
||||
// Display message if Tempo is connected but search 404's
|
||||
@ -143,8 +161,8 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props
|
||||
inputId="service"
|
||||
menuShouldPortal
|
||||
cacheOptions={false}
|
||||
loadOptions={fetchOptionsOfType('serviceName')}
|
||||
onOpenMenu={fetchOptionsOfType('serviceName')}
|
||||
loadOptions={serviceNameSearch}
|
||||
onOpenMenu={fetchOptionsOfType('service.name')}
|
||||
isLoading={isLoading.serviceName}
|
||||
value={asyncServiceNameValue.value}
|
||||
onChange={(v) => {
|
||||
@ -170,8 +188,8 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props
|
||||
inputId="spanName"
|
||||
menuShouldPortal
|
||||
cacheOptions={false}
|
||||
loadOptions={fetchOptionsOfType('spanName')}
|
||||
onOpenMenu={fetchOptionsOfType('spanName')}
|
||||
loadOptions={spanNameSearch}
|
||||
onOpenMenu={fetchOptionsOfType('name')}
|
||||
isLoading={isLoading.spanName}
|
||||
value={asyncSpanNameValue.value}
|
||||
onChange={(v) => {
|
||||
|
Loading…
Reference in New Issue
Block a user