mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Cloud Monitoring: Update Metrics to use experimental UI components (#51134)
* update metrics component * separate state variables * add additonal tests
This commit is contained in:
@@ -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};
|
||||
`;
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user