mirror of
https://github.com/grafana/grafana.git
synced 2025-02-11 08:05:43 -06:00
Cloudmonitor: Refactor query builder (#61410)
* Reset filters when service is changed * Update to reset any query props * Reset query properties on metric change * Update tests * Refresh labels on panel time update * Review * Refactor VisualMetricsQueryEditor - Move any Metrics functionality to VisualMetricsQueryEditor - Update tests - Expose timeSrv from datasource - Update getLabels to make use of provided timeRange * Review
This commit is contained in:
parent
021eda7aad
commit
f135c6cbf1
@ -5060,9 +5060,6 @@ exports[`better eslint`] = {
|
||||
"public/app/plugins/datasource/cloud-monitoring/components/MQLQueryEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloud-monitoring/components/Metrics.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloud-monitoring/components/VariableQueryEditor.test.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { TemplateSrvMock } from 'app/features/templating/template_srv.mock';
|
||||
|
||||
@ -15,6 +16,7 @@ export const createMockDatasource = (overrides?: Partial<Datasource>) => {
|
||||
templateSrv,
|
||||
getSLOServices: jest.fn().mockResolvedValue([]),
|
||||
migrateQuery: jest.fn().mockImplementation((query) => query),
|
||||
timeSrv: getTimeSrv(),
|
||||
...overrides,
|
||||
};
|
||||
|
||||
|
@ -1,203 +0,0 @@
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { openMenu, select } from 'react-select-event';
|
||||
|
||||
import { createMockDatasource } from '../__mocks__/cloudMonitoringDatasource';
|
||||
import { createMockMetricDescriptor } from '../__mocks__/cloudMonitoringMetricDescriptor';
|
||||
import { createMockTimeSeriesList } from '../__mocks__/cloudMonitoringQuery';
|
||||
|
||||
import { Metrics } from './Metrics';
|
||||
|
||||
describe('Metrics', () => {
|
||||
it('renders metrics fields', async () => {
|
||||
const onChange = jest.fn();
|
||||
const query = createMockTimeSeriesList();
|
||||
const datasource = createMockDatasource();
|
||||
|
||||
render(
|
||||
<Metrics
|
||||
refId="refId"
|
||||
metricType=""
|
||||
projectName="projectName"
|
||||
templateVariableOptions={[]}
|
||||
datasource={datasource}
|
||||
onChange={onChange}
|
||||
onProjectChange={jest.fn()}
|
||||
query={query}
|
||||
>
|
||||
{() => <div />}
|
||||
</Metrics>
|
||||
);
|
||||
|
||||
expect(await screen.findByLabelText('Service')).toBeInTheDocument();
|
||||
expect(await screen.findByLabelText('Metric name')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('can select a service', async () => {
|
||||
const onChange = jest.fn();
|
||||
const query = createMockTimeSeriesList();
|
||||
const datasource = createMockDatasource({
|
||||
getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor()]),
|
||||
});
|
||||
|
||||
render(
|
||||
<Metrics
|
||||
refId="refId"
|
||||
metricType=""
|
||||
projectName="projectName"
|
||||
templateVariableOptions={[]}
|
||||
datasource={datasource}
|
||||
onChange={onChange}
|
||||
onProjectChange={jest.fn()}
|
||||
query={query}
|
||||
>
|
||||
{() => <div />}
|
||||
</Metrics>
|
||||
);
|
||||
|
||||
const service = await screen.findByLabelText('Service');
|
||||
await openMenu(service);
|
||||
await select(service, 'Srv', { container: document.body });
|
||||
expect(onChange).toBeCalledWith(expect.objectContaining({ service: 'service' }));
|
||||
});
|
||||
|
||||
it('can select a metric name', async () => {
|
||||
const onChange = jest.fn();
|
||||
const query = createMockTimeSeriesList();
|
||||
const datasource = createMockDatasource({
|
||||
getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor()]),
|
||||
});
|
||||
|
||||
render(
|
||||
<Metrics
|
||||
refId="refId"
|
||||
metricType="type"
|
||||
projectName="projectName"
|
||||
templateVariableOptions={[]}
|
||||
datasource={datasource}
|
||||
onChange={onChange}
|
||||
onProjectChange={jest.fn()}
|
||||
query={query}
|
||||
>
|
||||
{() => <div />}
|
||||
</Metrics>
|
||||
);
|
||||
|
||||
const metricName = await screen.findByLabelText('Metric name');
|
||||
await openMenu(metricName);
|
||||
await select(metricName, 'metricName', { container: document.body });
|
||||
expect(onChange).toBeCalledWith(expect.objectContaining({ type: 'type' }));
|
||||
});
|
||||
|
||||
it('should render available metric options according to the selected service', async () => {
|
||||
const onChange = jest.fn();
|
||||
const query = createMockTimeSeriesList();
|
||||
const datasource = createMockDatasource({
|
||||
getMetricTypes: jest.fn().mockResolvedValue([
|
||||
createMockMetricDescriptor({
|
||||
service: 'service_a',
|
||||
serviceShortName: 'srv_a',
|
||||
type: 'metric1',
|
||||
description: 'description_metric1',
|
||||
displayName: 'displayName_metric1',
|
||||
}),
|
||||
createMockMetricDescriptor({
|
||||
service: 'service_b',
|
||||
serviceShortName: 'srv_b',
|
||||
type: 'metric2',
|
||||
description: 'description_metric2',
|
||||
displayName: 'displayName_metric2',
|
||||
}),
|
||||
createMockMetricDescriptor({
|
||||
service: 'service_b',
|
||||
serviceShortName: 'srv_b',
|
||||
type: 'metric3',
|
||||
description: 'description_metric3',
|
||||
displayName: 'displayName_metric3',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
render(
|
||||
<Metrics
|
||||
refId="refId"
|
||||
metricType="metric1"
|
||||
projectName="projectName"
|
||||
templateVariableOptions={[]}
|
||||
datasource={datasource}
|
||||
onChange={onChange}
|
||||
onProjectChange={jest.fn()}
|
||||
query={query}
|
||||
>
|
||||
{() => <div />}
|
||||
</Metrics>
|
||||
);
|
||||
|
||||
const metricName = await screen.findByLabelText('Metric name');
|
||||
await openMenu(metricName);
|
||||
|
||||
const metricNameOptions = screen.getByLabelText('Select options menu');
|
||||
expect(within(metricNameOptions).getByText('description_metric1')).toBeInTheDocument();
|
||||
expect(within(metricNameOptions).getByText('displayName_metric1')).toBeInTheDocument();
|
||||
expect(within(metricNameOptions).queryByText('displayName_metric2')).not.toBeInTheDocument();
|
||||
expect(within(metricNameOptions).queryByText('description_metric2')).not.toBeInTheDocument();
|
||||
expect(within(metricNameOptions).queryByText('displayName_metric3')).not.toBeInTheDocument();
|
||||
expect(within(metricNameOptions).queryByText('description_metric3')).not.toBeInTheDocument();
|
||||
|
||||
await select(screen.getByLabelText('Service'), 'Srv B', { container: document.body });
|
||||
expect(within(metricNameOptions).queryByText('displayName_metric1')).not.toBeInTheDocument();
|
||||
expect(within(metricNameOptions).queryByText('description_metric1')).not.toBeInTheDocument();
|
||||
expect(within(metricNameOptions).getByText('displayName_metric2')).toBeInTheDocument();
|
||||
expect(within(metricNameOptions).getByText('description_metric2')).toBeInTheDocument();
|
||||
expect(within(metricNameOptions).getByText('displayName_metric3')).toBeInTheDocument();
|
||||
expect(within(metricNameOptions).getByText('description_metric3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have a distinct list of services', async () => {
|
||||
const onChange = jest.fn();
|
||||
const datasource = createMockDatasource({
|
||||
getMetricTypes: jest.fn().mockResolvedValue([
|
||||
createMockMetricDescriptor({
|
||||
service: 'service_a',
|
||||
serviceShortName: 'srv_a',
|
||||
type: 'metric1',
|
||||
description: 'description_metric1',
|
||||
displayName: 'displayName_metric1',
|
||||
}),
|
||||
createMockMetricDescriptor({
|
||||
service: 'service_b',
|
||||
serviceShortName: 'srv_b',
|
||||
type: 'metric2',
|
||||
description: 'description_metric2',
|
||||
displayName: 'displayName_metric2',
|
||||
}),
|
||||
createMockMetricDescriptor({
|
||||
service: 'service_b',
|
||||
serviceShortName: 'srv_b',
|
||||
type: 'metric3',
|
||||
description: 'description_metric3',
|
||||
displayName: 'displayName_metric3',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
const query = createMockTimeSeriesList();
|
||||
|
||||
render(
|
||||
<Metrics
|
||||
refId="refId"
|
||||
metricType="metric1"
|
||||
projectName="projectName"
|
||||
templateVariableOptions={[]}
|
||||
datasource={datasource}
|
||||
onChange={onChange}
|
||||
onProjectChange={jest.fn()}
|
||||
query={query}
|
||||
>
|
||||
{() => <div />}
|
||||
</Metrics>
|
||||
);
|
||||
const service = await screen.findByLabelText('Service');
|
||||
await openMenu(service);
|
||||
expect(screen.getAllByLabelText('Select option').length).toEqual(2);
|
||||
});
|
||||
});
|
@ -1,197 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { startCase, uniqBy } from 'lodash';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/experimental';
|
||||
import { getSelectStyles, Select, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import CloudMonitoringDatasource from '../datasource';
|
||||
import { MetricDescriptor, TimeSeriesList } from '../types';
|
||||
|
||||
import { Project } from './Project';
|
||||
|
||||
export interface Props {
|
||||
refId: string;
|
||||
onChange: (metricDescriptor: MetricDescriptor) => void;
|
||||
templateVariableOptions: Array<SelectableValue<string>>;
|
||||
datasource: CloudMonitoringDatasource;
|
||||
projectName: string;
|
||||
metricType: string;
|
||||
query: TimeSeriesList;
|
||||
children: (metricDescriptor?: MetricDescriptor) => JSX.Element;
|
||||
onProjectChange: (query: TimeSeriesList) => void;
|
||||
}
|
||||
|
||||
export function Metrics(props: Props) {
|
||||
const [metricDescriptors, setMetricDescriptors] = useState<MetricDescriptor[]>([]);
|
||||
const [metricDescriptor, setMetricDescriptor] = useState<MetricDescriptor>();
|
||||
const [metrics, setMetrics] = useState<Array<SelectableValue<string>>>([]);
|
||||
const [services, setServices] = useState<Array<SelectableValue<string>>>([]);
|
||||
const [service, setService] = useState<string>('');
|
||||
|
||||
const theme = useTheme2();
|
||||
const selectStyles = getSelectStyles(theme);
|
||||
|
||||
const customStyle = useStyles2(getStyles);
|
||||
|
||||
const {
|
||||
onProjectChange,
|
||||
query,
|
||||
refId,
|
||||
metricType,
|
||||
templateVariableOptions,
|
||||
projectName,
|
||||
datasource,
|
||||
onChange,
|
||||
children,
|
||||
} = props;
|
||||
const { templateSrv } = datasource;
|
||||
|
||||
const getSelectedMetricDescriptor = useCallback(
|
||||
(metricDescriptors: MetricDescriptor[], metricType: string) => {
|
||||
return metricDescriptors.find((md) => md.type === templateSrv.replace(metricType))!;
|
||||
},
|
||||
[templateSrv]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const loadMetricDescriptors = async () => {
|
||||
if (projectName) {
|
||||
const metricDescriptors = await datasource.getMetricTypes(projectName);
|
||||
const services = getServicesList(metricDescriptors);
|
||||
setMetricDescriptors(metricDescriptors);
|
||||
setServices(services);
|
||||
}
|
||||
};
|
||||
loadMetricDescriptors();
|
||||
}, [datasource, projectName, customStyle, selectStyles.optionDescription]);
|
||||
|
||||
useEffect(() => {
|
||||
const getMetricsList = (metricDescriptors: MetricDescriptor[]) => {
|
||||
const selectedMetricDescriptor = getSelectedMetricDescriptor(metricDescriptors, metricType);
|
||||
if (!selectedMetricDescriptor) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const metricsByService = metricDescriptors
|
||||
.filter((m) => m.service === selectedMetricDescriptor.service)
|
||||
.map((m) => ({
|
||||
service: m.service,
|
||||
value: m.type,
|
||||
label: m.displayName,
|
||||
component: function optionComponent() {
|
||||
return (
|
||||
<div>
|
||||
<div className={customStyle}>{m.type}</div>
|
||||
<div className={selectStyles.optionDescription}>{m.description}</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
return metricsByService;
|
||||
};
|
||||
|
||||
const metrics = getMetricsList(metricDescriptors);
|
||||
const service = metrics.length > 0 ? metrics[0].service : '';
|
||||
const metricDescriptor = getSelectedMetricDescriptor(metricDescriptors, metricType);
|
||||
setMetricDescriptor(metricDescriptor);
|
||||
setMetrics(metrics);
|
||||
setService(service);
|
||||
}, [metricDescriptors, getSelectedMetricDescriptor, metricType, customStyle, selectStyles.optionDescription]);
|
||||
|
||||
const onServiceChange = ({ value: service }: any) => {
|
||||
const metrics = metricDescriptors
|
||||
.filter((m: MetricDescriptor) => m.service === templateSrv.replace(service))
|
||||
.map((m: MetricDescriptor) => ({
|
||||
service: m.service,
|
||||
value: m.type,
|
||||
label: m.displayName,
|
||||
description: m.description,
|
||||
}));
|
||||
|
||||
if (metrics.length > 0 && !metrics.some((m) => m.value === templateSrv.replace(metricType))) {
|
||||
onMetricTypeChange(metrics[0]);
|
||||
setService(service);
|
||||
setMetrics(metrics);
|
||||
} else {
|
||||
setService(service);
|
||||
setMetrics(metrics);
|
||||
}
|
||||
};
|
||||
|
||||
const onMetricTypeChange = ({ value }: SelectableValue<string>) => {
|
||||
const metricDescriptor = getSelectedMetricDescriptor(metricDescriptors, value!);
|
||||
setMetricDescriptor(metricDescriptor);
|
||||
onChange({ ...metricDescriptor, type: value! });
|
||||
};
|
||||
|
||||
const getServicesList = (metricDescriptors: MetricDescriptor[]) => {
|
||||
const services = metricDescriptors.map((m) => ({
|
||||
value: m.service,
|
||||
label: startCase(m.serviceShortName),
|
||||
}));
|
||||
|
||||
return services.length > 0 ? uniqBy(services, (s) => s.value) : [];
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditorRow>
|
||||
<EditorFieldGroup>
|
||||
<Project
|
||||
refId={refId}
|
||||
templateVariableOptions={templateVariableOptions}
|
||||
projectName={projectName}
|
||||
datasource={datasource}
|
||||
onChange={(projectName) => {
|
||||
onProjectChange({ ...query, projectName });
|
||||
}}
|
||||
/>
|
||||
|
||||
<EditorField label="Service" width="auto">
|
||||
<Select
|
||||
width="auto"
|
||||
onChange={onServiceChange}
|
||||
value={[...services, ...templateVariableOptions].find((s) => s.value === service)}
|
||||
options={[
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: templateVariableOptions,
|
||||
},
|
||||
...services,
|
||||
]}
|
||||
placeholder="Select Services"
|
||||
inputId={`${props.refId}-service`}
|
||||
/>
|
||||
</EditorField>
|
||||
<EditorField label="Metric name" width="auto">
|
||||
<Select
|
||||
width="auto"
|
||||
onChange={onMetricTypeChange}
|
||||
value={[...metrics, ...templateVariableOptions].find((s) => s.value === metricType)}
|
||||
options={[
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: templateVariableOptions,
|
||||
},
|
||||
...metrics,
|
||||
]}
|
||||
placeholder="Select Metric"
|
||||
inputId={`${props.refId}-select-metric`}
|
||||
/>
|
||||
</EditorField>
|
||||
</EditorFieldGroup>
|
||||
</EditorRow>
|
||||
|
||||
{children(metricDescriptor)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => css`
|
||||
label: grafana-select-option-description;
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
color: ${theme.colors.text.secondary};
|
||||
`;
|
@ -0,0 +1,286 @@
|
||||
import { act, render, screen, waitFor, within } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { openMenu, select } from 'react-select-event';
|
||||
|
||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
|
||||
import { createMockDatasource } from '../__mocks__/cloudMonitoringDatasource';
|
||||
import { createMockMetricDescriptor } from '../__mocks__/cloudMonitoringMetricDescriptor';
|
||||
import { createMockTimeSeriesList } from '../__mocks__/cloudMonitoringQuery';
|
||||
import { MetricKind, PreprocessorType } from '../types';
|
||||
|
||||
import { defaultTimeSeriesList } from './MetricQueryEditor';
|
||||
import { VisualMetricQueryEditor } from './VisualMetricQueryEditor';
|
||||
|
||||
const defaultProps = {
|
||||
refId: 'refId',
|
||||
customMetaData: {},
|
||||
variableOptionGroup: { options: [] },
|
||||
aliasBy: '',
|
||||
onChangeAliasBy: jest.fn(),
|
||||
};
|
||||
|
||||
describe('VisualMetricQueryEditor', () => {
|
||||
it('renders metrics fields', async () => {
|
||||
const onChange = jest.fn();
|
||||
const query = createMockTimeSeriesList();
|
||||
const datasource = createMockDatasource();
|
||||
|
||||
render(<VisualMetricQueryEditor {...defaultProps} onChange={onChange} datasource={datasource} query={query} />);
|
||||
|
||||
expect(await screen.findByLabelText('Service')).toBeInTheDocument();
|
||||
expect(await screen.findByLabelText('Metric name')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('can select a service', async () => {
|
||||
const onChange = jest.fn();
|
||||
const query = createMockTimeSeriesList();
|
||||
const mockMetricDescriptor = createMockMetricDescriptor();
|
||||
const datasource = createMockDatasource({
|
||||
getMetricTypes: jest.fn().mockResolvedValue([mockMetricDescriptor]),
|
||||
getLabels: jest.fn().mockResolvedValue([]),
|
||||
});
|
||||
|
||||
render(<VisualMetricQueryEditor {...defaultProps} onChange={onChange} datasource={datasource} query={query} />);
|
||||
|
||||
const service = await screen.findByLabelText('Service');
|
||||
await openMenu(service);
|
||||
await act(async () => {
|
||||
await select(service, 'Srv', { container: document.body });
|
||||
expect(onChange).toBeCalledWith(
|
||||
expect.objectContaining({ filters: ['metric.type', '=', mockMetricDescriptor.type] })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('can select a metric name', async () => {
|
||||
const onChange = jest.fn();
|
||||
const query = createMockTimeSeriesList();
|
||||
const mockMetricDescriptor = createMockMetricDescriptor({ displayName: 'metricName_test', type: 'test_type' });
|
||||
const datasource = createMockDatasource({
|
||||
getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor(), mockMetricDescriptor]),
|
||||
getLabels: jest.fn().mockResolvedValue([]),
|
||||
});
|
||||
|
||||
render(<VisualMetricQueryEditor {...defaultProps} onChange={onChange} datasource={datasource} query={query} />);
|
||||
|
||||
const service = await screen.findByLabelText('Service');
|
||||
await openMenu(service);
|
||||
await act(async () => {
|
||||
await select(service, 'Srv', { container: document.body });
|
||||
});
|
||||
const metricName = await screen.findByLabelText('Metric name');
|
||||
await openMenu(metricName);
|
||||
await waitFor(() => expect(document.body).toHaveTextContent('metricName_test'));
|
||||
await act(async () => {
|
||||
await select(metricName, 'metricName_test', { container: document.body });
|
||||
expect(onChange).toBeCalledWith(
|
||||
expect.objectContaining({ filters: ['metric.type', '=', mockMetricDescriptor.type] })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render available metric options according to the selected service', async () => {
|
||||
const onChange = jest.fn();
|
||||
const query = createMockTimeSeriesList();
|
||||
const datasource = createMockDatasource({
|
||||
getMetricTypes: jest.fn().mockResolvedValue([
|
||||
createMockMetricDescriptor({
|
||||
service: 'service_a',
|
||||
serviceShortName: 'srv_a',
|
||||
type: 'metric1',
|
||||
description: 'description_metric1',
|
||||
displayName: 'displayName_metric1',
|
||||
}),
|
||||
createMockMetricDescriptor({
|
||||
service: 'service_b',
|
||||
serviceShortName: 'srv_b',
|
||||
type: 'metric2',
|
||||
description: 'description_metric2',
|
||||
displayName: 'displayName_metric2',
|
||||
}),
|
||||
createMockMetricDescriptor({
|
||||
service: 'service_b',
|
||||
serviceShortName: 'srv_b',
|
||||
type: 'metric3',
|
||||
description: 'description_metric3',
|
||||
displayName: 'displayName_metric3',
|
||||
}),
|
||||
]),
|
||||
getLabels: jest.fn().mockResolvedValue([]),
|
||||
});
|
||||
|
||||
render(<VisualMetricQueryEditor {...defaultProps} onChange={onChange} datasource={datasource} query={query} />);
|
||||
|
||||
const service = await screen.findByLabelText('Service');
|
||||
await openMenu(service);
|
||||
await act(async () => {
|
||||
await select(service, 'Srv A', { container: document.body });
|
||||
});
|
||||
const metricName = await screen.findByLabelText('Metric name');
|
||||
await openMenu(metricName);
|
||||
|
||||
const metricNameOptions = screen.getByLabelText('Select options menu');
|
||||
expect(within(metricNameOptions).getByText('description_metric1')).toBeInTheDocument();
|
||||
expect(within(metricNameOptions).getByText('displayName_metric1')).toBeInTheDocument();
|
||||
expect(within(metricNameOptions).queryByText('displayName_metric2')).not.toBeInTheDocument();
|
||||
expect(within(metricNameOptions).queryByText('description_metric2')).not.toBeInTheDocument();
|
||||
expect(within(metricNameOptions).queryByText('displayName_metric3')).not.toBeInTheDocument();
|
||||
expect(within(metricNameOptions).queryByText('description_metric3')).not.toBeInTheDocument();
|
||||
|
||||
await openMenu(service);
|
||||
await act(async () => {
|
||||
await select(service, 'Srv B', { container: document.body });
|
||||
});
|
||||
expect(within(metricNameOptions).queryByText('displayName_metric1')).not.toBeInTheDocument();
|
||||
expect(within(metricNameOptions).queryByText('description_metric1')).not.toBeInTheDocument();
|
||||
expect(within(metricNameOptions).getByText('displayName_metric2')).toBeInTheDocument();
|
||||
expect(within(metricNameOptions).getByText('description_metric2')).toBeInTheDocument();
|
||||
expect(within(metricNameOptions).getByText('displayName_metric3')).toBeInTheDocument();
|
||||
expect(within(metricNameOptions).getByText('description_metric3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have a distinct list of services', async () => {
|
||||
const onChange = jest.fn();
|
||||
const datasource = createMockDatasource({
|
||||
getMetricTypes: jest.fn().mockResolvedValue([
|
||||
createMockMetricDescriptor({
|
||||
service: 'service_a',
|
||||
serviceShortName: 'srv_a',
|
||||
type: 'metric1',
|
||||
description: 'description_metric1',
|
||||
displayName: 'displayName_metric1',
|
||||
}),
|
||||
createMockMetricDescriptor({
|
||||
service: 'service_b',
|
||||
serviceShortName: 'srv_b',
|
||||
type: 'metric2',
|
||||
description: 'description_metric2',
|
||||
displayName: 'displayName_metric2',
|
||||
}),
|
||||
createMockMetricDescriptor({
|
||||
service: 'service_b',
|
||||
serviceShortName: 'srv_b',
|
||||
type: 'metric3',
|
||||
description: 'description_metric3',
|
||||
displayName: 'displayName_metric3',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
const query = createMockTimeSeriesList();
|
||||
|
||||
render(<VisualMetricQueryEditor {...defaultProps} onChange={onChange} datasource={datasource} query={query} />);
|
||||
const service = await screen.findByLabelText('Service');
|
||||
await openMenu(service);
|
||||
expect(screen.getAllByLabelText('Select option').length).toEqual(2);
|
||||
});
|
||||
|
||||
it('resets query to default when service changes', async () => {
|
||||
const query = createMockTimeSeriesList({ filters: ['metric.test_label', '=', 'test', 'AND'] });
|
||||
const onChange = jest.fn();
|
||||
const datasource = createMockDatasource({
|
||||
getMetricTypes: jest
|
||||
.fn()
|
||||
.mockResolvedValue([
|
||||
createMockMetricDescriptor(),
|
||||
createMockMetricDescriptor({ type: 'type2', service: 'service2', serviceShortName: 'srv2' }),
|
||||
]),
|
||||
getLabels: jest.fn().mockResolvedValue([]),
|
||||
});
|
||||
const defaultQuery = { ...query, ...defaultTimeSeriesList(datasource), filters: ['metric.type', '=', 'type2'] };
|
||||
|
||||
render(<VisualMetricQueryEditor {...defaultProps} onChange={onChange} datasource={datasource} query={query} />);
|
||||
|
||||
expect(screen.getByText('metric.test_label')).toBeInTheDocument();
|
||||
const service = await screen.findByLabelText('Service');
|
||||
openMenu(service);
|
||||
await select(service, 'Srv 2', { container: document.body });
|
||||
expect(onChange).toBeCalledWith(expect.objectContaining({ filters: ['metric.type', '=', 'type2'] }));
|
||||
expect(query).toEqual(defaultQuery);
|
||||
expect(screen.queryByText('metric.test_label')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('resets query to defaults (except filters) when metric changes', async () => {
|
||||
const groupBys = ['metric.test_groupby'];
|
||||
const query = createMockTimeSeriesList({
|
||||
filters: ['metric.test_label', '=', 'test', 'AND', 'metric.type', '=', 'type'],
|
||||
groupBys,
|
||||
preprocessor: PreprocessorType.Delta,
|
||||
});
|
||||
const onChange = jest.fn();
|
||||
const datasource = createMockDatasource({
|
||||
getMetricTypes: jest
|
||||
.fn()
|
||||
.mockResolvedValue([
|
||||
createMockMetricDescriptor(),
|
||||
createMockMetricDescriptor({ type: 'type2', displayName: 'metricName2', metricKind: MetricKind.GAUGE }),
|
||||
]),
|
||||
getLabels: jest.fn().mockResolvedValue({ 'metric.test_groupby': '' }),
|
||||
templateSrv: new TemplateSrv(),
|
||||
});
|
||||
const defaultQuery = { ...query, ...defaultTimeSeriesList(datasource), filters: query.filters };
|
||||
|
||||
render(<VisualMetricQueryEditor {...defaultProps} onChange={onChange} datasource={datasource} query={query} />);
|
||||
expect(document.body).toHaveTextContent('metric.test_label');
|
||||
expect(await screen.findByText('Delta')).toBeInTheDocument();
|
||||
expect(await screen.findByText('metric.test_groupby')).toBeInTheDocument();
|
||||
const metric = await screen.findByLabelText('Metric name');
|
||||
openMenu(metric);
|
||||
await select(metric, 'metricName2', { container: document.body });
|
||||
expect(onChange).toBeCalledWith(
|
||||
expect.objectContaining({ filters: ['metric.test_label', '=', 'test', 'AND', 'metric.type', '=', 'type2'] })
|
||||
);
|
||||
expect(query).toEqual(defaultQuery);
|
||||
expect(document.body).toHaveTextContent('metric.test_label');
|
||||
expect(await screen.queryByText('Delta')).not.toBeInTheDocument();
|
||||
expect(await screen.queryByText('metric.test_groupby')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('updates labels on time range change', async () => {
|
||||
const timeSrv = getTimeSrv();
|
||||
const query = createMockTimeSeriesList();
|
||||
const onChange = jest.fn();
|
||||
const datasource = createMockDatasource({
|
||||
getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor()]),
|
||||
getLabels: jest
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
timeSrv.time.from === 'now-6h' ? { 'metric.test_groupby': '' } : { 'metric.test_groupby_1': '' }
|
||||
),
|
||||
templateSrv: new TemplateSrv(),
|
||||
timeSrv,
|
||||
});
|
||||
|
||||
const { rerender } = render(
|
||||
<VisualMetricQueryEditor {...defaultProps} onChange={onChange} datasource={datasource} query={query} />
|
||||
);
|
||||
|
||||
const service = await screen.findByLabelText('Service');
|
||||
await openMenu(service);
|
||||
await act(async () => {
|
||||
await select(service, 'Srv', { container: document.body });
|
||||
});
|
||||
const metricName = await screen.findByLabelText('Metric name');
|
||||
await openMenu(metricName);
|
||||
await waitFor(() => expect(document.body).toHaveTextContent('metricName'));
|
||||
await act(async () => {
|
||||
await select(metricName, 'metricName', { container: document.body });
|
||||
});
|
||||
const groupBy = await screen.findByLabelText('Group by');
|
||||
await openMenu(groupBy);
|
||||
await waitFor(() => expect(document.body).toHaveTextContent('metric.test_groupby'));
|
||||
await act(async () => {
|
||||
timeSrv.setTime({ from: 'now-12h', to: 'now' });
|
||||
const datasourceUpdated = createMockDatasource({
|
||||
timeSrv,
|
||||
getLabels: jest.fn().mockResolvedValue({ 'metric.test_groupby_1': '' }),
|
||||
});
|
||||
rerender(
|
||||
<VisualMetricQueryEditor {...defaultProps} onChange={onChange} datasource={datasourceUpdated} query={query} />
|
||||
);
|
||||
await openMenu(groupBy);
|
||||
await waitFor(() => expect(document.body).toHaveTextContent('metric.test_groupby_1'));
|
||||
});
|
||||
});
|
||||
});
|
@ -1,7 +1,10 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { startCase, uniqBy } from 'lodash';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { EditorRow } from '@grafana/experimental';
|
||||
import { GrafanaTheme2, SelectableValue, TimeRange } from '@grafana/data';
|
||||
import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/experimental';
|
||||
import { getSelectStyles, Select, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import CloudMonitoringDatasource from '../datasource';
|
||||
import { getAlignmentPickerData, getMetricType, setMetricType } from '../functions';
|
||||
@ -11,106 +14,263 @@ import { AliasBy } from './AliasBy';
|
||||
import { Alignment } from './Alignment';
|
||||
import { GroupBy } from './GroupBy';
|
||||
import { LabelFilter } from './LabelFilter';
|
||||
import { Metrics } from './Metrics';
|
||||
import { defaultTimeSeriesList } from './MetricQueryEditor';
|
||||
import { Preprocessor } from './Preprocessor';
|
||||
import { Project } from './Project';
|
||||
|
||||
export interface Props {
|
||||
refId: string;
|
||||
customMetaData: CustomMetaData;
|
||||
variableOptionGroup: SelectableValue<string>;
|
||||
onChange: (query: TimeSeriesList) => void;
|
||||
query: TimeSeriesList;
|
||||
datasource: CloudMonitoringDatasource;
|
||||
query: TimeSeriesList;
|
||||
variableOptionGroup: SelectableValue<string>;
|
||||
aliasBy?: string;
|
||||
onChangeAliasBy: (aliasBy: string) => void;
|
||||
}
|
||||
|
||||
function Editor({
|
||||
export function Editor({
|
||||
refId,
|
||||
query,
|
||||
datasource,
|
||||
onChange,
|
||||
customMetaData,
|
||||
datasource,
|
||||
query,
|
||||
variableOptionGroup,
|
||||
customMetaData,
|
||||
aliasBy,
|
||||
onChangeAliasBy,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const [labels, setLabels] = useState<{ [k: string]: any }>({});
|
||||
const [metricDescriptors, setMetricDescriptors] = useState<MetricDescriptor[]>([]);
|
||||
const [metricDescriptor, setMetricDescriptor] = useState<MetricDescriptor>();
|
||||
const [metrics, setMetrics] = useState<Array<SelectableValue<string>>>([]);
|
||||
const [services, setServices] = useState<Array<SelectableValue<string>>>([]);
|
||||
const [service, setService] = useState<string>('');
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>({ ...datasource.timeSrv.timeRange() });
|
||||
|
||||
const useTime = (time: TimeRange) => {
|
||||
if (timeRange !== null && (timeRange.raw.from !== time.raw.from || timeRange.raw.to !== time.raw.to)) {
|
||||
setTimeRange({ ...time });
|
||||
}
|
||||
};
|
||||
|
||||
useTime(datasource.timeSrv.timeRange());
|
||||
|
||||
const theme = useTheme2();
|
||||
const selectStyles = getSelectStyles(theme);
|
||||
|
||||
const customStyle = useStyles2(getStyles);
|
||||
|
||||
const { projectName, groupBys, crossSeriesReducer } = query;
|
||||
const metricType = getMetricType(query);
|
||||
const { templateSrv } = datasource;
|
||||
|
||||
const getSelectedMetricDescriptor = useCallback(
|
||||
(metricDescriptors: MetricDescriptor[], metricType: string) => {
|
||||
return metricDescriptors.find((md) => md.type === templateSrv.replace(metricType))!;
|
||||
},
|
||||
[templateSrv]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (projectName && metricType) {
|
||||
datasource.getLabels(metricType, refId, projectName).then((labels) => setLabels(labels));
|
||||
datasource
|
||||
.getLabels(metricType, refId, projectName, { groupBys, crossSeriesReducer }, timeRange)
|
||||
.then((labels) => setLabels(labels));
|
||||
}
|
||||
}, [datasource, groupBys, metricType, projectName, refId, crossSeriesReducer]);
|
||||
}, [datasource, groupBys, metricType, projectName, refId, crossSeriesReducer, timeRange]);
|
||||
|
||||
const onMetricTypeChange = useCallback(
|
||||
({ valueType, metricKind, type }: MetricDescriptor) => {
|
||||
const preprocessor =
|
||||
metricKind === MetricKind.GAUGE || valueType === ValueTypes.DISTRIBUTION
|
||||
? PreprocessorType.None
|
||||
: PreprocessorType.Rate;
|
||||
const { perSeriesAligner } = getAlignmentPickerData(valueType, metricKind, query.perSeriesAligner, preprocessor);
|
||||
onChange({
|
||||
...setMetricType(
|
||||
{
|
||||
...query,
|
||||
perSeriesAligner,
|
||||
useEffect(() => {
|
||||
const loadMetricDescriptors = async () => {
|
||||
if (projectName) {
|
||||
const metricDescriptors = await datasource.getMetricTypes(projectName);
|
||||
const services = getServicesList(metricDescriptors);
|
||||
setMetricDescriptors(metricDescriptors);
|
||||
setServices(services);
|
||||
}
|
||||
};
|
||||
loadMetricDescriptors();
|
||||
}, [datasource, projectName, customStyle, selectStyles.optionDescription]);
|
||||
|
||||
useEffect(() => {
|
||||
const getMetricsList = (metricDescriptors: MetricDescriptor[]) => {
|
||||
const selectedMetricDescriptor = getSelectedMetricDescriptor(metricDescriptors, metricType);
|
||||
if (!selectedMetricDescriptor) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const metricsByService = metricDescriptors
|
||||
.filter((m) => m.service === selectedMetricDescriptor.service)
|
||||
.map((m) => ({
|
||||
service: m.service,
|
||||
value: m.type,
|
||||
label: m.displayName,
|
||||
component: function optionComponent() {
|
||||
return (
|
||||
<div>
|
||||
<div className={customStyle}>{m.type}</div>
|
||||
<div className={selectStyles.optionDescription}>{m.description}</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
type
|
||||
),
|
||||
preprocessor,
|
||||
});
|
||||
},
|
||||
[onChange, query]
|
||||
);
|
||||
}));
|
||||
return metricsByService;
|
||||
};
|
||||
|
||||
const metrics = getMetricsList(metricDescriptors);
|
||||
const service = metrics.length > 0 ? metrics[0].service : '';
|
||||
const metricDescriptor = getSelectedMetricDescriptor(metricDescriptors, metricType);
|
||||
setMetricDescriptor(metricDescriptor);
|
||||
setMetrics(metrics);
|
||||
setService(service);
|
||||
}, [metricDescriptors, getSelectedMetricDescriptor, metricType, customStyle, selectStyles.optionDescription]);
|
||||
|
||||
const onServiceChange = ({ value: service }: SelectableValue<string>) => {
|
||||
const metrics = metricDescriptors
|
||||
.filter((m: MetricDescriptor) => m.service === templateSrv.replace(service))
|
||||
.map((m: MetricDescriptor) => ({
|
||||
service: m.service,
|
||||
value: m.type,
|
||||
label: m.displayName,
|
||||
description: m.description,
|
||||
}));
|
||||
// On service change reset all query values except the project name
|
||||
query.filters = [];
|
||||
|
||||
if (metrics.length > 0 && !metrics.some((m) => m.value === templateSrv.replace(metricType))) {
|
||||
onMetricTypeChange(metrics[0]);
|
||||
setService(service!);
|
||||
setMetrics(metrics);
|
||||
} else {
|
||||
setService(service!);
|
||||
setMetrics(metrics);
|
||||
}
|
||||
};
|
||||
|
||||
const getServicesList = (metricDescriptors: MetricDescriptor[]) => {
|
||||
const services = metricDescriptors.map((m) => ({
|
||||
value: m.service,
|
||||
label: startCase(m.serviceShortName),
|
||||
}));
|
||||
|
||||
return services.length > 0 ? uniqBy(services, (s) => s.value) : [];
|
||||
};
|
||||
|
||||
const onMetricTypeChange = ({ value }: SelectableValue<string>) => {
|
||||
const metricDescriptor = getSelectedMetricDescriptor(metricDescriptors, value!);
|
||||
setMetricDescriptor(metricDescriptor);
|
||||
const { metricKind, valueType } = metricDescriptor;
|
||||
const preprocessor =
|
||||
metricKind === MetricKind.GAUGE || valueType === ValueTypes.DISTRIBUTION
|
||||
? PreprocessorType.None
|
||||
: PreprocessorType.Rate;
|
||||
const { perSeriesAligner } = getAlignmentPickerData(valueType, metricKind, query.perSeriesAligner, preprocessor);
|
||||
|
||||
// On metric name change reset query to defaults except project name and filters
|
||||
Object.assign(query, {
|
||||
...defaultTimeSeriesList(datasource),
|
||||
projectName: query.projectName,
|
||||
filters: query.filters,
|
||||
});
|
||||
onChange({
|
||||
...setMetricType(
|
||||
{
|
||||
...query,
|
||||
perSeriesAligner,
|
||||
},
|
||||
value!
|
||||
),
|
||||
preprocessor,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Metrics
|
||||
refId={refId}
|
||||
projectName={query.projectName}
|
||||
metricType={metricType}
|
||||
templateVariableOptions={variableOptionGroup.options}
|
||||
datasource={datasource}
|
||||
onChange={onMetricTypeChange}
|
||||
onProjectChange={onChange}
|
||||
query={query}
|
||||
>
|
||||
{(metric) => (
|
||||
<>
|
||||
<LabelFilter
|
||||
labels={labels}
|
||||
filters={query.filters!}
|
||||
onChange={(filters: string[]) => onChange({ ...query, filters })}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
<>
|
||||
<EditorRow>
|
||||
<EditorFieldGroup>
|
||||
<Project
|
||||
refId={refId}
|
||||
templateVariableOptions={variableOptionGroup.options}
|
||||
projectName={projectName}
|
||||
datasource={datasource}
|
||||
onChange={(projectName) => {
|
||||
onChange({ ...query, projectName });
|
||||
}}
|
||||
/>
|
||||
<EditorRow>
|
||||
<Preprocessor metricDescriptor={metric} query={query} onChange={onChange} />
|
||||
<GroupBy
|
||||
refId={refId}
|
||||
labels={Object.keys(labels)}
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
metricDescriptor={metric}
|
||||
|
||||
<EditorField label="Service" width="auto">
|
||||
<Select
|
||||
width="auto"
|
||||
onChange={onServiceChange}
|
||||
value={[...services, ...variableOptionGroup.options].find((s) => s.value === service)}
|
||||
options={[
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: variableOptionGroup.options,
|
||||
},
|
||||
...services,
|
||||
]}
|
||||
placeholder="Select Services"
|
||||
inputId={`${refId}-service`}
|
||||
/>
|
||||
<Alignment
|
||||
refId={refId}
|
||||
datasource={datasource}
|
||||
templateVariableOptions={variableOptionGroup.options}
|
||||
query={query}
|
||||
customMetaData={customMetaData}
|
||||
onChange={onChange}
|
||||
metricDescriptor={metric}
|
||||
preprocessor={query.preprocessor}
|
||||
</EditorField>
|
||||
<EditorField label="Metric name" width="auto">
|
||||
<Select
|
||||
width="auto"
|
||||
onChange={onMetricTypeChange}
|
||||
value={[...metrics, ...variableOptionGroup.options].find((s) => s.value === metricType)}
|
||||
options={[
|
||||
{
|
||||
label: 'Template Variables',
|
||||
options: variableOptionGroup.options,
|
||||
},
|
||||
...metrics,
|
||||
]}
|
||||
placeholder="Select Metric"
|
||||
inputId={`${refId}-select-metric`}
|
||||
/>
|
||||
<AliasBy refId={refId} value={aliasBy} onChange={onChangeAliasBy} />
|
||||
</EditorRow>
|
||||
</>
|
||||
)}
|
||||
</Metrics>
|
||||
</EditorField>
|
||||
</EditorFieldGroup>
|
||||
</EditorRow>
|
||||
|
||||
<>
|
||||
<LabelFilter
|
||||
labels={labels}
|
||||
filters={query.filters!}
|
||||
onChange={(filters: string[]) => onChange({ ...query, filters })}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
/>
|
||||
<EditorRow>
|
||||
<Preprocessor metricDescriptor={metricDescriptor} query={query} onChange={onChange} />
|
||||
<GroupBy
|
||||
refId={refId}
|
||||
labels={Object.keys(labels)}
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
metricDescriptor={metricDescriptor}
|
||||
/>
|
||||
<Alignment
|
||||
refId={refId}
|
||||
datasource={datasource}
|
||||
templateVariableOptions={variableOptionGroup.options}
|
||||
query={query}
|
||||
customMetaData={customMetaData}
|
||||
onChange={onChange}
|
||||
metricDescriptor={metricDescriptor}
|
||||
preprocessor={query.preprocessor}
|
||||
/>
|
||||
<AliasBy refId={refId} value={aliasBy} onChange={onChangeAliasBy} />
|
||||
</EditorRow>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => css`
|
||||
label: grafana-select-option-description;
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
color: ${theme.colors.text.secondary};
|
||||
`;
|
||||
|
||||
export const VisualMetricQueryEditor = React.memo(Editor);
|
||||
|
@ -1,5 +1,4 @@
|
||||
export { Project } from './Project';
|
||||
export { Metrics } from './Metrics';
|
||||
export { GroupBy } from './GroupBy';
|
||||
export { Alignment } from './Alignment';
|
||||
export { LabelFilter } from './LabelFilter';
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
DataSourceInstanceSettings,
|
||||
ScopedVars,
|
||||
SelectableValue,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { DataSourceWithBackend, getBackendSrv, toDataQueryResponse, BackendSrv } from '@grafana/runtime';
|
||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
@ -39,7 +40,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
||||
constructor(
|
||||
private instanceSettings: DataSourceInstanceSettings<CloudMonitoringOptions>,
|
||||
public templateSrv: TemplateSrv = getTemplateSrv(),
|
||||
private readonly timeSrv: TimeSrv = getTimeSrv()
|
||||
readonly timeSrv: TimeSrv = getTimeSrv()
|
||||
) {
|
||||
super(instanceSettings);
|
||||
this.authenticationType = instanceSettings.jsonData.authenticationType || 'jwt';
|
||||
@ -89,7 +90,13 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
||||
};
|
||||
}
|
||||
|
||||
async getLabels(metricType: string, refId: string, projectName: string, aggregation?: Aggregation) {
|
||||
async getLabels(
|
||||
metricType: string,
|
||||
refId: string,
|
||||
projectName: string,
|
||||
aggregation?: Aggregation,
|
||||
timeRange?: TimeRange
|
||||
) {
|
||||
const options = {
|
||||
targets: [
|
||||
{
|
||||
@ -107,7 +114,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
||||
),
|
||||
},
|
||||
],
|
||||
range: this.timeSrv.timeRange(),
|
||||
range: timeRange ?? this.timeSrv.timeRange(),
|
||||
};
|
||||
|
||||
const queries = options.targets;
|
||||
|
Loading…
Reference in New Issue
Block a user