mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
7d21eb0631
commit
06b5875c3c
@ -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.
|
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.
|
Click the check box above the **Ok** and **Cancel** buttons to add or remove all displayed values to/from the filter.
|
||||||
|
|
||||||
### Clear column filters
|
### Clear column filters
|
||||||
|
@ -2,9 +2,9 @@ import { css, cx } from '@emotion/css';
|
|||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { FixedSizeList as List } from 'react-window';
|
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';
|
import { useStyles2, useTheme2 } from '../../themes';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -12,23 +12,104 @@ interface Props {
|
|||||||
options: SelectableValue[];
|
options: SelectableValue[];
|
||||||
onChange: (options: SelectableValue[]) => void;
|
onChange: (options: SelectableValue[]) => void;
|
||||||
caseSensitive?: boolean;
|
caseSensitive?: boolean;
|
||||||
|
showOperators?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ITEM_HEIGHT = 28;
|
const ITEM_HEIGHT = 28;
|
||||||
const MIN_HEIGHT = ITEM_HEIGHT * 5;
|
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 [searchFilter, setSearchFilter] = useState('');
|
||||||
const regex = useMemo(() => new RegExp(searchFilter, caseSensitive ? undefined : 'i'), [searchFilter, caseSensitive]);
|
const regex = useMemo(() => new RegExp(searchFilter, caseSensitive ? undefined : 'i'), [searchFilter, caseSensitive]);
|
||||||
const items = useMemo(
|
const items = useMemo(
|
||||||
() =>
|
() =>
|
||||||
options.filter((option) => {
|
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 false;
|
||||||
}
|
}
|
||||||
return regex.test(option.label);
|
|
||||||
}),
|
}),
|
||||||
[options, regex]
|
[options, regex, showOperators, operator, searchFilter]
|
||||||
);
|
);
|
||||||
const selectedItems = useMemo(() => items.filter((item) => values.includes(item)), [items, values]);
|
const selectedItems = useMemo(() => items.filter((item) => values.includes(item)), [items, values]);
|
||||||
|
|
||||||
@ -77,7 +158,19 @@ export const FilterList = ({ options, values, caseSensitive, onChange }: Props)
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<VerticalGroup spacing="md">
|
<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 && <Label>No values</Label>}
|
||||||
{items.length && (
|
{items.length && (
|
||||||
<List
|
<List
|
||||||
|
@ -67,7 +67,13 @@ export const FilterPopup = ({ column: { preFilteredRows, filterValue, setFilter
|
|||||||
/>
|
/>
|
||||||
</HorizontalGroup>
|
</HorizontalGroup>
|
||||||
<div className={cx(styles.listDivider)} />
|
<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>
|
</VerticalGroup>
|
||||||
<HorizontalGroup spacing="lg">
|
<HorizontalGroup spacing="lg">
|
||||||
<HorizontalGroup>
|
<HorizontalGroup>
|
||||||
|
Loading…
Reference in New Issue
Block a user