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:
parent
b1e5a8c7c6
commit
4fb37c187e
93 changed files with 20 additions and 22 deletions
|
|
@ -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 doesn’t 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
130
src/Components/CreateImageWizard/steps/Review/Footer/Footer.tsx
Normal file
130
src/Components/CreateImageWizard/steps/Review/Footer/Footer.tsx
Normal 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;
|
||||
312
src/Components/CreateImageWizard/steps/Review/ReviewStep.tsx
Normal file
312
src/Components/CreateImageWizard/steps/Review/ReviewStep.tsx
Normal 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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
33
src/Components/CreateImageWizard/steps/Review/index.tsx
Normal file
33
src/Components/CreateImageWizard/steps/Review/index.tsx
Normal 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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue