From f135c6cbf1c6477c9f42cc492a63d3fe82262f10 Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Mon, 16 Jan 2023 17:57:12 +0000 Subject: [PATCH] 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 --- .betterer.results | 3 - .../__mocks__/cloudMonitoringDatasource.ts | 2 + .../components/Metrics.test.tsx | 203 ------------ .../cloud-monitoring/components/Metrics.tsx | 197 ------------ .../VisualMetricQueryEditor.test.tsx | 286 +++++++++++++++++ .../components/VisualMetricQueryEditor.tsx | 300 ++++++++++++++---- .../cloud-monitoring/components/index.ts | 1 - .../datasource/cloud-monitoring/datasource.ts | 13 +- 8 files changed, 528 insertions(+), 477 deletions(-) delete mode 100644 public/app/plugins/datasource/cloud-monitoring/components/Metrics.test.tsx delete mode 100644 public/app/plugins/datasource/cloud-monitoring/components/Metrics.tsx create mode 100644 public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.test.tsx diff --git a/.betterer.results b/.betterer.results index 86ada2c5135..43be61f43d5 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"] diff --git a/public/app/plugins/datasource/cloud-monitoring/__mocks__/cloudMonitoringDatasource.ts b/public/app/plugins/datasource/cloud-monitoring/__mocks__/cloudMonitoringDatasource.ts index 5ce6325ac66..87c377637bf 100644 --- a/public/app/plugins/datasource/cloud-monitoring/__mocks__/cloudMonitoringDatasource.ts +++ b/public/app/plugins/datasource/cloud-monitoring/__mocks__/cloudMonitoringDatasource.ts @@ -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) => { templateSrv, getSLOServices: jest.fn().mockResolvedValue([]), migrateQuery: jest.fn().mockImplementation((query) => query), + timeSrv: getTimeSrv(), ...overrides, }; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Metrics.test.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Metrics.test.tsx deleted file mode 100644 index 74973ba7abe..00000000000 --- a/public/app/plugins/datasource/cloud-monitoring/components/Metrics.test.tsx +++ /dev/null @@ -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( - - {() =>
} - - ); - - 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( - - {() =>
} - - ); - - 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( - - {() =>
} - - ); - - 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( - - {() =>
} - - ); - - 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( - - {() =>
} - - ); - const service = await screen.findByLabelText('Service'); - await openMenu(service); - expect(screen.getAllByLabelText('Select option').length).toEqual(2); - }); -}); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Metrics.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Metrics.tsx deleted file mode 100644 index 980c4ecb6ee..00000000000 --- a/public/app/plugins/datasource/cloud-monitoring/components/Metrics.tsx +++ /dev/null @@ -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>; - 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([]); - const [metricDescriptor, setMetricDescriptor] = useState(); - const [metrics, setMetrics] = useState>>([]); - const [services, setServices] = useState>>([]); - const [service, setService] = useState(''); - - 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 ( -
-
{m.type}
-
{m.description}
-
- ); - }, - })); - 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) => { - 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 ( - <> - - - { - onProjectChange({ ...query, projectName }); - }} - /> - - - s.value === metricType)} - options={[ - { - label: 'Template Variables', - options: templateVariableOptions, - }, - ...metrics, - ]} - placeholder="Select Metric" - inputId={`${props.refId}-select-metric`} - /> - - - - - {children(metricDescriptor)} - - ); -} - -const getStyles = (theme: GrafanaTheme2) => css` - label: grafana-select-option-description; - font-weight: normal; - font-style: italic; - color: ${theme.colors.text.secondary}; -`; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.test.tsx b/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.test.tsx new file mode 100644 index 00000000000..b408714dc49 --- /dev/null +++ b/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.test.tsx @@ -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(); + + 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(); + + 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(); + + 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(); + + 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(); + 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(); + + 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(); + 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( + + ); + + 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( + + ); + await openMenu(groupBy); + await waitFor(() => expect(document.body).toHaveTextContent('metric.test_groupby_1')); + }); + }); +}); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx index ebaae3bf0aa..be1fe2a1b03 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx @@ -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; onChange: (query: TimeSeriesList) => void; - query: TimeSeriesList; datasource: CloudMonitoringDatasource; + query: TimeSeriesList; + variableOptionGroup: SelectableValue; aliasBy?: string; onChangeAliasBy: (aliasBy: string) => void; } -function Editor({ +export function Editor({ refId, - query, - datasource, onChange, - customMetaData, + datasource, + query, variableOptionGroup, + customMetaData, aliasBy, onChangeAliasBy, }: React.PropsWithChildren) { const [labels, setLabels] = useState<{ [k: string]: any }>({}); + const [metricDescriptors, setMetricDescriptors] = useState([]); + const [metricDescriptor, setMetricDescriptor] = useState(); + const [metrics, setMetrics] = useState>>([]); + const [services, setServices] = useState>>([]); + const [service, setService] = useState(''); + const [timeRange, setTimeRange] = useState({ ...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 ( +
+
{m.type}
+
{m.description}
+
+ ); }, - 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) => { + 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) => { + 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 ( - - {(metric) => ( - <> - onChange({ ...query, filters })} - variableOptionGroup={variableOptionGroup} + <> + + + { + onChange({ ...query, projectName }); + }} /> - - - + s.value === metricType)} + options={[ + { + label: 'Template Variables', + options: variableOptionGroup.options, + }, + ...metrics, + ]} + placeholder="Select Metric" + inputId={`${refId}-select-metric`} /> - - - - )} - + + + + + <> + onChange({ ...query, filters })} + variableOptionGroup={variableOptionGroup} + /> + + + + + + + + ); } +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); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/index.ts b/public/app/plugins/datasource/cloud-monitoring/components/index.ts index 1651a77898c..734d8c47c22 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/index.ts +++ b/public/app/plugins/datasource/cloud-monitoring/components/index.ts @@ -1,5 +1,4 @@ export { Project } from './Project'; -export { Metrics } from './Metrics'; export { GroupBy } from './GroupBy'; export { Alignment } from './Alignment'; export { LabelFilter } from './LabelFilter'; diff --git a/public/app/plugins/datasource/cloud-monitoring/datasource.ts b/public/app/plugins/datasource/cloud-monitoring/datasource.ts index 2211c09942a..60df2221082 100644 --- a/public/app/plugins/datasource/cloud-monitoring/datasource.ts +++ b/public/app/plugins/datasource/cloud-monitoring/datasource.ts @@ -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, 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;