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 { css } from '@emotion/css';
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { InlineField, InlineFieldRow, Input, Select } from '@grafana/ui';
|
import { AsyncSelect, InlineField, InlineFieldRow, Input } from '@grafana/ui';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { JaegerDatasource } from '../datasource';
|
import { JaegerDatasource } from '../datasource';
|
||||||
import { JaegerQuery } from '../types';
|
import { JaegerQuery } from '../types';
|
||||||
import { transformToLogfmt } from '../util';
|
import { transformToLogfmt } from '../util';
|
||||||
import { AdvancedOptions } from './AdvancedOptions';
|
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 = {
|
type Props = {
|
||||||
datasource: JaegerDatasource;
|
datasource: JaegerDatasource;
|
||||||
@ -22,69 +25,110 @@ const allOperationsOption: SelectableValue<string> = {
|
|||||||
export function SearchForm({ datasource, query, onChange }: Props) {
|
export function SearchForm({ datasource, query, onChange }: Props) {
|
||||||
const [serviceOptions, setServiceOptions] = useState<Array<SelectableValue<string>>>();
|
const [serviceOptions, setServiceOptions] = useState<Array<SelectableValue<string>>>();
|
||||||
const [operationOptions, setOperationOptions] = 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(() => {
|
useEffect(() => {
|
||||||
const getServices = async () => {
|
const getServices = async () => {
|
||||||
const services = await loadServices({
|
const services = await loadServices('/api/services', 'services');
|
||||||
dataSource: datasource,
|
|
||||||
url: '/api/services',
|
|
||||||
notFoundLabel: 'No service found',
|
|
||||||
});
|
|
||||||
setServiceOptions(services);
|
setServiceOptions(services);
|
||||||
};
|
};
|
||||||
getServices();
|
getServices();
|
||||||
}, [datasource]);
|
}, [datasource, loadServices]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getOperations = async () => {
|
const getOperations = async () => {
|
||||||
const operations = await loadServices({
|
const operations = await loadServices(
|
||||||
dataSource: datasource,
|
`/api/services/${encodeURIComponent(query.service!)}/operations`,
|
||||||
url: `/api/services/${encodeURIComponent(query.service!)}/operations`,
|
'operations'
|
||||||
notFoundLabel: 'No operation found',
|
);
|
||||||
});
|
|
||||||
setOperationOptions([allOperationsOption, ...operations]);
|
setOperationOptions([allOperationsOption, ...operations]);
|
||||||
};
|
};
|
||||||
if (query.service) {
|
if (query.service) {
|
||||||
getOperations();
|
getOperations();
|
||||||
}
|
}
|
||||||
}, [datasource, query.service]);
|
}, [datasource, query.service, loadServices]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={css({ maxWidth: '500px' })}>
|
<div className={css({ maxWidth: '500px' })}>
|
||||||
<InlineFieldRow>
|
<InlineFieldRow>
|
||||||
<InlineField label="Service" labelWidth={14} grow>
|
<InlineField label="Service" labelWidth={14} grow>
|
||||||
<Select
|
<AsyncSelect
|
||||||
inputId="service"
|
inputId="service"
|
||||||
menuShouldPortal
|
menuShouldPortal
|
||||||
options={serviceOptions}
|
cacheOptions={false}
|
||||||
value={serviceOptions?.find((v) => v.value === query.service) || null}
|
loadOptions={() => loadServices('/api/services', 'services')}
|
||||||
onChange={(v) => {
|
onOpenMenu={() => loadServices('/api/services', 'services')}
|
||||||
|
isLoading={isLoading.services}
|
||||||
|
value={serviceOptions?.find((v) => v?.value === query.service) || undefined}
|
||||||
|
onChange={(v) =>
|
||||||
onChange({
|
onChange({
|
||||||
...query,
|
...query,
|
||||||
service: v.value!,
|
service: v?.value!,
|
||||||
operation: query.service !== v.value ? undefined : query.operation,
|
operation: query.service !== v?.value ? undefined : query.operation,
|
||||||
});
|
})
|
||||||
}}
|
}
|
||||||
menuPlacement="bottom"
|
menuPlacement="bottom"
|
||||||
isClearable
|
isClearable
|
||||||
|
defaultOptions
|
||||||
|
aria-label={'select-service-name'}
|
||||||
/>
|
/>
|
||||||
</InlineField>
|
</InlineField>
|
||||||
</InlineFieldRow>
|
</InlineFieldRow>
|
||||||
<InlineFieldRow>
|
<InlineFieldRow>
|
||||||
<InlineField label="Operation" labelWidth={14} grow disabled={!query.service}>
|
<InlineField label="Operation" labelWidth={14} grow disabled={!query.service}>
|
||||||
<Select
|
<AsyncSelect
|
||||||
inputId="operation"
|
inputId="operation"
|
||||||
menuShouldPortal
|
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}
|
value={operationOptions?.find((v) => v.value === query.operation) || null}
|
||||||
onChange={(v) =>
|
onChange={(v) =>
|
||||||
onChange({
|
onChange({
|
||||||
...query,
|
...query,
|
||||||
operation: v.value!,
|
operation: v?.value! || undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
menuPlacement="bottom"
|
menuPlacement="bottom"
|
||||||
isClearable
|
isClearable
|
||||||
|
defaultOptions
|
||||||
|
aria-label={'select-operation-name'}
|
||||||
/>
|
/>
|
||||||
</InlineField>
|
</InlineField>
|
||||||
</InlineFieldRow>
|
</InlineFieldRow>
|
||||||
@ -108,19 +152,4 @@ export function SearchForm({ datasource, query, onChange }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type Options = { dataSource: JaegerDatasource; url: string; notFoundLabel: string };
|
export default SearchForm;
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
Loading…
Reference in New Issue
Block a user