@@ -17,14 +17,14 @@ import {
1717import _ from 'lodash' ;
1818import * as React from 'react' ;
1919import { useTranslation } from 'react-i18next' ;
20- import { FilterDefinition , Filters , FilterValue , findFromFilters } from '../../../model/filters' ;
20+ import { FilterDefinition , FilterOption , Filters , FilterValue , findFromFilters } from '../../../model/filters' ;
2121import { getHTTPErrorDetails } from '../../../utils/errors' ;
2222import { matcher } from '../../../utils/filter-definitions' ;
2323import { Indicator , setTargeteableFilters } from '../../../utils/filters-helper' ;
2424import { useOutsideClickEvent } from '../../../utils/outside-hook' ;
2525import { Direction } from '../filters-toolbar' ;
2626import AutocompleteFilter from './autocomplete-filter' ;
27- import CompareFilter , { FilterCompare } from './compare-filter' ;
27+ import CompareFilter , { FilterCompare , getCompareText } from './compare-filter' ;
2828import { FilterHints } from './filter-hints' ;
2929import './filter-search-input.css' ;
3030import 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+
4046export 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 >
0 commit comments