Cloud Monitoring: Update Metrics to use experimental UI components (#51134)

* update metrics component

* separate state variables

* add additonal tests
This commit is contained in:
Kevin Yu
2022-06-22 05:39:08 -07:00
committed by GitHub
parent 421f7a999a
commit f9becc2d4f
4 changed files with 361 additions and 3 deletions

View File

@@ -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(
<Metrics
refId="refId"
metricType=""
projectName="projectName"
templateVariableOptions={[]}
datasource={datasource}
onChange={onChange}
>
{() => <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 datasource = createMockDatasource({
getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor()]),
});
render(
<Metrics
refId="refId"
metricType=""
projectName="projectName"
templateVariableOptions={[]}
datasource={datasource}
onChange={onChange}
>
{() => <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 datasource = createMockDatasource({
getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor()]),
});
render(
<Metrics
refId="refId"
metricType="type"
projectName="projectName"
templateVariableOptions={[]}
datasource={datasource}
onChange={onChange}
>
{() => <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 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}
>
{() => <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',
}),
]),
});
render(
<Metrics
refId="refId"
metricType="metric1"
projectName="projectName"
templateVariableOptions={[]}
datasource={datasource}
onChange={onChange}
>
{() => <div />}
</Metrics>
);
const service = await screen.findByLabelText('Service');
await openMenu(service);
expect(screen.getAllByLabelText('Select option').length).toEqual(2);
});
});

View File

@@ -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<SelectableValue<string>>;
datasource: CloudMonitoringDatasource;
projectName: string;
metricType: string;
children: (metricDescriptor?: MetricDescriptor) => JSX.Element;
}
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 { 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 (
<div>
<div className={customStyle}>{m.type}</div>
<div className={selectStyles.optionDescription}>{m.description}</div>
</div>
);
},
}));
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<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>
<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

@@ -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 (
<Metrics
refId={refId}
templateSrv={datasource.templateSrv}
projectName={query.projectName}
metricType={query.metricType}
templateVariableOptions={variableOptionGroup.options}