diff --git a/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx b/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx
index 00171650969..eb4ef137ad4 100644
--- a/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx
+++ b/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx
@@ -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();
+ 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();
+
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( {}} />);
+
+ 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) {
diff --git a/public/app/plugins/datasource/jaeger/components/SearchForm.tsx b/public/app/plugins/datasource/jaeger/components/SearchForm.tsx
index 22d563ad651..27a20538cfc 100644
--- a/public/app/plugins/datasource/jaeger/components/SearchForm.tsx
+++ b/public/app/plugins/datasource/jaeger/components/SearchForm.tsx
@@ -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>> => {
+ async (url: string, loaderOfType: string, query = ''): Promise>> => {
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')
}
diff --git a/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.test.tsx b/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.test.tsx
index 00b8096dd41..21ae2466c0c 100644
--- a/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.test.tsx
+++ b/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.test.tsx
@@ -99,4 +99,26 @@ describe('NativeSearch', () => {
expect(handleOnChange).toHaveBeenCalledWith(fakeOptionChoice);
});
+
+ it('should filter the span dropdown when user types a search value', async () => {
+ render(
+ {}} 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();
+ });
});
diff --git a/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.tsx b/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.tsx
index 9606c17b424..994f2af75c8 100644
--- a/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.tsx
+++ b/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.tsx
@@ -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) => {