V2Wizard: Add <TargetEnvironment> to Image Output step

DDF has been dropped from `<TargetEnvironment>`, state is managed with
RTK.
This commit is contained in:
lucasgarfield 2024-01-06 09:05:09 +01:00 committed by Sanne Raymaekers
parent 937219ba51
commit a3ef0cc2f7
3 changed files with 189 additions and 236 deletions

View file

@ -1,149 +1,64 @@
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
import {
Alert,
Bullseye,
Checkbox,
FormGroup,
Popover,
Radio,
Spinner,
Text,
TextContent,
TextVariants,
Tile,
} from '@patternfly/react-core';
import { HelpIcon } from '@patternfly/react-icons';
import PropTypes from 'prop-types';
import { useField } from 'react-final-form';
import { useGetArchitecturesQuery } from '../../../store/imageBuilderApi';
import { provisioningApi } from '../../../store/provisioningApi';
import { useGetEnvironment } from '../../../Utilities/useGetEnvironment';
import { useAppSelector, useAppDispatch } from '../../../../store/hooks';
import {
ImageTypes,
useGetArchitecturesQuery,
} from '../../../../store/imageBuilderApi';
import { provisioningApi } from '../../../../store/provisioningApi';
import {
addImageType,
removeImageType,
selectArchitecture,
selectDistribution,
selectImageTypes,
} from '../../../../store/wizardSlice';
import { useGetEnvironment } from '../../../../Utilities/useGetEnvironment';
const useGetAllowedTargets = ({ architecture, release }) => {
const { data, isFetching, isSuccess, isError } = useGetArchitecturesQuery({
distribution: release,
const TargetEnvironment = () => {
const arch = useAppSelector((state) => selectArchitecture(state));
const environments = useAppSelector((state) => selectImageTypes(state));
const distribution = useAppSelector((state) => selectDistribution(state));
const { data } = useGetArchitecturesQuery({
distribution: distribution,
});
// TODO: Handle isFetching state (add skeletons)
// TODO: Handle isError state (very unlikely...)
let image_types = [];
if (isSuccess && data) {
data.forEach((elem) => {
if (elem.arch === architecture) {
image_types = elem.image_types;
}
});
}
const [hasVSphere, setHasVSphere] = useState(false);
return {
data: image_types,
isFetching: isFetching,
isSuccess: isSuccess,
isError: isError,
};
};
const TargetEnvironment = ({ label, isRequired, ...props }) => {
const { getState, change } = useFormApi();
const { input } = useFieldApi({ label, isRequired, ...props });
const [environment, setEnvironment] = useState({
aws: false,
azure: false,
gcp: false,
oci: false,
'vsphere-ova': false,
vsphere: false,
'guest-image': false,
'image-installer': false,
wsl: false,
});
const dispatch = useAppDispatch();
const prefetchSources = provisioningApi.usePrefetch('getSourceList');
const { isBeta } = useGetEnvironment();
const release = getState()?.values?.release;
useEffect(() => {
if (getState()?.values?.[input.name]) {
setEnvironment(getState().values[input.name]);
}
}, [getState, input.name]);
const supportedEnvironments = data?.find(
(elem) => elem.arch === arch
)?.image_types;
const handleSetEnvironment = (env, checked) =>
setEnvironment((prevEnv) => {
const newEnv = {
...prevEnv,
[env]: checked,
};
change(input.name, newEnv);
return newEnv;
});
const handleKeyDown = (e, env, checked) => {
if (e.key === ' ') {
handleSetEnvironment(env, checked);
const handleToggleEnvironment = (environment: ImageTypes) => {
if (environments.includes(environment)) {
dispatch(removeImageType(environment));
} else {
dispatch(addImageType(environment));
}
};
// Load all the allowed targets from the backend
const architecture = useField('arch').input.value;
const {
data: allowedTargets,
isFetching,
isSuccess,
isError,
} = useGetAllowedTargets({
architecture: architecture,
release: release,
});
if (isFetching) {
return (
<Bullseye>
<Spinner size="lg" />
</Bullseye>
);
}
if (isError || !isSuccess) {
return (
<Alert
variant={'danger'}
isPlain
isInline
title={'Allowed targets unavailable'}
>
Allowed targets cannot be reached, try again later.
</Alert>
);
}
// If the user already made a choice for some targets but then changes their
// architecture or distribution, only keep the target choices that are still
// compatible.
const allTargets = [
'aws',
'gcp',
'azure',
'vsphere',
'vsphere-ova',
'guest-image',
'image-installer',
'wsl',
];
allTargets.forEach((target) => {
if (environment[target] && !allowedTargets.includes(target)) {
handleSetEnvironment(target, false);
}
});
// each item the user can select is depending on what's compatible with the
// architecture and the distribution they previously selected
return (
<FormGroup
isRequired={isRequired}
label={label}
isRequired={true}
label="Select target environments"
data-testid="target-select"
>
<FormGroup
@ -151,7 +66,7 @@ const TargetEnvironment = ({ label, isRequired, ...props }) => {
data-testid="target-public"
>
<div className="tiles">
{allowedTargets.includes('aws') && (
{supportedEnvironments?.includes('aws') && (
<Tile
className="tile pf-u-mr-sm"
data-testid="upload-aws"
@ -163,15 +78,16 @@ const TargetEnvironment = ({ label, isRequired, ...props }) => {
alt="Amazon Web Services logo"
/>
}
onClick={() => handleSetEnvironment('aws', !environment.aws)}
onKeyDown={(e) => handleKeyDown(e, 'aws', !environment.aws)}
onClick={() => {
handleToggleEnvironment('aws');
}}
onMouseEnter={() => prefetchSources({ provider: 'aws' })}
isSelected={environment.aws}
isSelected={environments.includes('aws')}
isStacked
isDisplayLarge
/>
)}
{allowedTargets.includes('gcp') && (
{supportedEnvironments?.includes('gcp') && (
<Tile
className="tile pf-u-mr-sm"
data-testid="upload-google"
@ -185,15 +101,16 @@ const TargetEnvironment = ({ label, isRequired, ...props }) => {
alt="Google Cloud Platform logo"
/>
}
onClick={() => handleSetEnvironment('gcp', !environment.gcp)}
isSelected={environment.gcp}
onKeyDown={(e) => handleKeyDown(e, 'gcp', !environment.gcp)}
onClick={() => {
handleToggleEnvironment('gcp');
}}
isSelected={environments.includes('gcp')}
onMouseEnter={() => prefetchSources({ provider: 'gcp' })}
isStacked
isDisplayLarge
/>
)}
{allowedTargets.includes('azure') && (
{supportedEnvironments?.includes('azure') && (
<Tile
className="tile pf-u-mr-sm"
data-testid="upload-azure"
@ -207,15 +124,16 @@ const TargetEnvironment = ({ label, isRequired, ...props }) => {
alt="Microsoft Azure logo"
/>
}
onClick={() => handleSetEnvironment('azure', !environment.azure)}
onKeyDown={(e) => handleKeyDown(e, 'azure', !environment.azure)}
onClick={() => {
handleToggleEnvironment('azure');
}}
onMouseEnter={() => prefetchSources({ provider: 'azure' })}
isSelected={environment.azure}
isSelected={environments.includes('azure')}
isStacked
isDisplayLarge
/>
)}
{allowedTargets.includes('oci') && (
{supportedEnvironments?.includes('oci') && isBeta() && (
<Tile
className="tile pf-u-mr-sm"
data-testid="upload-oci"
@ -227,59 +145,101 @@ const TargetEnvironment = ({ label, isRequired, ...props }) => {
alt="Oracle Cloud Infrastructure logo"
/>
}
onClick={() => handleSetEnvironment('oci', !environment.oci)}
onKeyDown={(e) => handleKeyDown(e, 'oci', !environment.oci)}
isSelected={environment.oci}
onClick={() => {
handleToggleEnvironment('oci');
}}
isSelected={environments.includes('oci')}
isStacked
isDisplayLarge
/>
)}
</div>
</FormGroup>
{allowedTargets.includes('vsphere') && (
<FormGroup
label={<Text component={TextVariants.small}>Private cloud</Text>}
className="pf-u-mt-sm"
data-testid="target-private"
>
<Checkbox
label="VMware vSphere"
isChecked={environment.vsphere || environment['vsphere-ova']}
onChange={(_event, checked) => {
handleSetEnvironment('vsphere-ova', checked);
handleSetEnvironment('vsphere', false);
}}
aria-label="VMware checkbox"
id="checkbox-vmware"
name="VMware"
data-testid="checkbox-vmware"
/>
</FormGroup>
)}
{allowedTargets.includes('vsphere') && (
<FormGroup
className="pf-u-mt-sm pf-u-mb-sm pf-u-ml-xl"
data-testid="target-private-vsphere-radio"
>
{allowedTargets.includes('vsphere-ova') && (
{supportedEnvironments?.includes('vsphere') && (
<>
<FormGroup
label={<Text component={TextVariants.small}>Private cloud</Text>}
className="pf-u-mt-sm"
data-testid="target-private"
>
<Checkbox
label="VMWare vSphere"
isChecked={
environments.includes('vsphere') ||
environments.includes('vsphere-ova')
}
onChange={() => {
setHasVSphere(!hasVSphere);
handleToggleEnvironment('vsphere-ova');
}}
aria-label="VMWare checkbox"
id="checkbox-vmware"
name="VMWare"
data-testid="checkbox-vmware"
/>
</FormGroup>
<FormGroup
className="pf-u-mt-sm pf-u-mb-sm pf-u-ml-xl"
data-testid="target-private-vsphere-radio"
>
{supportedEnvironments?.includes('vsphere-ova') && (
<Radio
name="vsphere-radio"
aria-label="VMWare vSphere radio button OVA"
id="vsphere-radio-ova"
label={
<>
Open virtualization format (.ova)
<Popover
maxWidth="30rem"
position="right"
bodyContent={
<TextContent>
<Text>
An OVA file is a virtual appliance used by
virtualization platforms such as VMWare vSphere. It
is a package that contains files used to describe a
virtual machine, which includes a VMDK image, OVF
descriptor file and a manifest file.
</Text>
</TextContent>
}
>
<HelpIcon className="pf-u-ml-sm" />
</Popover>
</>
}
onChange={() => {
handleToggleEnvironment('vsphere-ova');
handleToggleEnvironment('vsphere');
}}
isChecked={environments.includes('vsphere-ova')}
isDisabled={
!(
environments.includes('vsphere') ||
environments.includes('vsphere-ova')
)
}
/>
)}
<Radio
className="pf-u-mt-sm"
name="vsphere-radio"
aria-label="VMware vSphere radio button OVA"
id="vsphere-radio-ova"
aria-label="VMWare vSphere radio button VMDK"
id="vsphere-radio-vmdk"
label={
<>
Open virtualization format (.ova)
Virtual disk (.vmdk)
<Popover
maxWidth="30rem"
position="right"
bodyContent={
<TextContent>
<Text>
An OVA file is a virtual appliance used by
virtualization platforms such as VMware vSphere. It is
a package that contains files used to describe a
virtual machine, which includes a VMDK image, OVF
descriptor file and a manifest file.
A VMDK file is a virtual disk that stores the contents
of a virtual machine. This disk has to be imported
into vSphere using govc import.vmdk, use the OVA
version when using the vSphere UI.
</Text>
</TextContent>
}
@ -288,84 +248,58 @@ const TargetEnvironment = ({ label, isRequired, ...props }) => {
</Popover>
</>
}
onChange={(_event, checked) => {
handleSetEnvironment('vsphere-ova', checked);
handleSetEnvironment('vsphere', !checked);
onChange={() => {
handleToggleEnvironment('vsphere-ova');
handleToggleEnvironment('vsphere');
}}
isChecked={environment['vsphere-ova']}
isDisabled={!(environment.vsphere || environment['vsphere-ova'])}
isChecked={environments.includes('vsphere')}
isDisabled={
!(
environments.includes('vsphere') ||
environments.includes('vsphere-ova')
)
}
/>
)}
<Radio
className="pf-u-mt-sm"
name="vsphere-radio"
aria-label="VMware vSphere radio button VMDK"
id="vsphere-radio-vmdk"
label={
<>
Virtual disk (.vmdk)
<Popover
maxWidth="30rem"
position="right"
bodyContent={
<TextContent>
<Text>
A VMDK file is a virtual disk that stores the contents
of a virtual machine. This disk has to be imported into
vSphere using govc import.vmdk, use the OVA version when
using the vSphere UI.
</Text>
</TextContent>
}
>
<HelpIcon className="pf-u-ml-sm" />
</Popover>
</>
}
onChange={(_event, checked) => {
handleSetEnvironment('vsphere-ova', !checked);
handleSetEnvironment('vsphere', checked);
}}
isChecked={environment.vsphere}
isDisabled={!(environment.vsphere || environment['vsphere-ova'])}
/>
</FormGroup>
</FormGroup>
</>
)}
<FormGroup
label={<Text component={TextVariants.small}>Other</Text>}
data-testid="target-other"
>
{allowedTargets.includes('guest-image') && (
{supportedEnvironments?.includes('guest-image') && (
<Checkbox
label="Virtualization - Guest image (.qcow2)"
isChecked={environment['guest-image']}
onChange={(_event, checked) =>
handleSetEnvironment('guest-image', checked)
}
isChecked={environments.includes('guest-image')}
onChange={() => {
handleToggleEnvironment('guest-image');
}}
aria-label="Virtualization guest image checkbox"
id="checkbox-guest-image"
name="Virtualization guest image"
data-testid="checkbox-guest-image"
/>
)}
{allowedTargets.includes('image-installer') && (
{supportedEnvironments?.includes('image-installer') && (
<Checkbox
label="Bare metal - Installer (.iso)"
isChecked={environment['image-installer']}
onChange={(_event, checked) =>
handleSetEnvironment('image-installer', checked)
}
isChecked={environments.includes('image-installer')}
onChange={() => {
handleToggleEnvironment('image-installer');
}}
aria-label="Bare metal installer checkbox"
id="checkbox-image-installer"
name="Bare metal installer"
data-testid="checkbox-image-installer"
/>
)}
{allowedTargets.includes('wsl') && isBeta() && (
{supportedEnvironments?.includes('wsl') && isBeta() && (
<Checkbox
label="WSL - Windows Subsystem for Linux (.tar.gz)"
isChecked={environment['wsl']}
onChange={(_event, checked) => handleSetEnvironment('wsl', checked)}
isChecked={environments.includes('wsl')}
onChange={() => {
handleToggleEnvironment('wsl');
}}
aria-label="windows subsystem for linux checkbox"
id="checkbox-wsl"
name="WSL"
@ -377,14 +311,4 @@ const TargetEnvironment = ({ label, isRequired, ...props }) => {
);
};
TargetEnvironment.propTypes = {
label: PropTypes.node,
isRequired: PropTypes.bool,
};
TargetEnvironment.defaultProps = {
label: '',
isRequired: false,
};
export default TargetEnvironment;

View file

@ -5,6 +5,7 @@ import { Text, Form, Title } from '@patternfly/react-core';
import ArchSelect from './ArchSelect';
import ReleaseLifecycle from './ReleaseLifecycle';
import ReleaseSelect from './ReleaseSelect';
import TargetEnvironment from './TargetEnvironment';
import DocumentationButton from '../../../sharedComponents/DocumentationButton';
@ -21,6 +22,7 @@ const ImageOutputStep = () => {
<ReleaseSelect />
<ReleaseLifecycle />
<ArchSelect />
<TargetEnvironment />
</Form>
);
};

View file

@ -1,6 +1,6 @@
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import { Distributions, ImageRequest } from './imageBuilderApi';
import { Distributions, ImageRequest, ImageTypes } from './imageBuilderApi';
import { RHEL_9, X86_64 } from '../constants';
@ -9,11 +9,13 @@ import { RootState } from '.';
type wizardState = {
architecture: ImageRequest['architecture'];
distribution: Distributions;
imageTypes: ImageTypes[];
};
const initialState: wizardState = {
architecture: X86_64,
distribution: RHEL_9,
imageTypes: [],
};
export const selectArchitecture = (state: RootState) => {
@ -24,6 +26,10 @@ export const selectDistribution = (state: RootState) => {
return state.wizard.distribution;
};
export const selectImageTypes = (state: RootState) => {
return state.wizard.imageTypes;
};
export const wizardSlice = createSlice({
name: 'wizard',
initialState,
@ -38,9 +44,30 @@ export const wizardSlice = createSlice({
changeDistribution: (state, action: PayloadAction<Distributions>) => {
state.distribution = action.payload;
},
addImageType: (state, action: PayloadAction<ImageTypes>) => {
// Remove (if present) before adding to avoid duplicates
state.imageTypes = state.imageTypes.filter(
(imageType) => imageType !== action.payload
);
state.imageTypes.push(action.payload);
},
removeImageType: (state, action: PayloadAction<ImageTypes>) => {
state.imageTypes = state.imageTypes.filter(
(imageType) => imageType !== action.payload
);
},
changeImageTypes: (state, action: PayloadAction<ImageTypes[]>) => {
state.imageTypes = action.payload;
},
},
});
export const { initializeWizard, changeArchitecture, changeDistribution } =
wizardSlice.actions;
export const {
initializeWizard,
changeArchitecture,
changeDistribution,
addImageType,
removeImageType,
changeImageTypes,
} = wizardSlice.actions;
export default wizardSlice.reducer;