diff --git a/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx b/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx new file mode 100644 index 00000000000..77d85ac8526 --- /dev/null +++ b/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx @@ -0,0 +1,154 @@ +import { act, render, screen, waitFor } from '@testing-library/react'; +import { backendSrv } from 'app/core/services/backend_srv'; +import { createFetchResponse } from 'test/helpers/createFetchResponse'; +import { DataQueryRequest, DataSourceInstanceSettings, dateTime, PluginType } from '@grafana/data'; +import { of } from 'rxjs'; +import { JaegerDatasource, JaegerJsonData } from '../datasource'; +import { JaegerQuery } from '../types'; +import React from 'react'; +import SearchForm from './SearchForm'; +import { testResponse } from '../testResponse'; +import userEvent from '@testing-library/user-event'; + +describe('SearchForm', () => { + it('should call the `onChange` function on click of the Input', async () => { + const promise = Promise.resolve(); + const handleOnChange = jest.fn(() => promise); + const query = { + ...defaultQuery, + targets: [ + { + query: 'a/b', + refId: '1', + }, + ], + refId: '121314', + }; + const ds = { + async metadataRequest(url: string, params?: Record): Promise { + if (url === '/api/services') { + return Promise.resolve(['jaeger-query', 'service2', 'service3']); + } + }, + } as JaegerDatasource; + setupFetchMock({ data: [testResponse] }); + + render(); + + const asyncServiceSelect = await waitFor(() => screen.getByRole('combobox', { name: 'select-service-name' })); + expect(asyncServiceSelect).toBeInTheDocument(); + + userEvent.click(asyncServiceSelect); + + const jaegerService = await screen.findByText('jaeger-query'); + expect(jaegerService).toBeInTheDocument(); + }); + + it('should be able to select operation name if query.service exists', async () => { + const promise = Promise.resolve(); + const handleOnChange = jest.fn(() => promise); + const query2 = { + ...defaultQuery, + targets: [ + { + query: 'a/b', + refId: '1', + }, + ], + refId: '121314', + service: 'jaeger-query', + }; + setupFetchMock({ data: [testResponse] }); + + render(); + + const asyncOperationSelect2 = await waitFor(() => screen.getByRole('combobox', { name: 'select-operation-name' })); + expect(asyncOperationSelect2).toBeInTheDocument(); + }); +}); + +describe('SearchForm', () => { + it('should show loader if there is a delay fetching options', async () => { + const promise = Promise.resolve(); + const handleOnChange = jest.fn(() => { + setTimeout(() => { + return promise; + }, 3000); + }); + const query = { + ...defaultQuery, + targets: [ + { + query: 'a/b', + refId: '1', + }, + ], + refId: '121314', + service: 'jaeger-query', + }; + const ds = new JaegerDatasource(defaultSettings); + setupFetchMock({ data: [testResponse] }); + + render(); + + const asyncServiceSelect = screen.getByRole('combobox', { name: 'select-service-name' }); + userEvent.click(asyncServiceSelect); + const loader = screen.getByText('Loading options...'); + + expect(loader).toBeInTheDocument(); + await act(() => promise); + }); +}); + +function setupFetchMock(response: any, mock?: any) { + const defaultMock = () => mock ?? of(createFetchResponse(response)); + + const fetchMock = jest.spyOn(backendSrv, 'fetch'); + fetchMock.mockImplementation(defaultMock); + return fetchMock; +} + +const defaultSettings: DataSourceInstanceSettings = { + id: 0, + uid: '0', + type: 'tracing', + name: 'jaeger', + url: 'http://grafana.com', + access: 'proxy', + meta: { + id: 'jaeger', + name: 'jaeger', + type: PluginType.datasource, + info: {} as any, + module: '', + baseUrl: '', + }, + jsonData: { + nodeGraph: { + enabled: true, + }, + }, +}; + +const defaultQuery: DataQueryRequest = { + requestId: '1', + dashboardId: 0, + interval: '0', + intervalMs: 10, + panelId: 0, + scopedVars: {}, + range: { + from: dateTime().subtract(1, 'h'), + to: dateTime(), + raw: { from: '1h', to: 'now' }, + }, + timezone: 'browser', + app: 'explore', + startTime: 0, + targets: [ + { + query: '12345', + refId: '1', + }, + ], +}; diff --git a/public/app/plugins/datasource/jaeger/components/SearchForm.tsx b/public/app/plugins/datasource/jaeger/components/SearchForm.tsx index 1c4e6de235b..c362314bbed 100644 --- a/public/app/plugins/datasource/jaeger/components/SearchForm.tsx +++ b/public/app/plugins/datasource/jaeger/components/SearchForm.tsx @@ -1,11 +1,14 @@ import { css } from '@emotion/css'; import { SelectableValue } from '@grafana/data'; -import { InlineField, InlineFieldRow, Input, Select } from '@grafana/ui'; -import React, { useEffect, useState } from 'react'; +import { AsyncSelect, InlineField, InlineFieldRow, Input } from '@grafana/ui'; +import React, { useCallback, useEffect, useState } from 'react'; import { JaegerDatasource } from '../datasource'; import { JaegerQuery } from '../types'; import { transformToLogfmt } from '../util'; import { AdvancedOptions } from './AdvancedOptions'; +import { dispatch } from 'app/store/store'; +import { notifyApp } from 'app/core/actions'; +import { createErrorNotification } from 'app/core/copy/appNotification'; type Props = { datasource: JaegerDatasource; @@ -22,69 +25,110 @@ const allOperationsOption: SelectableValue = { export function SearchForm({ datasource, query, onChange }: Props) { const [serviceOptions, setServiceOptions] = useState>>(); const [operationOptions, setOperationOptions] = useState>>(); + const [isLoading, setIsLoading] = useState<{ + services: boolean; + operations: boolean; + }>({ + services: false, + operations: false, + }); + + const loadServices = useCallback( + async (url: string, loaderOfType: string): Promise>> => { + setIsLoading((prevValue) => ({ ...prevValue, [loaderOfType]: true })); + + try { + const values: string[] | null = await datasource.metadataRequest(url); + if (!values) { + return [{ label: `No ${loaderOfType} found`, value: `No ${loaderOfType} found` }]; + } + + const serviceOptions: SelectableValue[] = values.sort().map((service) => ({ + label: service, + value: service, + })); + return serviceOptions; + } catch (error) { + dispatch(notifyApp(createErrorNotification('Error', error))); + return []; + } finally { + setIsLoading((prevValue) => ({ ...prevValue, [loaderOfType]: false })); + } + }, + [datasource] + ); useEffect(() => { const getServices = async () => { - const services = await loadServices({ - dataSource: datasource, - url: '/api/services', - notFoundLabel: 'No service found', - }); + const services = await loadServices('/api/services', 'services'); setServiceOptions(services); }; getServices(); - }, [datasource]); + }, [datasource, loadServices]); useEffect(() => { const getOperations = async () => { - const operations = await loadServices({ - dataSource: datasource, - url: `/api/services/${encodeURIComponent(query.service!)}/operations`, - notFoundLabel: 'No operation found', - }); + const operations = await loadServices( + `/api/services/${encodeURIComponent(query.service!)}/operations`, + 'operations' + ); setOperationOptions([allOperationsOption, ...operations]); }; if (query.service) { getOperations(); } - }, [datasource, query.service]); + }, [datasource, query.service, loadServices]); return (
- + loadServices(`/api/services/${encodeURIComponent(query.service!)}/operations`, 'operations') + } + onOpenMenu={() => + loadServices(`/api/services/${encodeURIComponent(query.service!)}/operations`, 'operations') + } + isLoading={isLoading.operations} value={operationOptions?.find((v) => v.value === query.operation) || null} onChange={(v) => onChange({ ...query, - operation: v.value!, + operation: v?.value! || undefined, }) } menuPlacement="bottom" isClearable + defaultOptions + aria-label={'select-operation-name'} /> @@ -108,19 +152,4 @@ export function SearchForm({ datasource, query, onChange }: Props) { ); } -type Options = { dataSource: JaegerDatasource; url: string; notFoundLabel: string }; - -const loadServices = async ({ dataSource, url, notFoundLabel }: Options): Promise>> => { - const services: string[] | null = await dataSource.metadataRequest(url); - - if (!services) { - return [{ label: notFoundLabel, value: notFoundLabel }]; - } - - const serviceOptions: SelectableValue[] = services.sort().map((service) => ({ - label: service, - value: service, - })); - - return serviceOptions; -}; +export default SearchForm;