mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Cloud Monitoring: Update LabelFilter to use experimental UI components (#51342)
* add label filter * add tests * define labels once * update betterer results
This commit is contained in:
parent
93e2a0eddc
commit
b5eef488ce
8332
.betterer.results
8332
.betterer.results
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,92 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { openMenu, select } from 'react-select-event';
|
||||
|
||||
import { LabelFilter } from './LabelFilter';
|
||||
|
||||
const labels = {
|
||||
'metric.label.instance_name': ['instance_name_1', 'instance_name_2'],
|
||||
'resource.label.instance_id': ['instance_id_1', 'instance_id_2'],
|
||||
'resource.label.project_id': ['project_id_1', 'project_id_2'],
|
||||
'resource.label.zone': ['zone_1', 'zone_2'],
|
||||
'resource.type': ['type_1', 'type_2'],
|
||||
};
|
||||
|
||||
describe('LabelFilter', () => {
|
||||
it('should render an add button with no filters passed in', () => {
|
||||
render(<LabelFilter labels={{}} filters={[]} onChange={() => {}} variableOptionGroup={[]} />);
|
||||
expect(screen.getByLabelText('Add')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render filters if any are passed in', () => {
|
||||
const filters = ['key_1', '=', 'value_1'];
|
||||
render(<LabelFilter labels={{}} filters={filters} onChange={() => {}} variableOptionGroup={[]} />);
|
||||
expect(screen.getByText('key_1')).toBeInTheDocument();
|
||||
expect(screen.getByText('value_1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('can add filters', async () => {
|
||||
const onChange = jest.fn();
|
||||
render(<LabelFilter labels={{}} filters={[]} onChange={onChange} variableOptionGroup={[]} />);
|
||||
await userEvent.click(screen.getByLabelText('Add'));
|
||||
expect(onChange).toBeCalledWith(expect.arrayContaining(['', '=', '']));
|
||||
});
|
||||
|
||||
it('should render grouped labels', async () => {
|
||||
const filters = ['key_1', '=', 'value_1'];
|
||||
render(<LabelFilter labels={labels} filters={filters} onChange={() => {}} variableOptionGroup={[]} />);
|
||||
|
||||
await openMenu(screen.getByLabelText('Filter label key'));
|
||||
|
||||
expect(screen.getByText('Metric Label')).toBeInTheDocument();
|
||||
expect(screen.getByText('metric.label.instance_name')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Resource Label')).toBeInTheDocument();
|
||||
expect(screen.getByText('resource.label.instance_id')).toBeInTheDocument();
|
||||
expect(screen.getByText('resource.label.project_id')).toBeInTheDocument();
|
||||
expect(screen.getByText('resource.label.zone')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Resource Type')).toBeInTheDocument();
|
||||
expect(screen.getByText('resource.type')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('can select a label key to filter on', async () => {
|
||||
const onChange = jest.fn();
|
||||
const filters = ['key_1', '=', ''];
|
||||
render(<LabelFilter labels={labels} filters={filters} onChange={onChange} variableOptionGroup={[]} />);
|
||||
|
||||
const key = screen.getByLabelText('Filter label key');
|
||||
await select(key, 'metric.label.instance_name', { container: document.body });
|
||||
|
||||
expect(onChange).toBeCalledWith(expect.arrayContaining(['metric.label.instance_name', '=', '']));
|
||||
});
|
||||
|
||||
it('should on render label values for the selected filter key', async () => {
|
||||
const filters = ['metric.label.instance_name', '=', ''];
|
||||
render(<LabelFilter labels={labels} filters={filters} onChange={() => {}} variableOptionGroup={[]} />);
|
||||
|
||||
await openMenu(screen.getByLabelText('Filter label value'));
|
||||
expect(screen.getByText('instance_name_1')).toBeInTheDocument();
|
||||
expect(screen.getByText('instance_name_2')).toBeInTheDocument();
|
||||
expect(screen.queryByText('instance_id_1')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('instance_id_2')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('project_id_1')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('project_id_2')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('zone_1')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('zone_2')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('type_1')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('type_2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('can select a label value to filter on', async () => {
|
||||
const onChange = jest.fn();
|
||||
const filters = ['metric.label.instance_name', '=', ''];
|
||||
render(<LabelFilter labels={labels} filters={filters} onChange={onChange} variableOptionGroup={[]} />);
|
||||
|
||||
const key = screen.getByLabelText('Filter label value');
|
||||
await select(key, 'instance_name_1', { container: document.body });
|
||||
|
||||
expect(onChange).toBeCalledWith(expect.arrayContaining(['metric.label.instance_name', '=', 'instance_name_1']));
|
||||
});
|
||||
});
|
@ -0,0 +1,119 @@
|
||||
import React, { FunctionComponent, useMemo } from 'react';
|
||||
|
||||
import { SelectableValue, toOption } from '@grafana/data';
|
||||
import { AccessoryButton, EditorRow, EditorField, EditorList } from '@grafana/experimental';
|
||||
import { HorizontalGroup, Select } from '@grafana/ui';
|
||||
|
||||
import { labelsToGroupedOptions, stringArrayToFilters } from '../../functions';
|
||||
|
||||
export interface Props {
|
||||
labels: { [key: string]: string[] };
|
||||
filters: string[];
|
||||
onChange: (filters: string[]) => void;
|
||||
variableOptionGroup: SelectableValue<string>;
|
||||
}
|
||||
|
||||
interface Filter {
|
||||
key: string;
|
||||
operator: string;
|
||||
value: string;
|
||||
condition: string;
|
||||
}
|
||||
|
||||
const DEFAULT_OPERATOR = '=';
|
||||
const DEFAULT_CONDITION = 'AND';
|
||||
|
||||
const filtersToStringArray = (filters: Filter[]) =>
|
||||
filters.flatMap(({ key, operator, value, condition }) => [key, operator, value, condition]).slice(0, -1);
|
||||
|
||||
const operators = ['=', '!=', '=~', '!=~'].map(toOption);
|
||||
|
||||
export const LabelFilter: FunctionComponent<Props> = ({
|
||||
labels = {},
|
||||
filters: filterArray,
|
||||
onChange: _onChange,
|
||||
variableOptionGroup,
|
||||
}) => {
|
||||
const filters: Filter[] = useMemo(() => stringArrayToFilters(filterArray), [filterArray]);
|
||||
const options = useMemo(
|
||||
() => [variableOptionGroup, ...labelsToGroupedOptions(Object.keys(labels))],
|
||||
[labels, variableOptionGroup]
|
||||
);
|
||||
|
||||
const getOptions = ({ key = '', value = '' }: Partial<Filter>) => {
|
||||
// Add the current key and value as options if they are manually entered
|
||||
const keyPresent = options.some((op) => {
|
||||
if (op.options) {
|
||||
return options.some((opp) => opp.label === key);
|
||||
}
|
||||
return op.label === key;
|
||||
});
|
||||
if (!keyPresent) {
|
||||
options.push({ label: key, value: key });
|
||||
}
|
||||
|
||||
const valueOptions = labels.hasOwnProperty(key)
|
||||
? [variableOptionGroup, ...labels[key].map(toOption)]
|
||||
: [variableOptionGroup];
|
||||
const valuePresent = valueOptions.some((op) => op.label === value);
|
||||
if (!valuePresent) {
|
||||
valueOptions.push({ label: value, value });
|
||||
}
|
||||
|
||||
return { options, valueOptions };
|
||||
};
|
||||
|
||||
const onChange = (items: Array<Partial<Filter>>) => {
|
||||
const filters = items.map(({ key, operator, value, condition }) => ({
|
||||
key: key || '',
|
||||
operator: operator || DEFAULT_OPERATOR,
|
||||
value: value || '',
|
||||
condition: condition || DEFAULT_CONDITION,
|
||||
}));
|
||||
_onChange(filtersToStringArray(filters));
|
||||
};
|
||||
|
||||
const renderItem = (item: Partial<Filter>, onChangeItem: (item: Filter) => void, onDeleteItem: () => void) => {
|
||||
const { key = '', operator = DEFAULT_OPERATOR, value = '', condition = DEFAULT_CONDITION } = item;
|
||||
const { options, valueOptions } = getOptions(item);
|
||||
|
||||
return (
|
||||
<HorizontalGroup spacing="xs" width="auto">
|
||||
<Select
|
||||
aria-label="Filter label key"
|
||||
formatCreateLabel={(v) => `Use label key: ${v}`}
|
||||
allowCustomValue
|
||||
value={key}
|
||||
options={options}
|
||||
onChange={({ value: key = '' }) => onChangeItem({ key, operator, value, condition })}
|
||||
/>
|
||||
<Select
|
||||
value={operator}
|
||||
options={operators}
|
||||
onChange={({ value: operator = DEFAULT_OPERATOR }) => onChangeItem({ key, operator, value, condition })}
|
||||
/>
|
||||
<Select
|
||||
aria-label="Filter label value"
|
||||
placeholder="add filter value"
|
||||
formatCreateLabel={(v) => `Use label value: ${v}`}
|
||||
allowCustomValue
|
||||
value={value}
|
||||
options={valueOptions}
|
||||
onChange={({ value = '' }) => onChangeItem({ key, operator, value, condition })}
|
||||
/>
|
||||
<AccessoryButton aria-label="Remove" icon="times" variant="secondary" onClick={onDeleteItem} type="button" />
|
||||
</HorizontalGroup>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorRow>
|
||||
<EditorField
|
||||
label="Filter"
|
||||
tooltip="To reduce the amount of data charted, apply a filter. A filter has three components: a label, a comparison, and a value. The comparison can be an equality, inequality, or regular expression."
|
||||
>
|
||||
<EditorList items={filters} renderItem={renderItem} onChange={onChange} />
|
||||
</EditorField>
|
||||
</EditorRow>
|
||||
);
|
||||
};
|
@ -4,10 +4,10 @@ import { SelectableValue } from '@grafana/data';
|
||||
|
||||
import CloudMonitoringDatasource from '../../datasource';
|
||||
import { CustomMetaData, MetricDescriptor, MetricQuery, SLOQuery } from '../../types';
|
||||
import { LabelFilter } from '../index';
|
||||
|
||||
import { Alignment } from './Alignment';
|
||||
import { GroupBy } from './GroupBy';
|
||||
import { LabelFilter } from './LabelFilter';
|
||||
import { Metrics } from './Metrics';
|
||||
import { Preprocessor } from './Preprocessor';
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user