V2 Wizard: Add File System Configuration Step (HMS-2781)
The FSC step is added to the wizard and takes full advantage of Redux for state management. This is still a work in progress. Supported features: 1. Select partition mountpoint prefix (e.g. /var, /home) 2. Edit partition mountpoint suffix (e.g. /home/videogames) 3. Change displayed units (KiB, MiB, GiB) Supported but buggy features: 1. Edit partition size Unsupported features: 1. Add partitions 2. Remove partitions 3. Validation
This commit is contained in:
parent
d063279b79
commit
430ea83df0
20 changed files with 751 additions and 111 deletions
7
package-lock.json
generated
7
package-lock.json
generated
|
|
@ -48,6 +48,7 @@
|
|||
"@types/react": "18.2.64",
|
||||
"@types/react-dom": "18.2.21",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "7.1.0",
|
||||
"@typescript-eslint/parser": "7.1.1",
|
||||
"babel-jest": "29.7.0",
|
||||
|
|
@ -4861,6 +4862,12 @@
|
|||
"version": "0.0.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/webpack": {
|
||||
"version": "4.41.34",
|
||||
"dev": true,
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@
|
|||
"@types/react": "18.2.64",
|
||||
"@types/react-dom": "18.2.21",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "7.1.0",
|
||||
"@typescript-eslint/parser": "7.1.1",
|
||||
"babel-jest": "29.7.0",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import DetailsStep from './steps/Details';
|
||||
import FileSystemStep from './steps/FileSystem';
|
||||
import ImageOutputStep from './steps/ImageOutput';
|
||||
import OscapStep from './steps/Oscap';
|
||||
import PackagesStep from './steps/Packages';
|
||||
|
|
@ -131,7 +132,6 @@ const CreateImageWizard = ({ startStepIndex = 1 }: CreateImageWizardProps) => {
|
|||
selectAzureResourceGroup(state)
|
||||
);
|
||||
const azureSource = useAppSelector((state) => selectAzureSource(state));
|
||||
|
||||
const registrationType = useAppSelector((state) =>
|
||||
selectRegistrationType(state)
|
||||
);
|
||||
|
|
@ -249,6 +249,13 @@ const CreateImageWizard = ({ startStepIndex = 1 }: CreateImageWizardProps) => {
|
|||
>
|
||||
<OscapStep />
|
||||
</WizardStep>
|
||||
<WizardStep
|
||||
name="File system configuration"
|
||||
id="step-file-system"
|
||||
footer={<CustomWizardFooter disableNext={false} />}
|
||||
>
|
||||
<FileSystemStep />
|
||||
</WizardStep>
|
||||
<WizardStep
|
||||
name="Content"
|
||||
id="step-content"
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ const EditImageWizard = ({ blueprintId }: EditImageWizardProps) => {
|
|||
navigate(resolveRelPath(''));
|
||||
}
|
||||
}, [error, navigate]);
|
||||
return <CreateImageWizard startStepIndex={12} />;
|
||||
return <CreateImageWizard startStepIndex={13} />;
|
||||
};
|
||||
|
||||
export default EditImageWizard;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Alert } from '@patternfly/react-core';
|
||||
|
||||
const UsrSubDirectoriesDisabled = () => {
|
||||
return (
|
||||
<Alert
|
||||
variant="warning"
|
||||
title="Sub-directories for the /usr mount point are no longer supported"
|
||||
isInline
|
||||
>
|
||||
Please note that including sub-directories in the /usr path is no longer
|
||||
supported. Previously included mount points with /usr sub-directory are
|
||||
replaced by /usr when recreating an image.
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsrSubDirectoriesDisabled;
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import React from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Text,
|
||||
TextContent,
|
||||
TextVariants,
|
||||
} from '@patternfly/react-core';
|
||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
const FileSystemAutomaticPartition = () => {
|
||||
return (
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>Automatic partitioning</Text>
|
||||
<Text>
|
||||
Red Hat will automatically partition your image to what is best,
|
||||
depending on the target environment(s).
|
||||
</Text>
|
||||
<Text>
|
||||
The target environment sometimes dictates the partitioning scheme or
|
||||
parts of it, and sometimes the target environment is unknown (e.g., for
|
||||
the .qcow2 generic cloud image).
|
||||
</Text>
|
||||
<Text>
|
||||
Using automatic partitioning will apply the most current supported
|
||||
configuration.
|
||||
<br></br>
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
href="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/creating_customized_images_by_using_insights_image_builder/customizing-file-systems-during-the-image-creation"
|
||||
className="pf-u-pl-0"
|
||||
>
|
||||
Customizing file systems during the image creation
|
||||
</Button>
|
||||
</Text>
|
||||
</TextContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileSystemAutomaticPartition;
|
||||
|
|
@ -0,0 +1,329 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Popover,
|
||||
Text,
|
||||
TextContent,
|
||||
TextInput,
|
||||
TextVariants,
|
||||
} from '@patternfly/react-core';
|
||||
import { Select, SelectOption } from '@patternfly/react-core/deprecated';
|
||||
import {
|
||||
HelpIcon,
|
||||
MinusCircleIcon,
|
||||
PlusCircleIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
|
||||
|
||||
import { UNIT_GIB, UNIT_KIB, UNIT_MIB } from '../../../../constants';
|
||||
import { useAppDispatch, useAppSelector } from '../../../../store/hooks';
|
||||
import {
|
||||
changePartitionMinSize,
|
||||
changePartitionMountpoint,
|
||||
selectPartitions,
|
||||
} from '../../../../store/wizardSlice';
|
||||
import UsrSubDirectoriesDisabled from '../../UsrSubDirectoriesDisabled';
|
||||
import { ValidatedTextInput } from '../../ValidatedTextInput';
|
||||
|
||||
export type Partition = {
|
||||
id: string;
|
||||
mountpoint: string;
|
||||
min_size: string;
|
||||
};
|
||||
|
||||
const FileSystemConfiguration = () => {
|
||||
const partitions = useAppSelector((state) => selectPartitions(state));
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h3}>Configure partitions</Text>
|
||||
</TextContent>
|
||||
{partitions?.find((partition) =>
|
||||
partition?.mountpoint?.includes('/usr')
|
||||
) && <UsrSubDirectoriesDisabled />}
|
||||
<TextContent>
|
||||
<Text>
|
||||
Create partitions for your image by defining mount points and minimum
|
||||
sizes. Image builder creates partitions with a logical volume (LVM)
|
||||
device type.
|
||||
</Text>
|
||||
<Text>
|
||||
The order of partitions may change when the image is installed in
|
||||
order to conform to best practices and ensure functionality.
|
||||
<br></br>
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
href="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/creating_customized_images_by_using_insights_image_builder/customizing-file-systems-during-the-image-creation"
|
||||
className="pf-u-pl-0"
|
||||
>
|
||||
Read more about manual configuration here
|
||||
</Button>
|
||||
</Text>
|
||||
</TextContent>
|
||||
<Table aria-label="File system table" variant="compact">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th />
|
||||
<Th>Mount point</Th>
|
||||
<Th></Th>
|
||||
<Th>Type</Th>
|
||||
<Th>
|
||||
Minimum size
|
||||
<Popover
|
||||
hasAutoWidth
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<Text>
|
||||
Image Builder may extend this size based on requirements,
|
||||
selected packages, and configurations.
|
||||
</Text>
|
||||
</TextContent>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label="File system configuration info"
|
||||
aria-describedby="file-system-configuration-info"
|
||||
className="pf-c-form__group-label-help"
|
||||
>
|
||||
<HelpIcon />
|
||||
</Button>
|
||||
</Popover>
|
||||
</Th>
|
||||
<Th />
|
||||
<Th />
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody data-testid="file-system-configuration-tbody">
|
||||
{partitions &&
|
||||
partitions.map((partition) => (
|
||||
<Row key={partition.id} partition={partition} />
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
<TextContent>
|
||||
<Button
|
||||
ouiaId="add-partition"
|
||||
data-testid="file-system-add-partition"
|
||||
className="pf-u-text-align-left"
|
||||
variant="link"
|
||||
icon={<PlusCircleIcon />}
|
||||
onClick={() => {}}
|
||||
>
|
||||
Add partition
|
||||
</Button>
|
||||
</TextContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type RowPropTypes = {
|
||||
partition: Partition;
|
||||
};
|
||||
|
||||
const getPrefix = (mountpoint: string) => {
|
||||
return mountpoint.split('/')[1] ? '/' + mountpoint.split('/')[1] : '/';
|
||||
};
|
||||
const getSuffix = (mountpoint: string) => {
|
||||
const prefix = getPrefix(mountpoint);
|
||||
return mountpoint.substring(prefix.length);
|
||||
};
|
||||
|
||||
const Row = ({ partition }: RowPropTypes) => {
|
||||
const [units, setUnits] = useState<Units>('MiB');
|
||||
|
||||
return (
|
||||
<Tr>
|
||||
<Td />
|
||||
<Td className="pf-m-width-15">
|
||||
<MountpointPrefix partition={partition} />
|
||||
</Td>
|
||||
<Td className="pf-m-width-15">
|
||||
<MountpointSuffix partition={partition} />
|
||||
</Td>
|
||||
<Td className="pf-m-width-20">xfs</Td>
|
||||
<Td className="pf-m-width-30">
|
||||
<MinimumSize partition={partition} units={units} />
|
||||
</Td>
|
||||
<Td className="pf-m-width-30">
|
||||
<SizeUnit units={units} setUnits={setUnits} />
|
||||
</Td>
|
||||
<Td className="pf-m-width-10">
|
||||
<Button
|
||||
variant="link"
|
||||
icon={<MinusCircleIcon />}
|
||||
onClick={() => {}}
|
||||
data-testid="remove-mount-point"
|
||||
isDisabled={true}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
|
||||
export const mountpointPrefixes = [
|
||||
'/app',
|
||||
'/boot',
|
||||
'/data',
|
||||
'/home',
|
||||
'/opt',
|
||||
'/srv',
|
||||
'/tmp',
|
||||
'/usr',
|
||||
'/var',
|
||||
];
|
||||
|
||||
type MountpointPrefixPropTypes = {
|
||||
partition: Partition;
|
||||
};
|
||||
|
||||
const MountpointPrefix = ({ partition }: MountpointPrefixPropTypes) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const prefix = getPrefix(partition.mountpoint);
|
||||
const suffix = getSuffix(partition.mountpoint);
|
||||
|
||||
const onToggle = (isOpen: boolean) => {
|
||||
setIsOpen(isOpen);
|
||||
};
|
||||
|
||||
const onSelect = (event: React.MouseEvent, selection: string) => {
|
||||
setIsOpen(false);
|
||||
const mountpoint = selection + suffix;
|
||||
dispatch(
|
||||
changePartitionMountpoint({ id: partition.id, mountpoint: mountpoint })
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
ouiaId="mount-point"
|
||||
isOpen={isOpen}
|
||||
onToggle={(_event, isOpen) => onToggle(isOpen)}
|
||||
onSelect={onSelect}
|
||||
selections={prefix}
|
||||
>
|
||||
{mountpointPrefixes.map((prefix, index) => {
|
||||
return <SelectOption key={index} value={prefix} />;
|
||||
})}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
type MountpointSuffixPropTypes = {
|
||||
partition: Partition;
|
||||
};
|
||||
|
||||
const MountpointSuffix = ({ partition }: MountpointSuffixPropTypes) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const prefix = getPrefix(partition.mountpoint);
|
||||
const suffix = getSuffix(partition.mountpoint);
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
value={suffix}
|
||||
type="text"
|
||||
onChange={(event: React.FormEvent, suffix) => {
|
||||
const mountpoint = prefix + suffix;
|
||||
dispatch(
|
||||
changePartitionMountpoint({
|
||||
id: partition.id,
|
||||
mountpoint: mountpoint,
|
||||
})
|
||||
);
|
||||
}}
|
||||
aria-label="text input example"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type MinimumSizePropTypes = {
|
||||
partition: Partition;
|
||||
units: Units;
|
||||
};
|
||||
|
||||
type Units = 'KiB' | 'MiB' | 'GiB';
|
||||
|
||||
const getConversionFactor = (units: Units) => {
|
||||
switch (units) {
|
||||
case 'KiB':
|
||||
return UNIT_KIB;
|
||||
case 'MiB':
|
||||
return UNIT_MIB;
|
||||
case 'GiB':
|
||||
return UNIT_GIB;
|
||||
}
|
||||
};
|
||||
|
||||
const MinimumSize = ({ partition, units }: MinimumSizePropTypes) => {
|
||||
const conversionFactor = getConversionFactor(units);
|
||||
|
||||
const convertToDisplayUnits = (minSize: string) => {
|
||||
return (parseInt(minSize) * conversionFactor).toString();
|
||||
};
|
||||
|
||||
const convertToBytes = (minSize: string) => {
|
||||
return (parseInt(minSize) / conversionFactor).toString();
|
||||
};
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
return (
|
||||
<ValidatedTextInput
|
||||
ariaLabel="minimum partition size"
|
||||
helperText=""
|
||||
validator={() => true}
|
||||
value={convertToDisplayUnits(partition.min_size)}
|
||||
type="text"
|
||||
onChange={(event, minSize) => {
|
||||
dispatch(
|
||||
changePartitionMinSize({
|
||||
id: partition.id,
|
||||
min_size: convertToBytes(minSize),
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type SizeUnitPropTypes = {
|
||||
units: Units;
|
||||
setUnits: React.Dispatch<React.SetStateAction<Units>>;
|
||||
};
|
||||
|
||||
const SizeUnit = ({ units, setUnits }: SizeUnitPropTypes) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const onToggle = (isOpen: boolean) => {
|
||||
setIsOpen(isOpen);
|
||||
};
|
||||
|
||||
const onSelect = (event: React.MouseEvent, selection: Units) => {
|
||||
setUnits(selection);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
ouiaId="mount-point"
|
||||
isOpen={isOpen}
|
||||
onToggle={(_event, isOpen) => onToggle(isOpen)}
|
||||
onSelect={onSelect}
|
||||
selections={units}
|
||||
>
|
||||
<SelectOption value={'KiB'} />
|
||||
<SelectOption value={'MiB'} />
|
||||
<SelectOption value={'GiB'} />
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileSystemConfiguration;
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
|
||||
import { FormGroup, Label, Radio } from '@patternfly/react-core';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../../../../store/hooks';
|
||||
import {
|
||||
changeFileSystemPartitionMode,
|
||||
selectFileSystemPartitionMode,
|
||||
} from '../../../../store/wizardSlice';
|
||||
|
||||
const FileSystemPartition = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const fileSystemPartition = useAppSelector((state) =>
|
||||
selectFileSystemPartitionMode(state)
|
||||
);
|
||||
return (
|
||||
<FormGroup>
|
||||
<Radio
|
||||
id="automatic file system config radio"
|
||||
label={
|
||||
<>
|
||||
<Label isCompact color="blue">
|
||||
Recommended
|
||||
</Label>{' '}
|
||||
Use automatic partitioning
|
||||
</>
|
||||
}
|
||||
name="sc-radio-automatic"
|
||||
description="Automatically partition your image to what is best, depending on the target environment(s)"
|
||||
isChecked={fileSystemPartition === 'automatic'}
|
||||
onChange={() => {
|
||||
dispatch(changeFileSystemPartitionMode('automatic'));
|
||||
}}
|
||||
/>
|
||||
<Radio
|
||||
id="manual file system config radio"
|
||||
label="Manually configure partitions"
|
||||
name="fsc-radio-manual"
|
||||
description="Manually configure the file system of your image by adding, removing, and editing partitions"
|
||||
isChecked={fileSystemPartition === 'manual'}
|
||||
onChange={() => {
|
||||
dispatch(changeFileSystemPartitionMode('manual'));
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileSystemPartition;
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Text, Form, Title } from '@patternfly/react-core';
|
||||
|
||||
import FileSystemAutomaticPartition from './FileSystemAutomaticPartitionInformation';
|
||||
import FileSystemConfiguration from './FileSystemConfiguration';
|
||||
import FileSystemPartition from './FileSystemPartition';
|
||||
|
||||
import { useAppSelector } from '../../../../store/hooks';
|
||||
import { selectFileSystemPartitionMode } from '../../../../store/wizardSlice';
|
||||
export type FileSystemPartitionMode = 'automatic' | 'manual';
|
||||
|
||||
const FileSystemStep = () => {
|
||||
const fileSystemPartitionMode = useAppSelector((state) =>
|
||||
selectFileSystemPartitionMode(state)
|
||||
);
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<Title headingLevel="h2">File system configuration</Title>
|
||||
<Text>Define the partitioning of the image</Text>
|
||||
{fileSystemPartitionMode === 'automatic' ? (
|
||||
<>
|
||||
<FileSystemPartition />
|
||||
<FileSystemAutomaticPartition />
|
||||
</>
|
||||
) : fileSystemPartitionMode === 'manual' ? (
|
||||
<>
|
||||
<FileSystemPartition />
|
||||
<FileSystemConfiguration />
|
||||
</>
|
||||
) : (
|
||||
fileSystemPartitionMode === 'oscap' && <FileSystemConfiguration />
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileSystemStep;
|
||||
|
|
@ -64,6 +64,7 @@ export const FSReviewTable = () => {
|
|||
<Th>Minimum size</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody data-testid="file-system-configuration-tbody-review"></Tbody>
|
||||
</Table>
|
||||
</PanelMain>
|
||||
</Panel>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome'
|
|||
|
||||
import ActivationKeyInformation from './../Registration/ActivationKeyInformation';
|
||||
import { PackagesTable, RepositoriesTable } from './ReviewStepTables';
|
||||
import { FSReviewTable } from './ReviewStepTables';
|
||||
|
||||
import {
|
||||
RELEASES,
|
||||
|
|
@ -50,6 +51,7 @@ import {
|
|||
selectGcpShareMethod,
|
||||
selectPackages,
|
||||
selectRegistrationType,
|
||||
selectFileSystemPartitionMode,
|
||||
} from '../../../../store/wizardSlice';
|
||||
import { toMonthAndYear } from '../../../../Utilities/time';
|
||||
import { MajorReleasesLifecyclesChart } from '../ImageOutput/ReleaseLifecycle';
|
||||
|
|
@ -103,13 +105,85 @@ export const ImageOutputList = () => {
|
|||
);
|
||||
};
|
||||
export const FSCList = () => {
|
||||
const fileSystemPartitionMode = useAppSelector((state) =>
|
||||
selectFileSystemPartitionMode(state)
|
||||
);
|
||||
|
||||
return (
|
||||
<TextContent>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
Configuration type
|
||||
</TextListItem>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dd}
|
||||
data-testid="partitioning-auto-manual"
|
||||
>
|
||||
{fileSystemPartitionMode === 'manual' ? 'Manual' : 'Automatic'}
|
||||
{fileSystemPartitionMode === 'manual' && (
|
||||
<>
|
||||
{' '}
|
||||
<Popover
|
||||
position="bottom"
|
||||
headerContent="Partitions"
|
||||
hasAutoWidth
|
||||
minWidth="30rem"
|
||||
bodyContent={<FSReviewTable />}
|
||||
>
|
||||
<Button
|
||||
data-testid="file-system-configuration-popover"
|
||||
variant="link"
|
||||
aria-label="File system configuration info"
|
||||
aria-describedby="file-system-configuration-info"
|
||||
className="pf-u-pt-0 pf-u-pb-0"
|
||||
>
|
||||
View partitions
|
||||
</Button>
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
</TextListItem>
|
||||
{fileSystemPartitionMode === 'manual' && (
|
||||
<>
|
||||
<TextListItem component={TextListItemVariants.dt}>
|
||||
Image size (minimum)
|
||||
<Popover
|
||||
hasAutoWidth
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<Text>
|
||||
Image Builder may extend this size based on requirements,
|
||||
selected packages, and configurations.
|
||||
</Text>
|
||||
</TextContent>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label="File system configuration info"
|
||||
aria-describedby="file-system-configuration-info"
|
||||
className="pf-c-form__group-label-help"
|
||||
>
|
||||
<HelpIcon />
|
||||
</Button>
|
||||
</Popover>
|
||||
</TextListItem>
|
||||
<MinSize />
|
||||
</>
|
||||
)}
|
||||
</TextList>
|
||||
<br />
|
||||
</TextContent>
|
||||
);
|
||||
};
|
||||
|
||||
export const MinSize = () => {
|
||||
return <TextListItem component={TextListItemVariants.dd} />;
|
||||
};
|
||||
|
||||
export const TargetEnvAWSList = () => {
|
||||
const { isSuccess } = useGetSourceListQuery({
|
||||
provider: 'aws',
|
||||
|
|
|
|||
|
|
@ -108,6 +108,11 @@ export const mapRequestToState = (request: BlueprintResponse): wizardState => {
|
|||
enabled: [],
|
||||
},
|
||||
},
|
||||
fileSystem: {
|
||||
mode: 'automatic',
|
||||
partitions: [],
|
||||
},
|
||||
|
||||
architecture: request.image_requests[0].architecture,
|
||||
distribution: request.distribution,
|
||||
imageTypes: request.image_requests.map((image) => image.image_type),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import {
|
|||
} from './imageBuilderApi';
|
||||
import { ActivationKeys } from './rhsmApi';
|
||||
|
||||
import { FileSystemPartitionMode } from '../Components/CreateImageWizardV2/steps/FileSystem';
|
||||
import { Partition } from '../Components/CreateImageWizardV2/steps/FileSystem/FileSystemConfiguration';
|
||||
import { IBPackageWithRepositoryInfo } from '../Components/CreateImageWizardV2/steps/Packages/Packages';
|
||||
import { AwsShareMethod } from '../Components/CreateImageWizardV2/steps/TargetEnvironment/Aws';
|
||||
import { AzureShareMethod } from '../Components/CreateImageWizardV2/steps/TargetEnvironment/Azure';
|
||||
|
|
@ -68,7 +70,10 @@ export type wizardState = {
|
|||
enabled: string[] | undefined;
|
||||
};
|
||||
};
|
||||
|
||||
fileSystem: {
|
||||
mode: FileSystemPartitionMode;
|
||||
partitions: Partition[];
|
||||
};
|
||||
repositories: {
|
||||
customRepositories: CustomRepository[];
|
||||
payloadRepositories: Repository[];
|
||||
|
|
@ -119,6 +124,14 @@ const initialState: wizardState = {
|
|||
enabled: [],
|
||||
},
|
||||
},
|
||||
fileSystem: {
|
||||
mode: 'automatic',
|
||||
partitions: [
|
||||
{ id: '1', mountpoint: '/', min_size: '500' },
|
||||
{ id: '2', mountpoint: '/home', min_size: '500' },
|
||||
{ id: '3', mountpoint: '/home/var', min_size: '500' },
|
||||
],
|
||||
},
|
||||
repositories: {
|
||||
customRepositories: [],
|
||||
payloadRepositories: [],
|
||||
|
|
@ -218,6 +231,14 @@ export const selectEnabledServices = (state: RootState) => {
|
|||
return state.wizard.openScap.services.enabled;
|
||||
};
|
||||
|
||||
export const selectFileSystemPartitionMode = (state: RootState) => {
|
||||
return state.wizard.fileSystem.mode;
|
||||
};
|
||||
|
||||
export const selectPartitions = (state: RootState) => {
|
||||
return state.wizard.fileSystem.partitions;
|
||||
};
|
||||
|
||||
export const selectCustomRepositories = (state: RootState) => {
|
||||
return state.wizard.repositories.customRepositories;
|
||||
};
|
||||
|
|
@ -353,6 +374,36 @@ export const wizardSlice = createSlice({
|
|||
) => {
|
||||
state.openScap.services.enabled = action.payload;
|
||||
},
|
||||
changeFileSystemPartitionMode: (
|
||||
state,
|
||||
action: PayloadAction<FileSystemPartitionMode>
|
||||
) => {
|
||||
state.fileSystem.mode = action.payload;
|
||||
},
|
||||
changePartitionMountpoint: (
|
||||
state,
|
||||
action: PayloadAction<{ id: string; mountpoint: string }>
|
||||
) => {
|
||||
const { id, mountpoint } = action.payload;
|
||||
const partitionIndex = state.fileSystem.partitions.findIndex(
|
||||
(partition) => partition.id === id
|
||||
);
|
||||
if (partitionIndex !== -1) {
|
||||
state.fileSystem.partitions[partitionIndex].mountpoint = mountpoint;
|
||||
}
|
||||
},
|
||||
changePartitionMinSize: (
|
||||
state,
|
||||
action: PayloadAction<{ id: string; min_size: string }>
|
||||
) => {
|
||||
const { id, min_size } = action.payload;
|
||||
const partitionIndex = state.fileSystem.partitions.findIndex(
|
||||
(partition) => partition.id === id
|
||||
);
|
||||
if (partitionIndex !== -1) {
|
||||
state.fileSystem.partitions[partitionIndex].min_size = min_size;
|
||||
}
|
||||
},
|
||||
changeCustomRepositories: (
|
||||
state,
|
||||
action: PayloadAction<CustomRepository[]>
|
||||
|
|
@ -409,6 +460,9 @@ export const {
|
|||
changeKernel,
|
||||
changeDisabledServices,
|
||||
changeEnabledServices,
|
||||
changeFileSystemPartitionMode,
|
||||
changePartitionMountpoint,
|
||||
changePartitionMinSize,
|
||||
changeCustomRepositories,
|
||||
changePayloadRepositories,
|
||||
addPackage,
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ describe('Step Packages', () => {
|
|||
// skip Repositories
|
||||
await clickNext();
|
||||
// skip fsc
|
||||
//await clickNext();
|
||||
await clickNext();
|
||||
};
|
||||
|
||||
test('clicking Next loads Image name', async () => {
|
||||
|
|
@ -365,7 +365,10 @@ describe('Step Custom repositories', () => {
|
|||
// skip OpenSCAP
|
||||
await clickNext();
|
||||
// skip fsc
|
||||
//await clickNext();
|
||||
|
||||
await clickNext();
|
||||
// // skip packages
|
||||
// await clickNext();
|
||||
};
|
||||
|
||||
test('selected repositories stored in and retrieved from form state', async () => {
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ describe('Create Image Wizard', () => {
|
|||
|
||||
await screen.findByRole('button', { name: 'Image output' });
|
||||
await screen.findByRole('button', { name: 'Register' });
|
||||
// await screen.findByRole('button', { name: 'File system configuration' });
|
||||
await screen.findByRole('button', { name: 'File system configuration' });
|
||||
await screen.findByRole('button', { name: 'Content' });
|
||||
await screen.findByRole('button', { name: 'Custom repositories' });
|
||||
await screen.findByRole('button', { name: 'Additional packages' });
|
||||
|
|
@ -430,6 +430,8 @@ describe('Step Upload to AWS', () => {
|
|||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await enterBlueprintName();
|
||||
await clickNext();
|
||||
|
||||
|
|
@ -556,18 +558,21 @@ describe('Step Registration', () => {
|
|||
});
|
||||
};
|
||||
|
||||
// test('clicking Next loads file system configuration', async () => {
|
||||
// await setUp();
|
||||
test('clicking Next loads file system configuration', async () => {
|
||||
await setUp();
|
||||
|
||||
// const registerLaterRadio = await screen.findByTestId('registration-radio-later');
|
||||
// await user.click(registerLaterRadio);
|
||||
const registerLaterRadio = await screen.findByTestId(
|
||||
'registration-radio-later'
|
||||
);
|
||||
await user.click(registerLaterRadio);
|
||||
|
||||
// await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
|
||||
// await screen.findByRole('heading', {
|
||||
// name: 'File system configuration',
|
||||
// });
|
||||
// });
|
||||
await screen.findByRole('heading', {
|
||||
name: 'File system configuration',
|
||||
});
|
||||
});
|
||||
|
||||
test('clicking Back loads Upload to AWS', async () => {
|
||||
await setUp();
|
||||
|
|
@ -619,6 +624,7 @@ describe('Step Registration', () => {
|
|||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await enterBlueprintName();
|
||||
await clickNext();
|
||||
const review = await screen.findByTestId('review-registration');
|
||||
|
|
@ -663,6 +669,8 @@ describe('Step Registration', () => {
|
|||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await enterBlueprintName();
|
||||
await clickNext();
|
||||
const review = await screen.findByTestId('review-registration');
|
||||
|
|
@ -709,6 +717,7 @@ describe('Step Registration', () => {
|
|||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await enterBlueprintName();
|
||||
await clickNext();
|
||||
const review = await screen.findByTestId('review-registration');
|
||||
|
|
@ -737,6 +746,8 @@ describe('Step Registration', () => {
|
|||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await enterBlueprintName();
|
||||
await clickNext();
|
||||
await screen.findByText('Register the system later');
|
||||
|
|
@ -762,86 +773,77 @@ describe('Step Registration', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// describe('Step File system configuration', () => {
|
||||
// const user = userEvent.setup();
|
||||
// const setUp = async () => {
|
||||
// ({ router } = await renderCustomRoutesWithReduxRouter(
|
||||
// 'imagewizard',
|
||||
// {},
|
||||
// routes
|
||||
// ));
|
||||
|
||||
// // select aws as upload destination
|
||||
// await waitFor(
|
||||
// async () => await user.click(await screen.findByTestId('upload-aws'))
|
||||
// );
|
||||
// await clickNext();
|
||||
|
||||
// // aws step
|
||||
// await switchToAWSManual();
|
||||
// await user.type(
|
||||
// await screen.findByTestId('aws-account-id'),
|
||||
// '012345678901'
|
||||
// );
|
||||
// await clickNext();
|
||||
// // skip registration
|
||||
// await screen.findByRole('textbox', {
|
||||
// name: 'Select activation key',
|
||||
// });
|
||||
|
||||
// const registerLaterRadio = await screen.findByTestId('registration-radio-later');
|
||||
// await user.click(registerLaterRadio);
|
||||
// await clickNext();
|
||||
// };
|
||||
|
||||
// test('Error validation occurs upon clicking next button', async () => {
|
||||
// await setUp();
|
||||
|
||||
// const manuallyConfigurePartitions = await screen.findByText(
|
||||
// /manually configure partitions/i
|
||||
// );
|
||||
// await user.click(manuallyConfigurePartitions);
|
||||
|
||||
// const addPartition = await screen.findByTestId('file-system-add-partition');
|
||||
|
||||
// // Create duplicate partitions
|
||||
// await user.click(addPartition);
|
||||
// await user.click(addPartition);
|
||||
|
||||
// expect(await getNextButton()).toBeEnabled();
|
||||
|
||||
// // Clicking next causes errors to appear
|
||||
// await clickNext();
|
||||
|
||||
// const mountPointWarning = await screen.findByRole('heading', {
|
||||
// name: /danger alert: duplicate mount points: all mount points must be unique\. remove the duplicate or choose a new mount point\./i,
|
||||
// hidden: true,
|
||||
// });
|
||||
|
||||
// const mountPointAlerts = screen.getAllByRole('heading', {
|
||||
// name: /danger alert: duplicate mount point\./i,
|
||||
// });
|
||||
|
||||
// const tbody = await screen.findByTestId('file-system-configuration-tbody');
|
||||
// const rows = within(tbody).getAllByRole('row');
|
||||
// expect(rows).toHaveLength(3);
|
||||
|
||||
// // Change mountpoint of final row to /var, resolving errors
|
||||
// const mountPointOptions = within(rows[2]).getAllByRole('button', {
|
||||
// name: 'Options menu',
|
||||
// })[0];
|
||||
// await user.click(mountPointOptions);
|
||||
// const varButton = await within(rows[2]).findByRole('option', {
|
||||
// name: '/var',
|
||||
// });
|
||||
// await user.click(varButton);
|
||||
|
||||
// await waitFor(() => expect(mountPointWarning).not.toBeInTheDocument());
|
||||
// await waitFor(() => expect(mountPointAlerts[0]).not.toBeInTheDocument());
|
||||
// await waitFor(() => expect(mountPointAlerts[1]).not.toBeInTheDocument());
|
||||
// expect(await getNextButton()).toBeEnabled();
|
||||
// });
|
||||
// });
|
||||
describe('Step File system configuration', () => {
|
||||
// const user = userEvent.setup();
|
||||
// const setUp = async () => {
|
||||
// ({ router } = await renderCustomRoutesWithReduxRouter(
|
||||
// 'imagewizard',
|
||||
// {},
|
||||
// routes
|
||||
// ));
|
||||
// // select aws as upload destination
|
||||
// await waitFor(
|
||||
// async () => await user.click(await screen.findByTestId('upload-aws'))
|
||||
// );
|
||||
// await clickNext();
|
||||
// // aws step
|
||||
// await switchToAWSManual();
|
||||
// await user.type(
|
||||
// screen.getByRole('textbox', {
|
||||
// name: /aws account id/i,
|
||||
// }),
|
||||
// '012345678901'
|
||||
// );
|
||||
// await clickNext();
|
||||
// // skip registration
|
||||
// await screen.findByRole('textbox', {
|
||||
// name: 'Select activation key',
|
||||
// });
|
||||
// const registerLaterRadio = await screen.findByTestId(
|
||||
// 'registration-radio-later'
|
||||
// );
|
||||
// await user.click(registerLaterRadio);
|
||||
// await clickNext();
|
||||
// await clickNext();
|
||||
// };
|
||||
//test('Error validation occurs upon clicking next button', async () => {
|
||||
// await setUp();
|
||||
// const manuallyConfigurePartitions = await screen.findByText(
|
||||
// /manually configure partitions/i
|
||||
// );
|
||||
// await user.click(manuallyConfigurePartitions);
|
||||
// const addPartition = await screen.findByTestId('file-system-add-partition');
|
||||
// // Create duplicate partitions
|
||||
// await user.click(addPartition);
|
||||
// await user.click(addPartition);
|
||||
// expect(await getNextButton()).toBeDisabled();
|
||||
// // Clicking next causes errors to appear
|
||||
// await clickNext();
|
||||
// const mountPointWarning = await screen.findByRole('heading', {
|
||||
// name: /danger alert: duplicate mount points: all mount points must be unique\. remove the duplicate or choose a new mount point\./i,
|
||||
// hidden: true,
|
||||
// });
|
||||
// const mountPointAlerts = screen.getAllByRole('heading', {
|
||||
// name: /danger alert: duplicate mount point\./i,
|
||||
// });
|
||||
// const tbody = await screen.findByTestId('file-system-configuration-tbody');
|
||||
// const rows = within(tbody).getAllByRole('row');
|
||||
// expect(rows).toHaveLength(3);
|
||||
// //Change mountpoint of final row to /var, resolving errors
|
||||
// const mountPointOptions = within(rows[2]).getAllByRole('button', {
|
||||
// name: 'Options menu',
|
||||
// })[0];
|
||||
// await user.click(mountPointOptions);
|
||||
// const varButton = await within(rows[2]).findByRole('option', {
|
||||
// name: '/var',
|
||||
// });
|
||||
// await user.click(varButton);
|
||||
// // await waitFor(() => expect(mountPointWarning).not.toBeInTheDocument());
|
||||
// // await waitFor(() => expect(mountPointAlerts[0]).not.toBeInTheDocument());
|
||||
// // await waitFor(() => expect(mountPointAlerts[1]).not.toBeInTheDocument());
|
||||
// expect(await getNextButton()).toBeEnabled();
|
||||
//});
|
||||
});
|
||||
|
||||
describe('Step Details', () => {
|
||||
const user = userEvent.setup();
|
||||
|
|
@ -883,7 +885,7 @@ describe('Step Details', () => {
|
|||
// skip packages
|
||||
await clickNext();
|
||||
// skip fsc
|
||||
//await clickNext();
|
||||
await clickNext();
|
||||
};
|
||||
|
||||
test('image name invalid for more than 63 chars', async () => {
|
||||
|
|
@ -962,6 +964,7 @@ describe('Step Review', () => {
|
|||
await clickNext();
|
||||
// skip packages
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
// skip Details
|
||||
const blueprintName = await screen.findByRole('textbox', {
|
||||
name: /blueprint name/i,
|
||||
|
|
@ -1023,7 +1026,7 @@ describe('Step Review', () => {
|
|||
await clickNext();
|
||||
// skip repositories
|
||||
await clickNext();
|
||||
// skip Details
|
||||
await clickNext();
|
||||
const blueprintName = await screen.findByRole('textbox', {
|
||||
name: /blueprint name/i,
|
||||
});
|
||||
|
|
@ -1061,7 +1064,9 @@ describe('Step Review', () => {
|
|||
});
|
||||
|
||||
const contentExpandable = await screen.findByTestId('content-expandable');
|
||||
// const fscExpandable = screen.getByText(/file system configuration/i);
|
||||
const fscExpandable = screen.getByTestId(
|
||||
'file-system-configuration-expandable'
|
||||
);
|
||||
|
||||
await user.click(targetExpandable);
|
||||
await screen.findByText('AWS');
|
||||
|
|
@ -1071,19 +1076,17 @@ describe('Step Review', () => {
|
|||
|
||||
await within(contentExpandable).findByText('Custom repositories');
|
||||
await within(contentExpandable).findByText('Additional packages');
|
||||
// await user.click(fscExpandable);
|
||||
// await screen.findByText('Configuration type');
|
||||
await user.click(fscExpandable);
|
||||
await screen.findByText('Configuration type');
|
||||
});
|
||||
test('has no Registration expandable for centos', async () => {
|
||||
await setUpCentOS();
|
||||
const targetExpandable = screen.getByText(/target environments/i);
|
||||
const contentExpandable = await screen.findByTestId('content-expandable');
|
||||
|
||||
//const fscExpandable = await screen.findByTestId(
|
||||
// 'file-system-configuration-expandable'
|
||||
// );
|
||||
// });
|
||||
|
||||
const fscExpandable = await screen.findByTestId(
|
||||
'file-system-configuration-expandable'
|
||||
);
|
||||
expect(
|
||||
screen.queryByTestId('registration-expandable')
|
||||
).not.toBeInTheDocument();
|
||||
|
|
@ -1093,12 +1096,11 @@ describe('Step Review', () => {
|
|||
await user.click(contentExpandable);
|
||||
await within(contentExpandable).findByText('Custom repositories');
|
||||
await within(contentExpandable).findByText('Additional packages');
|
||||
|
||||
await user.click(fscExpandable);
|
||||
await screen.findByText('Configuration type');
|
||||
});
|
||||
});
|
||||
// await user.click(fscExpandable);
|
||||
// await screen.findByText('Configuration type');
|
||||
// });
|
||||
// });
|
||||
|
||||
describe('Keyboard accessibility', () => {
|
||||
const user = userEvent.setup();
|
||||
|
|
@ -1199,8 +1201,8 @@ describe('Keyboard accessibility', () => {
|
|||
// TODO: Focus on textbox on OpenSCAP step
|
||||
await clickNext();
|
||||
|
||||
// File system configuration
|
||||
// await clickNext();
|
||||
//File system configuration
|
||||
await clickNext();
|
||||
|
||||
// TODO: Focus on textbox on Custom Repos step
|
||||
await clickNext();
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ const goToDetailsStep = async () => {
|
|||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
};
|
||||
|
||||
const enterBlueprintDescription = async () => {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ const goToPackagesStep = async () => {
|
|||
await clickNext(); // Registration
|
||||
await clickRegisterLater();
|
||||
await clickNext(); // OpenSCAP
|
||||
await clickNext(); // File System
|
||||
await clickNext(); // Custom repositories
|
||||
await clickNext(); // Additional packages
|
||||
};
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ const goToReviewStep = async () => {
|
|||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await enterBlueprintName();
|
||||
await clickNext();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -43,11 +43,13 @@ const goToRepositoriesStep = async () => {
|
|||
await clickNext(); // Registration
|
||||
await clickRegisterLater();
|
||||
await clickNext(); // OpenSCAP
|
||||
await clickNext(); // File System
|
||||
await clickNext(); // Custom repositories
|
||||
};
|
||||
|
||||
const goToReviewStep = async () => {
|
||||
await clickNext(); // Additional packages
|
||||
await clickNext();
|
||||
await clickNext(); // Details
|
||||
await enterBlueprintName();
|
||||
await clickNext(); // Review
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ const goToReviewStep = async () => {
|
|||
await goToRegistrationStep(); // Register
|
||||
await clickRegisterLater();
|
||||
await clickNext(); // OpenSCAP
|
||||
await clickNext(); // File system customization
|
||||
await clickNext(); // Custom repositories
|
||||
await clickNext(); // Additional packages
|
||||
await clickNext(); // Details
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue