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:
Andreas Christou 2023-01-16 17:57:12 +00:00 committed by GitHub
parent 021eda7aad
commit f135c6cbf1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 528 additions and 477 deletions

View File

@ -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"]

View File

@ -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,
};

View File

@ -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);
});
});

View File

@ -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};
`;

View File

@ -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'));
});
});
});

View File

@ -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);

View File

@ -1,5 +1,4 @@
export { Project } from './Project';
export { Metrics } from './Metrics';
export { GroupBy } from './GroupBy';
export { Alignment } from './Alignment';
export { LabelFilter } from './LabelFilter';

View File

@ -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;