Skip to content

Commit 534e9da

Browse files
committed
feat: NodeListSearchFilter - keydown navigation + code optimization
1 parent 26508ec commit 534e9da

File tree

1 file changed

+80
-62
lines changed

1 file changed

+80
-62
lines changed

src/components/ResourceBrowser/ResourceList/NodeListSearchFilter.tsx

Lines changed: 80 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { Fragment, useEffect, useMemo, useRef, useState } from 'react'
17+
import { useEffect, useMemo, useRef, useState } from 'react'
1818
import { useHistory, useLocation, useParams } from 'react-router-dom'
1919
import { MultiValue, SelectInstance } from 'react-select'
2020
import { parse as parseQueryString, ParsedQuery, stringify as stringifyQueryString } from 'query-string'
@@ -24,6 +24,7 @@ import {
2424
Icon,
2525
OptionType,
2626
SelectPicker,
27+
SelectPickerOptionType,
2728
SelectPickerProps,
2829
useAsync,
2930
useRegisterShortcut,
@@ -51,6 +52,7 @@ const NodeListSearchFilter = ({
5152
}: NodeListSearchFilterType) => {
5253
// STATES
5354
const [nodeSearchKey, setNodeSearchKey] = useState<NODE_SEARCH_KEYS | null>(null)
55+
const [isNodeListSearchOpen, setIsNodeListSearchOpen] = useState(false)
5456

5557
// HOOKS
5658
const { clusterId } = useParams<ClusterDetailBaseParams>()
@@ -78,11 +80,17 @@ const NodeListSearchFilter = ({
7880
}, [])
7981

8082
useEffect(() => {
81-
// focusing select picker after custom option click
82-
if (nodeSearchKey) {
83+
// focusing select picker whenever secondary menu is opened or closed (handled via nodeSearchKey)
84+
if (nodeSearchKey || (!nodeSearchKey && isNodeListSearchOpen)) {
8385
searchFilterRef.current?.focus()
8486
}
85-
}, [nodeSearchKey])
87+
}, [nodeSearchKey, isNodeListSearchOpen])
88+
89+
// CONSTANTS
90+
const isNodeSearchFilterApplied =
91+
searchParams[NODE_SEARCH_KEYS.NAME] ||
92+
searchParams[NODE_SEARCH_KEYS.LABEL] ||
93+
searchParams[NODE_SEARCH_KEYS.NODE_GROUP]
8694

8795
// ASYNC CALLS
8896
const [nodeK8sVersionsLoading, nodeK8sVersionOptions, nodeK8sVersionsError, refetchNodeK8sVersions] =
@@ -113,23 +121,24 @@ const NodeListSearchFilter = ({
113121
const { nodeGroups, labels, nodeNames } = useMemo(() => getNodeSearchKeysOptionsList(rows), [JSON.stringify(rows)])
114122

115123
const searchOptions = useMemo(
116-
() => getNodeListSearchOptions({ labels, nodeGroups, nodeNames, nodeSearchKey }),
124+
() =>
125+
nodeSearchKey
126+
? getNodeListSearchOptions({ labels, nodeGroups, nodeNames, nodeSearchKey })
127+
: [{ label: 'Filter by', options: NODE_LIST_SEARCH_FILTER_OPTIONS }],
117128
[nodeGroups, labels, nodeNames, nodeSearchKey],
118129
)
119130

120131
const searchValue = useMemo(() => {
121-
const queryObject = parseQueryString(search)
122-
123-
const nameMap = new Set(((queryObject[NODE_SEARCH_KEYS.NAME] as string) || '').split(','))
124-
const nodeGroupMap = new Set(((queryObject[NODE_SEARCH_KEYS.NODE_GROUP] as string) || '').split(','))
125-
const labelMap = new Set(((queryObject[NODE_SEARCH_KEYS.LABEL] as string) || '').split(','))
132+
const nameMap = new Set(((searchParams[NODE_SEARCH_KEYS.NAME] as string) || '').split(','))
133+
const nodeGroupMap = new Set(((searchParams[NODE_SEARCH_KEYS.NODE_GROUP] as string) || '').split(','))
134+
const labelMap = new Set(((searchParams[NODE_SEARCH_KEYS.LABEL] as string) || '').split(','))
126135

127136
return [
128137
...nodeNames.filter(({ value }) => nameMap.has(value)),
129138
...nodeGroups.filter(({ value }) => nodeGroupMap.has(value)),
130139
...labels.filter(({ value }) => labelMap.has(value)),
131140
]
132-
}, [search])
141+
}, [searchParams])
133142

134143
// HANDLERS
135144
const handleQueryParamsUpdate = (callback: (queryObject: ParsedQuery) => ParsedQuery) => {
@@ -157,61 +166,67 @@ const NodeListSearchFilter = ({
157166
})
158167
}
159168

160-
const handleFilterGroupSelection = (value: typeof nodeSearchKey) => () => {
161-
setNodeSearchKey(value)
162-
}
169+
const handleSearchFilterChange = (
170+
newValue: SelectPickerOptionType<NODE_SEARCH_KEYS> | MultiValue<NodeSearchListOptionType>,
171+
) => {
172+
if (newValue && !Array.isArray(newValue) && !isNodeSearchFilterApplied) {
173+
setNodeSearchKey((newValue as SelectPickerOptionType<NODE_SEARCH_KEYS>).value)
174+
return
175+
}
163176

164-
const renderCustomOptions = () => (
165-
<>
166-
<div className="py-4 px-8 bg__menu--secondary fs-12 fw-6 lh-20 cn-9">
167-
{nodeSearchKey ? 'Match' : 'Filter by'}
168-
</div>
169-
<div>
170-
{NODE_LIST_SEARCH_FILTER_OPTIONS.map(({ label, value }) => (
171-
<Fragment key={value}>
172-
<button
173-
className="dc__transparent dc__truncate dc__hover-n50 fs-13 fw-4 lh-20 cn-9 px-8 py-6 w-100 dc__align-left"
174-
type="button"
175-
onClick={handleFilterGroupSelection(value)}
176-
>
177-
{label}
178-
</button>
179-
</Fragment>
180-
))}
181-
</div>
182-
</>
183-
)
177+
if (
178+
Array.isArray(newValue) &&
179+
newValue.length &&
180+
isNodeSearchFilterApplied &&
181+
!('identifier' in newValue[newValue.length - 1])
182+
) {
183+
setNodeSearchKey(newValue[newValue.length - 1].value as NODE_SEARCH_KEYS)
184+
return
185+
}
184186

185-
const handleSearchFilterChange = (newValue: MultiValue<NodeSearchListOptionType>) => {
186-
handleQueryParamsUpdate((queryObject) => {
187-
const updatedQueryObject = structuredClone(queryObject)
188-
189-
const queries = newValue.reduce<Record<string, string[]>>(
190-
(acc, curr) => {
191-
acc[curr.identifier].push(curr.value)
192-
return acc
193-
},
194-
{
195-
[NODE_SEARCH_KEYS.NAME]: [],
196-
[NODE_SEARCH_KEYS.LABEL]: [],
197-
[NODE_SEARCH_KEYS.NODE_GROUP]: [],
198-
},
199-
)
200-
201-
Object.values(NODE_SEARCH_KEYS).forEach((key) => {
202-
if (queries[key]?.length) {
203-
updatedQueryObject[key] = queries[key].join(',')
204-
} else {
205-
delete updatedQueryObject[key]
206-
}
187+
if (Array.isArray(newValue)) {
188+
handleQueryParamsUpdate((queryObject) => {
189+
const updatedQueryObject = structuredClone(queryObject)
190+
191+
const queries = newValue.reduce<Record<string, string[]>>(
192+
(acc, curr) => {
193+
acc[curr.identifier].push(curr.value)
194+
return acc
195+
},
196+
{
197+
[NODE_SEARCH_KEYS.NAME]: [],
198+
[NODE_SEARCH_KEYS.LABEL]: [],
199+
[NODE_SEARCH_KEYS.NODE_GROUP]: [],
200+
},
201+
)
202+
203+
Object.values(NODE_SEARCH_KEYS).forEach((key) => {
204+
if (queries[key]?.length) {
205+
updatedQueryObject[key] = queries[key].join(',')
206+
} else {
207+
delete updatedQueryObject[key]
208+
}
209+
})
210+
211+
return updatedQueryObject
207212
})
213+
}
214+
}
208215

209-
return updatedQueryObject
210-
})
216+
const handleSearchFilterKeyDown: SelectPickerProps['onKeyDown'] = (e) => {
217+
if (e.key === 'Backspace' && !isNodeSearchFilterApplied) {
218+
e.preventDefault()
219+
setNodeSearchKey(null)
220+
}
221+
}
222+
223+
const handleMenuOpen = () => {
224+
setIsNodeListSearchOpen(true)
211225
}
212226

213227
const handleMenuClose = () => {
214228
searchFilterRef.current?.blur()
229+
setIsNodeListSearchOpen(false)
215230
setNodeSearchKey(null)
216231
}
217232

@@ -227,21 +242,24 @@ const NodeListSearchFilter = ({
227242

228243
return (
229244
<div className="node-listing-search-container pt-16 px-20 pb-12 dc__zi-5">
230-
<SelectPicker<string, true>
245+
<SelectPicker
231246
selectRef={searchFilterRef}
247+
menuIsOpen={isNodeListSearchOpen}
248+
onMenuOpen={handleMenuOpen}
232249
onMenuClose={handleMenuClose}
233250
options={searchOptions}
234-
isMulti
251+
isMulti={!!nodeSearchKey || isNodeSearchFilterApplied}
252+
showCheckboxForMultiSelect={!!nodeSearchKey}
235253
placeholder={NODE_SEARCH_KEY_PLACEHOLDER[nodeSearchKey] || 'Filter by Node, Labels or Node groups'}
236254
required
237255
inputId="node-list-search"
238256
isSearchable={!!nodeSearchKey}
239257
isClearable
240258
value={searchValue}
241259
onChange={handleSearchFilterChange}
260+
onKeyDown={handleSearchFilterKeyDown}
242261
getOptionValue={getOptionValue}
243-
renderCustomOptions={renderCustomOptions}
244-
shouldRenderCustomOptions={!nodeSearchKey}
262+
closeMenuOnSelect={false}
245263
icon={<Icon name="ic-filter" color="N600" />}
246264
keyboardShortcut="/"
247265
formatOptionLabel={formatOptionLabel}

0 commit comments

Comments
 (0)