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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user