mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
06ed5efdf0
commit
83664121bc
@ -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',
|
||||
},
|
||||
],
|
||||
};
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user