From f9becc2d4fc587dd2cbc8fbc9f5fa34a4c723865 Mon Sep 17 00:00:00 2001 From: Kevin Yu Date: Wed, 22 Jun 2022 05:39:08 -0700 Subject: [PATCH] Cloud Monitoring: Update Metrics to use experimental UI components (#51134) * update metrics component * separate state variables * add additonal tests --- .../cloudMonitoringMetricDescriptor.ts | 2 +- .../components/Experimental/Metrics.test.tsx | 188 ++++++++++++++++++ .../components/Experimental/Metrics.tsx | 170 ++++++++++++++++ .../Experimental/VisualMetricQueryEditor.tsx | 4 +- 4 files changed, 361 insertions(+), 3 deletions(-) create mode 100644 public/app/plugins/datasource/cloud-monitoring/components/Experimental/Metrics.test.tsx create mode 100644 public/app/plugins/datasource/cloud-monitoring/components/Experimental/Metrics.tsx diff --git a/public/app/plugins/datasource/cloud-monitoring/__mocks__/cloudMonitoringMetricDescriptor.ts b/public/app/plugins/datasource/cloud-monitoring/__mocks__/cloudMonitoringMetricDescriptor.ts index 3cb1069169c..8ec4a0374e5 100644 --- a/public/app/plugins/datasource/cloud-monitoring/__mocks__/cloudMonitoringMetricDescriptor.ts +++ b/public/app/plugins/datasource/cloud-monitoring/__mocks__/cloudMonitoringMetricDescriptor.ts @@ -8,7 +8,7 @@ export const createMockMetricDescriptor = (overrides?: Partial unit: 'unit', service: 'service', serviceShortName: 'srv', - displayName: 'displayName', + displayName: 'metricName', description: 'description', ...overrides, }; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Experimental/Metrics.test.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Experimental/Metrics.test.tsx new file mode 100644 index 00000000000..a53d9670fb5 --- /dev/null +++ b/public/app/plugins/datasource/cloud-monitoring/components/Experimental/Metrics.test.tsx @@ -0,0 +1,188 @@ +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 { Metrics } from './Metrics'; + +describe('Metrics', () => { + it('renders metrics fields', async () => { + const onChange = jest.fn(); + 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 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 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 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', + }), + ]), + }); + + 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/Experimental/Metrics.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Experimental/Metrics.tsx new file mode 100644 index 00000000000..4951f8ea1ef --- /dev/null +++ b/public/app/plugins/datasource/cloud-monitoring/components/Experimental/Metrics.tsx @@ -0,0 +1,170 @@ +import { css } from '@emotion/css'; +import { startCase, uniqBy } from 'lodash'; +import React, { useCallback, useEffect, useState } from 'react'; + +import { GrafanaTheme2, SelectableValue } from '@grafana/data'; +import { EditorRow, EditorField, EditorFieldGroup } from '@grafana/experimental'; +import { getSelectStyles, Select, useStyles2, useTheme2 } from '@grafana/ui'; + +import CloudMonitoringDatasource from '../../datasource'; +import { MetricDescriptor } from '../../types'; + +export interface Props { + refId: string; + onChange: (metricDescriptor: MetricDescriptor) => void; + templateVariableOptions: Array>; + datasource: CloudMonitoringDatasource; + projectName: string; + metricType: string; + children: (metricDescriptor?: MetricDescriptor) => JSX.Element; +} + +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 { 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 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 loadMetricDescriptors = async () => { + if (projectName) { + const metricDescriptors = await datasource.getMetricTypes(projectName); + const services = getServicesList(metricDescriptors); + const metrics = getMetricsList(metricDescriptors); + const service = metrics.length > 0 ? metrics[0].service : ''; + const metricDescriptor = getSelectedMetricDescriptor(metricDescriptors, metricType); + setMetricDescriptors(metricDescriptors); + setServices(services); + setMetrics(metrics); + setService(service); + setMetricDescriptor(metricDescriptor); + } + }; + loadMetricDescriptors(); + }, [datasource, getSelectedMetricDescriptor, metricType, projectName, 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 ( + <> + + + + 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/Experimental/VisualMetricQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Experimental/VisualMetricQueryEditor.tsx index d6ea03639be..4e446d9739c 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Experimental/VisualMetricQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Experimental/VisualMetricQueryEditor.tsx @@ -4,10 +4,11 @@ import { SelectableValue } from '@grafana/data'; import CloudMonitoringDatasource from '../../datasource'; import { CustomMetaData, MetricDescriptor, MetricQuery, SLOQuery } from '../../types'; -import { LabelFilter, Metrics } from '../index'; +import { LabelFilter } from '../index'; import { Alignment } from './Alignment'; import { GroupBy } from './GroupBy'; +import { Metrics } from './Metrics'; import { Preprocessor } from './Preprocessor'; export interface Props { @@ -34,7 +35,6 @@ function Editor({ return (