mirror of https://github.com/grafana/grafana
Cloud Monitoring: Update LabelFilter to use experimental UI components (#51342)
* add label filter * add tests * define labels once * update betterer resultspull/51351/head
parent
93e2a0eddc
commit
b5eef488ce
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> |
||||||
|
); |
||||||
|
}; |
Loading…
Reference in new issue