V2Wizard: Copy components needed for image output step

The components for the architecture select, centos acknowledgement,
release select, and target environement have been copied and converted
to Typescript files. When these files are modified in future commits,
the result will be a clean diff where all changes made are obvious.
This commit is contained in:
lucasgarfield 2024-01-05 18:27:09 +01:00 committed by Sanne Raymaekers
parent bb5ff08e2c
commit b446f1d1e4
5 changed files with 796 additions and 0 deletions

View file

@ -0,0 +1,64 @@
import React, { useState } from 'react';
import { FormSpy } from '@data-driven-forms/react-form-renderer';
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 { FormGroup } from '@patternfly/react-core';
import {
Select,
SelectOption,
SelectVariant,
} from '@patternfly/react-core/deprecated';
import PropTypes from 'prop-types';
import { ARCHS } from '../../../constants';
const ArchSelect = ({ label, isRequired, ...props }) => {
const { change, getState } = useFormApi();
const { input } = useFieldApi(props);
const [isOpen, setIsOpen] = useState(false);
const setArch = (_, selection) => {
change(input.name, selection);
setIsOpen(false);
};
const setSelectOptions = () => {
var options = [];
ARCHS.forEach((arch) => {
options.push(
<SelectOption key={arch} value={arch}>
{arch}
</SelectOption>
);
});
return options;
};
return (
<FormSpy>
{() => (
<FormGroup isRequired={isRequired} label={label}>
<Select
ouiaId="arch_select"
variant={SelectVariant.single}
onToggle={() => setIsOpen(!isOpen)}
onSelect={setArch}
selections={getState()?.values?.[input.name]}
isOpen={isOpen}
>
{setSelectOptions()}
</Select>
</FormGroup>
)}
</FormSpy>
);
};
ArchSelect.propTypes = {
label: PropTypes.node,
isRequired: PropTypes.bool,
};
export default ArchSelect;

View file

@ -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;

View file

@ -0,0 +1,184 @@
import React, { useContext } from 'react';
import { useFormApi } from '@data-driven-forms/react-form-renderer';
import WizardContext from '@data-driven-forms/react-form-renderer/wizard-context';
import {
Button,
ExpandableSection,
FormGroup,
Panel,
PanelMain,
Text,
} from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import { Chart, registerables } from 'chart.js';
import annotationPlugin from 'chartjs-plugin-annotation';
import { Bar } from 'react-chartjs-2';
import {
RELEASES,
RHEL_8,
RHEL_8_FULL_SUPPORT,
RHEL_8_MAINTENANCE_SUPPORT,
RHEL_9,
RHEL_9_FULL_SUPPORT,
RHEL_9_MAINTENANCE_SUPPORT,
} from '../../../constants';
import 'chartjs-adapter-moment';
import { toMonthAndYear } from '../../../Utilities/time';
Chart.register(annotationPlugin);
Chart.register(...registerables);
const currentDate = new Date().toString();
export const chartMajorVersionCfg = {
data: {
labels: ['RHEL 9', 'RHEL 8'],
datasets: [
{
label: 'Full support',
backgroundColor: '#0066CC',
data: [
{
x: RHEL_9_FULL_SUPPORT,
y: 'RHEL 9',
},
{
x: RHEL_8_FULL_SUPPORT,
y: 'RHEL 8',
},
],
},
{
label: 'Maintenance support',
backgroundColor: '#8BC1F7',
data: [
{
x: RHEL_9_MAINTENANCE_SUPPORT,
y: 'RHEL 9',
},
{
x: RHEL_8_MAINTENANCE_SUPPORT,
y: 'RHEL 8',
},
],
},
],
},
options: {
indexAxis: 'y' as const,
scales: {
x: {
type: 'time' as const,
time: {
unit: 'year' as const,
},
min: '2019-01-01' as const,
max: '2033-01-01' as const,
},
y: {
stacked: true,
},
},
responsive: true,
maintainAspectRatio: true,
aspectRatio: 1 | 5,
plugins: {
tooltip: {
enabled: false,
},
legend: {
position: 'bottom' as const,
},
annotation: {
annotations: {
today: {
type: 'line' as const,
xMin: currentDate,
xMax: currentDate,
borderColor: 'black',
borderWidth: 2,
borderDash: [8, 2],
},
},
},
},
},
};
const MajorReleasesLifecyclesChart = () => {
return (
<Panel>
<PanelMain maxHeight="10rem">
<Bar
data-testid="release-lifecycle-chart"
options={chartMajorVersionCfg.options}
data={chartMajorVersionCfg.data}
/>
</PanelMain>
</Panel>
);
};
const ReleaseLifecycle = () => {
const { getState } = useFormApi();
const { currentStep } = useContext(WizardContext);
const release = getState().values.release;
const [isExpanded, setIsExpanded] = React.useState(true);
const onToggle = (_event: React.MouseEvent, isExpanded: boolean) => {
setIsExpanded(isExpanded);
};
if (release === RHEL_8) {
if (currentStep.name === 'image-output') {
return (
<ExpandableSection
toggleText={
isExpanded
? 'Hide information about release lifecycle'
: 'Show information about release lifecycle'
}
onToggle={onToggle}
isExpanded={isExpanded}
isIndented
>
<FormGroup label="Release lifecycle">
<MajorReleasesLifecyclesChart />
</FormGroup>
<br />
<Button
component="a"
target="_blank"
variant="link"
icon={<ExternalLinkAltIcon />}
iconPosition="right"
isInline
href={'https://access.redhat.com/support/policy/updates/errata'}
>
View Red Hat Enterprise Linux Life Cycle dates
</Button>
</ExpandableSection>
);
} else if (currentStep.name === 'review') {
return (
<>
<Text className="pf-v5-u-font-size-sm">
{RELEASES.get(release)} will be supported through{' '}
{toMonthAndYear(RHEL_8_FULL_SUPPORT[0])}, with optional ELS support
through {toMonthAndYear(RHEL_8_MAINTENANCE_SUPPORT[0])}. Consider
building an image with {RELEASES.get(RHEL_9)} to extend the support
period.
</Text>
<FormGroup label="Release lifecycle">
<MajorReleasesLifecyclesChart />
</FormGroup>
<br />
</>
);
}
}
};
export default ReleaseLifecycle;

View file

@ -0,0 +1,114 @@
import React, { useState } from 'react';
import { FormSpy } from '@data-driven-forms/react-form-renderer';
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 { FormGroup } from '@patternfly/react-core';
import {
Select,
SelectOption,
SelectVariant,
} from '@patternfly/react-core/deprecated';
import PropTypes from 'prop-types';
import {
RELEASES,
RHEL_8,
RHEL_8_FULL_SUPPORT,
RHEL_8_MAINTENANCE_SUPPORT,
RHEL_9,
RHEL_9_FULL_SUPPORT,
RHEL_9_MAINTENANCE_SUPPORT,
} from '../../../constants';
import isRhel from '../../../Utilities/isRhel';
import { toMonthAndYear } from '../../../Utilities/time';
const ImageOutputReleaseSelect = ({ label, isRequired, ...props }) => {
const { change, getState } = useFormApi();
const { input } = useFieldApi(props);
const [isOpen, setIsOpen] = useState(false);
const [showDevelopmentOptions, setShowDevelopmentOptions] = useState(false);
const setRelease = (_, selection) => {
change(input.name, selection);
setIsOpen(false);
};
const handleExpand = () => {
setShowDevelopmentOptions(true);
};
const setDescription = (key) => {
let fullSupportEnd = '';
let maintenanceSupportEnd = '';
if (key === RHEL_8) {
fullSupportEnd = toMonthAndYear(RHEL_8_FULL_SUPPORT[1]);
maintenanceSupportEnd = toMonthAndYear(RHEL_8_MAINTENANCE_SUPPORT[1]);
}
if (key === RHEL_9) {
fullSupportEnd = toMonthAndYear(RHEL_9_FULL_SUPPORT[1]);
maintenanceSupportEnd = toMonthAndYear(RHEL_9_MAINTENANCE_SUPPORT[1]);
}
if (isRhel(key)) {
return `Full support ends: ${fullSupportEnd} | Maintenance support ends: ${maintenanceSupportEnd}`;
}
};
const setSelectOptions = () => {
var options = [];
const filteredRhel = new Map(
[...RELEASES].filter(([key]) => {
// Only show non-RHEL distros if expanded
if (showDevelopmentOptions) {
return true;
}
return isRhel(key);
})
);
filteredRhel.forEach((value, key) => {
options.push(
<SelectOption key={value} value={key} description={setDescription(key)}>
{RELEASES.get(key)}
</SelectOption>
);
});
return options;
};
return (
<FormSpy>
{() => (
<FormGroup isRequired={isRequired} label={label}>
<Select
ouiaId="release_select"
variant={SelectVariant.single}
onToggle={() => setIsOpen(!isOpen)}
onSelect={setRelease}
selections={RELEASES.get(getState()?.values?.[input.name])}
isOpen={isOpen}
{...(!showDevelopmentOptions && {
loadingVariant: {
text: 'Show options for further development of RHEL',
onClick: handleExpand,
},
})}
>
{setSelectOptions()}
</Select>
</FormGroup>
)}
</FormSpy>
);
};
ImageOutputReleaseSelect.propTypes = {
label: PropTypes.node,
isRequired: PropTypes.bool,
};
export default ImageOutputReleaseSelect;

View file

@ -0,0 +1,390 @@
import React, { useEffect, 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';
const useGetAllowedTargets = ({ architecture, release }) => {
const { data, isFetching, isSuccess, isError } = useGetArchitecturesQuery({
distribution: release,
});
let image_types = [];
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,
};
};
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 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 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);
}
};
// 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}
data-testid="target-select"
>
<FormGroup
label={<Text component={TextVariants.small}>Public cloud</Text>}
data-testid="target-public"
>
<div className="tiles">
{allowedTargets.includes('aws') && (
<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)}
onKeyDown={(e) => handleKeyDown(e, 'aws', !environment.aws)}
onMouseEnter={() => prefetchSources({ provider: 'aws' })}
isSelected={environment.aws}
isStacked
isDisplayLarge
/>
)}
{allowedTargets.includes('gcp') && (
<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)}
isSelected={environment.gcp}
onKeyDown={(e) => handleKeyDown(e, 'gcp', !environment.gcp)}
onMouseEnter={() => prefetchSources({ provider: 'gcp' })}
isStacked
isDisplayLarge
/>
)}
{allowedTargets.includes('azure') && (
<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)}
onKeyDown={(e) => handleKeyDown(e, 'azure', !environment.azure)}
onMouseEnter={() => prefetchSources({ provider: 'azure' })}
isSelected={environment.azure}
isStacked
isDisplayLarge
/>
)}
{allowedTargets.includes('oci') && (
<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)}
onKeyDown={(e) => handleKeyDown(e, 'oci', !environment.oci)}
isSelected={environment.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') && (
<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']}
isDisabled={!(environment.vsphere || environment['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
label={<Text component={TextVariants.small}>Other</Text>}
data-testid="target-other"
>
{allowedTargets.includes('guest-image') && (
<Checkbox
label="Virtualization - Guest image (.qcow2)"
isChecked={environment['guest-image']}
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"
/>
)}
{allowedTargets.includes('image-installer') && (
<Checkbox
label="Bare metal - Installer (.iso)"
isChecked={environment['image-installer']}
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"
/>
)}
{allowedTargets.includes('wsl') && isBeta() && (
<Checkbox
label="WSL - Windows Subsystem for Linux (.tar.gz)"
isChecked={environment['wsl']}
onChange={(_event, checked) => handleSetEnvironment('wsl', checked)}
aria-label="windows subsystem for linux checkbox"
id="checkbox-wsl"
name="WSL"
data-testid="checkbox-wsl"
/>
)}
</FormGroup>
</FormGroup>
);
};
TargetEnvironment.propTypes = {
label: PropTypes.node,
isRequired: PropTypes.bool,
};
TargetEnvironment.defaultProps = {
label: '',
isRequired: false,
};
export default TargetEnvironment;