Skip to content

Commit b4a80ab

Browse files
authored
autocomplete menu button (#459)
1 parent f71c56d commit b4a80ab

File tree

4 files changed

+54
-44
lines changed

4 files changed

+54
-44
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#autocomplete-menu-button {
2+
width: 30px;
3+
padding: 0;
4+
}

web/src/components/filters/autocomplete-filter.tsx

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import {
1010
TextInput,
1111
ValidatedOptions
1212
} from '@patternfly/react-core';
13-
import { SearchIcon } from '@patternfly/react-icons';
13+
import { SearchIcon, CaretDownIcon } from '@patternfly/react-icons';
1414
import { createFilterValue, FilterDefinition, FilterOption, FilterValue } from '../../model/filters';
1515
import { getHTTPErrorDetails } from '../../utils/errors';
1616
import { autoCompleteCache } from '../../utils/autocomplete-cache';
1717
import { Indicator } from './filters-helper';
1818
import { usePrevious } from '../../utils/previous-hook';
19+
import './autocomplete-filter.css';
1920

2021
const optionsMenuID = 'options-menu-list';
2122
const isMenuOption = (elt?: Element) => {
@@ -40,16 +41,16 @@ export const AutocompleteFilter: React.FC<AutocompleteFilterProps> = ({
4041
const autocompleteContainerRef = React.useRef<HTMLDivElement | null>(null);
4142
const searchInputRef = React.useRef<HTMLInputElement | null>(null);
4243
const optionsRef = React.useRef<HTMLDivElement | null>(null);
43-
const [autocompleteOptions, setAutocompleteOptions] = React.useState<FilterOption[]>([]);
44-
const [isPopperVisible, setPopperVisible] = React.useState(false);
44+
const [options, setOptions] = React.useState<FilterOption[]>([]);
4545
const [currentValue, setCurrentValue] = React.useState<string>('');
4646
const previousFilterDefinition = usePrevious(filterDefinition);
4747

4848
React.useEffect(() => {
4949
if (filterDefinition !== previousFilterDefinition) {
5050
//reset filter value if definition has changed
5151
resetFilterValue();
52-
searchInputRef?.current?.focus();
52+
searchInputRef.current?.focus();
53+
searchInputRef.current?.setAttribute('autocomplete', 'off');
5354
autoCompleteCache.clear();
5455
} else if (_.isEmpty(currentValue)) {
5556
setIndicator(ValidatedOptions.default);
@@ -61,17 +62,12 @@ export const AutocompleteFilter: React.FC<AutocompleteFilterProps> = ({
6162
// eslint-disable-next-line react-hooks/exhaustive-deps
6263
}, [currentValue, filterDefinition, setIndicator]);
6364

64-
React.useEffect(() => {
65-
// The menu is hidden if there are no options
66-
setPopperVisible(autocompleteOptions.length > 0);
67-
}, [autocompleteOptions]);
68-
6965
const resetFilterValue = React.useCallback(() => {
7066
setCurrentValue('');
71-
setAutocompleteOptions([]);
67+
setOptions([]);
7268
setMessageWithDelay(undefined);
7369
setIndicator(ValidatedOptions.default);
74-
}, [setCurrentValue, setAutocompleteOptions, setMessageWithDelay, setIndicator]);
70+
}, [setCurrentValue, setMessageWithDelay, setIndicator, setOptions]);
7571

7672
const addFilter = React.useCallback(
7773
(option: FilterOption) => {
@@ -90,14 +86,16 @@ export const AutocompleteFilter: React.FC<AutocompleteFilterProps> = ({
9086
setCurrentValue(newValue);
9187
filterDefinition
9288
.getOptions(newValue)
93-
.then(setAutocompleteOptions)
89+
.then(opts => {
90+
setOptions(opts);
91+
})
9492
.catch(err => {
9593
const errorMessage = getHTTPErrorDetails(err);
9694
setMessageWithDelay(errorMessage);
97-
setAutocompleteOptions([]);
95+
setOptions([]);
9896
});
9997
},
100-
[setCurrentValue, filterDefinition, setAutocompleteOptions, setMessageWithDelay]
98+
[setOptions, setCurrentValue, filterDefinition, setMessageWithDelay]
10199
);
102100

103101
const onAutoCompleteOptionSelected = React.useCallback(
@@ -111,28 +109,28 @@ export const AutocompleteFilter: React.FC<AutocompleteFilterProps> = ({
111109
}
112110
} else {
113111
addFilter(option);
114-
setAutocompleteOptions([]);
112+
setOptions([]);
115113
}
116114
},
117-
[addFilter, onAutoCompleteChange, filterDefinition, currentValue]
115+
[addFilter, onAutoCompleteChange, filterDefinition, currentValue, setOptions]
118116
);
119117

120118
const onAutoCompleteSelect = React.useCallback(
121119
(e: React.MouseEvent<Element, MouseEvent> | undefined, itemId: string) => {
122120
e?.stopPropagation();
123-
const option = autocompleteOptions.find(opt => opt.value === itemId);
121+
const option = options.find(opt => opt.value === itemId);
124122
if (!option) {
125123
return;
126124
}
127125
onAutoCompleteOptionSelected(option);
128126
},
129-
[autocompleteOptions, onAutoCompleteOptionSelected]
127+
[options, onAutoCompleteOptionSelected]
130128
);
131129

132130
const onEnter = React.useCallback(() => {
133131
// Only one choice is present, consider this is what is desired
134-
if (autocompleteOptions.length === 1) {
135-
onAutoCompleteOptionSelected(autocompleteOptions[0]);
132+
if (options.length === 1) {
133+
onAutoCompleteOptionSelected(options[0]);
136134
return;
137135
}
138136

@@ -150,7 +148,7 @@ export const AutocompleteFilter: React.FC<AutocompleteFilterProps> = ({
150148
}
151149
});
152150
}, [
153-
autocompleteOptions,
151+
options,
154152
filterDefinition,
155153
currentValue,
156154
onAutoCompleteOptionSelected,
@@ -166,7 +164,7 @@ export const AutocompleteFilter: React.FC<AutocompleteFilterProps> = ({
166164
// We need to skip a couple of render frames to get the new focused element
167165
setTimeout(() => {
168166
if (!isMenuOption(document.activeElement || undefined)) {
169-
setAutocompleteOptions([]);
167+
setOptions([]);
170168
}
171169
}, 50);
172170
}, []);
@@ -191,10 +189,10 @@ export const AutocompleteFilter: React.FC<AutocompleteFilterProps> = ({
191189
/>
192190
}
193191
popper={
194-
<Menu ref={optionsRef} onSelect={onAutoCompleteSelect}>
192+
<Menu ref={optionsRef} onSelect={onAutoCompleteSelect} isScrollable={options.length > 8}>
195193
<MenuContent>
196194
<MenuList id={optionsMenuID}>
197-
{autocompleteOptions.map(option => (
195+
{options.map(option => (
198196
<MenuItem data-test={option.value} itemId={option.value} key={option.name} onBlur={onBlur}>
199197
{option.name}
200198
</MenuItem>
@@ -203,11 +201,28 @@ export const AutocompleteFilter: React.FC<AutocompleteFilterProps> = ({
203201
</MenuContent>
204202
</Menu>
205203
}
206-
isVisible={isPopperVisible}
204+
isVisible={!_.isEmpty(options)}
207205
enableFlip={false}
208206
appendTo={autocompleteContainerRef.current!}
209207
/>
210208
</div>
209+
<Button
210+
data-test="autocomplete-menu-button"
211+
id="autocomplete-menu-button"
212+
variant="control"
213+
aria-label="show values"
214+
onClick={() =>
215+
setTimeout(() => {
216+
if (_.isEmpty(options)) {
217+
onAutoCompleteChange(currentValue);
218+
} else {
219+
setOptions([]);
220+
}
221+
}, 100)
222+
}
223+
>
224+
<CaretDownIcon />
225+
</Button>
211226
<Button
212227
data-test="search-button"
213228
id="search-button"

web/src/utils/filter-definitions.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import {
2323
getProtocolOptions,
2424
getResourceOptions,
2525
noOption,
26-
cap10,
2726
getDnsResponseCodeOptions,
2827
getDropStateOptions,
2928
getDropCauseOptions,
@@ -251,16 +250,16 @@ export const getFilterDefinitions = (
251250
| undefined = undefined;
252251

253252
if (d.id.includes('namespace')) {
254-
getOptions = cap10(getNamespaceOptions);
253+
getOptions = getNamespaceOptions;
255254
validate = k8sNameValidation;
256255
} else if (d.id.includes('name')) {
257256
validate = k8sNameValidation;
258257
} else if (d.id.includes('kind')) {
259-
getOptions = cap10(getKindOptions);
258+
getOptions = getKindOptions;
260259
validate = rejectEmptyValue;
261260
encoder = kindFiltersEncoder(`${isSrc ? 'Src' : 'Dst'}K8S_Type`, `${isSrc ? 'Src' : 'Dst'}K8S_OwnerType`);
262261
} else if (d.id.includes('resource')) {
263-
getOptions = cap10(getResourceOptions);
262+
getOptions = getResourceOptions;
264263
validate = k8sResourceValidation;
265264
checkCompletion = k8sResourceCompletion;
266265
encoder = k8sResourceFiltersEncoder(
@@ -273,28 +272,28 @@ export const getFilterDefinitions = (
273272
} else if (d.id.includes('address')) {
274273
validate = addressValidation;
275274
} else if (d.id.includes('port')) {
276-
getOptions = cap10(getPortOptions);
275+
getOptions = getPortOptions;
277276
validate = portValidation;
278277
} else if (d.id.includes('mac')) {
279278
validate = macValidation;
280279
} else if (d.id.includes('proto')) {
281-
getOptions = cap10(getProtocolOptions);
280+
getOptions = getProtocolOptions;
282281
validate = protoValidation;
283282
} else if (d.id.includes('direction')) {
284283
getOptions = v => getDirectionOptionsAsync(v, t);
285284
validate = dirValidation;
286285
} else if (d.id.includes('drop_state')) {
287-
getOptions = cap10(getDropStateOptions);
286+
getOptions = getDropStateOptions;
288287
encoder = simpleFiltersEncoder('PktDropLatestState');
289288
} else if (d.id.includes('drop_cause')) {
290-
getOptions = cap10(getDropCauseOptions);
289+
getOptions = getDropCauseOptions;
291290
encoder = simpleFiltersEncoder('PktDropLatestDropCause');
292291
} else if (d.id.includes('dns_flag_response_code')) {
293-
getOptions = cap10(getDnsResponseCodeOptions);
292+
getOptions = getDnsResponseCodeOptions;
294293
} else if (d.id.includes('dns_errno')) {
295-
getOptions = cap10(getDnsErrorCodeOptions);
294+
getOptions = getDnsErrorCodeOptions;
296295
} else if (d.id.includes('dscp')) {
297-
getOptions = cap10(getDSCPOptions);
296+
getOptions = getDSCPOptions;
298297
}
299298
return { getOptions, validate, encoder, checkCompletion };
300299
};

web/src/utils/filter-options.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,3 @@ export const findProtocolOption = (nameOrVal: string) => {
149149
export const findDirectionOption = (nameOrVal: string, t: TFunction) => {
150150
return getDirectionOptions(t).find(o => o.name.toLowerCase() === nameOrVal.toLowerCase() || o.value === nameOrVal);
151151
};
152-
153-
export const cap10 = (getOptions: (value: string) => Promise<FilterOption[]>) => {
154-
return (value: string) => {
155-
return getOptions(value).then(opts => {
156-
return opts.length <= 10 ? opts : opts.slice(0, 10);
157-
});
158-
};
159-
};

0 commit comments

Comments
 (0)