Skip to content

Commit 83dd1dc

Browse files
committed
improved suggestions and display
1 parent 49a0ed9 commit 83dd1dc

File tree

5 files changed

+129
-57
lines changed

5 files changed

+129
-57
lines changed

web/locales/en/plugin__netobserv-plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@
375375
"Equals": "Equals",
376376
"Learn more": "Learn more",
377377
"Filter already exists": "Filter already exists",
378-
"`>=` is not allowed with `{{searchValue}}`. Use `=` or `!=` instead.": "`>=` is not allowed with `{{searchValue}}`. Use `=` or `!=` instead.",
378+
"More than operator `>=` is not allowed with `{{searchValue}}`. Use equals or contains operators instead.": "More than operator `>=` is not allowed with `{{searchValue}}`. Use equals or contains operators instead.",
379379
"Can't find filter `{{searchValue}}`": "Can't find filter `{{searchValue}}`",
380380
"Invalid format. The input should be <filter><comparator><value> such as `name=netobserv`.": "Invalid format. The input should be <filter><comparator><value> such as `name=netobserv`.",
381381
"Filter": "Filter",

web/src/components/toolbar/filters-toolbar.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ button.pf-v5-c-button.pf-v5-m-link.pf-v5-m-inline:empty {
4848
div#filter-toolbar-search-filters {
4949
padding: 0;
5050
padding-top: 1em;
51+
padding-bottom: 1em;
5152
}
5253

5354
.pf-v5-c-toolbar__item.pf-v5-m-align-right {
@@ -123,7 +124,7 @@ div#filter-toolbar-search-filters {
123124
background-color: #6A6E73;
124125
}
125126

126-
.pf-v5-theme-dark .custom-chip-box {
127+
.pf-v5-theme-dark .custom-chip-peer {
127128
color: #fff;
128129
background-color: #002952;
129130
}

web/src/components/toolbar/filters/compare-filter.tsx

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Badge, Dropdown, DropdownItem, MenuToggle, MenuToggleElement } from '@patternfly/react-core';
22
import * as React from 'react';
3-
import { useTranslation } from 'react-i18next';
3+
import { TFunction, useTranslation } from 'react-i18next';
44
import { FilterComponent } from '../../../model/filters';
55
import { usePrevious } from '../../../utils/previous-hook';
66

@@ -11,6 +11,23 @@ export enum FilterCompare {
1111
notEqual = '!=',
1212
moreThanOrEqual = '>='
1313
}
14+
15+
export const getCompareText = (v: FilterCompare, t: TFunction) => {
16+
switch (v) {
17+
case FilterCompare.match:
18+
return t('Contains');
19+
case FilterCompare.notMatch:
20+
return t('Not contains');
21+
case FilterCompare.notEqual:
22+
return t('Not equals');
23+
case FilterCompare.moreThanOrEqual:
24+
return t('More than');
25+
case FilterCompare.equal:
26+
default:
27+
return t('Equals');
28+
}
29+
};
30+
1431
export interface CompareFilterProps {
1532
value: FilterCompare;
1633
setValue: (newState: FilterCompare) => void;
@@ -22,24 +39,7 @@ export const CompareFilter: React.FC<CompareFilterProps> = ({ value, setValue, c
2239
const [isOpen, setOpen] = React.useState(false);
2340
const prevComponent = usePrevious(component);
2441

25-
const getText = React.useCallback(
26-
(v: FilterCompare) => {
27-
switch (v) {
28-
case FilterCompare.match:
29-
return t('Contains');
30-
case FilterCompare.notMatch:
31-
return t('Not contains');
32-
case FilterCompare.notEqual:
33-
return t('Not equals');
34-
case FilterCompare.moreThanOrEqual:
35-
return t('More than');
36-
case FilterCompare.equal:
37-
default:
38-
return t('Equals');
39-
}
40-
},
41-
[t]
42-
);
42+
const getText = React.useCallback((v: FilterCompare) => getCompareText(v, t), [t]);
4343

4444
const onSelect = React.useCallback(
4545
(v: FilterCompare) => {

web/src/components/toolbar/filters/filter-search-input.tsx

Lines changed: 104 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ import {
1717
import _ from 'lodash';
1818
import * as React from 'react';
1919
import { useTranslation } from 'react-i18next';
20-
import { FilterDefinition, Filters, FilterValue, findFromFilters } from '../../../model/filters';
20+
import { FilterDefinition, FilterOption, Filters, FilterValue, findFromFilters } from '../../../model/filters';
2121
import { getHTTPErrorDetails } from '../../../utils/errors';
2222
import { matcher } from '../../../utils/filter-definitions';
2323
import { Indicator, setTargeteableFilters } from '../../../utils/filters-helper';
2424
import { useOutsideClickEvent } from '../../../utils/outside-hook';
2525
import { Direction } from '../filters-toolbar';
2626
import AutocompleteFilter from './autocomplete-filter';
27-
import CompareFilter, { FilterCompare } from './compare-filter';
27+
import CompareFilter, { FilterCompare, getCompareText } from './compare-filter';
2828
import { FilterHints } from './filter-hints';
2929
import './filter-search-input.css';
3030
import FiltersDropdown from './filters-dropdown';
@@ -37,6 +37,12 @@ interface FormUpdateResult {
3737
hasError: boolean;
3838
}
3939

40+
interface Suggestion {
41+
display?: string;
42+
value: string;
43+
validate: boolean;
44+
}
45+
4046
export interface FilterSearchInputProps {
4147
filterDefinitions: FilterDefinition[];
4248
filters?: Filters;
@@ -77,21 +83,24 @@ export const FilterSearchInput: React.FC<FilterSearchInputProps> = ({
7783
const { t } = useTranslation('plugin__netobserv-plugin');
7884

7985
const filterSearchInputContainerRef = React.useRef(null);
80-
const searchInputRef = React.useRef(null);
86+
const searchInputRef = React.useRef<HTMLInputElement>(null);
8187
const popperRef = useOutsideClickEvent(() => {
8288
// delay this to avoid conflict with onToggle event
8389
// clicking on the arrow will skip the onToggle and trigger this code after the delay
8490
setTimeout(() => {
85-
setSearchInputValue(getEncodedValue());
86-
setSuggestions([]);
87-
setPopperOpen(!isPopperOpen);
88-
// clear search field to show the placeholder back
89-
if (_.isEmpty(value)) {
90-
setSearchInputValue('');
91+
if (suggestions.length) {
92+
setSuggestions([]);
93+
} else {
94+
setPopperOpen(false);
95+
setSearchInputValue(getEncodedValue());
96+
// clear search field to show the placeholder back
97+
if (_.isEmpty(value)) {
98+
setSearchInputValue('');
99+
}
91100
}
92101
}, 100);
93102
});
94-
const [suggestions, setSuggestions] = React.useState<string[]>([]);
103+
const [suggestions, setSuggestions] = React.useState<Suggestion[]>([]);
95104
const [isPopperOpen, setPopperOpen] = React.useState(false);
96105
const [submitPending, setSubmitPending] = React.useState(false);
97106

@@ -135,11 +144,7 @@ export const FilterSearchInput: React.FC<FilterSearchInputProps> = ({
135144
const updateForm = React.useCallback(
136145
(v: string = searchInputValue, submitOnRefresh?: boolean) => {
137146
// parse search input value to form content
138-
let fv = v;
139-
Object.values(FilterCompare).forEach(fc => {
140-
fv = fv.replaceAll(fc, '|');
141-
});
142-
const fieldValue = fv.split('|');
147+
const fieldValue = v.split(/>=|!=|!~|=|~/);
143148
const result: FormUpdateResult = { hasError: false };
144149

145150
// if field + value are valid, we should end with 2 items only
@@ -150,19 +155,24 @@ export const FilterSearchInput: React.FC<FilterSearchInputProps> = ({
150155
// set compare
151156
if (v.includes(FilterCompare.moreThanOrEqual)) {
152157
if (def.component != 'number') {
153-
setMessage(t('`>=` is not allowed with `{{searchValue}}`. Use `=` or `!=` instead.', { searchValue }));
158+
setMessage(
159+
t(
160+
'More than operator `>=` is not allowed with `{{searchValue}}`. Use equals or contains operators instead.',
161+
{ searchValue }
162+
)
163+
);
154164
setIndicator(ValidatedOptions.error);
155165
return { ...result, hasError: true };
156166
}
157167
setCompare(FilterCompare.moreThanOrEqual);
158168
result.comparator = FilterCompare.moreThanOrEqual;
159-
} else if (v.includes('!=')) {
169+
} else if (v.includes(FilterCompare.notEqual)) {
160170
setCompare(FilterCompare.notEqual);
161171
result.comparator = FilterCompare.notEqual;
162-
} else if (v.includes('=')) {
172+
} else if (v.includes(FilterCompare.equal)) {
163173
setCompare(FilterCompare.equal);
164174
result.comparator = FilterCompare.equal;
165-
} else if (v.includes('!~')) {
175+
} else if (v.includes(FilterCompare.notMatch)) {
166176
setCompare(FilterCompare.notMatch);
167177
result.comparator = FilterCompare.notMatch;
168178
} else {
@@ -243,6 +253,23 @@ export const FilterSearchInput: React.FC<FilterSearchInputProps> = ({
243253

244254
const onSearchChange = React.useCallback(
245255
(v: string) => {
256+
const defToSuggestion = (fd: FilterDefinition) => {
257+
return {
258+
display:
259+
fd.category === 'source'
260+
? `${t('Source')} ${fd.name}`
261+
: fd.category === 'destination'
262+
? `${t('Destination')} ${fd.name}`
263+
: fd.name,
264+
value: fd.id,
265+
validate: false
266+
};
267+
};
268+
269+
const optionToSuggestion = (o: FilterOption) => {
270+
return { display: o.name !== o.value ? o.name : undefined, value: o.value, validate: true };
271+
};
272+
246273
setSearchInputValue(v);
247274
const updated = updateForm(v);
248275
if (!v.length || updated.hasError) {
@@ -255,7 +282,7 @@ export const FilterSearchInput: React.FC<FilterSearchInputProps> = ({
255282
filter
256283
.getOptions(updated.value || '')
257284
.then(v => {
258-
setSuggestions(v.map(o => o.value));
285+
setSuggestions(v.map(optionToSuggestion));
259286
})
260287
.catch(err => {
261288
const errorMessage = getHTTPErrorDetails(err);
@@ -268,31 +295,35 @@ export const FilterSearchInput: React.FC<FilterSearchInputProps> = ({
268295
}
269296
} else {
270297
// suggest comparators if field set but not value
271-
let suggestions = Object.values(FilterCompare) as string[];
298+
let suggestions = Object.values(FilterCompare).map(fc => {
299+
return { display: getCompareText(fc, t), value: fc, validate: false };
300+
}) as Suggestion[];
272301
if (filter.component === 'number') {
273-
suggestions = suggestions.filter(s => s !== FilterCompare.match && s !== FilterCompare.notMatch);
302+
suggestions = suggestions.filter(
303+
s => s.value !== FilterCompare.match && s.value !== FilterCompare.notMatch
304+
);
274305
} else {
275-
suggestions = suggestions.filter(s => s != FilterCompare.moreThanOrEqual);
306+
suggestions = suggestions.filter(s => s.value != FilterCompare.moreThanOrEqual);
276307
}
277308
// also suggest other definitions starting by the same id
278309
setSuggestions(
279310
suggestions.concat(
280311
filterDefinitions
281312
.filter(fd => fd.id !== updated.def!.id && fd.id.startsWith(updated.def!.id))
282-
.map(fd => fd.id)
313+
.map(defToSuggestion)
283314
)
284315
);
285316
}
286317
} else if (updated.value?.length) {
287318
// suggest fields if def is not matched yet
288319
const suggestions = filterDefinitions
289320
.filter(fd => fd.id.startsWith(updated.value!))
290-
.map(fd => fd.id) as string[];
321+
.map(defToSuggestion) as Suggestion[];
291322
if (filter.component === 'autocomplete') {
292323
filter
293324
.getOptions(updated.value)
294325
.then(v => {
295-
setSuggestions(suggestions.concat(v.map(o => o.value)));
326+
setSuggestions(suggestions.concat(v.map(optionToSuggestion)));
296327
})
297328
.catch(err => {
298329
const errorMessage = getHTTPErrorDetails(err);
@@ -304,13 +335,29 @@ export const FilterSearchInput: React.FC<FilterSearchInputProps> = ({
304335
}
305336
}
306337
},
338+
// eslint-disable-next-line react-hooks/exhaustive-deps
307339
[filter, filterDefinitions, setMessage, setSearchInputValue, updateForm]
308340
);
309341

310342
const searchInput = React.useCallback(
311343
() => (
312344
<SearchInput
313345
onClear={reset}
346+
onKeyDown={e => {
347+
if (suggestions.length) {
348+
// focus on suggestions on tab / arrow down keys
349+
if (e.key === 'Tab' || e.key === 'ArrowDown') {
350+
e.preventDefault();
351+
document.getElementById('suggestion-0')?.focus();
352+
} else if (e.key === 'Escape') {
353+
// clear suggestions on esc key
354+
setSuggestions([]);
355+
}
356+
} else if (e.key === 'ArrowDown') {
357+
// get suggestions back
358+
onSearchChange(searchInputValue);
359+
}
360+
}}
314361
onChange={(e, v) => onSearchChange(v)}
315362
onSearch={(e, v) => {
316363
setSuggestions([]);
@@ -338,6 +385,7 @@ export const FilterSearchInput: React.FC<FilterSearchInputProps> = ({
338385
reset,
339386
searchInputValue,
340387
setSearchInputValue,
388+
suggestions.length,
341389
updateForm
342390
]
343391
);
@@ -346,29 +394,52 @@ export const FilterSearchInput: React.FC<FilterSearchInputProps> = ({
346394
return (
347395
<div id="filter-popper" ref={popperRef} role="dialog">
348396
{suggestions.length ? (
349-
<Menu>
397+
<Menu
398+
onKeyDown={e => {
399+
if (e.key === 'Escape') {
400+
e.preventDefault();
401+
// clear suggestions on esc key
402+
setSuggestions([]);
403+
searchInputRef.current?.focus();
404+
}
405+
}}
406+
>
350407
<MenuContent>
351408
<MenuList>
352409
{suggestions.map((suggestion, index) => (
353410
<MenuItem
411+
id={`suggestion-${index}`}
354412
itemId={suggestion}
355413
key={`suggestion-${index}`}
414+
description={suggestion.display}
415+
onKeyDown={e => {
416+
if (index === 0 && e.key === 'ArrowUp') {
417+
e.preventDefault();
418+
searchInputRef.current?.focus();
419+
}
420+
}}
356421
onClick={() => {
357422
const updated = updateForm(searchInputValue);
358423
if (!updated.def) {
359-
if (suggestion !== searchInputValue) {
360-
onSearchChange(suggestion);
424+
if (!suggestion.validate) {
425+
onSearchChange(suggestion.value);
361426
} else {
362-
updateForm(searchInputValue, true);
427+
updateForm(suggestion.value, true);
363428
}
364429
} else if (!updated.comparator) {
365-
onSearchChange(`${updated.def.id}${suggestion}`);
430+
// check if it's a valid comparator
431+
if ((Object.values(FilterCompare) as string[]).includes(suggestion.value)) {
432+
onSearchChange(`${updated.def.id}${suggestion.value}`);
433+
} else {
434+
// else consider this as a field since ids can overlap (name / namespace)
435+
onSearchChange(suggestion.value);
436+
}
366437
} else {
367-
updateForm(`${updated.def.id}${updated.comparator}${suggestion}`, true);
438+
updateForm(`${updated.def.id}${updated.comparator}${suggestion.value}`, true);
368439
}
369440
}}
370441
>
371-
{suggestion}
442+
{suggestion.value}
372443
</MenuItem>
373444
))}
374445
</MenuList>

web/src/components/toolbar/filters/filters-chips.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ export const FiltersChips: React.FC<FiltersChipsProps> = ({
226226
{filter.values.map((filterValue, fvIndex) => {
227227
if (isForced || filterValue.disabled) {
228228
return (
229-
<div key={fvIndex}>
229+
<div key={fvIndex} className="flex-block">
230230
{getAndOrText('values', fvIndex)}
231231
<div className={`custom-chip ${filterValue.disabled ? 'disabled-value' : ''}`}>
232232
<Tooltip
@@ -251,7 +251,7 @@ export const FiltersChips: React.FC<FiltersChipsProps> = ({
251251

252252
const dropdownId = `${filter.def.id}-${fvIndex}`;
253253
return (
254-
<div key={fvIndex}>
254+
<div key={fvIndex} className="flex-block">
255255
{getAndOrText('values', fvIndex)}
256256
<Dropdown
257257
isOpen={dropdownId === openedDropdown}
@@ -404,7 +404,7 @@ export const FiltersChips: React.FC<FiltersChipsProps> = ({
404404
.filter(gp => gp.filters.length)
405405
.map((gp, index) => {
406406
return (
407-
<div key={gp.id}>
407+
<div key={gp.id} className="flex-block">
408408
{getAndOrText(filters.match, index)}
409409
<div className={`custom-chip-box ${gp.id !== 'common' ? 'custom-chip-peer' : ''}`}>
410410
{hasSrcOrDstFilters(filters.list) && <Text>{getGroupName(gp.id)}&nbsp;</Text>}

0 commit comments

Comments
 (0)