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) => {