src: Rename "V2" folders to just Wizard

This replaces all occurences of "CreateImageWizardV2" with just "CreateImageWizard" as it is the only version now.
This commit is contained in:
regexowl 2024-07-16 17:10:37 +02:00 committed by Ondřej Ezr
parent b1e5a8c7c6
commit 4fb37c187e
93 changed files with 20 additions and 22 deletions

View file

@ -0,0 +1,158 @@
import React, { useState } from 'react';
import {
DropdownList,
DropdownItem,
MenuToggleAction,
Spinner,
Flex,
FlexItem,
Modal,
Button,
} from '@patternfly/react-core';
import { setBlueprintId } from '../../../../../store/BlueprintSlice';
import { useAppDispatch } from '../../../../../store/hooks';
import {
CreateBlueprintRequest,
useComposeBlueprintMutation,
useCreateBlueprintMutation,
} from '../../../../../store/imageBuilderApi';
type CreateDropdownProps = {
getBlueprintPayload: () => Promise<'' | CreateBlueprintRequest | undefined>;
setIsOpen: (isOpen: boolean) => void;
isDisabled?: boolean;
};
export const CreateSaveAndBuildBtn = ({
getBlueprintPayload,
setIsOpen,
isDisabled,
}: CreateDropdownProps) => {
const [buildBlueprint] = useComposeBlueprintMutation();
const [createBlueprint] = useCreateBlueprintMutation({
fixedCacheKey: 'createBlueprintKey',
});
const dispatch = useAppDispatch();
const onSaveAndBuild = async () => {
const requestBody = await getBlueprintPayload();
setIsOpen(false);
const blueprint =
requestBody &&
(await createBlueprint({
createBlueprintRequest: requestBody,
}).unwrap()); // unwrap - access the success payload immediately after a mutation
if (blueprint) {
buildBlueprint({ id: blueprint.id, body: {} });
dispatch(setBlueprintId(blueprint.id));
}
};
return (
<DropdownList>
<DropdownItem
onClick={onSaveAndBuild}
ouiaId="wizard-create-build-btn"
isDisabled={isDisabled}
>
Create blueprint and build image(s)
</DropdownItem>
</DropdownList>
);
};
export const CreateSaveButton = ({
setIsOpen,
getBlueprintPayload,
isDisabled,
}: CreateDropdownProps) => {
const [createBlueprint, { isLoading }] = useCreateBlueprintMutation({
fixedCacheKey: 'createBlueprintKey',
});
const dispatch = useAppDispatch();
const [showModal, setShowModal] = useState(false);
const wasModalSeen = window.localStorage.getItem(
'imageBuilder.saveAndBuildModalSeen'
);
const SaveAndBuildImagesModal = () => {
const handleClose = () => {
setShowModal(false);
};
return (
<Modal
title="Save time by building images"
isOpen={showModal}
onClose={handleClose}
width="50%"
actions={[
<Button
key="back"
variant="primary"
data-testid="close-button-saveandbuild-modal"
onClick={handleClose}
>
Close
</Button>,
]}
>
Building blueprints and images doesnt need to be a two step process. To
build images simultaneously, use the dropdown arrow to the right side of
this button.
</Modal>
);
};
const onClick = () => {
if (!wasModalSeen) {
setShowModal(true);
window.localStorage.setItem('imageBuilder.saveAndBuildModalSeen', 'true');
} else {
onSave();
}
};
const onSave = async () => {
const requestBody = await getBlueprintPayload();
setIsOpen(false);
const blueprint =
requestBody &&
(await createBlueprint({
createBlueprintRequest: requestBody,
}).unwrap());
if (blueprint) {
dispatch(setBlueprintId(blueprint?.id));
}
};
return (
<>
{showModal && <SaveAndBuildImagesModal />}
<MenuToggleAction
onClick={onClick}
id="wizard-create-save-btn"
isDisabled={isDisabled}
>
<Flex display={{ default: 'inlineFlex' }}>
{isLoading && (
<FlexItem>
<Spinner
style={
{ '--pf-v5-c-spinner--Color': '#fff' } as React.CSSProperties
}
isInline
size="md"
/>
</FlexItem>
)}
<FlexItem>Create blueprint</FlexItem>
</Flex>
</MenuToggleAction>
</>
);
};

View file

@ -0,0 +1,97 @@
import React from 'react';
import {
DropdownList,
DropdownItem,
MenuToggleAction,
Spinner,
Flex,
FlexItem,
} from '@patternfly/react-core';
import {
CreateBlueprintRequest,
useComposeBlueprintMutation,
useUpdateBlueprintMutation,
} from '../../../../../store/imageBuilderApi';
type EditDropdownProps = {
getBlueprintPayload: () => Promise<'' | CreateBlueprintRequest | undefined>;
setIsOpen: (isOpen: boolean) => void;
blueprintId: string;
isDisabled?: boolean;
};
export const EditSaveAndBuildBtn = ({
getBlueprintPayload,
setIsOpen,
blueprintId,
isDisabled,
}: EditDropdownProps) => {
const [buildBlueprint] = useComposeBlueprintMutation();
const [updateBlueprint] = useUpdateBlueprintMutation({
fixedCacheKey: 'updateBlueprintKey',
});
const onSaveAndBuild = async () => {
const requestBody = await getBlueprintPayload();
setIsOpen(false);
requestBody &&
(await updateBlueprint({
id: blueprintId,
createBlueprintRequest: requestBody,
}));
buildBlueprint({ id: blueprintId, body: {} });
};
return (
<DropdownList>
<DropdownItem
onClick={onSaveAndBuild}
ouiaId="wizard-edit-build-btn"
isDisabled={isDisabled}
>
Save changes and build image(s)
</DropdownItem>
</DropdownList>
);
};
export const EditSaveButton = ({
setIsOpen,
getBlueprintPayload,
blueprintId,
isDisabled,
}: EditDropdownProps) => {
const [updateBlueprint, { isLoading }] = useUpdateBlueprintMutation({
fixedCacheKey: 'updateBlueprintKey',
});
const onSave = async () => {
const requestBody = await getBlueprintPayload();
setIsOpen(false);
requestBody &&
updateBlueprint({ id: blueprintId, createBlueprintRequest: requestBody });
};
return (
<MenuToggleAction
onClick={onSave}
id="wizard-edit-save-btn"
isDisabled={isDisabled}
>
<Flex display={{ default: 'inlineFlex' }}>
{isLoading && (
<FlexItem>
<Spinner
style={
{ '--pf-v5-c-spinner--Color': '#fff' } as React.CSSProperties
}
isInline
size="md"
/>
</FlexItem>
)}
<FlexItem>Save changes to blueprint</FlexItem>
</Flex>
</MenuToggleAction>
);
};

View file

@ -0,0 +1,130 @@
import React, { useState, useEffect } from 'react';
import {
Button,
Dropdown,
MenuToggle,
MenuToggleElement,
WizardFooterWrapper,
useWizardContext,
} from '@patternfly/react-core';
import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome';
import { useStore } from 'react-redux';
import { useNavigate, useParams } from 'react-router-dom';
import { CreateSaveAndBuildBtn, CreateSaveButton } from './CreateDropdown';
import { EditSaveAndBuildBtn, EditSaveButton } from './EditDropdown';
import { useServerStore } from '../../../../../store/hooks';
import {
useCreateBlueprintMutation,
useUpdateBlueprintMutation,
} from '../../../../../store/imageBuilderApi';
import { resolveRelPath } from '../../../../../Utilities/path';
import { mapRequestFromState } from '../../../utilities/requestMapper';
import { useIsBlueprintValid } from '../../../utilities/useValidation';
const ReviewWizardFooter = () => {
const { goToPrevStep, close } = useWizardContext();
const [, { isSuccess: isCreateSuccess, reset: resetCreate }] =
useCreateBlueprintMutation({ fixedCacheKey: 'createBlueprintKey' });
// initialize the server store with the data from RTK query
const serverStore = useServerStore();
const [, { isSuccess: isUpdateSuccess, reset: resetUpdate }] =
useUpdateBlueprintMutation({ fixedCacheKey: 'updateBlueprintKey' });
const { auth } = useChrome();
const { composeId } = useParams();
const [isOpen, setIsOpen] = useState(false);
const store = useStore();
const onToggleClick = () => {
setIsOpen(!isOpen);
};
const navigate = useNavigate();
const isValid = useIsBlueprintValid();
useEffect(() => {
if (isUpdateSuccess || isCreateSuccess) {
resetCreate();
resetUpdate();
navigate(resolveRelPath(''));
}
}, [isUpdateSuccess, isCreateSuccess, resetCreate, resetUpdate, navigate]);
const getBlueprintPayload = async () => {
const userData = await auth?.getUser();
const orgId = userData?.identity?.internal?.org_id;
const requestBody = orgId && mapRequestFromState(store, orgId, serverStore);
return requestBody;
};
return (
<WizardFooterWrapper>
<div data-testid="wizard-save-button-div">
<Dropdown
isOpen={isOpen}
onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)}
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
variant="primary"
ref={toggleRef}
onClick={onToggleClick}
isExpanded={isOpen}
isDisabled={!isValid}
splitButtonOptions={{
variant: 'action',
items: composeId
? [
<EditSaveButton
key="wizard-edit-save-btn"
getBlueprintPayload={getBlueprintPayload}
setIsOpen={setIsOpen}
blueprintId={composeId}
isDisabled={!isValid}
/>,
]
: [
<CreateSaveButton
key="wizard-create-save-btn"
getBlueprintPayload={getBlueprintPayload}
setIsOpen={setIsOpen}
isDisabled={!isValid}
/>,
],
}}
/>
)}
ouiaId="wizard-finish-dropdown"
shouldFocusToggleOnSelect
>
{composeId ? (
<EditSaveAndBuildBtn
getBlueprintPayload={getBlueprintPayload}
setIsOpen={setIsOpen}
blueprintId={composeId}
isDisabled={!isValid}
/>
) : (
<CreateSaveAndBuildBtn
getBlueprintPayload={getBlueprintPayload}
setIsOpen={setIsOpen}
isDisabled={!isValid}
/>
)}
</Dropdown>
</div>
<Button
ouiaId="wizard-back-btn"
variant="secondary"
onClick={goToPrevStep}
>
Back
</Button>
<Button ouiaId="wizard-cancel-btn" variant="link" onClick={close}>
Cancel
</Button>
</WizardFooterWrapper>
);
};
export default ReviewWizardFooter;

View file

@ -0,0 +1,312 @@
import React, { useState } from 'react';
import {
Button,
ExpandableSection,
Text,
TextContent,
TextVariants,
useWizardContext,
} from '@patternfly/react-core';
import { ArrowRightIcon } from '@patternfly/react-icons';
import { useFlag } from '@unleash/proxy-client-react';
import {
ContentList,
FSCList,
FirstBootList,
DetailsList,
ImageOutputList,
OscapList,
RegisterLaterList,
RegisterNowList,
TargetEnvAWSList,
TargetEnvAzureList,
TargetEnvGCPList,
TargetEnvOciList,
TargetEnvOtherList,
} from './ReviewStepTextLists';
import isRhel from '../../../../../src/Utilities/isRhel';
import { targetOptions } from '../../../../constants';
import { useAppSelector } from '../../../../store/hooks';
import {
selectBlueprintDescription,
selectBlueprintName,
selectDistribution,
selectImageTypes,
selectProfile,
selectRegistrationType,
} from '../../../../store/wizardSlice';
const Review = ({ snapshottingEnabled }: { snapshottingEnabled: boolean }) => {
const { goToStepById } = useWizardContext();
const blueprintName = useAppSelector(selectBlueprintName);
const blueprintDescription = useAppSelector(selectBlueprintDescription);
const distribution = useAppSelector(selectDistribution);
const environments = useAppSelector(selectImageTypes);
const oscapProfile = useAppSelector(selectProfile);
const registrationType = useAppSelector(selectRegistrationType);
const [isExpandedImageOutput, setIsExpandedImageOutput] = useState(true);
const [isExpandedTargetEnvs, setIsExpandedTargetEnvs] = useState(true);
const [isExpandedFSC, setIsExpandedFSC] = useState(true);
const [isExpandedContent, setIsExpandedContent] = useState(true);
const [isExpandedRegistration, setIsExpandedRegistration] = useState(true);
const [isExpandedImageDetail, setIsExpandedImageDetail] = useState(true);
const [isExpandedOscapDetail, setIsExpandedOscapDetail] = useState(true);
const [isExpandableFirstBoot, setIsExpandedFirstBoot] = useState(true);
const onToggleImageOutput = (isExpandedImageOutput: boolean) =>
setIsExpandedImageOutput(isExpandedImageOutput);
const onToggleTargetEnvs = (isExpandedTargetEnvs: boolean) =>
setIsExpandedTargetEnvs(isExpandedTargetEnvs);
const onToggleFSC = (isExpandedFSC: boolean) =>
setIsExpandedFSC(isExpandedFSC);
const onToggleContent = (isExpandedContent: boolean) =>
setIsExpandedContent(isExpandedContent);
const onToggleRegistration = (isExpandedRegistration: boolean) =>
setIsExpandedRegistration(isExpandedRegistration);
const onToggleImageDetail = (isExpandedImageDetail: boolean) =>
setIsExpandedImageDetail(isExpandedImageDetail);
const onToggleOscapDetails = (isExpandedOscapDetail: boolean) =>
setIsExpandedOscapDetail(isExpandedOscapDetail);
const onToggleFirstBoot = (isExpandableFirstBoot: boolean) =>
setIsExpandedFirstBoot(isExpandableFirstBoot);
type RevisitStepButtonProps = {
ariaLabel: string;
stepId: string;
};
const RevisitStepButton = ({ ariaLabel, stepId }: RevisitStepButtonProps) => {
return (
<Button
variant="link"
aria-label={ariaLabel}
component="span"
onClick={() => revisitStep(stepId)}
className="pf-u-p-0 pf-u-ml-xl"
isInline
>
Revisit step <ArrowRightIcon />
</Button>
);
};
const revisitStep = (stepId: string) => {
goToStepById(stepId);
};
const isFirstBootEnabled = useFlag('image-builder.firstboot.enabled');
return (
<>
<ExpandableSection
toggleContent={
<>
Image output{' '}
<RevisitStepButton
ariaLabel="Revisit Image output step"
stepId="step-image-output"
/>
</>
}
onToggle={(_event, isExpandedImageOutput) =>
onToggleImageOutput(isExpandedImageOutput)
}
isExpanded={isExpandedImageOutput}
isIndented
data-testid="image-output-expandable"
>
<ImageOutputList />
</ExpandableSection>
<ExpandableSection
toggleContent={
<>
Target environments{' '}
<RevisitStepButton
ariaLabel="Revisit Target environments step"
stepId="step-image-output"
/>
</>
}
onToggle={(_event, isExpandedTargetEnvs) =>
onToggleTargetEnvs(isExpandedTargetEnvs)
}
isExpanded={isExpandedTargetEnvs}
isIndented
data-testid="target-environments-expandable"
>
{environments.includes('aws') && <TargetEnvAWSList />}
{environments.includes('gcp') && <TargetEnvGCPList />}
{environments.includes('azure') && <TargetEnvAzureList />}
{environments.includes('oci') && <TargetEnvOciList />}
{environments.includes('vsphere') && (
<TextContent>
<Text component={TextVariants.h3}>
{targetOptions.vsphere} (.vmdk)
</Text>
<TargetEnvOtherList />
</TextContent>
)}
{environments.includes('vsphere-ova') && (
<TextContent>
<Text component={TextVariants.h3}>
{targetOptions['vsphere-ova']} (.ova)
</Text>
<TargetEnvOtherList />
</TextContent>
)}
{environments.includes('guest-image') && (
<TextContent>
<Text component={TextVariants.h3}>
{targetOptions['guest-image']} (.qcow2)
</Text>
<TargetEnvOtherList />
</TextContent>
)}
{environments.includes('image-installer') && (
<TextContent>
<Text component={TextVariants.h3}>
{targetOptions['image-installer']} (.iso)
</Text>
<TargetEnvOtherList />
</TextContent>
)}
{environments.includes('wsl') && (
<TextContent>
<Text component={TextVariants.h3}>
WSL - {targetOptions.wsl} (.tar.gz)
</Text>
<TargetEnvOtherList />
</TextContent>
)}
</ExpandableSection>
{isRhel(distribution) && (
<ExpandableSection
toggleContent={
<>
Registration{' '}
<RevisitStepButton
ariaLabel="Revisit Registration step"
stepId="step-register"
/>
</>
}
onToggle={(_event, isExpandedRegistration) =>
onToggleRegistration(isExpandedRegistration)
}
isExpanded={isExpandedRegistration}
isIndented
data-testid="registration-expandable"
>
{registrationType === 'register-later' && <RegisterLaterList />}
{registrationType.startsWith('register-now') && <RegisterNowList />}
</ExpandableSection>
)}
{oscapProfile && (
<ExpandableSection
toggleContent={
<>
OpenSCAP{' '}
<RevisitStepButton
ariaLabel="Revisit OpenSCAP step"
stepId="step-oscap"
/>
</>
}
onToggle={(_event, isExpandedOscapDetail) =>
onToggleOscapDetails(isExpandedOscapDetail)
}
isExpanded={isExpandedOscapDetail}
isIndented
data-testid="oscap-detail-expandable"
>
<OscapList />
</ExpandableSection>
)}
<ExpandableSection
toggleContent={
<>
File system configuration{' '}
<RevisitStepButton
ariaLabel="Revisit File system configuration step"
stepId="step-file-system"
/>
</>
}
onToggle={(_event, isExpandedFSC) => onToggleFSC(isExpandedFSC)}
isExpanded={isExpandedFSC}
isIndented
data-testid="file-system-configuration-expandable"
>
<FSCList />
</ExpandableSection>
<ExpandableSection
toggleContent={
<>
Content{' '}
<RevisitStepButton
ariaLabel="Revisit Content step"
stepId="wizard-custom-repositories"
/>
</>
}
onToggle={(_event, isExpandedContent) =>
onToggleContent(isExpandedContent)
}
isExpanded={isExpandedContent}
isIndented
data-testid="content-expandable"
>
{/* Intentional prop drilling for simplicity - To be removed */}
<ContentList snapshottingEnabled={snapshottingEnabled} />
</ExpandableSection>
{isFirstBootEnabled && (
<ExpandableSection
toggleContent={
<>
First boot{' '}
<RevisitStepButton
ariaLabel="Revisit First boot step"
stepId="wizard-first-boot"
/>
</>
}
onToggle={(_event, isExpandableFirstBoot) =>
onToggleFirstBoot(isExpandableFirstBoot)
}
isExpanded={isExpandableFirstBoot}
isIndented
data-testid="firstboot-expandable"
>
<FirstBootList />
</ExpandableSection>
)}
{(blueprintName || blueprintDescription) && (
<ExpandableSection
toggleContent={
<>
Details{' '}
<RevisitStepButton
ariaLabel="Revisit Details step"
stepId="step-details"
/>
</>
}
onToggle={(_event, isExpandedImageDetail) =>
onToggleImageDetail(isExpandedImageDetail)
}
isExpanded={isExpandedImageDetail}
isIndented
data-testid="image-details-expandable"
>
<DetailsList />
</ExpandableSection>
)}
</>
);
};
export default Review;

View file

@ -0,0 +1,284 @@
import React from 'react';
import {
Alert,
EmptyState,
EmptyStateHeader,
EmptyStateIcon,
Panel,
PanelMain,
Spinner,
} from '@patternfly/react-core';
import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
import {
ApiSnapshotForDate,
useListRepositoriesQuery,
} from '../../../../store/contentSourcesApi';
import { useAppSelector } from '../../../../store/hooks';
import {
selectCustomRepositories,
selectDistribution,
selectPackages,
selectGroups,
selectPartitions,
selectRecommendedRepositories,
} from '../../../../store/wizardSlice';
import PackageInfoNotAvailablePopover from '../Packages/components/PackageInfoNotAvailablePopover';
type repoPropType = {
repoUrl: string[] | undefined;
};
const RepoName = ({ repoUrl }: repoPropType) => {
const { data, isSuccess, isFetching, isError } = useListRepositoriesQuery(
{
// @ts-ignore if repoUrl is undefined the query is going to get skipped, so it's safe to ignore the linter here
url: repoUrl,
contentType: 'rpm',
origin: 'external',
},
{ skip: !repoUrl }
);
const errorLoading = () => {
return (
<Alert
variant="danger"
isInline
isPlain
title="Error loading repository name"
/>
);
};
return (
<>
{/*
this might be a tad bit hacky
"isSuccess" indicates only that the query fetched successfuly, but it
doesn't differentiate between a scenario when the repository was found
in the response and when it was not
for this reason I've split the "isSuccess" into two paths:
- query finished and the repo was found -> render the name of the repo
- query finished, but the repo was not found -> render an error
*/}
{isSuccess && data.data?.[0]?.name && <p>{data.data?.[0].name}</p>}
{isSuccess && !data.data?.[0]?.name && errorLoading()}
{isFetching && <Spinner size="md" />}
{isError && errorLoading()}
</>
);
};
export const FSReviewTable = () => {
const partitions = useAppSelector(selectPartitions);
return (
<Panel isScrollable>
<PanelMain maxHeight="30ch">
<Table aria-label="File system configuration table" variant="compact">
<Thead>
<Tr>
<Th>Mount point</Th>
<Th>File system type</Th>
<Th>Minimum size</Th>
</Tr>
</Thead>
<Tbody data-testid="file-system-configuration-tbody-review">
{partitions.map((partition, partitionIndex) => (
<Tr key={partitionIndex}>
<Td className="pf-m-width-30">{partition.mountpoint}</Td>
<Td className="pf-m-width-30">xfs</Td>
<Td className="pf-m-width-30">
{parseInt(partition.min_size).toString()} {partition.unit}
</Td>
</Tr>
))}
</Tbody>
</Table>
</PanelMain>
</Panel>
);
};
const Error = () => {
return (
<Alert title="Repositories unavailable" variant="danger" isPlain isInline>
Repositories cannot be reached, try again later.
</Alert>
);
};
const Loading = () => {
return (
<EmptyState>
<EmptyStateHeader
titleText="Loading"
icon={<EmptyStateIcon icon={Spinner} />}
headingLevel="h4"
/>
</EmptyState>
);
};
export const SnapshotTable = ({
snapshotForDate,
}: {
snapshotForDate: ApiSnapshotForDate[];
}) => {
const { data, isSuccess, isLoading, isError } = useListRepositoriesQuery({
uuid: snapshotForDate.map(({ repository_uuid }) => repository_uuid).join(),
origin: 'red_hat,external', // Make sure to show both redhat and custom
});
const isAfterSet = new Set(
snapshotForDate
.filter(({ is_after }) => is_after)
.map(({ repository_uuid }) => repository_uuid)
);
const stringToDateToMMDDYYYY = (strDate: string) => {
const date = new Date(strDate);
return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date
.getDate()
.toString()
.padStart(2, '0')}/${date.getFullYear()}`;
};
return (
(isError && <Error />) ||
(isLoading && <Loading />) ||
(isSuccess && (
<Panel isScrollable>
<PanelMain maxHeight="30ch">
<Table aria-label="Packages table" variant="compact">
<Thead>
<Tr>
<Th>Name</Th>
<Th>Last snapshot date</Th>
</Tr>
</Thead>
<Tbody data-testid="packages-tbody-review">
{data?.data?.map(({ uuid, name, last_snapshot }, pkgIndex) => (
<Tr key={pkgIndex}>
<Td>{name}</Td>
<Td>
{uuid && isAfterSet.has(uuid) ? (
<Alert
title={
last_snapshot?.created_at
? stringToDateToMMDDYYYY(last_snapshot.created_at)
: 'N/A'
}
variant="warning"
isPlain
isInline
/>
) : last_snapshot?.created_at ? (
stringToDateToMMDDYYYY(last_snapshot.created_at)
) : (
'N/A'
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
</PanelMain>
</Panel>
))
);
};
export const PackagesTable = () => {
const packages = useAppSelector(selectPackages);
const groups = useAppSelector(selectGroups);
return (
<Panel isScrollable>
<PanelMain maxHeight="30ch">
<Table aria-label="Packages table" variant="compact">
<Thead>
<Tr>
<Th>Name</Th>
<Th>
Description <PackageInfoNotAvailablePopover />
</Th>
<Th>Package repository</Th>
</Tr>
</Thead>
<Tbody data-testid="packages-tbody-review">
{packages.map((pkg, pkgIndex) => (
<Tr key={pkgIndex}>
<Td className="pf-m-width-30">{pkg.name}</Td>
<Td>{pkg.summary ? pkg.summary : 'Not available'}</Td>
<Td className="pf-m-width-30">
{pkg.repository === 'distro'
? 'Red Hat repository'
: pkg.repository === 'custom'
? 'Custom repository'
: pkg.repository === 'recommended'
? 'EPEL Everything x86_64'
: 'Not available'}
</Td>
</Tr>
))}
{groups.map((grp, grpIndex) => (
<Tr key={grpIndex}>
<Td className="pf-m-width-30">@{grp.name}</Td>
<Td>{grp.description ? grp.description : 'Not available'}</Td>
<Td className="pf-m-width-30">
{grp.repository === 'distro'
? 'Red Hat repository'
: grp.repository === 'custom'
? 'Custom repository'
: grp.repository === 'recommended'
? 'EPEL Everything x86_64'
: 'Not available'}
</Td>
</Tr>
))}
</Tbody>
</Table>
</PanelMain>
</Panel>
);
};
export const RepositoriesTable = () => {
const distribution = useAppSelector(selectDistribution);
const repositoriesList = useAppSelector(selectCustomRepositories);
const recommendedRepositoriesList = useAppSelector(
selectRecommendedRepositories
);
return (
<Panel isScrollable>
<PanelMain maxHeight="30ch">
<Table aria-label="Custom repositories table" variant="compact">
<Thead>
<Tr>
<Th>Name</Th>
</Tr>
</Thead>
<Tbody data-testid="repositories-tbody-review">
{repositoriesList?.map((repo, repoIndex) => (
<Tr key={repoIndex + 1}>
<Td className="pf-m-width-60">
<RepoName repoUrl={repo.baseurl} />
</Td>
</Tr>
))}
{recommendedRepositoriesList.length > 0 && (
<Tr key={0}>
<Td className="pf-m-width-60">
EPEL {distribution.startsWith('rhel-8') ? '8' : '9'}{' '}
Everything x86_64
</Td>
</Tr>
)}
</Tbody>
</Table>
</PanelMain>
</Panel>
);
};

View file

@ -0,0 +1,766 @@
import React, { useEffect, useMemo } from 'react';
import {
Alert,
Button,
Popover,
Text,
TextContent,
TextList,
TextListItem,
TextListVariants,
TextListItemVariants,
TextVariants,
FormGroup,
} from '@patternfly/react-core';
import { ExclamationTriangleIcon } from '@patternfly/react-icons';
import ActivationKeyInformation from './../Registration/ActivationKeyInformation';
import {
PackagesTable,
RepositoriesTable,
SnapshotTable,
} from './ReviewStepTables';
import { FSReviewTable } from './ReviewStepTables';
import {
RELEASES,
RHEL_8,
RHEL_8_FULL_SUPPORT,
RHEL_8_MAINTENANCE_SUPPORT,
RHEL_9,
targetOptions,
UNIT_GIB,
} from '../../../../constants';
import { useListSnapshotsByDateMutation } from '../../../../store/contentSourcesApi';
import { useAppSelector } from '../../../../store/hooks';
import { useGetSourceListQuery } from '../../../../store/provisioningApi';
import { useShowActivationKeyQuery } from '../../../../store/rhsmApi';
import {
selectActivationKey,
selectArchitecture,
selectAwsAccountId,
selectAwsShareMethod,
selectAzureShareMethod,
selectAzureSource,
selectAzureResourceGroup,
selectAzureSubscriptionId,
selectAzureTenantId,
selectAwsSourceId,
selectBlueprintDescription,
selectBlueprintName,
selectCustomRepositories,
selectDistribution,
selectGcpAccountType,
selectGcpEmail,
selectGcpShareMethod,
selectPackages,
selectGroups,
selectRegistrationType,
selectFileSystemPartitionMode,
selectRecommendedRepositories,
selectSnapshotDate,
selectUseLatest,
selectPartitions,
selectFirstBootScript,
} from '../../../../store/wizardSlice';
import {
convertMMDDYYYYToYYYYMMDD,
toMonthAndYear,
yyyyMMddFormat,
} from '../../../../Utilities/time';
import {
Partition,
getConversionFactor,
} from '../FileSystem/FileSystemConfiguration';
import { MinimumSizePopover } from '../FileSystem/FileSystemTable';
import { MajorReleasesLifecyclesChart } from '../ImageOutput/ReleaseLifecycle';
import OscapProfileInformation from '../Oscap/OscapProfileInformation';
import { PopoverActivation } from '../Registration/ActivationKeysList';
const ExpirationWarning = () => {
return (
<div className="pf-u-mr-sm pf-u-font-size-sm pf-v5-u-warning-color-200">
<ExclamationTriangleIcon /> Expires 14 days after creation
</div>
);
};
export const ImageOutputList = () => {
const distribution = useAppSelector(selectDistribution);
const arch = useAppSelector(selectArchitecture);
return (
<TextContent>
{distribution === RHEL_8 && (
<>
<Text className="pf-v5-u-font-size-sm">
{RELEASES.get(distribution)} will be supported through{' '}
{toMonthAndYear(RHEL_8_FULL_SUPPORT[1])}, with optional ELS support
through {toMonthAndYear(RHEL_8_MAINTENANCE_SUPPORT[1])}. Consider
building an image with {RELEASES.get(RHEL_9)} to extend the support
period.
</Text>
<FormGroup label="Release lifecycle">
<MajorReleasesLifecyclesChart />
</FormGroup>
<br />
</>
)}
<TextList component={TextListVariants.dl}>
<TextListItem
component={TextListItemVariants.dt}
className="pf-u-min-width"
>
Release
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{RELEASES.get(distribution)}
</TextListItem>
<TextListItem component={TextListItemVariants.dt}>
Architecture
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>{arch}</TextListItem>
</TextList>
</TextContent>
);
};
export const FSCList = () => {
const fileSystemPartitionMode = useAppSelector(selectFileSystemPartitionMode);
const partitions = useAppSelector(selectPartitions);
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) <MinimumSizePopover />
</TextListItem>
<MinSize partitions={partitions} />
</>
)}
</TextList>
</TextContent>
);
};
type MinSizeProps = {
partitions: Partition[];
};
export const MinSize = ({ partitions }: MinSizeProps) => {
let minSize = '';
if (partitions) {
let size = 0;
for (const partition of partitions) {
size += Number(partition.min_size) * getConversionFactor(partition.unit);
}
size = Number((size / UNIT_GIB).toFixed(1));
if (size < 1) {
minSize = `Less than 1 GiB`;
} else {
minSize = `${size} GiB`;
}
}
return (
<TextListItem component={TextListItemVariants.dd}> {minSize} </TextListItem>
);
};
export const TargetEnvAWSList = () => {
const { isSuccess } = useGetSourceListQuery({
provider: 'aws',
});
const awsAccountId = useAppSelector(selectAwsAccountId);
const awsShareMethod = useAppSelector(selectAwsShareMethod);
const sourceId = useAppSelector(selectAwsSourceId);
const { source } = useGetSourceListQuery(
{
provider: 'aws',
},
{
selectFromResult: ({ data }) => ({
source: data?.data?.find((source) => source.id === sourceId),
}),
}
);
return (
<TextContent>
<Text component={TextVariants.h3}>{targetOptions.aws}</Text>
<TextList component={TextListVariants.dl}>
<TextListItem
component={TextListItemVariants.dt}
className="pf-u-min-width"
>
Image type
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
Red Hat hosted image
<br />
<ExpirationWarning />
</TextListItem>
<TextListItem component={TextListItemVariants.dt}>
Shared to account
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{awsAccountId}
</TextListItem>
<TextListItem component={TextListItemVariants.dt}>
{awsShareMethod === 'sources' ? 'Source' : null}
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{isSuccess && awsShareMethod === 'sources' ? source?.name : null}
</TextListItem>
<TextListItem component={TextListItemVariants.dt}>
Default region
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
us-east-1
</TextListItem>
</TextList>
</TextContent>
);
};
export const TargetEnvGCPList = () => {
const accountType = useAppSelector(selectGcpAccountType);
const sharedMethod = useAppSelector(selectGcpShareMethod);
const email = useAppSelector(selectGcpEmail);
return (
<TextContent>
<Text component={TextVariants.h3}>{targetOptions.gcp}</Text>
<TextList component={TextListVariants.dl}>
<TextListItem
component={TextListItemVariants.dt}
className="pf-u-min-width"
>
Image type
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
Red Hat hosted image
<br />
<ExpirationWarning />
</TextListItem>
<>
{sharedMethod === 'withInsights' ? (
<>
<TextListItem component={TextListItemVariants.dt}>
Shared with
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
Red Hat Insights only
<br />
</TextListItem>
</>
) : (
<>
<TextListItem component={TextListItemVariants.dt}>
Account type
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{accountType === 'group'
? 'Google group'
: accountType === 'serviceAccount'
? 'Service account'
: accountType === 'user'
? 'Google account'
: 'Domain'}
</TextListItem>
<TextListItem component={TextListItemVariants.dt}>
{accountType === 'domain' ? 'Domain' : 'Principal'}
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{email || accountType}
</TextListItem>
</>
)}
</>
</TextList>
</TextContent>
);
};
export const TargetEnvAzureList = () => {
const { data: rawAzureSources, isSuccess: isSuccessAzureSources } =
useGetSourceListQuery({ provider: 'azure' });
const shareMethod = useAppSelector(selectAzureShareMethod);
const tenantId = useAppSelector(selectAzureTenantId);
const azureSource = useAppSelector(selectAzureSource);
const azureResourceGroup = useAppSelector(selectAzureResourceGroup);
const subscriptionId = useAppSelector(selectAzureSubscriptionId);
return (
<TextContent>
<Text component={TextVariants.h3}>{targetOptions.azure}</Text>
<TextList component={TextListVariants.dl}>
<TextListItem
component={TextListItemVariants.dt}
className="pf-u-min-width"
>
Image type
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
Red Hat hosted image
<br />
<ExpirationWarning />
</TextListItem>
{shareMethod === 'sources' && isSuccessAzureSources && (
<>
<TextListItem component={TextListItemVariants.dt}>
Azure Source
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{
rawAzureSources.data?.find(
(source) => source.id === azureSource
)?.name
}
</TextListItem>
</>
)}
{shareMethod === 'manual' && (
<>
<TextListItem component={TextListItemVariants.dt}>
Azure tenant ID
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{tenantId}
</TextListItem>
<TextListItem component={TextListItemVariants.dt}>
Subscription ID
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{subscriptionId}
</TextListItem>
</>
)}
<TextListItem component={TextListItemVariants.dt}>
Resource group
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{azureResourceGroup}
</TextListItem>
</TextList>
</TextContent>
);
};
export const TargetEnvOciList = () => {
return (
<TextContent>
<Text component={TextVariants.h3}>{targetOptions.oci}</Text>
<TextList component={TextListVariants.dl}>
<TextListItem
component={TextListItemVariants.dt}
className="pf-u-min-width"
>
Object Storage URL
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
The URL for the built image will be ready to copy
</TextListItem>
</TextList>
</TextContent>
);
};
export const TargetEnvOtherList = () => {
return (
<>
<TextList component={TextListVariants.dl}>
<TextListItem
component={TextListItemVariants.dt}
className="pf-u-min-width"
>
Image type
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
Built image will be available for download
</TextListItem>
</TextList>
</>
);
};
export const ContentList = ({
snapshottingEnabled,
}: {
snapshottingEnabled: boolean;
}) => {
const customRepositories = useAppSelector(selectCustomRepositories);
const packages = useAppSelector(selectPackages);
const groups = useAppSelector(selectGroups);
const recommendedRepositories = useAppSelector(selectRecommendedRepositories);
const snapshotDate = useAppSelector(selectSnapshotDate);
const useLatest = useAppSelector(selectUseLatest);
const customAndRecommendedRepositoryUUIDS = useMemo(
() =>
[
...customRepositories.map(({ id }) => id),
...recommendedRepositories.map(({ uuid }) => uuid),
] as string[],
[customRepositories, recommendedRepositories]
);
const [listSnapshotsByDate, { data, isSuccess, isLoading, isError }] =
useListSnapshotsByDateMutation();
useEffect(() => {
listSnapshotsByDate({
apiListSnapshotByDateRequest: {
repository_uuids: customAndRecommendedRepositoryUUIDS,
date: useLatest
? yyyyMMddFormat(new Date())
: convertMMDDYYYYToYYYYMMDD(snapshotDate),
},
});
}, [
customAndRecommendedRepositoryUUIDS,
listSnapshotsByDate,
snapshotDate,
useLatest,
]);
const duplicatePackages = packages.filter(
(item, index) => packages.indexOf(item) !== index
);
const noRepositoriesSelected =
customAndRecommendedRepositoryUUIDS.length === 0;
const hasSnapshotDateAfter = data?.data?.some(({ is_after }) => is_after);
const snapshottingText = useMemo(() => {
switch (true) {
case noRepositoriesSelected:
return 'No repositories selected';
case isLoading:
return '';
case useLatest:
return 'Use latest';
case !!snapshotDate:
return `State as of ${snapshotDate}`;
default:
return '';
}
}, [noRepositoriesSelected, isLoading, useLatest, snapshotDate]);
return (
<>
<TextContent>
<TextList component={TextListVariants.dl}>
{snapshottingEnabled ? (
<>
<TextListItem
component={TextListItemVariants.dt}
className="pf-u-min-width"
>
Repository snapshot
</TextListItem>
<TextListItem
component={TextListItemVariants.dd}
data-testid="snapshot-method"
>
<Popover
position="bottom"
headerContent={
useLatest
? 'Repositories as of today'
: `Repositories as of ${snapshotDate}`
}
hasAutoWidth
minWidth="60rem"
bodyContent={
<SnapshotTable snapshotForDate={data?.data || []} />
}
>
<Button
variant="link"
aria-label="Snapshot method"
className="pf-u-p-0"
isDisabled={noRepositoriesSelected || isLoading || isError}
isLoading={isLoading}
>
{snapshottingText}
</Button>
</Popover>
{!useLatest &&
!isLoading &&
isSuccess &&
hasSnapshotDateAfter ? (
<Alert
variant="warning"
isInline
isPlain
title="A snapshot for this date is not available for some repositories."
/>
) : (
''
)}
</TextListItem>
</>
) : (
''
)}
<TextListItem component={TextListItemVariants.dt}>
Custom repositories
</TextListItem>
<TextListItem
component={TextListItemVariants.dd}
data-testid="custom-repositories-count"
>
{customRepositories?.length + recommendedRepositories.length > 0 ? (
<Popover
position="bottom"
headerContent="Custom repositories"
hasAutoWidth
minWidth="30rem"
bodyContent={<RepositoriesTable />}
>
<Button
variant="link"
aria-label="About custom repositories"
className="pf-u-p-0"
>
{customRepositories?.length +
recommendedRepositories.length || 0}
</Button>
</Popover>
) : (
0
)}
</TextListItem>
<TextListItem
component={TextListItemVariants.dt}
className="pf-u-min-width"
>
Additional packages
</TextListItem>
<TextListItem
component={TextListItemVariants.dd}
data-testid="chosen-packages-count"
>
{packages?.length > 0 || groups?.length > 0 ? (
<Popover
position="bottom"
headerContent="Additional packages"
hasAutoWidth
minWidth="60rem"
bodyContent={<PackagesTable />}
>
<Button
variant="link"
aria-label="About packages"
className="pf-u-p-0"
>
{packages?.length + groups?.length}
</Button>
</Popover>
) : (
0
)}
</TextListItem>
</TextList>
</TextContent>
{duplicatePackages.length > 0 && (
<Alert
title="Can not guarantee where some selected packages will come from"
variant="warning"
isInline
>
Some of the packages added to this image belong to multiple added
repositories. We can not guarantee which repository the package will
come from.
</Alert>
)}
</>
);
};
export const RegisterLaterList = () => {
return (
<TextContent>
<TextList component={TextListVariants.dl}>
<TextListItem
component={TextListItemVariants.dt}
className="pf-u-min-width"
>
Registration type
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
Register the system later
</TextListItem>
</TextList>
</TextContent>
);
};
export const RegisterNowList = () => {
const activationKey = useAppSelector(selectActivationKey);
const registrationType = useAppSelector(selectRegistrationType);
const { isError } = useShowActivationKeyQuery(
// @ts-ignore type of 'activationKey' might not be strictly compatible with the expected type for 'name'.
{ name: activationKey },
{
skip: !activationKey,
}
);
return (
<>
<TextContent>
<TextList component={TextListVariants.dl}>
<TextListItem
component={TextListItemVariants.dt}
className="pf-u-min-width"
>
Registration type
</TextListItem>
<TextListItem
component={TextListItemVariants.dd}
data-testid="review-registration"
>
<TextList isPlain>
{registrationType?.startsWith('register-now') && (
<TextListItem>
Register with Red Hat Subscription Manager (RHSM)
<br />
</TextListItem>
)}
{(registrationType === 'register-now-insights' ||
registrationType === 'register-now-rhc') && (
<TextListItem>
Connect to Red Hat Insights
<br />
</TextListItem>
)}
{registrationType === 'register-now-rhc' && (
<TextListItem>
Use remote host configuration (rhc) utility
<br />
</TextListItem>
)}
</TextList>
</TextListItem>
<TextListItem component={TextListItemVariants.dt}>
Activation key <PopoverActivation />
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
<ActivationKeyInformation />
</TextListItem>
</TextList>
</TextContent>
{isError && (
<Alert
title="Information about the activation key unavailable"
variant="danger"
isPlain
isInline
>
Information about the activation key cannot be loaded. Please check
the key was not removed and try again later.
</Alert>
)}
</>
);
};
export const DetailsList = () => {
const blueprintName = useAppSelector(selectBlueprintName);
const blueprintDescription = useAppSelector(selectBlueprintDescription);
return (
<TextContent>
<TextList component={TextListVariants.dl}>
{blueprintName && (
<>
<TextListItem
component={TextListItemVariants.dt}
className="pf-u-min-width"
>
Blueprint name
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{blueprintName}
</TextListItem>
</>
)}
{blueprintDescription && (
<>
<TextListItem
component={TextListItemVariants.dt}
className="pf-u-min-width"
>
Description
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{blueprintDescription}
</TextListItem>
</>
)}
</TextList>
</TextContent>
);
};
export const OscapList = () => {
return <OscapProfileInformation />;
};
export const FirstBootList = () => {
const isFirstbootEnabled = !!useAppSelector(selectFirstBootScript);
return (
<TextContent>
<TextList component={TextListVariants.dl}>
<TextListItem
component={TextListItemVariants.dt}
className="pf-u-min-width"
>
First boot script
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{isFirstbootEnabled ? 'Enabled' : 'Disabled'}
</TextListItem>
</TextList>
</TextContent>
);
};

View file

@ -0,0 +1,33 @@
import React from 'react';
import { Form, Text, Title } from '@patternfly/react-core';
import Review from './ReviewStep';
import { useAppSelector } from '../../../../store/hooks';
import {
selectBlueprintDescription,
selectBlueprintName,
} from '../../../../store/wizardSlice';
const ReviewStep = ({
snapshottingEnabled,
}: {
snapshottingEnabled: boolean;
}) => {
const blueprintName = useAppSelector(selectBlueprintName);
const blueprintDescription = useAppSelector(selectBlueprintDescription);
return (
<Form>
<Title headingLevel="h1" size="xl">
Review {blueprintName} blueprint
</Title>
{blueprintDescription && <Text>{blueprintDescription}</Text>}
{/* Intentional prop drilling for simplicity - To be removed */}
<Review snapshottingEnabled={snapshottingEnabled} />
</Form>
);
};
export default ReviewStep;