diff --git a/web/locales/en/plugin__netobserv-plugin.json b/web/locales/en/plugin__netobserv-plugin.json index 1865b5255..2d2ac0d8d 100644 --- a/web/locales/en/plugin__netobserv-plugin.json +++ b/web/locales/en/plugin__netobserv-plugin.json @@ -411,6 +411,7 @@ "Last 1 week": "Last 1 week", "Last 2 weeks": "Last 2 weeks", "Value is empty": "Value is empty", + "Value is malformed": "Value is malformed", "Not a valid Kubernetes name": "Not a valid Kubernetes name", "Incomplete resource name, either kind, namespace or name is missing.": "Incomplete resource name, either kind, namespace or name is missing.", "Kind is empty": "Kind is empty", diff --git a/web/src/components/toolbar/filters-toolbar.tsx b/web/src/components/toolbar/filters-toolbar.tsx index e1cd1dfe9..b4aaa154a 100644 --- a/web/src/components/toolbar/filters-toolbar.tsx +++ b/web/src/components/toolbar/filters-toolbar.tsx @@ -121,11 +121,17 @@ export const FiltersToolbar: React.FC = ({ switch (selectedFilter.component) { case 'text': case 'number': - return ; + return ( + + ); case 'autocomplete': return ; } - }, [selectedFilter, addFilter, indicator, setIndicator, setMessageWithDelay]); + }, [selectedFilter, addFilter, setMessageWithDelay, indicator, selectedCompare]); const isForced = !_.isEmpty(forcedFilters); const filtersOrForced = isForced ? forcedFilters : filters; diff --git a/web/src/components/toolbar/filters/autocomplete-filter.tsx b/web/src/components/toolbar/filters/autocomplete-filter.tsx index adb578159..1d1b69c9e 100644 --- a/web/src/components/toolbar/filters/autocomplete-filter.tsx +++ b/web/src/components/toolbar/filters/autocomplete-filter.tsx @@ -14,6 +14,7 @@ import * as React from 'react'; import { createFilterValue, FilterDefinition, FilterOption, FilterValue } from '../../../model/filters'; import { autoCompleteCache } from '../../../utils/autocomplete-cache'; import { getHTTPErrorDetails } from '../../../utils/errors'; +import { undefinedValue } from '../../../utils/filter-definitions'; import { Indicator } from '../../../utils/filters-helper'; import { usePrevious } from '../../../utils/previous-hook'; import './autocomplete-filter.css'; @@ -128,13 +129,19 @@ export const AutocompleteFilter: React.FC = ({ ); const onEnter = React.useCallback(() => { + // override empty value by undefined value + let v = currentValue; + if (currentValue.length === 0) { + v = undefinedValue; + } + // Only one choice is present, consider this is what is desired if (options.length === 1) { onAutoCompleteOptionSelected(options[0]); return; } - const validation = filterDefinition.validate(currentValue); + const validation = filterDefinition.validate(v); //show tooltip and icon when user try to validate filter if (!_.isEmpty(validation.err)) { setMessageWithDelay(validation.err); diff --git a/web/src/components/toolbar/filters/text-filter.tsx b/web/src/components/toolbar/filters/text-filter.tsx index 0b22b321a..cfd330859 100644 --- a/web/src/components/toolbar/filters/text-filter.tsx +++ b/web/src/components/toolbar/filters/text-filter.tsx @@ -3,6 +3,7 @@ import { SearchIcon } from '@patternfly/react-icons'; import * as _ from 'lodash'; import * as React from 'react'; import { createFilterValue, FilterDefinition, FilterValue } from '../../../model/filters'; +import { doubleQuoteValue, undefinedValue } from '../../../utils/filter-definitions'; import { Indicator } from '../../../utils/filters-helper'; export interface TextFilterProps { @@ -11,6 +12,7 @@ export interface TextFilterProps { setMessageWithDelay: (m: string | undefined) => void; indicator: Indicator; setIndicator: (ind: Indicator) => void; + allowEmpty?: boolean; regexp?: RegExp; } @@ -20,6 +22,7 @@ export const TextFilter: React.FC = ({ setMessageWithDelay, indicator, setIndicator, + allowEmpty, regexp }) => { const searchInputRef = React.useRef(null); @@ -38,7 +41,7 @@ export const TextFilter: React.FC = ({ const updateValue = React.useCallback( (v: string) => { let filteredValue = v; - if (regexp) { + if (![doubleQuoteValue, undefinedValue].includes(filteredValue) && regexp) { filteredValue = filteredValue.replace(regexp, ''); } setCurrentValue(filteredValue); @@ -53,7 +56,17 @@ export const TextFilter: React.FC = ({ }, [setCurrentValue, setMessageWithDelay, setIndicator]); const onSelect = React.useCallback(() => { - const validation = filterDefinition.validate(String(currentValue)); + // override empty value by undefined value + let v = currentValue; + if (allowEmpty) { + if (currentValue.length === 0) { + v = undefinedValue; + } + } else if (v === undefinedValue) { + v = ''; + } + + const validation = filterDefinition.validate(String(v)); //show tooltip and icon when user try to validate filter if (!_.isEmpty(validation.err)) { setMessageWithDelay(validation.err); @@ -66,7 +79,7 @@ export const TextFilter: React.FC = ({ resetFilterValue(); } }); - }, [filterDefinition, currentValue, setMessageWithDelay, setIndicator, addFilter, resetFilterValue]); + }, [currentValue, allowEmpty, filterDefinition, setMessageWithDelay, setIndicator, addFilter, resetFilterValue]); return ( <> diff --git a/web/src/model/filters.ts b/web/src/model/filters.ts index f652b4c35..46872b3b2 100644 --- a/web/src/model/filters.ts +++ b/web/src/model/filters.ts @@ -1,5 +1,6 @@ import _ from 'lodash'; import { isEqual } from '../utils/base-compare'; +import { undefinedValue } from '../utils/filter-definitions'; export type FiltersEncoder = (values: FilterValue[], matchAny: boolean, not: boolean, moreThan: boolean) => string; @@ -94,7 +95,9 @@ export interface FilterOption { export const createFilterValue = (def: FilterDefinition, value: string): Promise => { return def.getOptions(value).then(opts => { const option = opts.find(opt => opt.name === value || opt.value === value); - return option ? { v: option.value, display: option.name } : { v: value }; + return option + ? { v: option.value, display: option.name } + : { v: value, display: value === undefinedValue ? 'n/a' : undefined }; }); }; diff --git a/web/src/utils/filter-definitions.ts b/web/src/utils/filter-definitions.ts index 44f5d0ec0..fa4e5f3a7 100644 --- a/web/src/utils/filter-definitions.ts +++ b/web/src/utils/filter-definitions.ts @@ -38,6 +38,8 @@ import { validateK8SName, validateStrictK8SName } from './label'; // Convenience string to filter by undefined field values export const undefinedValue = '""'; +// Unique double are allowed while typing but invalid +export const doubleQuoteValue = '"'; const matcher = (left: string, right: string[], not: boolean, moreThan: boolean) => `${left}${not ? '!=' : moreThan ? '>=' : '='}${right.join(',')}`; @@ -136,15 +138,19 @@ export const getFilterDefinitions = ( if (_.isEmpty(value)) { return invalid(t('Value is empty')); } + if (value === doubleQuoteValue) { + return invalid(t('Value is malformed')); + } return valid(value); }; const k8sNameValidation = (value: string) => { if (_.isEmpty(value)) { - // Replace with exact match - return valid('""'); + return invalid(t('Value is empty')); } - return value === '""' || validateK8SName(value) ? valid(value) : invalid(t('Not a valid Kubernetes name')); + return value === undefinedValue || validateK8SName(value) + ? valid(value) + : invalid(t('Not a valid Kubernetes name')); }; const k8sResourceValidation = (value: string) => { @@ -208,7 +214,7 @@ export const getFilterDefinitions = ( if (_.isEmpty(value)) { return invalid(t('Value is empty')); } - return /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})/.test(value) + return value == undefinedValue || /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})/.test(value) ? valid(value) : invalid(t('Not a valid MAC address')); };