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:
Kevin Yu 2022-07-06 08:28:54 -07:00 committed by GitHub
parent 93e2a0eddc
commit b5eef488ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 218 additions and 8327 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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']));
});
});

View File

@ -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>
);
};

View File

@ -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';