Wizard: Replace deprecated select in OpenSCAP step

This replaces v4 deprecated select for a non-deprecated one and moves some functions to separate files.
This commit is contained in:
regexowl 2025-04-01 14:00:29 +02:00 committed by Klara Simickova
parent 248bc1d67a
commit 698037a0ae

View file

@ -1,11 +1,20 @@
import React, { useEffect, useState } from 'react';
import { Alert, FormGroup, Spinner } from '@patternfly/react-core';
import {
Alert,
Button,
FormGroup,
MenuToggle,
MenuToggleElement,
Select,
SelectList,
SelectOption,
SelectVariant,
} from '@patternfly/react-core/deprecated';
Spinner,
TextInputGroup,
TextInputGroupMain,
TextInputGroupUtilities,
} from '@patternfly/react-core';
import { TimesIcon } from '@patternfly/react-icons';
import { v4 as uuidv4 } from 'uuid';
import OpenSCAPFGLabel from './OpenSCAPFGLabel';
@ -50,6 +59,91 @@ import { parseSizeUnit } from '../../../utilities/parseSizeUnit';
import { Partition, Units } from '../../FileSystem/FileSystemTable';
import { removeBetaFromRelease } from '../removeBetaFromRelease';
type OScapSelectOptionPropType = {
profile_id: DistributionProfileItem;
filter?: string;
};
type OScapSelectOptionValueType = {
profileID: DistributionProfileItem;
toString: () => string;
};
const OScapSelectOption = ({
profile_id,
filter,
}: OScapSelectOptionPropType) => {
const release = useAppSelector(selectDistribution);
const { data } = useGetOscapCustomizationsQuery({
distribution: release,
profile: profile_id,
});
const oscapProfile = data?.openscap as OpenScapProfile;
if (
filter &&
!oscapProfile?.profile_name?.toLowerCase().includes(filter.toLowerCase())
) {
return null;
}
const selectObject = (
id: DistributionProfileItem,
name?: string
): OScapSelectOptionValueType => ({
profileID: id,
toString: () => name || '',
});
return (
<SelectOption
key={profile_id}
value={selectObject(profile_id, oscapProfile?.profile_name)}
description={oscapProfile?.profile_description}
>
{oscapProfile?.profile_name}
</SelectOption>
);
};
type ComplianceSelectOptionPropType = {
policy: PolicyRead;
};
type ComplianceSelectOptionValueType = {
policyID: string;
profileID: string;
toString: () => string;
};
const ComplianceSelectOption = ({ policy }: ComplianceSelectOptionPropType) => {
const selectObj = (
policyID: string,
profileID: string,
title: string
): ComplianceSelectOptionValueType => ({
policyID,
profileID,
toString: () => title,
});
const descr = (
<>
Threshold: {policy.compliance_threshold}
<br />
Active systems: {policy.total_system_count}
</>
);
return (
<SelectOption
key={policy.id}
value={selectObj(policy.id!, policy.ref_id!, policy.title!)}
description={descr}
>
{policy.title}
</SelectOption>
);
};
const ProfileSelector = () => {
const policyID = useAppSelector(selectCompliancePolicyID);
const policyTitle = useAppSelector(selectCompliancePolicyTitle);
@ -59,6 +153,10 @@ const ProfileSelector = () => {
const hasWslTargetOnly = useHasSpecificTargetOnly('wsl');
const dispatch = useAppDispatch();
const [isOpen, setIsOpen] = useState(false);
const [inputValue, setInputValue] = useState<string>('');
const [filterValue, setFilterValue] = useState<string>('');
const [selectOptions, setSelectOptions] = useState<string[]>([]);
const [selected, setSelected] = useState<string>('None');
const complianceType = useAppSelector(selectComplianceType);
const prefetchProfile = useBackendPrefetch('getOscapCustomizations');
@ -134,6 +232,27 @@ const ProfileSelector = () => {
}
}, [isSuccessPolicies]);
useEffect(() => {
let filteredProfiles = profiles;
if (filterValue) {
filteredProfiles = profiles?.filter((profile: string) =>
String(profile).toLowerCase().includes(filterValue.toLowerCase())
);
if (!isOpen) {
setIsOpen(true);
}
}
if (filteredProfiles) {
setSelectOptions(filteredProfiles);
}
// 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, profiles]);
const handleToggle = () => {
if (!isOpen && complianceType === 'openscap') {
refetch();
@ -153,6 +272,8 @@ const ProfileSelector = () => {
dispatch(changeFileSystemConfigurationType('automatic'));
handleServices(undefined);
dispatch(clearKernelAppend());
setInputValue('');
setFilterValue('');
};
const handlePackages = (
@ -217,8 +338,30 @@ const ProfileSelector = () => {
}
};
const handleSelect = (
_event: React.MouseEvent<Element, MouseEvent>,
const onInputClick = () => {
if (!isOpen) {
setIsOpen(true);
} else if (!inputValue) {
setIsOpen(false);
}
};
const onTextInputChange = (_event: React.FormEvent, value: string) => {
setInputValue(value);
setFilterValue(value);
if (value !== profileID) {
dispatch(
changeCompliance({
profileID: undefined,
policyID: undefined,
policyTitle: undefined,
})
);
}
};
const applyChanges = (
selection: OScapSelectOptionValueType | ComplianceSelectOptionValueType
) => {
if (selection.profileID === undefined) {
@ -251,6 +394,7 @@ const ProfileSelector = () => {
);
} else {
const compl = selection as ComplianceSelectOptionValueType;
setSelected(compl.toString());
dispatch(
changeCompliance({
profileID: compl.profileID,
@ -261,20 +405,21 @@ const ProfileSelector = () => {
}
});
}
setIsOpen(false);
};
const options = () => {
if (isFetching) {
return [<OScapLoadingOption key="oscap-loading-option" />];
}
if (profiles) {
return [<OScapNoneOption key="oscap-none-option" />].concat(
profiles.map((profile_id, index) => {
return <OScapSelectOption key={index} profile_id={profile_id} />;
})
const handleSelect = (
_event: React.MouseEvent<Element, MouseEvent>,
selection: string
) => {
if (selection) {
setInputValue(selection);
setFilterValue('');
applyChanges(
selection as unknown as
| OScapSelectOptionValueType
| ComplianceSelectOptionValueType
);
setIsOpen(false);
}
};
@ -283,7 +428,14 @@ const ProfileSelector = () => {
return [];
}
const res = [<ComplianceNoneOption key="compliance-non-option" />];
const res = [
<SelectOption
key="compliance-none-option"
value={{ toString: () => 'None', compareTo: () => false }}
>
None
</SelectOption>,
];
for (const p of policies.data) {
if (p === undefined) {
continue;
@ -294,6 +446,58 @@ const ProfileSelector = () => {
return res;
};
const toggleOpenSCAP = (toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
ouiaId="profileSelect"
data-testid="profileSelect"
ref={toggleRef}
variant="typeahead"
onClick={() => setIsOpen(!isOpen)}
isExpanded={isOpen}
isDisabled={!isSuccess || hasWslTargetOnly}
>
<TextInputGroup isPlain>
<TextInputGroupMain
value={profileID ? profileID : inputValue}
onClick={onInputClick}
onChange={onTextInputChange}
autoComplete="off"
placeholder="Select a profile"
isExpanded={isOpen}
/>
{profileID && (
<TextInputGroupUtilities>
<Button
variant="plain"
onClick={handleClear}
aria-label="Clear input"
>
<TimesIcon />
</Button>
</TextInputGroupUtilities>
)}
</TextInputGroup>
</MenuToggle>
);
const toggleCompliance = (toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
ouiaId="compliancePolicySelect"
ref={toggleRef}
onClick={() => setIsOpen(!isOpen)}
isExpanded={isOpen}
isDisabled={isFetchingPolicies}
style={
{
width: '200px',
} as React.CSSProperties
}
>
{selected}
</MenuToggle>
);
return (
<FormGroup
data-testid="profiles-form-group"
@ -301,51 +505,55 @@ const ProfileSelector = () => {
>
{complianceType === 'openscap' && (
<Select
ouiaId="profileSelect"
variant={SelectVariant.typeahead}
onToggle={handleToggle}
onSelect={handleSelect}
onClear={handleClear}
maxHeight="300px"
selections={profileID}
isScrollable
isOpen={isOpen}
placeholderText="Select a profile"
typeAheadAriaLabel="Select a profile"
isDisabled={!isSuccess || hasWslTargetOnly}
onFilter={(_event, value) => {
if (isFetching) {
return [<OScapLoadingOption key="oscap-loading-option" />];
}
if (profiles) {
return [<OScapNoneOption key="oscap-none-option" />].concat(
profiles.map((profile_id, index) => {
return (
<OScapSelectOption
key={index}
profile_id={profile_id}
filter={value}
/>
);
})
);
}
}}
selected={profileID}
onSelect={handleSelect}
onOpenChange={handleToggle}
toggle={toggleOpenSCAP}
shouldFocusFirstItemOnOpen={false}
>
{options()}
<SelectList>
{isFetching && (
<SelectOption
value="loader"
data-testid="openscap-profiles-fetching"
>
<Spinner size="lg" />
</SelectOption>
)}
{selectOptions.length > 0 &&
[
<SelectOption
key="oscap-none-option"
value={{ toString: () => 'None', compareTo: () => false }}
>
None
</SelectOption>,
].concat(
selectOptions.map(
(name: DistributionProfileItem, index: number) => (
<OScapSelectOption key={index} profile_id={name} />
)
)
)}
{isSuccess && selectOptions.length === 0 && (
<SelectOption isDisabled>
{`No results found for "${filterValue}"`}
</SelectOption>
)}
</SelectList>
</Select>
)}
{complianceType === 'compliance' && (
<Select
isDisabled={isFetchingPolicies}
isScrollable
isOpen={isOpen}
onSelect={handleSelect}
onToggle={handleToggle}
selections={
isFetchingPolicies
? 'Loading policies'
: policyTitle || policyID || 'None'
}
ouiaId="compliancePolicySelect"
onOpenChange={handleToggle}
selected={selected}
toggle={toggleCompliance}
shouldFocusFirstItemOnOpen={false}
>
{complianceOptions()}
</Select>
@ -364,106 +572,4 @@ const ProfileSelector = () => {
);
};
const OScapNoneOption = () => {
return (
<SelectOption value={{ toString: () => 'None', compareTo: () => false }} />
);
};
const OScapLoadingOption = () => {
return (
<SelectOption
value={{ toString: () => 'Loading...', compareTo: () => false }}
>
<Spinner size="lg" />
</SelectOption>
);
};
type OScapSelectOptionPropType = {
profile_id: DistributionProfileItem;
filter?: string;
};
type OScapSelectOptionValueType = {
profileID: DistributionProfileItem;
toString: () => string;
};
const OScapSelectOption = ({
profile_id,
filter,
}: OScapSelectOptionPropType) => {
const release = useAppSelector(selectDistribution);
const { data } = useGetOscapCustomizationsQuery({
distribution: release,
profile: profile_id,
});
const oscapProfile = data?.openscap as OpenScapProfile;
if (
filter &&
!oscapProfile?.profile_name?.toLowerCase().includes(filter.toLowerCase())
) {
return null;
}
const selectObject = (
id: DistributionProfileItem,
name?: string
): OScapSelectOptionValueType => ({
profileID: id,
toString: () => name || '',
});
return (
<SelectOption
key={profile_id}
value={selectObject(profile_id, oscapProfile?.profile_name)}
description={oscapProfile?.profile_description}
/>
);
};
type ComplianceSelectOptionPropType = {
policy: PolicyRead;
};
type ComplianceSelectOptionValueType = {
policyID: string;
profileID: string;
toString: () => string;
};
const ComplianceNoneOption = () => {
return (
<SelectOption value={{ toString: () => 'None', compareTo: () => false }} />
);
};
const ComplianceSelectOption = ({ policy }: ComplianceSelectOptionPropType) => {
const selectObj = (
policyID: string,
profileID: string,
title: string
): ComplianceSelectOptionValueType => ({
policyID,
profileID,
toString: () => title,
});
const descr = (
<>
Threshold: {policy.compliance_threshold}
<br />
Active systems: {policy.total_system_count}
</>
);
return (
<SelectOption
key={policy.id}
value={selectObj(policy.id!, policy.ref_id!, policy.title!)}
description={descr}
/>
);
};
export default ProfileSelector;