Jaeger: Show loader when search options havent yet loaded (#45936)

* Swap out Select component for AsyncSelect to Jaeger search panel
This commit is contained in:
Cat Perry 2022-02-28 11:06:09 -08:00 committed by GitHub
parent 06ed5efdf0
commit 83664121bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 224 additions and 41 deletions

View File

@ -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<string, any>): Promise<any> {
if (url === '/api/services') {
return Promise.resolve(['jaeger-query', 'service2', 'service3']);
}
},
} as JaegerDatasource;
setupFetchMock({ data: [testResponse] });
render(<SearchForm datasource={ds} query={query} onChange={handleOnChange} />);
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(<SearchForm datasource={{} as JaegerDatasource} query={query2} onChange={handleOnChange} />);
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(<SearchForm datasource={ds} query={query} onChange={handleOnChange} />);
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<JaegerJsonData> = {
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<JaegerQuery> = {
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',
},
],
};

View File

@ -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<string> = {
export function SearchForm({ datasource, query, onChange }: Props) {
const [serviceOptions, setServiceOptions] = useState<Array<SelectableValue<string>>>();
const [operationOptions, setOperationOptions] = useState<Array<SelectableValue<string>>>();
const [isLoading, setIsLoading] = useState<{
services: boolean;
operations: boolean;
}>({
services: false,
operations: false,
});
const loadServices = useCallback(
async (url: string, loaderOfType: string): Promise<Array<SelectableValue<string>>> => {
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 (
<div className={css({ maxWidth: '500px' })}>
<InlineFieldRow>
<InlineField label="Service" labelWidth={14} grow>
<Select
<AsyncSelect
inputId="service"
menuShouldPortal
options={serviceOptions}
value={serviceOptions?.find((v) => v.value === query.service) || null}
onChange={(v) => {
cacheOptions={false}
loadOptions={() => loadServices('/api/services', 'services')}
onOpenMenu={() => loadServices('/api/services', 'services')}
isLoading={isLoading.services}
value={serviceOptions?.find((v) => v?.value === query.service) || undefined}
onChange={(v) =>
onChange({
...query,
service: v.value!,
operation: query.service !== v.value ? undefined : query.operation,
});
}}
service: v?.value!,
operation: query.service !== v?.value ? undefined : query.operation,
})
}
menuPlacement="bottom"
isClearable
defaultOptions
aria-label={'select-service-name'}
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Operation" labelWidth={14} grow disabled={!query.service}>
<Select
<AsyncSelect
inputId="operation"
menuShouldPortal
options={operationOptions}
cacheOptions={false}
loadOptions={() =>
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'}
/>
</InlineField>
</InlineFieldRow>
@ -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<Array<SelectableValue<string>>> => {
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;