Wizard: Replace deprecated selects on Azure step

This replaces deprecated selects on the Azure step for non-deprecated ones.
This commit is contained in:
regexowl 2025-03-18 09:56:48 +01:00 committed by Klara Simickova
parent 9c9ef15ad7
commit fe3257c0d9
4 changed files with 277 additions and 85 deletions

View file

@ -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<string>('');
const [filterValue, setFilterValue] = useState<string>('');
const [selectOptions, setSelectOptions] = useState<string[]>([]);
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<Element, 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(
<SelectOption
isNoResultsOption={true}
data-testid="azure-resource-groups-loading"
>
<Spinner size="lg" />
</SelectOption>
);
}
const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
ouiaId="resource_group_select"
ref={toggleRef}
variant="typeahead"
onClick={() => setIsOpen(!isOpen)}
isExpanded={isOpen}
isFullWidth
isDisabled={!azureSource}
>
<TextInputGroup isPlain>
<TextInputGroupMain
value={azureResourceGroup ? azureResourceGroup : inputValue}
onClick={onInputClick}
onChange={onTextInputChange}
autoComplete="off"
placeholder="Select resource group"
isExpanded={isOpen}
/>
{azureResourceGroup && (
<TextInputGroupUtilities>
<Button
variant="plain"
onClick={handleClear}
aria-label="Clear input"
>
<TimesIcon />
</Button>
</TextInputGroupUtilities>
)}
</TextInputGroup>
</MenuToggle>
);
return (
<FormGroup
@ -64,23 +138,40 @@ export const AzureResourceGroups = () => {
data-testid="azure-resource-groups"
>
<Select
ouiaId="resource_group_select"
variant={SelectVariant.typeahead}
onToggle={() => setIsOpen(!isOpen)}
onSelect={setResourceGroup}
onClear={handleClear}
selections={azureResourceGroup}
isScrollable
isOpen={isOpen}
placeholderText="Select resource group"
typeAheadAriaLabel="Select resource group"
selected={azureResourceGroup}
onSelect={setResourceGroup}
onOpenChange={() => setIsOpen(!isOpen)}
toggle={toggle}
shouldFocusFirstItemOnOpen={false}
popperProps={{ direction: 'up' }}
>
{resourceGroups.map((name: string, index: number) => (
<SelectOption
key={index}
value={name}
aria-label={`Resource group ${name}`}
/>
))}
<SelectList>
{isFetching && (
<SelectOption
value="loader"
data-testid="azure-resource-groups-loading"
>
<Spinner size="lg" />
</SelectOption>
)}
{selectOptions.length > 0 &&
selectOptions.map((name: string, index: number) => (
<SelectOption
key={index}
value={name}
aria-label={`Resource group ${name}`}
>
{name}
</SelectOption>
))}
{isSuccess && selectOptions.length === 0 && (
<SelectOption isDisabled>
{`No results found for "${filterValue}"`}
</SelectOption>
)}
</SelectList>
</Select>
</FormGroup>
);

View file

@ -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<string>('');
const [filterValue, setFilterValue] = useState<string>('');
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<Element, 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) => (
<SelectOption key={source.id} value={source.name} />
));
if (isSuccess) {
if (isFetching) {
selectOptions?.push(
<SelectOption key="loading" isNoResultsOption={true}>
<Spinner size="lg" />
</SelectOption>
);
}
}
const selectedSource = azureSource
? rawSources?.data?.find((source) => source.id === azureSource)?.name
: undefined;
const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
ouiaId="source_select"
ref={toggleRef}
variant="typeahead"
onClick={handleToggle}
isExpanded={isOpen}
isFullWidth
isDisabled={!isSuccess}
>
<TextInputGroup isPlain>
<TextInputGroupMain
value={selectedSource ? selectedSource : inputValue}
onClick={onInputClick}
onChange={onTextInputChange}
autoComplete="off"
placeholder="Select source"
isExpanded={isOpen}
/>
{selectedSource && (
<TextInputGroupUtilities>
<Button
variant="plain"
onClick={handleClear}
aria-label="Clear input"
>
<TimesIcon />
</Button>
</TextInputGroupUtilities>
)}
</TextInputGroup>
</MenuToggle>
);
return (
<>
<FormGroup isRequired label={'Source name'} data-testid="azure-sources">
<Select
ouiaId="source_select"
variant={SelectVariant.typeahead}
onToggle={handleToggle}
onSelect={handleSelect}
onClear={handleClear}
selections={
azureSource
? rawSources?.data?.find((source) => source.id === azureSource)
?.name
: undefined
}
isScrollable
isOpen={isOpen}
placeholderText="Select source"
typeAheadAriaLabel="Select source"
menuAppendTo="parent"
maxHeight="25rem"
isDisabled={!isSuccess}
selected={selectedSource}
onSelect={handleSelect}
onOpenChange={handleToggle}
toggle={toggle}
shouldFocusFirstItemOnOpen={false}
>
{selectOptions}
<SelectList>
{isFetching && (
<SelectOption key="loading" value="loader">
<Spinner size="lg" />
</SelectOption>
)}
{selectOptions.length > 0 &&
selectOptions.map((source, index) => (
<SelectOption key={index} value={source}>
{source}
</SelectOption>
))}
{isSuccess && selectOptions.length === 0 && (
<SelectOption isDisabled>
{`No results found for "${filterValue}"`}
</SelectOption>
)}
</SelectList>
</Select>
</FormGroup>
{isError && (

View file

@ -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(

View file

@ -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(