diff --git a/ui/apps/platform/src/Containers/Collections/CollectionForm.tsx b/ui/apps/platform/src/Containers/Collections/CollectionForm.tsx index ad29907d4ef30..30bcd603e387f 100644 --- a/ui/apps/platform/src/Containers/Collections/CollectionForm.tsx +++ b/ui/apps/platform/src/Containers/Collections/CollectionForm.tsx @@ -212,6 +212,7 @@ function CollectionForm({ )} void; validated: ValidatedOptions; isDisabled: boolean; + autocompleteProvider?: (search: string) => CancellableRequest; + OptionComponent?: ReactNode; }; -/* TODO Implement autocompletion */ +function getOptions( + OptionComponent: ReactNode, + data: string[] | undefined +): ReactElement[] | undefined { + return data?.map((value) => ( + + )); +} + export function AutoCompleteSelect({ id, selectedOption, @@ -21,14 +33,36 @@ export function AutoCompleteSelect({ onChange, validated, isDisabled, + autocompleteProvider, + OptionComponent = SelectOption, }: AutoCompleteSelectProps) { const { isOpen, onToggle, closeSelect } = useSelectToggle(); + const [typeahead, setTypeahead] = useState(selectedOption); + + const autocompleteCallback = useCallback(() => { + const shouldMakeRequest = isOpen && autocompleteProvider; + if (shouldMakeRequest) { + return autocompleteProvider(typeahead); + } + return { + request: Promise.resolve([]), + cancel: () => {}, + }; + }, [isOpen, autocompleteProvider, typeahead]); + + const { data } = useRestQuery(autocompleteCallback); function onSelect(_, value) { onChange(value); closeSelect(); } + // Debounce the autocomplete requests to not overload the backend + const updateTypeahead = useMemo( + () => debounce((value: string) => setTypeahead(value), 800), + [] + ); + return ( <> ); } diff --git a/ui/apps/platform/src/Containers/Collections/RuleSelector/ByLabelSelector.tsx b/ui/apps/platform/src/Containers/Collections/RuleSelector/ByLabelSelector.tsx index 2b48f3f642954..34708cc8ed6ef 100644 --- a/ui/apps/platform/src/Containers/Collections/RuleSelector/ByLabelSelector.tsx +++ b/ui/apps/platform/src/Containers/Collections/RuleSelector/ByLabelSelector.tsx @@ -201,6 +201,7 @@ function ByLabelSelector({ } > diff --git a/ui/apps/platform/src/Containers/Collections/RuleSelector/ByNameSelector.tsx b/ui/apps/platform/src/Containers/Collections/RuleSelector/ByNameSelector.tsx index 35704fb6cbecb..2eb14a170d5aa 100644 --- a/ui/apps/platform/src/Containers/Collections/RuleSelector/ByNameSelector.tsx +++ b/ui/apps/platform/src/Containers/Collections/RuleSelector/ByNameSelector.tsx @@ -1,14 +1,22 @@ -import React from 'react'; +import React, { ReactNode, useCallback } from 'react'; import { Button, Flex, FormGroup, ValidatedOptions } from '@patternfly/react-core'; import { TrashIcon } from '@patternfly/react-icons'; import cloneDeep from 'lodash/cloneDeep'; import { FormikErrors } from 'formik'; import useIndexKey from 'hooks/useIndexKey'; +import { getCollectionAutoComplete } from 'services/CollectionsService'; import { AutoCompleteSelect } from './AutoCompleteSelect'; -import { ByNameResourceSelector, ScopedResourceSelector, SelectorEntityType } from '../types'; +import { + ByNameResourceSelector, + Collection, + ScopedResourceSelector, + SelectorEntityType, +} from '../types'; +import { generateRequest } from '../converter'; export type ByNameSelectorProps = { + collection: Collection; entityType: SelectorEntityType; scopedResourceSelector: ByNameResourceSelector; handleChange: ( @@ -17,16 +25,26 @@ export type ByNameSelectorProps = { ) => void; validationErrors: FormikErrors | undefined; isDisabled: boolean; + OptionComponent: ReactNode; }; function ByNameSelector({ + collection, entityType, scopedResourceSelector, handleChange, validationErrors, isDisabled, + OptionComponent, }: ByNameSelectorProps) { const { keyFor, invalidateIndexKeys } = useIndexKey(); + const autocompleteProvider = useCallback( + (search: string) => { + const req = generateRequest(collection); + return getCollectionAutoComplete(req.resourceSelectors, entityType, search); + }, + [collection, entityType] + ); function onAddValue() { const selector = cloneDeep(scopedResourceSelector); @@ -77,6 +95,8 @@ function ByNameSelector({ : ValidatedOptions.default } isDisabled={isDisabled} + autocompleteProvider={autocompleteProvider} + OptionComponent={OptionComponent} /> {!isDisabled && ( + ) + ); + function onRuleOptionSelect(_, value) { if (!isRuleSelectorOption(value)) { return; @@ -84,11 +102,13 @@ function RuleSelector({ {scopedResourceSelector.type === 'ByName' && ( )} diff --git a/ui/apps/platform/src/Containers/Dashboard/hooks/useRestQuery.ts b/ui/apps/platform/src/Containers/Dashboard/hooks/useRestQuery.ts index 894ea98134a32..efc3f06c09e1e 100644 --- a/ui/apps/platform/src/Containers/Dashboard/hooks/useRestQuery.ts +++ b/ui/apps/platform/src/Containers/Dashboard/hooks/useRestQuery.ts @@ -19,6 +19,7 @@ export default function useRestQuery( const { request, cancel } = cancellableRequestFn(); setError(undefined); + setLoading(true); request .then((result) => { diff --git a/ui/apps/platform/src/services/CollectionsService.ts b/ui/apps/platform/src/services/CollectionsService.ts index 2c783991d3ca0..761f02dca94eb 100644 --- a/ui/apps/platform/src/services/CollectionsService.ts +++ b/ui/apps/platform/src/services/CollectionsService.ts @@ -211,13 +211,13 @@ export function getCollectionAutoComplete( searchCategory: string, searchLabel: string ): CancellableRequest { - const params = qs.stringify( - { resourceSelectors, searchCategory, searchLabel }, - { arrayFormat: 'repeat' } - ); return makeCancellableAxiosRequest((signal) => axios - .get<{ values: string[] }>(`${collectionsAutocompleteUrl}?${params}`, { signal }) + .post<{ values: string[] }>( + collectionsAutocompleteUrl, + { resourceSelectors, searchCategory, searchLabel }, + { signal } + ) .then((response) => response.data.values) ); } diff --git a/ui/apps/platform/src/test-utils/mocks/@patternfly/react-core.tsx b/ui/apps/platform/src/test-utils/mocks/@patternfly/react-core.tsx new file mode 100644 index 0000000000000..05cd2c19c57da --- /dev/null +++ b/ui/apps/platform/src/test-utils/mocks/@patternfly/react-core.tsx @@ -0,0 +1,9 @@ +import * as PFReactCore from '@patternfly/react-core'; + +const { debounce, ...rest } = jest.requireActual('@patternfly/react-core'); + +// Overrides the PF `debounce` function to do nothing other than return the original function. This +// can be used to avoid issues in tests that result from state updates in a debounced function. +export const mockDebounce = { ...rest, debounce: (fn: () => void) => fn } as jest.Mock< + typeof PFReactCore +>;