wizard/image_output: add the image output step
Port the image output step to the new wizard v2. This is also the first step in the new redesigned wizard and this commit message is an opportunity to detail a bit the organisation of it: The code is organised as followed: - CreateImageWizard is the root and contains all the code associated with the PF wizard. (i.e Wizard and WizardStep). - Each step has its code under the step/ sub directory. - The step directory is named after the step (i.e ImageOutput for the Image Output step - At the root of the step directory there is a file which contains the code of the step (i.e ImageOutput.tsx). - The main component of the step can access many subcomponents and they have to be stored alongside it in its directory. State management: - The state management is only done with react use states, prop drilling and state lifting. This is to ensure simplicity as it makes the wizard - If necessary in the future, there might be needs for context and reducers, but it's quite not the case RN. And using them come at the cost of making exploring the code harder. - CreateImageWizard will declare all the states
This commit is contained in:
parent
74afa2073a
commit
9c78456c57
7 changed files with 911 additions and 2 deletions
70
src/Components/CreateImageWizardV2/CreateImageWizard.scss
Normal file
70
src/Components/CreateImageWizardV2/CreateImageWizard.scss
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
.pf-v5-c-wizard__nav-list {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.pf-v5-c-wizard__nav {
|
||||
overflow-y: unset;
|
||||
}
|
||||
|
||||
.pf-c-popover[data-popper-reference-hidden="true"] {
|
||||
font-weight: initial;
|
||||
visibility: initial;
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
.pf-v5-c-dual-list-selector {
|
||||
--pf-v5-c-dual-list-selector__menu--MinHeight: 18rem;
|
||||
--pf-v5-c-dual-list-selector--GridTemplateColumns--pane--MinMax--max: 100vw;
|
||||
}
|
||||
|
||||
.pf-c-form {
|
||||
--pf-c-form--GridGap: var(--pf-global--spacer--md);
|
||||
}
|
||||
|
||||
.pf-c-form__group-label {
|
||||
--pf-c-form__group-label--PaddingBottom: var(--pf-global--spacer--xs);
|
||||
}
|
||||
|
||||
.tiles {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tile {
|
||||
flex: 1 0 0px;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.pf-c-tile:focus {
|
||||
--pf-c-tile__title--Color: var(--pf-c-tile__title--Color);
|
||||
--pf-c-tile__icon--Color: var(---pf-global--Color--100);
|
||||
--pf-c-tile--before--BorderWidth: var(--pf-global--BorderWidth--sm);
|
||||
--pf-c-tile--before--BorderColor: var(--pf-global--BorderColor--100);
|
||||
}
|
||||
|
||||
.pf-c-tile.pf-m-selected:focus {
|
||||
--pf-c-tile__title--Color: var(--pf-c-tile--focus__title--Color);
|
||||
--pf-c-tile__icon--Color: var(--pf-c-tile--focus__icon--Color);
|
||||
}
|
||||
|
||||
.provider-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.pf-u-min-width {
|
||||
--pf-u-min-width--MinWidth: 18ch;
|
||||
}
|
||||
|
||||
.pf-u-max-width {
|
||||
--pf-u-max-width--MaxWidth: 26rem;
|
||||
}
|
||||
|
||||
ul.pf-m-plain {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
button.pf-v5-c-menu-toggle {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
|
@ -1,7 +1,144 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Wizard,
|
||||
WizardFooterWrapper,
|
||||
WizardStep,
|
||||
useWizardContext,
|
||||
} from '@patternfly/react-core';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
EnvironmentStateType,
|
||||
filterEnvironment,
|
||||
hasUserSelectedAtLeastOneEnv,
|
||||
useGetAllowedTargets,
|
||||
} from './steps/ImageOutput/Environment';
|
||||
import ImageOutputStep from './steps/ImageOutput/ImageOutput';
|
||||
|
||||
import { RHEL_9, X86_64 } from '../../constants';
|
||||
import './CreateImageWizard.scss';
|
||||
import { ArchitectureItem, Distributions } from '../../store/imageBuilderApi';
|
||||
import { resolveRelPath } from '../../Utilities/path';
|
||||
import { ImageBuilderHeader } from '../sharedComponents/ImageBuilderHeader';
|
||||
|
||||
/**
|
||||
* @return true if the array in prevAllowedTargets is equivalent to the array
|
||||
* allowedTargets, false otherwise
|
||||
*/
|
||||
const isIdenticalToPrev = (
|
||||
prevAllowedTargets: string[],
|
||||
allowedTargets: string[]
|
||||
) => {
|
||||
let identicalToPrev = true;
|
||||
if (allowedTargets.length === prevAllowedTargets.length) {
|
||||
allowedTargets.forEach((elem) => {
|
||||
if (!prevAllowedTargets.includes(elem)) {
|
||||
identicalToPrev = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
identicalToPrev = false;
|
||||
}
|
||||
return identicalToPrev;
|
||||
};
|
||||
|
||||
type CustomWizardFooterPropType = {
|
||||
isNextDisabled: boolean;
|
||||
};
|
||||
/**
|
||||
* The custom wizard footer is only switching the order of the buttons compared
|
||||
* to the default wizard footer from the PF5 library.
|
||||
*/
|
||||
const CustomWizardFooter = ({ isNextDisabled }: CustomWizardFooterPropType) => {
|
||||
const { goToNextStep, goToPrevStep, close } = useWizardContext();
|
||||
return (
|
||||
<WizardFooterWrapper>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={goToNextStep}
|
||||
isDisabled={isNextDisabled}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={goToPrevStep}>
|
||||
Back
|
||||
</Button>
|
||||
<Button variant="link" onClick={close}>
|
||||
Cancel
|
||||
</Button>
|
||||
</WizardFooterWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const CreateImageWizard = () => {
|
||||
return <></>;
|
||||
const navigate = useNavigate();
|
||||
// Image output step states
|
||||
const [release, setRelease] = useState<Distributions>(RHEL_9);
|
||||
const [arch, setArch] = useState<ArchitectureItem['arch']>(X86_64);
|
||||
const {
|
||||
data: allowedTargets,
|
||||
isFetching,
|
||||
isSuccess,
|
||||
isError,
|
||||
} = useGetAllowedTargets({
|
||||
architecture: arch,
|
||||
release: release,
|
||||
});
|
||||
const [environment, setEnvironment] = useState<EnvironmentStateType>(
|
||||
filterEnvironment(
|
||||
{
|
||||
aws: { selected: false, authorized: false },
|
||||
azure: { selected: false, authorized: false },
|
||||
gcp: { selected: false, authorized: false },
|
||||
oci: { selected: false, authorized: false },
|
||||
'vsphere-ova': { selected: false, authorized: false },
|
||||
vsphere: { selected: false, authorized: false },
|
||||
'guest-image': { selected: false, authorized: false },
|
||||
'image-installer': { selected: false, authorized: false },
|
||||
wsl: { selected: false, authorized: false },
|
||||
},
|
||||
allowedTargets
|
||||
)
|
||||
);
|
||||
// Update of the environment when the architecture and release are changed.
|
||||
// This pattern prevents the usage of a useEffect See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes
|
||||
const [prevAllowedTargets, setPrevAllowedTargets] = useState(allowedTargets);
|
||||
if (!isIdenticalToPrev(prevAllowedTargets, allowedTargets)) {
|
||||
setPrevAllowedTargets(allowedTargets);
|
||||
setEnvironment(filterEnvironment(environment, allowedTargets));
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<ImageBuilderHeader />
|
||||
<section className="pf-l-page__main-section pf-c-page__main-section">
|
||||
<Wizard onClose={() => navigate(resolveRelPath(''))} isVisitRequired>
|
||||
<WizardStep
|
||||
name="Image output"
|
||||
id="step-image-output"
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
isNextDisabled={!hasUserSelectedAtLeastOneEnv(environment)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ImageOutputStep
|
||||
release={release}
|
||||
setRelease={setRelease}
|
||||
arch={arch}
|
||||
setArch={setArch}
|
||||
environment={environment}
|
||||
setEnvironment={setEnvironment}
|
||||
isFetching={isFetching}
|
||||
isError={isError}
|
||||
isSuccess={isSuccess}
|
||||
/>
|
||||
</WizardStep>
|
||||
</Wizard>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateImageWizard;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import React, { Dispatch, FormEvent, SetStateAction } from 'react';
|
||||
|
||||
import {
|
||||
FormGroup,
|
||||
FormSelect,
|
||||
FormSelectOption,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { ARCHS } from '../../../../constants';
|
||||
import { ArchitectureItem } from '../../../../store/imageBuilderApi';
|
||||
|
||||
type ArchSelectType = {
|
||||
setArch: Dispatch<SetStateAction<ArchitectureItem['arch']>>;
|
||||
arch: ArchitectureItem['arch'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Allows the user to pick the architecture to build
|
||||
*/
|
||||
const ArchSelect = ({ setArch, arch }: ArchSelectType) => {
|
||||
const onChange = (_event: FormEvent<HTMLSelectElement>, value: string) => {
|
||||
setArch(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
isRequired={true}
|
||||
label="Architecture"
|
||||
data-testid="architecture-select"
|
||||
>
|
||||
<FormSelect
|
||||
value={arch}
|
||||
onChange={onChange}
|
||||
aria-label="Architecture"
|
||||
ouiaId="arch_select"
|
||||
>
|
||||
{ARCHS.map((arch, index) => (
|
||||
<FormSelectOption key={index} value={arch} label={arch} />
|
||||
))}
|
||||
</FormSelect>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArchSelect;
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Alert, Button } from '@patternfly/react-core';
|
||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
const DeveloperProgramButton = () => {
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={'https://developers.redhat.com/about'}
|
||||
>
|
||||
Red Hat Developer Program
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const CentOSAcknowledgement = () => {
|
||||
return (
|
||||
<Alert
|
||||
variant="info"
|
||||
isPlain
|
||||
isInline
|
||||
title={
|
||||
<>
|
||||
CentOS Stream builds are intended for the development of future
|
||||
versions of RHEL and are not supported for production workloads or
|
||||
other use cases.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
Join the <DeveloperProgramButton /> to learn about paid and no-cost RHEL
|
||||
subscription options.
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
export default CentOSAcknowledgement;
|
||||
|
|
@ -0,0 +1,406 @@
|
|||
import React, { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
import {
|
||||
Checkbox,
|
||||
FormGroup,
|
||||
Popover,
|
||||
Radio,
|
||||
Text,
|
||||
TextContent,
|
||||
TextVariants,
|
||||
Tile,
|
||||
} from '@patternfly/react-core';
|
||||
import { HelpIcon } from '@patternfly/react-icons';
|
||||
|
||||
import {
|
||||
useGetArchitecturesQuery,
|
||||
Distributions,
|
||||
ArchitectureItem,
|
||||
} from '../../../../store/imageBuilderApi';
|
||||
import { provisioningApi } from '../../../../store/provisioningApi';
|
||||
import { useGetEnvironment } from '../../../../Utilities/useGetEnvironment';
|
||||
|
||||
type useGetAllowedTargetsPropType = {
|
||||
architecture: ArchitectureItem['arch'];
|
||||
release: Distributions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Contacts the backend to get a list of valid targets based on the user
|
||||
* requirements (release & architecture)
|
||||
*
|
||||
* @return an array of strings which contains the names of the authorized
|
||||
* targets. Alongside the array, a couple of flags indicate the status of the
|
||||
* request. isFetching stays true while the data are in transit. isError is set
|
||||
* to true if anything wrong happened. isSuccess is set to true otherwise.
|
||||
*
|
||||
* @param architecture the selected arch (x86_64 or aarch64)
|
||||
* @param release the selected release (see RELEASES in constants)
|
||||
*/
|
||||
export const useGetAllowedTargets = ({
|
||||
architecture,
|
||||
release,
|
||||
}: useGetAllowedTargetsPropType) => {
|
||||
const { data, isFetching, isSuccess, isError } = useGetArchitecturesQuery({
|
||||
distribution: release,
|
||||
});
|
||||
|
||||
let image_types: string[] = [];
|
||||
if (isSuccess && data) {
|
||||
data.forEach((elem) => {
|
||||
if (elem.arch === architecture) {
|
||||
image_types = elem.image_types;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
data: image_types,
|
||||
isFetching: isFetching,
|
||||
isSuccess: isSuccess,
|
||||
isError: isError,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Type to represent the state of a target.
|
||||
* A target can be selected and/or authorized. An authorized target means the
|
||||
* target can be displayed to the user, selected means the user has selected
|
||||
* the target.
|
||||
*/
|
||||
type TargetType = {
|
||||
selected: boolean;
|
||||
authorized: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines all the possible targets a user can build.
|
||||
*/
|
||||
export type EnvironmentStateType = {
|
||||
aws: TargetType;
|
||||
azure: TargetType;
|
||||
gcp: TargetType;
|
||||
oci: TargetType;
|
||||
'vsphere-ova': TargetType;
|
||||
vsphere: TargetType;
|
||||
'guest-image': TargetType;
|
||||
'image-installer': TargetType;
|
||||
wsl: TargetType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes an environment, a list of allowedTargets and updates the authorized
|
||||
* status of each targets in the environment accordingly.
|
||||
*
|
||||
* @param environment the environment to update
|
||||
* @param allowedTargets the list of targets authorized to get built
|
||||
* @return an updated environment
|
||||
*/
|
||||
export const filterEnvironment = (
|
||||
environment: EnvironmentStateType,
|
||||
allowedTargets: string[]
|
||||
) => {
|
||||
const newEnv = { ...environment };
|
||||
Object.keys(environment).forEach((target) => {
|
||||
newEnv[target as keyof EnvironmentStateType].authorized =
|
||||
allowedTargets.includes(target);
|
||||
});
|
||||
return newEnv;
|
||||
};
|
||||
|
||||
/**
|
||||
* @return true if at least one target has both its flags selected and
|
||||
* authorized set to true
|
||||
* @param env the environment to scan
|
||||
*/
|
||||
export const hasUserSelectedAtLeastOneEnv = (
|
||||
env: EnvironmentStateType
|
||||
): boolean => {
|
||||
let atLeastOne = false;
|
||||
Object.values(env).forEach(({ selected, authorized }) => {
|
||||
atLeastOne = atLeastOne || (selected && authorized);
|
||||
});
|
||||
return atLeastOne;
|
||||
};
|
||||
|
||||
type EnvironmentPropType = {
|
||||
environment: EnvironmentStateType;
|
||||
setEnvironment: Dispatch<SetStateAction<EnvironmentStateType>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays a component that allows the user to pick the target they want
|
||||
* to build on.
|
||||
*/
|
||||
const Environment = ({ setEnvironment, environment }: EnvironmentPropType) => {
|
||||
const prefetchSources = provisioningApi.usePrefetch('getSourceList');
|
||||
const { isBeta } = useGetEnvironment();
|
||||
|
||||
const handleSetEnvironment = (env: string, checked: boolean) =>
|
||||
setEnvironment((prevEnv) => {
|
||||
const newEnv: EnvironmentStateType = {
|
||||
...prevEnv,
|
||||
};
|
||||
newEnv[env as keyof EnvironmentStateType].selected = checked;
|
||||
return newEnv;
|
||||
});
|
||||
|
||||
// each item the user can select is depending on what's compatible with the
|
||||
// architecture and the distribution they previously selected. That's why
|
||||
// every sub parts are conditional to the `authorized` status of its
|
||||
// corresponding key in the state.
|
||||
return (
|
||||
<FormGroup
|
||||
isRequired={true}
|
||||
label="Select target environments"
|
||||
data-testid="target-select"
|
||||
>
|
||||
<FormGroup
|
||||
label={<Text component={TextVariants.small}>Public cloud</Text>}
|
||||
data-testid="target-public"
|
||||
>
|
||||
<div className="tiles">
|
||||
{environment['aws'].authorized && (
|
||||
<Tile
|
||||
className="tile pf-u-mr-sm"
|
||||
data-testid="upload-aws"
|
||||
title="Amazon Web Services"
|
||||
icon={
|
||||
<img
|
||||
className="provider-icon"
|
||||
src={'/apps/frontend-assets/partners-icons/aws.svg'}
|
||||
alt="Amazon Web Services logo"
|
||||
/>
|
||||
}
|
||||
onClick={() =>
|
||||
handleSetEnvironment('aws', !environment.aws.selected)
|
||||
}
|
||||
onMouseEnter={() => prefetchSources({ provider: 'aws' })}
|
||||
isSelected={environment.aws.selected}
|
||||
isStacked
|
||||
isDisplayLarge
|
||||
/>
|
||||
)}
|
||||
{environment['gcp'].authorized && (
|
||||
<Tile
|
||||
className="tile pf-u-mr-sm"
|
||||
data-testid="upload-google"
|
||||
title="Google Cloud Platform"
|
||||
icon={
|
||||
<img
|
||||
className="provider-icon"
|
||||
src={
|
||||
'/apps/frontend-assets/partners-icons/google-cloud-short.svg'
|
||||
}
|
||||
alt="Google Cloud Platform logo"
|
||||
/>
|
||||
}
|
||||
onClick={() =>
|
||||
handleSetEnvironment('gcp', !environment.gcp.selected)
|
||||
}
|
||||
isSelected={environment.gcp.selected}
|
||||
onMouseEnter={() => prefetchSources({ provider: 'gcp' })}
|
||||
isStacked
|
||||
isDisplayLarge
|
||||
/>
|
||||
)}
|
||||
{environment['azure'].authorized && (
|
||||
<Tile
|
||||
className="tile pf-u-mr-sm"
|
||||
data-testid="upload-azure"
|
||||
title="Microsoft Azure"
|
||||
icon={
|
||||
<img
|
||||
className="provider-icon"
|
||||
src={
|
||||
'/apps/frontend-assets/partners-icons/microsoft-azure-short.svg'
|
||||
}
|
||||
alt="Microsoft Azure logo"
|
||||
/>
|
||||
}
|
||||
onClick={() =>
|
||||
handleSetEnvironment('azure', !environment.azure.selected)
|
||||
}
|
||||
onMouseEnter={() => prefetchSources({ provider: 'azure' })}
|
||||
isSelected={environment.azure.selected}
|
||||
isStacked
|
||||
isDisplayLarge
|
||||
/>
|
||||
)}
|
||||
{environment.oci.authorized && isBeta() && (
|
||||
<Tile
|
||||
className="tile pf-u-mr-sm"
|
||||
data-testid="upload-oci"
|
||||
title="Oracle Cloud Infrastructure"
|
||||
icon={
|
||||
<img
|
||||
className="provider-icon"
|
||||
src={'/apps/frontend-assets/partners-icons/oracle-short.svg'}
|
||||
alt="Oracle Cloud Infrastructure logo"
|
||||
/>
|
||||
}
|
||||
onClick={() =>
|
||||
handleSetEnvironment('oci', !environment.oci.selected)
|
||||
}
|
||||
isSelected={environment.oci.selected}
|
||||
isStacked
|
||||
isDisplayLarge
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</FormGroup>
|
||||
{environment['vsphere'].authorized && (
|
||||
<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.selected ||
|
||||
environment['vsphere-ova'].selected
|
||||
}
|
||||
onChange={(_event, checked) => {
|
||||
handleSetEnvironment('vsphere-ova', checked);
|
||||
handleSetEnvironment('vsphere', false);
|
||||
}}
|
||||
aria-label="VMWare checkbox"
|
||||
id="checkbox-vmware"
|
||||
name="VMWare"
|
||||
data-testid="checkbox-vmware"
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
{environment['vsphere'].authorized && (
|
||||
<FormGroup
|
||||
className="pf-u-mt-sm pf-u-mb-sm pf-u-ml-xl"
|
||||
data-testid="target-private-vsphere-radio"
|
||||
>
|
||||
{environment['vsphere-ova'].authorized && (
|
||||
<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={(_event, checked) => {
|
||||
handleSetEnvironment('vsphere-ova', checked);
|
||||
handleSetEnvironment('vsphere', !checked);
|
||||
}}
|
||||
isChecked={environment['vsphere-ova'].selected}
|
||||
isDisabled={
|
||||
!(
|
||||
environment.vsphere.selected ||
|
||||
environment['vsphere-ova'].selected
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<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.selected}
|
||||
isDisabled={
|
||||
!(
|
||||
environment.vsphere.selected ||
|
||||
environment['vsphere-ova'].selected
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
<FormGroup
|
||||
label={<Text component={TextVariants.small}>Other</Text>}
|
||||
data-testid="target-other"
|
||||
>
|
||||
{environment['guest-image'].authorized && (
|
||||
<Checkbox
|
||||
label="Virtualization - Guest image (.qcow2)"
|
||||
isChecked={environment['guest-image'].selected}
|
||||
onChange={(_event, checked) =>
|
||||
handleSetEnvironment('guest-image', checked)
|
||||
}
|
||||
aria-label="Virtualization guest image checkbox"
|
||||
id="checkbox-guest-image"
|
||||
name="Virtualization guest image"
|
||||
data-testid="checkbox-guest-image"
|
||||
/>
|
||||
)}
|
||||
{environment['image-installer'].authorized && (
|
||||
<Checkbox
|
||||
label="Bare metal - Installer (.iso)"
|
||||
isChecked={environment['image-installer'].selected}
|
||||
onChange={(_event, checked) =>
|
||||
handleSetEnvironment('image-installer', checked)
|
||||
}
|
||||
aria-label="Bare metal installer checkbox"
|
||||
id="checkbox-image-installer"
|
||||
name="Bare metal installer"
|
||||
data-testid="checkbox-image-installer"
|
||||
/>
|
||||
)}
|
||||
{environment['wsl'].authorized && isBeta() && (
|
||||
<Checkbox
|
||||
label="WSL - Windows Subsystem for Linux (.tar.gz)"
|
||||
isChecked={environment['wsl'].selected}
|
||||
onChange={(_event, checked) => handleSetEnvironment('wsl', checked)}
|
||||
aria-label="windows subsystem for linux checkbox"
|
||||
id="checkbox-wsl"
|
||||
name="WSL"
|
||||
data-testid="checkbox-wsl"
|
||||
/>
|
||||
)}
|
||||
</FormGroup>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default Environment;
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import React, { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
import {
|
||||
Text,
|
||||
Alert,
|
||||
Bullseye,
|
||||
Form,
|
||||
Spinner,
|
||||
Title,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import ArchSelect from './ArchSelect';
|
||||
import CentOSAcknowledgement from './CentOSAcknowledgement';
|
||||
import Environment, { EnvironmentStateType } from './Environment';
|
||||
import ReleaseSelect from './ReleaseSelect';
|
||||
|
||||
import {
|
||||
ArchitectureItem,
|
||||
Distributions,
|
||||
} from '../../../../store/imageBuilderApi';
|
||||
import DocumentationButton from '../../../sharedComponents/DocumentationButton';
|
||||
|
||||
type ImageOutputPropTypes = {
|
||||
release: Distributions;
|
||||
setRelease: Dispatch<SetStateAction<Distributions>>;
|
||||
arch: ArchitectureItem['arch'];
|
||||
setArch: Dispatch<SetStateAction<ArchitectureItem['arch']>>;
|
||||
environment: EnvironmentStateType;
|
||||
setEnvironment: Dispatch<SetStateAction<EnvironmentStateType>>;
|
||||
isFetching: boolean;
|
||||
isError: boolean;
|
||||
isSuccess: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Manages the form for the image output step by providing the user with a
|
||||
* choice for:
|
||||
* - a distribution
|
||||
* - a release
|
||||
* - a set of environments
|
||||
*/
|
||||
const ImageOutputStep = ({
|
||||
release,
|
||||
setRelease,
|
||||
arch,
|
||||
setArch,
|
||||
setEnvironment,
|
||||
environment,
|
||||
isFetching,
|
||||
isError,
|
||||
isSuccess,
|
||||
}: ImageOutputPropTypes) => {
|
||||
return (
|
||||
<Form>
|
||||
<Title headingLevel="h2">Image output</Title>
|
||||
<Text>
|
||||
Image builder allows you to create a custom image and push it to target
|
||||
environments.
|
||||
<br />
|
||||
<DocumentationButton />
|
||||
</Text>
|
||||
<ReleaseSelect setRelease={setRelease} release={release} />
|
||||
<ArchSelect setArch={setArch} arch={arch} />
|
||||
{release.match('centos-*') && <CentOSAcknowledgement />}
|
||||
{isFetching && (
|
||||
<Bullseye>
|
||||
<Spinner size="lg" />
|
||||
</Bullseye>
|
||||
)}
|
||||
{isError && (
|
||||
<Alert
|
||||
variant={'danger'}
|
||||
isPlain
|
||||
isInline
|
||||
title={'Environments unavailable'}
|
||||
>
|
||||
API cannot be reached, try again later.
|
||||
</Alert>
|
||||
)}
|
||||
{isSuccess && !isFetching && (
|
||||
<Environment
|
||||
setEnvironment={setEnvironment}
|
||||
environment={environment}
|
||||
/>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageOutputStep;
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
import React, {
|
||||
Dispatch,
|
||||
ReactElement,
|
||||
SetStateAction,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectList,
|
||||
MenuToggle,
|
||||
FormGroup,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { RELEASES } from '../../../../constants';
|
||||
import { Distributions } from '../../../../store/imageBuilderApi';
|
||||
import isRhel from '../../../../Utilities/isRhel';
|
||||
|
||||
type ReleaseSelectType = {
|
||||
setRelease: Dispatch<SetStateAction<Distributions>>;
|
||||
release: Distributions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Allows the user to choose the release they want to build.
|
||||
* Follows the PF5 pattern: https://www.patternfly.org/components/menus/select#view-more
|
||||
*/
|
||||
const ReleaseSelect = ({ setRelease, release }: ReleaseSelectType) => {
|
||||
// By default the component doesn't show the Centos releases and only the RHEL
|
||||
// ones. The user has the option to click on a button to make them appear.
|
||||
const [showDevelopmentOptions, setShowDevelopmentOptions] = useState(false);
|
||||
const releaseOptions = () => {
|
||||
const options: ReactElement[] = [];
|
||||
const filteredRhel = new Map<string, string>();
|
||||
RELEASES.forEach((value, key) => {
|
||||
// Only show non-RHEL distros if expanded
|
||||
if (showDevelopmentOptions || isRhel(key)) {
|
||||
filteredRhel.set(key, value);
|
||||
}
|
||||
});
|
||||
filteredRhel.forEach((value, key) => {
|
||||
if (value && key) {
|
||||
options.push(
|
||||
<SelectOption key={value} value={key} label={key}>
|
||||
{RELEASES.get(key)}
|
||||
</SelectOption>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const onToggleClick = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const viewMoreRef = useRef<HTMLLIElement>(null);
|
||||
const toggleRef = useRef<HTMLButtonElement>(null);
|
||||
const onSelect = (
|
||||
_event: React.MouseEvent<Element, MouseEvent> | undefined,
|
||||
value: string | number | undefined
|
||||
) => {
|
||||
if (value !== 'loader') {
|
||||
if (typeof value === 'string') {
|
||||
setRelease(value as Distributions);
|
||||
}
|
||||
setIsOpen(false);
|
||||
toggleRef?.current?.focus(); // Only focus the toggle when a non-loader option is selected
|
||||
}
|
||||
};
|
||||
|
||||
const toggle = (
|
||||
<MenuToggle
|
||||
ref={toggleRef}
|
||||
onClick={onToggleClick}
|
||||
isExpanded={isOpen}
|
||||
isFullWidth
|
||||
>
|
||||
{RELEASES.get(release)}
|
||||
</MenuToggle>
|
||||
);
|
||||
|
||||
return (
|
||||
<FormGroup isRequired={true} label="Release" data-testid="release-select">
|
||||
<Select
|
||||
ouiaId="release_select"
|
||||
id="release_select"
|
||||
isOpen={isOpen}
|
||||
selected={release}
|
||||
onSelect={onSelect}
|
||||
onOpenChange={(isOpen) => setIsOpen(isOpen)}
|
||||
toggle={{ toggleNode: toggle, toggleRef }}
|
||||
>
|
||||
<SelectList>
|
||||
{releaseOptions()}
|
||||
<SelectOption
|
||||
{...(!showDevelopmentOptions && { isLoadButton: true })}
|
||||
onClick={() => setShowDevelopmentOptions(true)}
|
||||
value="loader"
|
||||
ref={viewMoreRef}
|
||||
>
|
||||
{!showDevelopmentOptions
|
||||
? 'Show options for further development of RHEL'
|
||||
: undefined}
|
||||
</SelectOption>
|
||||
</SelectList>
|
||||
</Select>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReleaseSelect;
|
||||
Loading…
Add table
Add a link
Reference in a new issue