debian-image-builder-frontend/src/Components/CreateImageWizard/steps/Oscap/components/ProfileSelector.tsx
regexowl 248bc1d67a Wizard: Move stuff around, split into more components
This moves several files into `components` subdirectory and splits `Oscap.tsx` into more components to make the file shorter and easier to navigate.
2025-04-02 12:17:23 +02:00

469 lines
12 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { Alert, FormGroup, Spinner } from '@patternfly/react-core';
import {
Select,
SelectOption,
SelectVariant,
} from '@patternfly/react-core/deprecated';
import { v4 as uuidv4 } from 'uuid';
import OpenSCAPFGLabel from './OpenSCAPFGLabel';
import {
useGetOscapProfilesQuery,
useGetOscapCustomizationsQuery,
useLazyGetOscapCustomizationsQuery,
useBackendPrefetch,
} from '../../../../../store/backendApi';
import {
usePoliciesQuery,
PolicyRead,
} from '../../../../../store/complianceApi';
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
import {
DistributionProfileItem,
Filesystem,
OpenScapProfile,
Services,
} from '../../../../../store/imageBuilderApi';
import {
changeCompliance,
selectDistribution,
selectComplianceProfileID,
selectCompliancePolicyID,
selectCompliancePolicyTitle,
addPackage,
addPartition,
changeFileSystemConfigurationType,
removePackage,
clearPartitions,
changeEnabledServices,
changeMaskedServices,
changeDisabledServices,
selectComplianceType,
clearKernelAppend,
addKernelArg,
} from '../../../../../store/wizardSlice';
import { useHasSpecificTargetOnly } from '../../../utilities/hasSpecificTargetOnly';
import { parseSizeUnit } from '../../../utilities/parseSizeUnit';
import { Partition, Units } from '../../FileSystem/FileSystemTable';
import { removeBetaFromRelease } from '../removeBetaFromRelease';
const ProfileSelector = () => {
const policyID = useAppSelector(selectCompliancePolicyID);
const policyTitle = useAppSelector(selectCompliancePolicyTitle);
const profileID = useAppSelector(selectComplianceProfileID);
const release = removeBetaFromRelease(useAppSelector(selectDistribution));
const majorVersion = release.split('-')[1];
const hasWslTargetOnly = useHasSpecificTargetOnly('wsl');
const dispatch = useAppDispatch();
const [isOpen, setIsOpen] = useState(false);
const complianceType = useAppSelector(selectComplianceType);
const prefetchProfile = useBackendPrefetch('getOscapCustomizations');
const {
data: profiles,
isFetching,
isSuccess,
isError,
refetch,
} = useGetOscapProfilesQuery(
{
distribution: release,
},
{
skip: complianceType === 'compliance',
}
);
const {
data: policies,
isFetching: isFetchingPolicies,
isSuccess: isSuccessPolicies,
} = usePoliciesQuery(
{
filter: `os_major_version=${majorVersion}`,
},
{
skip: complianceType === 'openscap',
}
);
const { data: currentProfileData } = useGetOscapCustomizationsQuery(
{
distribution: release,
// @ts-ignore if openScapProfile is undefined the query is going to get skipped
profile: profileID,
},
{ skip: !profileID }
);
const [trigger] = useLazyGetOscapCustomizationsQuery();
// prefetch the profiles customizations for on-prem
// and save the results to the cache, since the request
// is quite slow
if (process.env.IS_ON_PREMISE) {
profiles?.forEach((profile) => {
prefetchProfile({
distribution: release,
profile: profile,
});
});
}
useEffect(() => {
if (!policies || policies.data === undefined) {
return;
}
if (policyID && !policyTitle) {
for (const p of policies.data) {
const pol = p as PolicyRead;
if (pol.id === policyID) {
dispatch(
changeCompliance({
policyID: pol.id,
profileID: pol.ref_id,
policyTitle: pol.title,
})
);
}
}
}
}, [isSuccessPolicies]);
const handleToggle = () => {
if (!isOpen && complianceType === 'openscap') {
refetch();
}
setIsOpen(!isOpen);
};
const handleClear = () => {
dispatch(
changeCompliance({
profileID: undefined,
policyID: undefined,
policyTitle: undefined,
})
);
clearOscapPackages(currentProfileData?.packages || []);
dispatch(changeFileSystemConfigurationType('automatic'));
handleServices(undefined);
dispatch(clearKernelAppend());
};
const handlePackages = (
oldOscapPackages: string[],
newOscapPackages: string[]
) => {
clearOscapPackages(oldOscapPackages);
for (const pkg of newOscapPackages) {
dispatch(
addPackage({
name: pkg,
summary: 'Required by chosen OpenSCAP profile',
repository: 'distro',
})
);
}
};
const clearOscapPackages = (oscapPackages: string[]) => {
for (const pkg of oscapPackages) {
dispatch(removePackage(pkg));
}
};
const handlePartitions = (oscapPartitions: Filesystem[]) => {
dispatch(clearPartitions());
const newPartitions = oscapPartitions.map((filesystem) => {
const [size, unit] = parseSizeUnit(filesystem.min_size);
const partition: Partition = {
mountpoint: filesystem.mountpoint,
min_size: size.toString(),
unit: unit as Units,
id: uuidv4(),
};
return partition;
});
if (newPartitions) {
dispatch(changeFileSystemConfigurationType('manual'));
for (const partition of newPartitions) {
dispatch(addPartition(partition));
}
}
};
const handleServices = (services: Services | undefined) => {
dispatch(changeEnabledServices(services?.enabled || []));
dispatch(changeMaskedServices(services?.masked || []));
dispatch(changeDisabledServices(services?.disabled || []));
};
const handleKernelAppend = (kernelAppend: string | undefined) => {
dispatch(clearKernelAppend());
if (kernelAppend) {
const kernelArgsArray = kernelAppend.split(' ');
for (const arg in kernelArgsArray) {
dispatch(addKernelArg(kernelArgsArray[arg]));
}
}
};
const handleSelect = (
_event: React.MouseEvent<Element, MouseEvent>,
selection: OScapSelectOptionValueType | ComplianceSelectOptionValueType
) => {
if (selection.profileID === undefined) {
// handle user has selected 'None' case
handleClear();
} else {
const oldOscapPackages = currentProfileData?.packages || [];
trigger(
{
distribution: release,
profile: selection.profileID as DistributionProfileItem,
},
true // preferCacheValue
)
.unwrap()
.then((response) => {
const oscapPartitions = response.filesystem || [];
const newOscapPackages = response.packages || [];
handlePartitions(oscapPartitions);
handlePackages(oldOscapPackages, newOscapPackages);
handleServices(response.services);
handleKernelAppend(response.kernel?.append);
if (complianceType === 'openscap') {
dispatch(
changeCompliance({
profileID: selection.profileID,
policyID: undefined,
policyTitle: undefined,
})
);
} else {
const compl = selection as ComplianceSelectOptionValueType;
dispatch(
changeCompliance({
profileID: compl.profileID,
policyID: compl.policyID,
policyTitle: compl.toString(),
})
);
}
});
}
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 complianceOptions = () => {
if (!policies || policies.data === undefined) {
return [];
}
const res = [<ComplianceNoneOption key="compliance-non-option" />];
for (const p of policies.data) {
if (p === undefined) {
continue;
}
const pol = p as PolicyRead;
res.push(<ComplianceSelectOption key={pol.id} policy={pol} />);
}
return res;
};
return (
<FormGroup
data-testid="profiles-form-group"
label={complianceType === 'openscap' ? <OpenSCAPFGLabel /> : <>Policy</>}
>
{complianceType === 'openscap' && (
<Select
ouiaId="profileSelect"
variant={SelectVariant.typeahead}
onToggle={handleToggle}
onSelect={handleSelect}
onClear={handleClear}
maxHeight="300px"
selections={profileID}
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}
/>
);
})
);
}
}}
>
{options()}
</Select>
)}
{complianceType === 'compliance' && (
<Select
isDisabled={isFetchingPolicies}
isOpen={isOpen}
onSelect={handleSelect}
onToggle={handleToggle}
selections={
isFetchingPolicies
? 'Loading policies'
: policyTitle || policyID || 'None'
}
ouiaId="compliancePolicySelect"
>
{complianceOptions()}
</Select>
)}
{isError && (
<Alert
title="Error fetching the profiles"
variant="danger"
isPlain
isInline
>
Cannot get the list of profiles
</Alert>
)}
</FormGroup>
);
};
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;