Table Panel: Filter column values with operators or expressions (#79853)

* Select column values using comparison operators

* Select column values using expressions

* Capitalize operator labels

* Update docs/sources/panels-visualizations/visualizations/table/index.md

Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com>

* Use $ instead of v to represent a variable

* Define operators as a map string of objects

* Fix typo

---------

Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com>
This commit is contained in:
Alvaro Huarte 2024-02-14 16:36:30 +01:00 committed by GitHub
parent 7d21eb0631
commit 06b5875c3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 113 additions and 8 deletions

View File

@ -191,6 +191,12 @@ To filter column values, click the filter (funnel) icon next to a column title.
Click the check box next to the values that you want to display. Enter text in the search field at the top to show those values in the display so that you can select them rather than scroll to find them.
Choose from several operators to display column values:
- **Contains** - Matches a regex pattern (operator by default).
- **Expression** - Evaluates a boolean expression. The character `$` represents the column value in the expression (for example, "$ >= 10 && $ <= 12").
- The typical comparison operators: `=`, `!=`, `<`, `<=`, `>`, `>=`.
Click the check box above the **Ok** and **Cancel** buttons to add or remove all displayed values to/from the filter.
### Clear column filters

View File

@ -2,9 +2,9 @@ import { css, cx } from '@emotion/css';
import React, { useCallback, useMemo, useState } from 'react';
import { FixedSizeList as List } from 'react-window';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { GrafanaTheme2, formattedValueToString, getValueFormat, SelectableValue } from '@grafana/data';
import { Checkbox, FilterInput, Label, VerticalGroup } from '..';
import { ButtonSelect, Checkbox, FilterInput, HorizontalGroup, Label, VerticalGroup } from '..';
import { useStyles2, useTheme2 } from '../../themes';
interface Props {
@ -12,23 +12,104 @@ interface Props {
options: SelectableValue[];
onChange: (options: SelectableValue[]) => void;
caseSensitive?: boolean;
showOperators?: boolean;
}
const ITEM_HEIGHT = 28;
const MIN_HEIGHT = ITEM_HEIGHT * 5;
export const FilterList = ({ options, values, caseSensitive, onChange }: Props) => {
const operatorSelectableValues: { [key: string]: SelectableValue<string> } = {
Contains: { label: 'Contains', value: 'Contains', description: 'Contains' },
'=': { label: '=', value: '=', description: 'Equals' },
'!=': { label: '!=', value: '!=', description: 'Not equals' },
'>': { label: '>', value: '>', description: 'Greater' },
'>=': { label: '>=', value: '>=', description: 'Greater or Equal' },
'<': { label: '<', value: '<', description: 'Less' },
'<=': { label: '<=', value: '<=', description: 'Less or Equal' },
Expression: {
label: 'Expression',
value: 'Expression',
description: 'Bool Expression (Char $ represents the column value in the expression, e.g. "$ >= 10 && $ <= 12")',
},
};
const OPERATORS = Object.values(operatorSelectableValues);
const REGEX_OPERATOR = operatorSelectableValues['Contains'];
const XPR_OPERATOR = operatorSelectableValues['Expression'];
const comparableValue = (value: string): string | number | Date | boolean => {
value = value.trim().replace(/\\/g, '');
// Does it look like a Date (Starting with pattern YYYY-MM-DD* or YYYY/MM/DD*)?
if (/^(\d{4}-\d{2}-\d{2}|\d{4}\/\d{2}\/\d{2})/.test(value)) {
const date = new Date(value);
if (!isNaN(date.getTime())) {
const fmt = getValueFormat('dateTimeAsIso');
return formattedValueToString(fmt(date.getTime()));
}
}
// Does it look like a Number?
const num = parseFloat(value);
if (!isNaN(num)) {
return num;
}
// Does it look like a Bool?
const lvalue = value.toLowerCase();
if (lvalue === 'true' || lvalue === 'false') {
return lvalue === 'true';
}
// Anything else
return value;
};
export const FilterList = ({ options, values, caseSensitive, showOperators, onChange }: Props) => {
const [operator, setOperator] = useState<SelectableValue<string>>(REGEX_OPERATOR);
const [searchFilter, setSearchFilter] = useState('');
const regex = useMemo(() => new RegExp(searchFilter, caseSensitive ? undefined : 'i'), [searchFilter, caseSensitive]);
const items = useMemo(
() =>
options.filter((option) => {
if (option.label === undefined) {
if (!showOperators || !searchFilter || operator.value === REGEX_OPERATOR.value) {
if (option.label === undefined) {
return false;
}
return regex.test(option.label);
} else if (operator.value === XPR_OPERATOR.value) {
if (option.value === undefined) {
return false;
}
try {
const xpr = searchFilter.replace(/\\/g, '');
const fnc = new Function('$', `'use strict'; return ${xpr};`);
const val = comparableValue(option.value);
return fnc(val);
} catch (_) {}
return false;
} else {
if (option.value === undefined) {
return false;
}
const value1 = comparableValue(option.value);
const value2 = comparableValue(searchFilter);
switch (operator.value) {
case '=':
return value1 === value2;
case '!=':
return value1 !== value2;
case '>':
return value1 > value2;
case '>=':
return value1 >= value2;
case '<':
return value1 < value2;
case '<=':
return value1 <= value2;
}
return false;
}
return regex.test(option.label);
}),
[options, regex]
[options, regex, showOperators, operator, searchFilter]
);
const selectedItems = useMemo(() => items.filter((item) => values.includes(item)), [items, values]);
@ -77,7 +158,19 @@ export const FilterList = ({ options, values, caseSensitive, onChange }: Props)
return (
<VerticalGroup spacing="md">
<FilterInput placeholder="Filter values" onChange={setSearchFilter} value={searchFilter} />
{!showOperators && <FilterInput placeholder="Filter values" onChange={setSearchFilter} value={searchFilter} />}
{showOperators && (
<HorizontalGroup>
<ButtonSelect<string>
variant="canvas"
options={OPERATORS}
onChange={setOperator}
value={operator}
tooltip={operator.description}
/>
<FilterInput placeholder="Filter values" onChange={setSearchFilter} value={searchFilter} />
</HorizontalGroup>
)}
{!items.length && <Label>No values</Label>}
{items.length && (
<List

View File

@ -67,7 +67,13 @@ export const FilterPopup = ({ column: { preFilteredRows, filterValue, setFilter
/>
</HorizontalGroup>
<div className={cx(styles.listDivider)} />
<FilterList onChange={setValues} values={values} options={options} caseSensitive={matchCase} />
<FilterList
onChange={setValues}
values={values}
options={options}
caseSensitive={matchCase}
showOperators={true}
/>
</VerticalGroup>
<HorizontalGroup spacing="lg">
<HorizontalGroup>