WizardV2: map oscap to wizard request

This commit is contained in:
Amir 2024-04-04 20:22:22 +03:00 committed by Lucas Garfield
parent a7f46e938d
commit ba3a2dc333
6 changed files with 138 additions and 186 deletions

View file

@ -1,9 +1,8 @@
import React, { useEffect, useState } from 'react';
import React, { useState, useEffect } from 'react';
import {
Alert,
FormGroup,
Spinner,
Popover,
TextContent,
Text,
@ -26,14 +25,8 @@ import {
} from '../../../../store/imageBuilderApi';
import {
changeOscapProfile,
changeKernel,
selectDistribution,
selectProfile,
selectKernel,
selectDisabledServices,
selectEnabledServices,
changeDisabledServices,
changeEnabledServices,
clearOscapPackages,
addPackage,
selectPackages,
@ -42,13 +35,10 @@ import {
const ProfileSelector = () => {
const oscapProfile = useAppSelector(selectProfile);
let kernel = useAppSelector(selectKernel);
let disabledServices = useAppSelector(selectDisabledServices);
let enabledServices = useAppSelector(selectEnabledServices);
const release = useAppSelector(selectDistribution);
const packages = useAppSelector(selectPackages);
const dispatch = useAppDispatch();
const [profileName, setProfileName] = useState<string | undefined>('None');
const [isOpen, setIsOpen] = useState(false);
const {
data: profiles,
@ -60,7 +50,7 @@ const ProfileSelector = () => {
distribution: release,
});
const { data } = useGetOscapCustomizationsQuery(
const { data: oscapData } = useGetOscapCustomizationsQuery(
{
distribution: release,
// @ts-ignore if oscapProfile is undefined the query is going to get skipped, so it's safe here to ignore the linter here
@ -70,55 +60,28 @@ const ProfileSelector = () => {
skip: !oscapProfile,
}
);
kernel = data?.kernel?.append;
disabledServices = data?.services?.disabled;
enabledServices = data?.services?.enabled;
useEffect(() => {
if (isFetching || !isSuccess) return;
dispatch(changeKernel(kernel));
dispatch(changeDisabledServices(disabledServices));
dispatch(changeEnabledServices(enabledServices));
}, [
isFetching,
isSuccess,
dispatch,
data?.kernel?.append,
data?.services?.disabled,
data?.services?.enabled,
disabledServices,
enabledServices,
kernel,
]);
useEffect(() => {
if (
data &&
data.openscap &&
typeof data.openscap.profile_name === 'string'
) {
setProfileName(data.openscap.profile_name);
}
}, [data]);
const profileName = oscapProfile ? oscapData?.openscap?.profile_name : 'None';
useEffect(() => {
dispatch(clearOscapPackages());
for (const pkg in data?.packages) {
for (const pkg in oscapData?.packages) {
if (
packages.map((pkg) => pkg.name).includes(data?.packages[Number(pkg)])
packages
.map((pkg) => pkg.name)
.includes(oscapData?.packages[Number(pkg)])
) {
dispatch(removePackage(data?.packages[Number(pkg)]));
dispatch(removePackage(oscapData?.packages[Number(pkg)]));
}
dispatch(
addPackage({
name: data?.packages[Number(pkg)],
name: oscapData?.packages[Number(pkg)],
summary: 'Required by chosen OpenSCAP profile',
repository: 'distro',
isRequiredByOpenScap: true,
})
);
}
}, [data?.packages, dispatch]);
}, [oscapData?.packages, dispatch]);
const handleToggle = () => {
if (!isOpen) {
@ -129,52 +92,26 @@ const ProfileSelector = () => {
const handleClear = () => {
dispatch(changeOscapProfile(undefined));
dispatch(changeKernel(undefined));
dispatch(changeDisabledServices(undefined));
dispatch(changeEnabledServices(undefined));
dispatch(clearOscapPackages());
setProfileName(undefined);
};
const handleSelect = (
_event: React.MouseEvent<Element, MouseEvent>,
selection: DistributionProfileItem
selection: OScapSelectOptionValueType
) => {
dispatch(changeOscapProfile(selection));
dispatch(changeKernel(kernel));
dispatch(changeDisabledServices(disabledServices));
dispatch(changeEnabledServices(enabledServices));
dispatch(changeOscapProfile(selection.id));
setIsOpen(false);
};
const options = [
<OScapNoneOption setProfileName={setProfileName} key="oscap-none-option" />,
];
if (isSuccess) {
options.concat(
profiles.map((profile_id) => {
return (
<OScapSelectOption
key={profile_id}
profile_id={profile_id}
setProfileName={setProfileName}
/>
);
})
);
}
if (isFetching) {
options.push(
<SelectOption
isNoResultsOption={true}
data-testid="policies-loading"
key={'None'}
>
<Spinner size="md" />
</SelectOption>
);
}
const options = () => {
if (profiles) {
return [<OScapNoneOption key="oscap-none-option" />].concat(
profiles.map((profile_id, index) => {
return <OScapSelectOption key={index} profile_id={profile_id} />;
})
);
}
};
return (
<FormGroup
@ -203,11 +140,13 @@ const ProfileSelector = () => {
}
>
<Select
loadingVariant={isFetching ? 'spinner' : undefined}
ouiaId="profileSelect"
variant={SelectVariant.typeahead}
onToggle={handleToggle}
onSelect={handleSelect}
onClear={handleClear}
maxHeight="300px"
selections={profileName}
isOpen={isOpen}
placeholderText="Select a profile"
@ -215,19 +154,13 @@ const ProfileSelector = () => {
isDisabled={!isSuccess}
onFilter={(_event, value) => {
if (profiles) {
return [
<OScapNoneOption
setProfileName={setProfileName}
key="oscap-none-option"
/>,
].concat(
return [<OScapNoneOption key="oscap-none-option" />].concat(
profiles.map((profile_id, index) => {
return (
<OScapSelectOption
key={index}
profile_id={profile_id}
setProfileName={setProfileName}
input={value}
filter={value}
/>
);
})
@ -235,7 +168,7 @@ const ProfileSelector = () => {
}
}}
>
{options}
{options()}
</Select>
{isError && (
<Alert
@ -251,59 +184,51 @@ const ProfileSelector = () => {
);
};
type OScapNoneOptionPropType = {
setProfileName: (name: string) => void;
};
const OScapNoneOption = ({ setProfileName }: OScapNoneOptionPropType) => {
const OScapNoneOption = () => {
return (
<SelectOption
value={undefined}
onClick={() => {
setProfileName('None');
}}
>
<p>{'None'}</p>
</SelectOption>
<SelectOption value={{ toString: () => 'None', compareTo: () => false }} />
);
};
type OScapSelectOptionPropType = {
profile_id: DistributionProfileItem;
setProfileName: (name: string) => void;
input?: string;
filter?: string;
};
type OScapSelectOptionValueType = {
id: DistributionProfileItem;
toString: () => string;
};
const OScapSelectOption = ({
profile_id,
setProfileName,
input,
filter,
}: OScapSelectOptionPropType) => {
const release = useAppSelector(selectDistribution);
const { data } = useGetOscapCustomizationsQuery({
distribution: release,
profile: profile_id,
});
if (
input &&
!data?.openscap?.profile_name?.toLowerCase().includes(input.toLowerCase())
filter &&
!data?.openscap?.profile_name?.toLowerCase().includes(filter.toLowerCase())
) {
return null;
}
const selectObject = (
id: DistributionProfileItem,
name?: string
): OScapSelectOptionValueType => ({
id,
toString: () => name || '',
});
return (
<SelectOption
key={profile_id}
value={profile_id}
onClick={() => {
if (data?.openscap?.profile_name) {
setProfileName(data?.openscap?.profile_name);
}
}}
>
<p>{data?.openscap?.profile_name}</p>
</SelectOption>
value={selectObject(profile_id, data?.openscap?.profile_name)}
description={data?.openscap?.profile_description}
/>
);
};

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import { Button, Form, Text, Title } from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
@ -10,9 +10,14 @@ import { useAppSelector } from '../../../../store/hooks';
import { selectDistribution } from '../../../../store/wizardSlice';
const OscapStep = () => {
const prefetchOscapProfile = imageBuilderApi.usePrefetch('getOscapProfiles');
const prefetchOscapProfile = imageBuilderApi.usePrefetch(
'getOscapProfiles',
{}
);
const release = useAppSelector(selectDistribution);
prefetchOscapProfile({ distribution: release });
useEffect(() => {
prefetchOscapProfile({ distribution: release });
}, []);
return (
<Form>

View file

@ -16,6 +16,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import CreateDropdown from './CreateDropdown';
import EditDropdown from './EditDropdown';
import { useServerStore } from '../../../../../store/hooks';
import {
useCreateBlueprintMutation,
useUpdateBlueprintMutation,
@ -33,6 +34,9 @@ const ReviewWizardFooter = () => {
reset: resetCreate,
},
] = useCreateBlueprintMutation({ fixedCacheKey: 'createBlueprintKey' });
// initialize the server store with the data from RTK query
const serverStore = useServerStore();
const [
,
{
@ -61,7 +65,7 @@ const ReviewWizardFooter = () => {
const getBlueprintPayload = async () => {
const userData = await auth?.getUser();
const orgId = userData?.identity?.internal?.org_id;
const requestBody = orgId && mapRequestFromState(store, orgId);
const requestBody = orgId && mapRequestFromState(store, orgId, serverStore);
return requestBody;
};

View file

@ -7,9 +7,12 @@ import {
BlueprintResponse,
CreateBlueprintRequest,
Customizations,
DistributionProfileItem,
GcpUploadRequestOptions,
ImageRequest,
ImageTypes,
OpenScap,
Services,
Subscription,
UploadTypes,
} from '../../../store/imageBuilderApi';
@ -36,6 +39,7 @@ import {
selectPackages,
selectPayloadRepositories,
selectRecommendedRepositories,
selectProfile,
selectRegistrationType,
selectServerUrl,
wizardState,
@ -46,18 +50,26 @@ import {
} from '../steps/Repositories/Repositories';
import { GcpAccountType } from '../steps/TargetEnvironment/Gcp';
type ServerStore = {
kernel?: { append?: string };
services?: { enabled?: string[]; disabled?: string[] };
};
/**
* This function maps the wizard state to a valid CreateBlueprint request object
* @param {Store} store redux store
* @param {string} orgID organization ID
*
* @returns {CreateBlueprintRequest} blueprint creation request payload
*/
export const mapRequestFromState = (
store: Store,
orgID: string
orgID: string,
serverStore: ServerStore
): CreateBlueprintRequest => {
const state = store.getState();
const imageRequests = getImageRequests(state);
const customizations = getCustomizations(state, orgID);
const customizations = getCustomizations(state, orgID, serverStore);
return {
name: selectBlueprintName(state),
@ -102,16 +114,9 @@ export const mapRequestToState = (request: BlueprintResponse): wizardState => {
serverUrl: request.customizations.subscription?.['server-url'] || '',
baseUrl: request.customizations.subscription?.['base-url'] || '',
},
// TODO: add openscap support
openScap: {
profile: undefined,
kernel: {
kernelAppend: '',
},
services: {
disabled: [],
enabled: [],
},
profile: request.customizations.openscap
?.profile_id as DistributionProfileItem,
},
fileSystem: {
mode: 'automatic',
@ -258,7 +263,11 @@ const getImageOptions = (
return {};
};
const getCustomizations = (state: RootState, orgID: string): Customizations => {
const getCustomizations = (
state: RootState,
orgID: string,
serverStore: ServerStore
): Customizations => {
return {
containers: undefined,
directories: undefined,
@ -267,12 +276,14 @@ const getCustomizations = (state: RootState, orgID: string): Customizations => {
packages: getPackages(state),
payload_repositories: getPayloadRepositories(state),
custom_repositories: getCustomRepositories(state),
openscap: undefined,
openscap: getOpenscapProfile(state),
filesystem: undefined,
users: undefined,
services: undefined,
services: getServices(serverStore),
hostname: undefined,
kernel: undefined,
kernel: serverStore.kernel?.append
? { append: serverStore.kernel?.append }
: undefined,
groups: undefined,
timezone: undefined,
locale: undefined,
@ -285,6 +296,27 @@ const getCustomizations = (state: RootState, orgID: string): Customizations => {
};
};
const getServices = (serverStore: ServerStore): Services | undefined => {
const enabledServices = serverStore.services?.enabled;
const disabledServices = serverStore.services?.disabled;
if (enabledServices || disabledServices) {
return {
enabled: enabledServices,
disabled: disabledServices,
};
}
return undefined;
};
const getOpenscapProfile = (state: RootState): OpenScap | undefined => {
const profile = selectProfile(state);
if (profile) {
return { profile_id: profile };
}
return undefined;
};
const getPackages = (state: RootState) => {
const packages = selectPackages(state);

View file

@ -1,7 +1,37 @@
import { useDispatch, useSelector } from 'react-redux';
import { useGetOscapCustomizationsQuery } from './imageBuilderApi';
import { selectDistribution, selectProfile } from './wizardSlice';
import type { RootState, AppDispatch } from './index';
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();
// common hooks
export const useOscapData = () => {
const release = useAppSelector(selectDistribution);
const openScapProfile = useAppSelector(selectProfile);
const { data } = useGetOscapCustomizationsQuery(
{
distribution: release,
// @ts-ignore if openScapProfile is undefined the query is going to get skipped
profile: openScapProfile,
},
{ skip: !openScapProfile }
);
if (!openScapProfile) return undefined;
return {
kernel: { append: data?.kernel?.append },
services: {
enabled: data?.services?.enabled,
disabled: data?.services?.disabled,
},
};
};
export const useServerStore = () => {
const oscap = useOscapData();
return { ...oscap };
};

View file

@ -66,13 +66,6 @@ export type wizardState = {
};
openScap: {
profile: DistributionProfileItem | undefined;
kernel: {
kernelAppend: string | undefined;
};
services: {
disabled: string[] | undefined;
enabled: string[] | undefined;
};
};
fileSystem: {
mode: FileSystemPartitionMode;
@ -122,13 +115,6 @@ const initialState: wizardState = {
},
openScap: {
profile: undefined,
kernel: {
kernelAppend: '',
},
services: {
disabled: [],
enabled: [],
},
},
fileSystem: {
mode: 'automatic',
@ -223,18 +209,6 @@ export const selectProfile = (state: RootState) => {
return state.wizard.openScap.profile;
};
export const selectKernel = (state: RootState) => {
return state.wizard.openScap.kernel.kernelAppend;
};
export const selectDisabledServices = (state: RootState) => {
return state.wizard.openScap.services.disabled;
};
export const selectEnabledServices = (state: RootState) => {
return state.wizard.openScap.services.enabled;
};
export const selectFileSystemPartitionMode = (state: RootState) => {
return state.wizard.fileSystem.mode;
};
@ -370,21 +344,6 @@ export const wizardSlice = createSlice({
state.openScap.profile = action.payload;
},
changeKernel: (state, action: PayloadAction<string | undefined>) => {
state.openScap.kernel.kernelAppend = action.payload;
},
changeDisabledServices: (
state,
action: PayloadAction<string[] | undefined>
) => {
state.openScap.services.disabled = action.payload;
},
changeEnabledServices: (
state,
action: PayloadAction<string[] | undefined>
) => {
state.openScap.services.enabled = action.payload;
},
changeFileSystemConfiguration: (
state,
action: PayloadAction<Partition[]>
@ -527,9 +486,6 @@ export const {
changeRegistrationType,
changeActivationKey,
changeOscapProfile,
changeKernel,
changeDisabledServices,
changeEnabledServices,
changeFileSystemConfiguration,
setIsNextButtonTouched,
changeFileSystemPartitionMode,