Cloud Monitoring: Update Alignment fields to use experimental UI components (#50536)

* Cloud Monitoring: Update Alignment fields to use experimental UI components

* remove unreachable code

* remove alias

* remove custom label style

* add tests
This commit is contained in:
Kevin Yu 2022-06-15 18:03:47 -07:00 committed by GitHub
parent 7c175e019c
commit d88108a3b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 382 additions and 3 deletions

View File

@ -1,12 +1,19 @@
import { TemplateSrv } from 'app/features/templating/template_srv';
import { TemplateSrvMock } from 'app/features/templating/template_srv.mock';
import Datasource from '../datasource';
export const createMockDatasource = () => {
export const createMockDatasource = (overrides?: Partial<Datasource>) => {
const templateSrv = new TemplateSrvMock({ ALIGN_DELTA: 'delta' }) as unknown as TemplateSrv;
const datasource: Partial<Datasource> = {
intervalMs: 0,
getVariables: jest.fn().mockReturnValue([]),
getMetricTypes: jest.fn().mockResolvedValue([]),
getProjects: jest.fn().mockResolvedValue([]),
getDefaultProject: jest.fn().mockReturnValue('cloud-monitoring-default-project'),
templateSrv,
...overrides,
};
return jest.mocked(datasource as Datasource, true);

View File

@ -1,12 +1,15 @@
import { CloudMonitoringQuery, EditorMode, MetricQuery, QueryType } from '../types';
export const createMockMetricQuery: () => MetricQuery = () => {
export const createMockMetricQuery: (overrides?: Partial<MetricQuery>) => MetricQuery = (
overrides?: Partial<MetricQuery>
) => {
return {
editorMode: EditorMode.Visual,
metricType: '',
crossSeriesReducer: 'REDUCE_NONE',
query: '',
projectName: 'cloud-monitoring-default-project',
...overrides,
};
};

View File

@ -0,0 +1,102 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { openMenu } from 'react-select-event';
import { TemplateSrvMock } from 'app/features/templating/template_srv.mock';
import { createMockDatasource } from '../../__mocks__/cloudMonitoringDatasource';
import { createMockMetricQuery } from '../../__mocks__/cloudMonitoringQuery';
import { MetricKind, ValueTypes } from '../../types';
import { Alignment } from './Alignment';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getTemplateSrv: () => new TemplateSrvMock({}),
}));
describe('Alignment', () => {
it('renders alignment fields', () => {
const datasource = createMockDatasource();
const query = createMockMetricQuery();
const onChange = jest.fn();
render(
<Alignment
refId="refId"
customMetaData={{}}
datasource={datasource}
query={query}
onChange={onChange}
templateVariableOptions={[]}
/>
);
expect(screen.getByLabelText('Alignment function')).toBeInTheDocument();
expect(screen.getByLabelText('Alignment period')).toBeInTheDocument();
});
it('can set the alignment function', async () => {
const datasource = createMockDatasource();
const query = createMockMetricQuery({ metricKind: MetricKind.GAUGE, valueType: ValueTypes.INT64 });
const onChange = jest.fn();
render(
<Alignment
refId="refId"
customMetaData={{}}
datasource={datasource}
query={query}
onChange={onChange}
templateVariableOptions={[]}
/>
);
const alignmentFunction = screen.getByLabelText('Alignment function');
openMenu(alignmentFunction);
await userEvent.click(screen.getByText('percent change'));
expect(onChange).toBeCalledWith(expect.objectContaining({ perSeriesAligner: 'ALIGN_PERCENT_CHANGE' }));
});
it('can set the alignment period', async () => {
const datasource = createMockDatasource();
const query = createMockMetricQuery();
const onChange = jest.fn();
render(
<Alignment
refId="refId"
customMetaData={{}}
datasource={datasource}
query={query}
onChange={onChange}
templateVariableOptions={[]}
/>
);
const alignmentPeriod = screen.getByLabelText('Alignment period');
openMenu(alignmentPeriod);
await userEvent.click(screen.getByText('1m'));
expect(onChange).toBeCalledWith(expect.objectContaining({ alignmentPeriod: '+60s' }));
});
it('renders period label if alignment period and per series aligner is set', () => {
const datasource = createMockDatasource();
const query = createMockMetricQuery();
const onChange = jest.fn();
render(
<Alignment
refId="refId"
customMetaData={{ perSeriesAligner: 'ALIGN_DELTA', alignmentPeriod: '10' }}
datasource={datasource}
query={query}
onChange={onChange}
templateVariableOptions={[]}
/>
);
expect(screen.getByText('10s interval (delta)'));
});
});

View File

@ -0,0 +1,61 @@
import React, { FC } from 'react';
import { SelectableValue } from '@grafana/data';
import { EditorRow, EditorField, EditorFieldGroup, Stack } from '@grafana/experimental';
import { ALIGNMENT_PERIODS, SELECT_WIDTH } from '../../constants';
import CloudMonitoringDatasource from '../../datasource';
import { CustomMetaData, MetricQuery, SLOQuery } from '../../types';
import { AlignmentFunction } from './AlignmentFunction';
import { AlignmentPeriodLabel } from './AlignmentPeriodLabel';
import { PeriodSelect } from './PeriodSelect';
export interface Props {
refId: string;
onChange: (query: MetricQuery | SLOQuery) => void;
query: MetricQuery;
templateVariableOptions: Array<SelectableValue<string>>;
customMetaData: CustomMetaData;
datasource: CloudMonitoringDatasource;
}
export const Alignment: FC<Props> = ({
refId,
templateVariableOptions,
onChange,
query,
customMetaData,
datasource,
}) => {
return (
<EditorRow>
<EditorFieldGroup>
<EditorField
label="Alignment function"
tooltip="The process of alignment consists of collecting all data points received in a fixed length of time, applying a function to combine those data points, and assigning a timestamp to the result."
>
<AlignmentFunction
inputId={`${refId}-alignment-function`}
templateVariableOptions={templateVariableOptions}
query={query}
onChange={onChange}
/>
</EditorField>
<EditorField label="Alignment period">
<PeriodSelect
inputId={`${refId}-alignment-period`}
selectWidth={SELECT_WIDTH}
templateVariableOptions={templateVariableOptions}
current={query.alignmentPeriod}
onChange={(period) => onChange({ ...query, alignmentPeriod: period })}
aligmentPeriods={ALIGNMENT_PERIODS}
/>
</EditorField>
<Stack alignItems="flex-end">
<AlignmentPeriodLabel datasource={datasource} customMetaData={customMetaData} />
</Stack>
</EditorFieldGroup>
</EditorRow>
);
};

View File

@ -0,0 +1,44 @@
import React, { FC, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select } from '@grafana/ui';
import { SELECT_WIDTH } from '../../constants';
import { getAlignmentPickerData } from '../../functions';
import { MetricQuery } from '../../types';
export interface Props {
inputId: string;
onChange: (query: MetricQuery) => void;
query: MetricQuery;
templateVariableOptions: Array<SelectableValue<string>>;
}
export const AlignmentFunction: FC<Props> = ({ inputId, query, templateVariableOptions, onChange }) => {
const { valueType, metricKind, perSeriesAligner: psa, preprocessor } = query;
const { perSeriesAligner, alignOptions } = useMemo(
() => getAlignmentPickerData(valueType, metricKind, psa, preprocessor),
[valueType, metricKind, psa, preprocessor]
);
return (
<Select
width={SELECT_WIDTH}
onChange={({ value }) => onChange({ ...query, perSeriesAligner: value! })}
value={[...alignOptions, ...templateVariableOptions].find((s) => s.value === perSeriesAligner)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
{
label: 'Alignment options',
expanded: true,
options: alignOptions,
},
]}
placeholder="Select Alignment"
inputId={inputId}
/>
);
};

View File

@ -0,0 +1,28 @@
import React, { FC, useMemo } from 'react';
import { rangeUtil } from '@grafana/data';
import { ALIGNMENTS } from '../../constants';
import CloudMonitoringDatasource from '../../datasource';
import { CustomMetaData } from '../../types';
export interface Props {
customMetaData: CustomMetaData;
datasource: CloudMonitoringDatasource;
}
export const AlignmentPeriodLabel: FC<Props> = ({ customMetaData, datasource }) => {
const { perSeriesAligner, alignmentPeriod } = customMetaData;
const formatAlignmentText = useMemo(() => {
if (!alignmentPeriod || !perSeriesAligner) {
return '';
}
const alignment = ALIGNMENTS.find((ap) => ap.value === datasource.templateSrv.replace(perSeriesAligner));
const seconds = parseInt(alignmentPeriod, 10);
const hms = rangeUtil.secondsToHms(seconds);
return `${hms} interval (${alignment?.text ?? ''})`;
}, [datasource, perSeriesAligner, alignmentPeriod]);
return <label>{formatAlignmentText}</label>;
};

View File

@ -16,11 +16,12 @@ import {
SLOQuery,
ValueTypes,
} from '../../types';
import { Project, VisualMetricQueryEditor } from '../index';
import { Project } from '../index';
import { GraphPeriod } from './../GraphPeriod';
import { MQLQueryEditor } from './../MQLQueryEditor';
import { AliasBy } from './AliasBy';
import { VisualMetricQueryEditor } from './VisualMetricQueryEditor';
export interface Props {
refId: string;

View File

@ -0,0 +1,60 @@
import React, { useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select } from '@grafana/ui';
import { periodOption } from '../../constants';
export interface Props {
inputId: string;
onChange: (period: string) => void;
templateVariableOptions: Array<SelectableValue<string>>;
aligmentPeriods: periodOption[];
selectWidth?: number;
category?: string;
disabled?: boolean;
current?: string;
}
export function PeriodSelect({
inputId,
templateVariableOptions,
onChange,
current,
selectWidth,
disabled,
aligmentPeriods,
}: Props) {
const options = useMemo(
() =>
aligmentPeriods.map((ap) => ({
...ap,
label: ap.text,
})),
[aligmentPeriods]
);
const visibleOptions = useMemo(() => options.filter((ap) => !ap.hidden), [options]);
return (
<Select
width={selectWidth}
onChange={({ value }) => onChange(value!)}
value={[...options, ...templateVariableOptions].find((s) => s.value === current)}
options={[
{
label: 'Template Variables',
options: templateVariableOptions,
},
{
label: 'Aggregations',
expanded: true,
options: visibleOptions,
},
]}
placeholder="Select Period"
inputId={inputId}
disabled={disabled}
allowCustomValue
/>
);
}

View File

@ -0,0 +1,73 @@
import React from 'react';
import { SelectableValue } from '@grafana/data';
import CloudMonitoringDatasource from '../../datasource';
import { CustomMetaData, MetricDescriptor, MetricQuery, SLOQuery } from '../../types';
import { GroupBy, LabelFilter, Metrics, Preprocessor } from '../index';
import { Alignment } from './Alignment';
export interface Props {
refId: string;
customMetaData: CustomMetaData;
variableOptionGroup: SelectableValue<string>;
onMetricTypeChange: (query: MetricDescriptor) => void;
onChange: (query: MetricQuery | SLOQuery) => void;
query: MetricQuery;
datasource: CloudMonitoringDatasource;
labels: any;
}
function Editor({
refId,
query,
labels,
datasource,
onChange,
onMetricTypeChange,
customMetaData,
variableOptionGroup,
}: React.PropsWithChildren<Props>) {
return (
<Metrics
refId={refId}
templateSrv={datasource.templateSrv}
projectName={query.projectName}
metricType={query.metricType}
templateVariableOptions={variableOptionGroup.options}
datasource={datasource}
onChange={onMetricTypeChange}
>
{(metric) => (
<>
<LabelFilter
labels={labels}
filters={query.filters!}
onChange={(filters: string[]) => onChange({ ...query, filters })}
variableOptionGroup={variableOptionGroup}
/>
<Preprocessor metricDescriptor={metric} query={query} onChange={onChange} />
<GroupBy
refId={refId}
labels={Object.keys(labels)}
query={query}
onChange={onChange}
variableOptionGroup={variableOptionGroup}
metricDescriptor={metric}
/>
<Alignment
refId={refId}
datasource={datasource}
templateVariableOptions={variableOptionGroup.options}
query={query}
customMetaData={customMetaData}
onChange={onChange}
/>
</>
)}
</Metrics>
);
}
export const VisualMetricQueryEditor = React.memo(Editor);