mirror of https://github.com/grafana/grafana
Cloud Monitoring: Update GroupBy fields to use experimental UI components (#50541)
* Cloud Monitoring: Update GroupBy fields to use experimental UI components * let group by field grow horizontally * remove fixed width constants from inputs * add test * Cloud Monitoring: Update GraphPeriod to use experimental UI components (#50545) * Cloud Monitoring: Update GraphPeriod to use experimental UI components * Cloud Monitoring: Update Preprocessing to use experimental UI components (#50548) * Cloud Monitoring: Update Preprocessing to use experimental UI components * add tests * make overrides optional * move preprocessor back into its own rowpull/51072/head
parent
902101c524
commit
e889dfdc5c
@ -0,0 +1,15 @@ |
||||
import { MetricDescriptor, MetricKind, ValueTypes } from '../types'; |
||||
|
||||
export const createMockMetricDescriptor = (overrides?: Partial<MetricDescriptor>): MetricDescriptor => { |
||||
return { |
||||
metricKind: MetricKind.CUMULATIVE, |
||||
valueType: ValueTypes.DOUBLE, |
||||
type: 'type', |
||||
unit: 'unit', |
||||
service: 'service', |
||||
serviceShortName: 'srv', |
||||
displayName: 'displayName', |
||||
description: 'description', |
||||
...overrides, |
||||
}; |
||||
}; |
@ -0,0 +1,65 @@ |
||||
import { render, screen } from '@testing-library/react'; |
||||
import React from 'react'; |
||||
import { openMenu } from 'react-select-event'; |
||||
import { TemplateSrvStub } from 'test/specs/helpers'; |
||||
|
||||
import { ValueTypes, MetricKind } from '../../types'; |
||||
|
||||
import { Aggregation, Props } from './Aggregation'; |
||||
|
||||
const props: Props = { |
||||
onChange: () => {}, |
||||
// @ts-ignore
|
||||
templateSrv: new TemplateSrvStub(), |
||||
metricDescriptor: { |
||||
valueType: '', |
||||
metricKind: '', |
||||
} as any, |
||||
crossSeriesReducer: '', |
||||
groupBys: [], |
||||
templateVariableOptions: [], |
||||
}; |
||||
|
||||
describe('Aggregation', () => { |
||||
it('renders correctly', () => { |
||||
render(<Aggregation {...props} />); |
||||
expect(screen.getByTestId('cloud-monitoring-aggregation')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
describe('options', () => { |
||||
describe('when DOUBLE and GAUGE is passed as props', () => { |
||||
const nextProps = { |
||||
...props, |
||||
metricDescriptor: { |
||||
valueType: ValueTypes.DOUBLE, |
||||
metricKind: MetricKind.GAUGE, |
||||
} as any, |
||||
}; |
||||
|
||||
it('should not have the reduce values', () => { |
||||
render(<Aggregation {...nextProps} />); |
||||
const label = screen.getByLabelText('Group by function'); |
||||
openMenu(label); |
||||
expect(screen.queryByText('count true')).not.toBeInTheDocument(); |
||||
expect(screen.queryByText('count false')).not.toBeInTheDocument(); |
||||
}); |
||||
}); |
||||
|
||||
describe('when MONEY and CUMULATIVE is passed as props', () => { |
||||
const nextProps = { |
||||
...props, |
||||
metricDescriptor: { |
||||
valueType: ValueTypes.MONEY, |
||||
metricKind: MetricKind.CUMULATIVE, |
||||
} as any, |
||||
}; |
||||
|
||||
it('should have the reduce values', () => { |
||||
render(<Aggregation {...nextProps} />); |
||||
const label = screen.getByLabelText('Group by function'); |
||||
openMenu(label); |
||||
expect(screen.getByText('none')).toBeInTheDocument(); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,68 @@ |
||||
import React, { FC, useMemo } from 'react'; |
||||
|
||||
import { SelectableValue } from '@grafana/data'; |
||||
import { EditorField } from '@grafana/experimental'; |
||||
import { Select } from '@grafana/ui'; |
||||
|
||||
import { getAggregationOptionsByMetric } from '../../functions'; |
||||
import { MetricDescriptor, MetricKind, ValueTypes } from '../../types'; |
||||
|
||||
export interface Props { |
||||
refId: string; |
||||
onChange: (metricDescriptor: string) => void; |
||||
metricDescriptor?: MetricDescriptor; |
||||
crossSeriesReducer: string; |
||||
groupBys: string[]; |
||||
templateVariableOptions: Array<SelectableValue<string>>; |
||||
} |
||||
|
||||
export const Aggregation: FC<Props> = (props) => { |
||||
const aggOptions = useAggregationOptionsByMetric(props); |
||||
const selected = useSelectedFromOptions(aggOptions, props); |
||||
|
||||
return ( |
||||
<EditorField label="Group by function" data-testid="cloud-monitoring-aggregation"> |
||||
<Select |
||||
width="auto" |
||||
onChange={({ value }) => props.onChange(value!)} |
||||
value={selected} |
||||
options={[ |
||||
{ |
||||
label: 'Template Variables', |
||||
options: props.templateVariableOptions, |
||||
}, |
||||
{ |
||||
label: 'Aggregations', |
||||
expanded: true, |
||||
options: aggOptions, |
||||
}, |
||||
]} |
||||
placeholder="Select Reducer" |
||||
inputId={`${props.refId}-group-by-function`} |
||||
/> |
||||
</EditorField> |
||||
); |
||||
}; |
||||
|
||||
const useAggregationOptionsByMetric = ({ metricDescriptor }: Props): Array<SelectableValue<string>> => { |
||||
const valueType = metricDescriptor?.valueType; |
||||
const metricKind = metricDescriptor?.metricKind; |
||||
|
||||
return useMemo(() => { |
||||
if (!valueType || !metricKind) { |
||||
return []; |
||||
} |
||||
|
||||
return getAggregationOptionsByMetric(valueType as ValueTypes, metricKind as MetricKind).map((a) => ({ |
||||
...a, |
||||
label: a.text, |
||||
})); |
||||
}, [valueType, metricKind]); |
||||
}; |
||||
|
||||
const useSelectedFromOptions = (aggOptions: Array<SelectableValue<string>>, props: Props) => { |
||||
return useMemo(() => { |
||||
const allOptions = [...aggOptions, ...props.templateVariableOptions]; |
||||
return allOptions.find((s) => s.value === props.crossSeriesReducer); |
||||
}, [aggOptions, props.crossSeriesReducer, props.templateVariableOptions]); |
||||
}; |
@ -0,0 +1,39 @@ |
||||
import { render, screen } from '@testing-library/react'; |
||||
import userEvent from '@testing-library/user-event'; |
||||
import React from 'react'; |
||||
import { select } from 'react-select-event'; |
||||
|
||||
import { GraphPeriod, Props } from './GraphPeriod'; |
||||
|
||||
const props: Props = { |
||||
onChange: jest.fn(), |
||||
refId: 'A', |
||||
variableOptionGroup: { options: [] }, |
||||
}; |
||||
|
||||
describe('Graph Period', () => { |
||||
it('should enable graph_period by default', () => { |
||||
render(<GraphPeriod {...props} />); |
||||
expect(screen.getByLabelText('Graph period')).not.toBeDisabled(); |
||||
}); |
||||
|
||||
it('should disable graph_period when toggled', async () => { |
||||
const onChange = jest.fn(); |
||||
render(<GraphPeriod {...props} onChange={onChange} />); |
||||
const s = screen.getByTestId('A-switch-graph-period'); |
||||
await userEvent.click(s); |
||||
expect(onChange).toHaveBeenCalledWith('disabled'); |
||||
}); |
||||
|
||||
it('should set a different value when selected', async () => { |
||||
const onChange = jest.fn(); |
||||
render(<GraphPeriod {...props} onChange={onChange} />); |
||||
const selectEl = screen.getByLabelText('Graph period'); |
||||
expect(selectEl).toBeInTheDocument(); |
||||
|
||||
await select(selectEl, '1m', { |
||||
container: document.body, |
||||
}); |
||||
expect(onChange).toHaveBeenCalledWith('1m'); |
||||
}); |
||||
}); |
@ -0,0 +1,49 @@ |
||||
import React, { FunctionComponent } from 'react'; |
||||
|
||||
import { SelectableValue } from '@grafana/data'; |
||||
import { EditorField, EditorRow } from '@grafana/experimental'; |
||||
import { HorizontalGroup, Switch } from '@grafana/ui'; |
||||
|
||||
import { GRAPH_PERIODS, SELECT_WIDTH } from '../../constants'; |
||||
import { PeriodSelect } from '../index'; |
||||
|
||||
export interface Props { |
||||
refId: string; |
||||
onChange: (period: string) => void; |
||||
variableOptionGroup: SelectableValue<string>; |
||||
graphPeriod?: string; |
||||
} |
||||
|
||||
export const GraphPeriod: FunctionComponent<Props> = ({ refId, onChange, graphPeriod, variableOptionGroup }) => { |
||||
return ( |
||||
<EditorRow> |
||||
<EditorField |
||||
label="Graph period" |
||||
htmlFor={`${refId}-graph-period`} |
||||
tooltip={ |
||||
<> |
||||
Set <code>graph_period</code> which forces a preferred period between points. Automatically set to the |
||||
current interval if left blank. |
||||
</> |
||||
} |
||||
> |
||||
<HorizontalGroup> |
||||
<Switch |
||||
data-testid={`${refId}-switch-graph-period`} |
||||
value={graphPeriod !== 'disabled'} |
||||
onChange={(e) => onChange(e.currentTarget.checked ? '' : 'disabled')} |
||||
/> |
||||
<PeriodSelect |
||||
inputId={`${refId}-graph-period`} |
||||
templateVariableOptions={variableOptionGroup.options} |
||||
current={graphPeriod} |
||||
onChange={onChange} |
||||
selectWidth={SELECT_WIDTH} |
||||
disabled={graphPeriod === 'disabled'} |
||||
aligmentPeriods={GRAPH_PERIODS} |
||||
/> |
||||
</HorizontalGroup> |
||||
</EditorField> |
||||
</EditorRow> |
||||
); |
||||
}; |
@ -0,0 +1,42 @@ |
||||
import { render, screen } from '@testing-library/react'; |
||||
import React from 'react'; |
||||
import { openMenu, select } from 'react-select-event'; |
||||
|
||||
import { createMockMetricQuery } from '../../__mocks__/cloudMonitoringQuery'; |
||||
|
||||
import { GroupBy, Props } from './GroupBy'; |
||||
|
||||
const props: Props = { |
||||
onChange: jest.fn(), |
||||
refId: 'refId', |
||||
metricDescriptor: { |
||||
valueType: '', |
||||
metricKind: '', |
||||
} as any, |
||||
variableOptionGroup: { options: [] }, |
||||
labels: [], |
||||
query: createMockMetricQuery(), |
||||
}; |
||||
|
||||
describe('GroupBy', () => { |
||||
it('renders group by fields', () => { |
||||
render(<GroupBy {...props} />); |
||||
expect(screen.getByLabelText('Group by')).toBeInTheDocument(); |
||||
expect(screen.getByLabelText('Group by function')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('can select a group by', async () => { |
||||
const onChange = jest.fn(); |
||||
render(<GroupBy {...props} onChange={onChange} />); |
||||
|
||||
const groupBy = screen.getByLabelText('Group by'); |
||||
const option = 'metadata.system_labels.cloud_account'; |
||||
|
||||
expect(screen.queryByText(option)).not.toBeInTheDocument(); |
||||
await openMenu(groupBy); |
||||
expect(screen.getByText(option)).toBeInTheDocument(); |
||||
|
||||
await select(groupBy, option, { container: document.body }); |
||||
expect(onChange).toBeCalledWith(expect.objectContaining({ groupBys: expect.arrayContaining([option]) })); |
||||
}); |
||||
}); |
@ -0,0 +1,64 @@ |
||||
import React, { FunctionComponent, useMemo } from 'react'; |
||||
|
||||
import { SelectableValue } from '@grafana/data'; |
||||
import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/experimental'; |
||||
import { MultiSelect } from '@grafana/ui'; |
||||
|
||||
import { SYSTEM_LABELS } from '../../constants'; |
||||
import { labelsToGroupedOptions } from '../../functions'; |
||||
import { MetricDescriptor, MetricQuery } from '../../types'; |
||||
|
||||
import { Aggregation } from './Aggregation'; |
||||
|
||||
export interface Props { |
||||
refId: string; |
||||
variableOptionGroup: SelectableValue<string>; |
||||
labels: string[]; |
||||
metricDescriptor?: MetricDescriptor; |
||||
onChange: (query: MetricQuery) => void; |
||||
query: MetricQuery; |
||||
} |
||||
|
||||
export const GroupBy: FunctionComponent<Props> = ({ |
||||
refId, |
||||
labels: groupBys = [], |
||||
query, |
||||
onChange, |
||||
variableOptionGroup, |
||||
metricDescriptor, |
||||
}) => { |
||||
const options = useMemo( |
||||
() => [variableOptionGroup, ...labelsToGroupedOptions([...groupBys, ...SYSTEM_LABELS])], |
||||
[groupBys, variableOptionGroup] |
||||
); |
||||
|
||||
return ( |
||||
<EditorRow> |
||||
<EditorFieldGroup> |
||||
<EditorField |
||||
label="Group by" |
||||
tooltip="You can reduce the amount of data returned for a metric by combining different time series. To combine multiple time series, you can specify a grouping and a function. Grouping is done on the basis of labels. The grouping function is used to combine the time series in the group into a single time series." |
||||
> |
||||
<MultiSelect |
||||
inputId={`${refId}-group-by`} |
||||
width="auto" |
||||
placeholder="Choose label" |
||||
options={options} |
||||
value={query.groupBys ?? []} |
||||
onChange={(options) => { |
||||
onChange({ ...query, groupBys: options.map((o) => o.value!) }); |
||||
}} |
||||
/> |
||||
</EditorField> |
||||
<Aggregation |
||||
metricDescriptor={metricDescriptor} |
||||
templateVariableOptions={variableOptionGroup.options} |
||||
crossSeriesReducer={query.crossSeriesReducer} |
||||
groupBys={query.groupBys ?? []} |
||||
onChange={(crossSeriesReducer) => onChange({ ...query, crossSeriesReducer })} |
||||
refId={refId} |
||||
/> |
||||
</EditorFieldGroup> |
||||
</EditorRow> |
||||
); |
||||
}; |
@ -0,0 +1,95 @@ |
||||
import { render, screen } from '@testing-library/react'; |
||||
import userEvent from '@testing-library/user-event'; |
||||
import React from 'react'; |
||||
|
||||
import { TemplateSrvMock } from 'app/features/templating/template_srv.mock'; |
||||
|
||||
import { createMockMetricDescriptor } from '../../__mocks__/cloudMonitoringMetricDescriptor'; |
||||
import { createMockMetricQuery } from '../../__mocks__/cloudMonitoringQuery'; |
||||
import { MetricKind, ValueTypes } from '../../types'; |
||||
|
||||
import { Preprocessor } from './Preprocessor'; |
||||
|
||||
jest.mock('@grafana/runtime', () => ({ |
||||
...jest.requireActual('@grafana/runtime'), |
||||
getTemplateSrv: () => new TemplateSrvMock({}), |
||||
})); |
||||
|
||||
describe('Preprocessor', () => { |
||||
it('only provides "None" as an option if no metric descriptor is provided', () => { |
||||
const query = createMockMetricQuery(); |
||||
const onChange = jest.fn(); |
||||
|
||||
render(<Preprocessor onChange={onChange} query={query} />); |
||||
expect(screen.getByText('Pre-processing')).toBeInTheDocument(); |
||||
expect(screen.getByText('None')).toBeInTheDocument(); |
||||
expect(screen.queryByText('Rate')).not.toBeInTheDocument(); |
||||
expect(screen.queryByText('Delta')).not.toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('only provides "None" as an option if metric kind is "Gauge"', () => { |
||||
const query = createMockMetricQuery(); |
||||
const onChange = jest.fn(); |
||||
const metricDescriptor = createMockMetricDescriptor({ metricKind: MetricKind.GAUGE }); |
||||
|
||||
render(<Preprocessor onChange={onChange} query={query} metricDescriptor={metricDescriptor} />); |
||||
expect(screen.getByText('Pre-processing')).toBeInTheDocument(); |
||||
expect(screen.getByText('None')).toBeInTheDocument(); |
||||
expect(screen.queryByText('Rate')).not.toBeInTheDocument(); |
||||
expect(screen.queryByText('Delta')).not.toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('only provides "None" as an option if value type is "Distribution"', () => { |
||||
const query = createMockMetricQuery(); |
||||
const onChange = jest.fn(); |
||||
const metricDescriptor = createMockMetricDescriptor({ valueType: ValueTypes.DISTRIBUTION }); |
||||
|
||||
render(<Preprocessor onChange={onChange} query={query} metricDescriptor={metricDescriptor} />); |
||||
expect(screen.getByText('Pre-processing')).toBeInTheDocument(); |
||||
expect(screen.getByText('None')).toBeInTheDocument(); |
||||
expect(screen.queryByText('Rate')).not.toBeInTheDocument(); |
||||
expect(screen.queryByText('Delta')).not.toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('provides "None" and "Rate" as options if metric kind is not "Delta" or "Cumulative" and value type is not "Distribution"', () => { |
||||
const query = createMockMetricQuery(); |
||||
const onChange = jest.fn(); |
||||
const metricDescriptor = createMockMetricDescriptor({ metricKind: MetricKind.DELTA }); |
||||
|
||||
render(<Preprocessor onChange={onChange} query={query} metricDescriptor={metricDescriptor} />); |
||||
expect(screen.getByText('Pre-processing')).toBeInTheDocument(); |
||||
expect(screen.getByText('None')).toBeInTheDocument(); |
||||
expect(screen.queryByText('Rate')).toBeInTheDocument(); |
||||
expect(screen.queryByText('Delta')).not.toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('provides all options if metric kind is "Cumulative" and value type is not "Distribution"', () => { |
||||
const query = createMockMetricQuery(); |
||||
const onChange = jest.fn(); |
||||
const metricDescriptor = createMockMetricDescriptor({ metricKind: MetricKind.CUMULATIVE }); |
||||
|
||||
render(<Preprocessor onChange={onChange} query={query} metricDescriptor={metricDescriptor} />); |
||||
expect(screen.getByText('Pre-processing')).toBeInTheDocument(); |
||||
expect(screen.getByText('None')).toBeInTheDocument(); |
||||
expect(screen.queryByText('Rate')).toBeInTheDocument(); |
||||
expect(screen.queryByText('Delta')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('provides all options if metric kind is "Cumulative" and value type is not "Distribution"', async () => { |
||||
const query = createMockMetricQuery(); |
||||
const onChange = jest.fn(); |
||||
const metricDescriptor = createMockMetricDescriptor({ metricKind: MetricKind.CUMULATIVE }); |
||||
|
||||
render(<Preprocessor onChange={onChange} query={query} metricDescriptor={metricDescriptor} />); |
||||
const none = screen.getByLabelText('None'); |
||||
const rate = screen.getByLabelText('Rate'); |
||||
const delta = screen.getByLabelText('Delta'); |
||||
expect(none).toBeChecked(); |
||||
expect(rate).not.toBeChecked(); |
||||
expect(delta).not.toBeChecked(); |
||||
|
||||
await userEvent.click(rate); |
||||
|
||||
expect(onChange).toBeCalledWith(expect.objectContaining({ preprocessor: 'rate' })); |
||||
}); |
||||
}); |
@ -0,0 +1,69 @@ |
||||
import React, { FunctionComponent, useMemo } from 'react'; |
||||
|
||||
import { SelectableValue } from '@grafana/data'; |
||||
import { EditorField, EditorRow } from '@grafana/experimental'; |
||||
import { RadioButtonGroup } from '@grafana/ui'; |
||||
|
||||
import { getAlignmentPickerData } from '../../functions'; |
||||
import { MetricDescriptor, MetricKind, MetricQuery, PreprocessorType, ValueTypes } from '../../types'; |
||||
|
||||
const NONE_OPTION = { label: 'None', value: PreprocessorType.None }; |
||||
|
||||
export interface Props { |
||||
metricDescriptor?: MetricDescriptor; |
||||
onChange: (query: MetricQuery) => void; |
||||
query: MetricQuery; |
||||
} |
||||
|
||||
export const Preprocessor: FunctionComponent<Props> = ({ query, metricDescriptor, onChange }) => { |
||||
const options = useOptions(metricDescriptor); |
||||
return ( |
||||
<EditorRow> |
||||
<EditorField |
||||
label="Pre-processing" |
||||
tooltip="Preprocessing options are displayed when the selected metric has a metric kind of delta or cumulative. The specific options available are determined by the metic's value type. If you select 'Rate', data points are aligned and converted to a rate per time series. If you select 'Delta', data points are aligned by their delta (difference) per time series" |
||||
> |
||||
<RadioButtonGroup |
||||
onChange={(value: PreprocessorType) => { |
||||
const { valueType, metricKind, perSeriesAligner: psa } = query; |
||||
const { perSeriesAligner } = getAlignmentPickerData(valueType, metricKind, psa, value); |
||||
onChange({ ...query, preprocessor: value, perSeriesAligner }); |
||||
}} |
||||
value={query.preprocessor ?? PreprocessorType.None} |
||||
options={options} |
||||
/> |
||||
</EditorField> |
||||
</EditorRow> |
||||
); |
||||
}; |
||||
|
||||
const useOptions = (metricDescriptor?: MetricDescriptor): Array<SelectableValue<PreprocessorType>> => { |
||||
const metricKind = metricDescriptor?.metricKind; |
||||
const valueType = metricDescriptor?.valueType; |
||||
|
||||
return useMemo(() => { |
||||
if (!metricKind || metricKind === MetricKind.GAUGE || valueType === ValueTypes.DISTRIBUTION) { |
||||
return [NONE_OPTION]; |
||||
} |
||||
|
||||
const options = [ |
||||
NONE_OPTION, |
||||
{ |
||||
label: 'Rate', |
||||
value: PreprocessorType.Rate, |
||||
description: 'Data points are aligned and converted to a rate per time series', |
||||
}, |
||||
]; |
||||
|
||||
return metricKind === MetricKind.CUMULATIVE |
||||
? [ |
||||
...options, |
||||
{ |
||||
label: 'Delta', |
||||
value: PreprocessorType.Delta, |
||||
description: 'Data points are aligned by their delta (difference) per time series', |
||||
}, |
||||
] |
||||
: options; |
||||
}, [metricKind, valueType]); |
||||
}; |
Loading…
Reference in new issue