From 06b5875c3c3396f9df01383b1a425a7347a158eb Mon Sep 17 00:00:00 2001 From: Alvaro Huarte Date: Wed, 14 Feb 2024 16:36:30 +0100 Subject: [PATCH] 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> --- .../visualizations/table/index.md | 6 + .../src/components/Table/FilterList.tsx | 107 ++++++++++++++++-- .../src/components/Table/FilterPopup.tsx | 8 +- 3 files changed, 113 insertions(+), 8 deletions(-) diff --git a/docs/sources/panels-visualizations/visualizations/table/index.md b/docs/sources/panels-visualizations/visualizations/table/index.md index 91862f56b01..8c0c66cb480 100644 --- a/docs/sources/panels-visualizations/visualizations/table/index.md +++ b/docs/sources/panels-visualizations/visualizations/table/index.md @@ -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 diff --git a/packages/grafana-ui/src/components/Table/FilterList.tsx b/packages/grafana-ui/src/components/Table/FilterList.tsx index 1c56ff7f631..7d24c3847d1 100644 --- a/packages/grafana-ui/src/components/Table/FilterList.tsx +++ b/packages/grafana-ui/src/components/Table/FilterList.tsx @@ -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 } = { + 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>(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 ( - + {!showOperators && } + {showOperators && ( + + + variant="canvas" + options={OPERATORS} + onChange={setOperator} + value={operator} + tooltip={operator.description} + /> + + + )} {!items.length && } {items.length && (
- +