Cloud Monitoring: Update Metrics to use experimental UI components (#51134)
* update metrics component * separate state variables * add additonal tests
This commit is contained in:
+1
-1
@@ -8,7 +8,7 @@ export const createMockMetricDescriptor = (overrides?: Partial<MetricDescriptor>
|
|||||||
unit: 'unit',
|
unit: 'unit',
|
||||||
service: 'service',
|
service: 'service',
|
||||||
serviceShortName: 'srv',
|
serviceShortName: 'srv',
|
||||||
displayName: 'displayName',
|
displayName: 'metricName',
|
||||||
description: 'description',
|
description: 'description',
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
|
|||||||
+188
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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};
|
||||||
|
`;
|
||||||
+2
-2
@@ -4,10 +4,11 @@ import { SelectableValue } from '@grafana/data';
|
|||||||
|
|
||||||
import CloudMonitoringDatasource from '../../datasource';
|
import CloudMonitoringDatasource from '../../datasource';
|
||||||
import { CustomMetaData, MetricDescriptor, MetricQuery, SLOQuery } from '../../types';
|
import { CustomMetaData, MetricDescriptor, MetricQuery, SLOQuery } from '../../types';
|
||||||
import { LabelFilter, Metrics } from '../index';
|
import { LabelFilter } from '../index';
|
||||||
|
|
||||||
import { Alignment } from './Alignment';
|
import { Alignment } from './Alignment';
|
||||||
import { GroupBy } from './GroupBy';
|
import { GroupBy } from './GroupBy';
|
||||||
|
import { Metrics } from './Metrics';
|
||||||
import { Preprocessor } from './Preprocessor';
|
import { Preprocessor } from './Preprocessor';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@@ -34,7 +35,6 @@ function Editor({
|
|||||||
return (
|
return (
|
||||||
<Metrics
|
<Metrics
|
||||||
refId={refId}
|
refId={refId}
|
||||||
templateSrv={datasource.templateSrv}
|
|
||||||
projectName={query.projectName}
|
projectName={query.projectName}
|
||||||
metricType={query.metricType}
|
metricType={query.metricType}
|
||||||
templateVariableOptions={variableOptionGroup.options}
|
templateVariableOptions={variableOptionGroup.options}
|
||||||
|
|||||||
Reference in New Issue
Block a user