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