diff --git a/src/Components/CreateImageWizard/steps/TargetEnvironment/Azure/AzureResourceGroups.tsx b/src/Components/CreateImageWizard/steps/TargetEnvironment/Azure/AzureResourceGroups.tsx index 130b75e8..c5e0a262 100644 --- a/src/Components/CreateImageWizard/steps/TargetEnvironment/Azure/AzureResourceGroups.tsx +++ b/src/Components/CreateImageWizard/steps/TargetEnvironment/Azure/AzureResourceGroups.tsx @@ -1,11 +1,19 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; -import { FormGroup, Spinner } from '@patternfly/react-core'; import { + FormGroup, + Spinner, Select, + SelectList, SelectOption, - SelectVariant, -} from '@patternfly/react-core/deprecated'; + MenuToggleElement, + MenuToggle, + TextInputGroup, + TextInputGroupMain, + TextInputGroupUtilities, + Button, +} from '@patternfly/react-core'; +import { TimesIcon } from '@patternfly/react-icons'; import { useAppDispatch, useAppSelector } from '../../../../../store/hooks'; import { useGetSourceUploadInfoQuery } from '../../../../../store/provisioningApi'; @@ -20,42 +28,108 @@ export const AzureResourceGroups = () => { const azureResourceGroup = useAppSelector(selectAzureResourceGroup); const dispatch = useAppDispatch(); const [isOpen, setIsOpen] = useState(false); + const [inputValue, setInputValue] = useState(''); + const [filterValue, setFilterValue] = useState(''); + const [selectOptions, setSelectOptions] = useState([]); - const { data: sourceDetails, isFetching } = useGetSourceUploadInfoQuery( + const { + data: sourceDetails, + isFetching, + isSuccess, + } = useGetSourceUploadInfoQuery( { id: parseInt(azureSource as string) }, { skip: !azureSource, } ); - const resourceGroups = - (azureSource && sourceDetails?.azure?.resource_groups) || []; + const resourceGroups = sourceDetails?.azure?.resource_groups || []; + + useEffect(() => { + let filteredGroups = resourceGroups; + + if (filterValue) { + filteredGroups = resourceGroups.filter((group: string) => + String(group).toLowerCase().includes(filterValue.toLowerCase()) + ); + if (!isOpen) { + setIsOpen(true); + } + } + setSelectOptions(filteredGroups); + + // This useEffect hook should run *only* on when the filter value changes. + // eslint's exhaustive-deps rule does not support this use. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filterValue, resourceGroups]); + + const onInputClick = () => { + if (!isOpen) { + setIsOpen(true); + } else if (!inputValue) { + setIsOpen(false); + } + }; + + const onTextInputChange = (_event: React.FormEvent, value: string) => { + setInputValue(value); + setFilterValue(value); + + if (value !== azureResourceGroup) { + dispatch(changeAzureResourceGroup('')); + } + }; const setResourceGroup = ( _event: React.MouseEvent, selection: string ) => { const resource = - resourceGroups?.find((resource) => resource === selection) || ''; + resourceGroups.find((resource) => resource === selection) || ''; setIsOpen(false); dispatch(changeAzureResourceGroup(resource)); }; const handleClear = () => { dispatch(changeAzureResourceGroup('')); + setInputValue(''); + setFilterValue(''); }; - const options: JSX.Element[] = []; - if (isFetching) { - options.push( - - - - ); - } + const toggle = (toggleRef: React.Ref) => ( + setIsOpen(!isOpen)} + isExpanded={isOpen} + isFullWidth + isDisabled={!azureSource} + > + + + + {azureResourceGroup && ( + + + + )} + + + ); return ( { data-testid="azure-resource-groups" > ); diff --git a/src/Components/CreateImageWizard/steps/TargetEnvironment/Azure/AzureSourcesSelect.tsx b/src/Components/CreateImageWizard/steps/TargetEnvironment/Azure/AzureSourcesSelect.tsx index 49de7e0e..52f56cd4 100644 --- a/src/Components/CreateImageWizard/steps/TargetEnvironment/Azure/AzureSourcesSelect.tsx +++ b/src/Components/CreateImageWizard/steps/TargetEnvironment/Azure/AzureSourcesSelect.tsx @@ -1,12 +1,20 @@ import React, { useState, useEffect } from 'react'; -import { Alert } from '@patternfly/react-core'; -import { FormGroup, Spinner } from '@patternfly/react-core'; import { + Alert, + Button, + MenuToggle, + MenuToggleElement, + TextInputGroup, + TextInputGroupMain, + TextInputGroupUtilities, + FormGroup, + Spinner, Select, + SelectList, SelectOption, - SelectVariant, -} from '@patternfly/react-core/deprecated'; +} from '@patternfly/react-core'; +import { TimesIcon } from '@patternfly/react-icons'; import { useAppDispatch, useAppSelector } from '../../../../../store/hooks'; import { @@ -25,6 +33,8 @@ export const AzureSourcesSelect = () => { const azureSource = useAppSelector(selectAzureSource); const dispatch = useAppDispatch(); const [isOpen, setIsOpen] = useState(false); + const [inputValue, setInputValue] = useState(''); + const [filterValue, setFilterValue] = useState(''); const { data: rawSources, @@ -46,6 +56,10 @@ export const AzureSourcesSelect = () => { } ); + const [selectOptions, setSelectOptions] = useState<(string | undefined)[]>( + rawSources?.data?.map((source) => source.name) || [] + ); + useEffect(() => { if (isFetchingDetails || !isSuccessDetails) return; dispatch(changeAzureTenantId(sourceDetails?.azure?.tenant_id || '')); @@ -60,12 +74,52 @@ export const AzureSourcesSelect = () => { dispatch, ]); + useEffect(() => { + let filteredSources = rawSources?.data?.map((source) => source.name); + + if (filterValue) { + filteredSources = rawSources?.data + ?.map((source) => source.name) + .filter((source: string) => + String(source).toLowerCase().includes(filterValue.toLowerCase()) + ); + if (!isOpen) { + setIsOpen(true); + } + } + + if (filteredSources) { + setSelectOptions(filteredSources); + } + + // This useEffect hook should run *only* on when the filter value changes. + // eslint's exhaustive-deps rule does not support this use. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filterValue, rawSources?.data]); + + const onInputClick = () => { + if (!isOpen) { + setIsOpen(true); + } else if (!inputValue) { + setIsOpen(false); + } + }; + + const onTextInputChange = (_event: React.FormEvent, value: string) => { + setInputValue(value); + setFilterValue(value); + + if (value !== selectedSource) { + dispatch(changeAzureSource('')); + } + }; + const handleSelect = ( _event: React.MouseEvent, sourceName: string ) => { const sourceId = rawSources?.data?.find( - (source) => source?.name === sourceName + (source) => source.name === sourceName )?.id; dispatch(changeAzureSource(sourceId || '')); dispatch(changeAzureResourceGroup('')); @@ -77,6 +131,8 @@ export const AzureSourcesSelect = () => { dispatch(changeAzureTenantId('')); dispatch(changeAzureSubscriptionId('')); dispatch(changeAzureResourceGroup('')); + setInputValue(''); + setFilterValue(''); }; const handleToggle = () => { @@ -87,43 +143,76 @@ export const AzureSourcesSelect = () => { setIsOpen(!isOpen); }; - const selectOptions = rawSources?.data?.map((source) => ( - - )); - if (isSuccess) { - if (isFetching) { - selectOptions?.push( - - - - ); - } - } + const selectedSource = azureSource + ? rawSources?.data?.find((source) => source.id === azureSource)?.name + : undefined; + + const toggle = (toggleRef: React.Ref) => ( + + + + + {selectedSource && ( + + + + )} + + + ); return ( <> {isError && ( diff --git a/src/test/Components/CreateImageWizard/CreateImageWizard.test.tsx b/src/test/Components/CreateImageWizard/CreateImageWizard.test.tsx index b38ed1b2..ede33171 100644 --- a/src/test/Components/CreateImageWizard/CreateImageWizard.test.tsx +++ b/src/test/Components/CreateImageWizard/CreateImageWizard.test.tsx @@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event'; import { clickNext, renderCreateMode } from './wizardTestUtils'; -const getSourceDropdown = async () => { +const getAwsSourceDropdown = async () => { const sourceDropdown = await screen.findByRole('textbox', { name: /select source/i, }); @@ -12,6 +12,13 @@ const getSourceDropdown = async () => { return sourceDropdown; }; +const getAzureSourceDropdown = async () => { + const sourceDropdown = await screen.findByPlaceholderText(/select source/i); + await waitFor(() => expect(sourceDropdown).toBeEnabled()); + + return sourceDropdown; +}; + const selectAllEnvironments = async () => { const user = userEvent.setup(); @@ -88,7 +95,7 @@ describe('Keyboard accessibility', () => { name: /use an account configured from sources\./i, }) ).toHaveFocus(); - const awsSourceDropdown = await getSourceDropdown(); + const awsSourceDropdown = await getAwsSourceDropdown(); await waitFor(() => user.click(awsSourceDropdown)); const awsSource = await screen.findByRole('option', { name: /my_source/i, @@ -117,16 +124,16 @@ describe('Keyboard accessibility', () => { name: /use an account configured from sources\./i, }) ).toHaveFocus(); - const azureSourceDropdown = await getSourceDropdown(); + const azureSourceDropdown = await getAzureSourceDropdown(); await waitFor(() => user.click(azureSourceDropdown)); const azureSource = await screen.findByRole('option', { name: /azureSource1/i, }); await waitFor(() => user.click(azureSource)); - const resourceGroupDropdown = await screen.findByRole('textbox', { - name: /select resource group/i, - }); + const resourceGroupDropdown = await screen.findByPlaceholderText( + /select resource group/i + ); await waitFor(() => user.click(resourceGroupDropdown)); await waitFor(async () => user.click( diff --git a/src/test/Components/CreateImageWizard/steps/TargetEnvironment/AzureTarget.test.tsx b/src/test/Components/CreateImageWizard/steps/TargetEnvironment/AzureTarget.test.tsx index 7c1346b0..a562213a 100644 --- a/src/test/Components/CreateImageWizard/steps/TargetEnvironment/AzureTarget.test.tsx +++ b/src/test/Components/CreateImageWizard/steps/TargetEnvironment/AzureTarget.test.tsx @@ -87,9 +87,7 @@ const deselectAzureAndSelectGuestImage = async () => { const selectSource = async (sourceName: string) => { const user = userEvent.setup(); - const sourceTexbox = await screen.findByRole('textbox', { - name: /select source/i, - }); + const sourceTexbox = await screen.findByPlaceholderText(/select source/i); await waitFor(async () => user.click(sourceTexbox)); const azureSource = await screen.findByRole('option', { @@ -100,9 +98,9 @@ const selectSource = async (sourceName: string) => { const selectResourceGroup = async () => { const user = userEvent.setup(); - const resourceGrpTextbox = await screen.findByRole('textbox', { - name: /select resource group/i, - }); + const resourceGrpTextbox = await screen.findByPlaceholderText( + /select resource group/i + ); await waitFor(async () => user.click(resourceGrpTextbox)); const myResourceGroup1 = await screen.findByRole('option', { @@ -170,16 +168,23 @@ const selectV1 = async () => { await waitFor(() => user.click(v1)); }; -const getResourceGroupInput = async () => { +const getResourceGroupTextInput = async () => { const resourceGroupInput = await screen.findByRole('textbox', { name: /resource group/i, }); return resourceGroupInput; }; +const getResourceGroupSelect = async () => { + const resourceGroupInput = await screen.findByPlaceholderText( + /select resource group/i + ); + return resourceGroupInput; +}; + const enterResourceGroup = async () => { const user = userEvent.setup(); - const resourceGroup = await getResourceGroupInput(); + const resourceGroup = await getResourceGroupTextInput(); await waitFor(() => user.type(resourceGroup, 'testResourceGroup')); }; @@ -238,7 +243,7 @@ describe('Step Upload to Azure', () => { expect(subscription).toBeEnabled(); await enterSubscriptionId(); - const resourceGroup = await getResourceGroupInput(); + const resourceGroup = await getResourceGroupTextInput(); expect(resourceGroup).toHaveValue(''); expect(resourceGroup).toBeEnabled(); await enterResourceGroup(); @@ -251,7 +256,7 @@ describe('Step Upload to Azure', () => { // manual values should be cleared out expect(await getTenantGuidInput()).toHaveValue(''); expect(await getSubscriptionIdInput()).toHaveValue(''); - expect(await getResourceGroupInput()).toHaveValue(''); + expect(await getResourceGroupSelect()).toHaveValue(''); expect(nextButton).toBeDisabled(); @@ -285,7 +290,7 @@ describe('Step Upload to Azure', () => { ); }); - user.click(await getResourceGroupInput()); + user.click(await getResourceGroupSelect()); const groups = await screen.findByLabelText(/Resource group/); expect(groups).toBeInTheDocument(); expect(