Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐾 Add a fast Create Plan page - step 7 #899

Merged
merged 1 commit into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/eslint-plugin/cspell.wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ fullname
cncf
omitempty
nics
NICS
pnic
virtio
SSHA
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
"Add source and target providers for the migration.": "Add source and target providers for the migration.",
"All discovered networks have been mapped to the default network.": "All discovered networks have been mapped to the default network.",
"All discovered storages have been mapped to the default storage.": "All discovered storages have been mapped to the default storage.",
"All networks detected on the selected VMs require a mapping.": "All networks detected on the selected VMs require a mapping.",
"All storages detected on the selected VMs require a mapping.": "All storages detected on the selected VMs require a mapping.",
"API Error": "API Error",
"Application credential ID": "Application credential ID",
"Application credential name": "Application credential name",
Expand Down Expand Up @@ -64,9 +66,8 @@
"Controls the interval at which a new snapshot is requested prior to initiating a warm migration. The default value is 60 minutes.": "Controls the interval at which a new snapshot is requested prior to initiating a warm migration. The default value is 60 minutes.",
"Copied": "Copied",
"Copy": "Copy",
"Create": "Create",
"Create a migration plan and select VMs from the source provider for migration.": "Create a migration plan and select VMs from the source provider for migration.",
"Create and edit": "Create and edit",
"Create and start": "Create and start",
"Create NetworkMap": "Create NetworkMap",
"Create new provider": "Create new provider",
"Create plan": "Create plan",
Expand Down Expand Up @@ -160,6 +161,7 @@
"If true, the provider's CA certificate won't be validated.": "If true, the provider's CA certificate won't be validated.",
"If true, the provider's TLS certificate won't be validated.": "If true, the provider's TLS certificate won't be validated.",
"Image": "Image",
"Incomplete mapping": "Incomplete mapping",
"Information concerns": "Information concerns",
"Invalid password": "Invalid password",
"Invalid username": "Invalid username",
Expand Down Expand Up @@ -194,6 +196,8 @@
"Migrations (last 7 days)": "Migrations (last 7 days)",
"Migrations for virtualization": "Migrations for virtualization",
"MTU": "MTU",
"Multiple NICs mapped to Pod Networking ": "Multiple NICs mapped to Pod Networking ",
"Multiple NICs on the same network": "Multiple NICs on the same network",
"Must gather cleanup after (hours)": "Must gather cleanup after (hours)",
"Name": "Name",
"Name is primarily intended for creation idempotence and configuration definition. Cannot be updated.": "Name is primarily intended for creation idempotence and configuration definition. Cannot be updated.",
Expand All @@ -212,6 +216,8 @@
"Networks": "Networks",
"Networks used by the selected VMs": "Networks used by the selected VMs",
"New name was generated for the Network Map due to naming conflict.": "New name was generated for the Network Map due to naming conflict.",
"New name was generated for the Storage Map due to naming conflict.": "New name was generated for the Storage Map due to naming conflict.",
"NICs with empty NIC profile": "NICs with empty NIC profile",
"No credentials found.": "No credentials found.",
"No inventory data available.": "No inventory data available.",
"No NetworkMaps found.": "No NetworkMaps found.",
Expand All @@ -227,7 +233,6 @@
"No secret.": "No secret.",
"No StorageMaps found.": "No StorageMaps found.",
"No storages in this category": "No storages in this category",
"No target provider exists ": "No target provider exists ",
"Not Ready": "Not Ready",
"NUMA": "NUMA",
"Number of cluster in provider": "Number of cluster in provider",
Expand Down Expand Up @@ -344,6 +349,7 @@
"Storage": "Storage",
"Storage classes": "Storage classes",
"Storage domains": "Storage domains",
"Storage Map name re-generated": "Storage Map name re-generated",
"Storage map:": "Storage map:",
"Storage mappings have been re-generated": "Storage mappings have been re-generated",
"StorageMaps": "StorageMaps",
Expand Down Expand Up @@ -413,6 +419,9 @@
"Virtual Machine Migrations (last 7 days)": "Virtual Machine Migrations (last 7 days)",
"Virtual machines": "Virtual machines",
"Virtual Machines": "Virtual Machines",
"VM(s) with more than one interface mapped to Pod Networking were detected.": "VM(s) with more than one interface mapped to Pod Networking were detected.",
"VM(s) with multiple NICs on the same network were detected.": "VM(s) with multiple NICs on the same network were detected.",
"VM(s) with NICs that are not linked with a NIC profile were detected.": "VM(s) with NICs that are not linked with a NIC profile were detected.",
"VMs": "VMs",
"VMware only: vSphere product name.": "VMware only: vSphere product name.",
"VMware Virtual Disk Development Kit (VDDK) image, for example: quay.io/kubev2v/vddk:latest .": "VMware Virtual Disk Development Kit (VDDK) image, for example: quay.io/kubev2v/vddk:latest .",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,16 @@ import SectionHeading from 'src/components/headers/SectionHeading';
import { PlanCreateProgress } from 'src/modules/Plans/views/create';
import { ForkliftTrans, useForkliftTranslation } from 'src/utils/i18n';

import { Alert, AlertVariant, Button, Flex, FlexItem, PageSection } from '@patternfly/react-core';
import { LoadingDots } from '@kubev2v/common';
import { Alert, Button, Flex, FlexItem, PageSection } from '@patternfly/react-core';
import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon';

import { PlansCreateForm } from './components/PlansCreateForm';
import { StateAlerts } from './components/StateAlerts';
import { startCreate } from './reducer/actions';
import { GeneralAlerts } from './types';
import { isDone } from './reducer/helpers';
import { useFetchEffects } from './useFetchEffects';
import { useSaveEffect } from './useSaveEffect';

const generalMessages = (
t: (key: string) => string,
): { [key in GeneralAlerts]: { title: string; body: string } } => ({
NEXT_VALID_PROVIDER_SELECTED: { title: t('Error'), body: t('No target provider exists ') },
});

const ProvidersCreateVmMigrationPage: FC = () => {
const { t } = useForkliftTranslation();
const history = useHistory();
Expand All @@ -35,25 +29,29 @@ const ProvidersCreateVmMigrationPage: FC = () => {
return <></>;
}

if (!isDone(state.flow.initialLoading) && !state.flow.apiError) {
return <LoadingDots />;
}

return (
<PageSection variant="light">
<Alert
className="co-alert forklift--create-vm-migration-plan--alert"
customIcon={<BellIcon />}
variant="info"
title={t('How to create a migration plan')}
>
<ForkliftTrans>
To migrate virtual machines select target provider, namespace, mappings and click the{' '}
<strong>Create</strong> button to crete the plan.
</ForkliftTrans>
</Alert>

<PlanCreateProgress step="migrate" />
<PlansCreateForm state={state} dispatch={dispatch}>
<Alert
className="co-alert forklift--create-vm-migration-plan--alert"
customIcon={<BellIcon />}
variant="info"
title={t('How to create a migration plan')}
>
<ForkliftTrans>
To migrate virtual machines select target provider, namespace, mappings and click the{' '}
<strong>Create</strong> button to crete the plan.
</ForkliftTrans>
</Alert>

<SectionHeading text={t('Migrate')} />
<PlanCreateProgress step="migrate" />

<PlansCreateForm state={state} dispatch={dispatch} />
<SectionHeading text={t('Migrate')} />
</PlansCreateForm>
{state.flow.apiError && (
<Alert
className="co-alert co-alert--margin-top"
Expand All @@ -64,29 +62,18 @@ const ProvidersCreateVmMigrationPage: FC = () => {
{state.flow.apiError.message || state.flow.apiError.toString()}
</Alert>
)}
<StateAlerts
variant={AlertVariant.danger}
messages={Array.from(state.alerts.general.errors).map((key) => ({
key,
...generalMessages(t)[key],
}))}
/>
<Flex>
<FlexItem>
<Button
variant="primary"
isDisabled={Object.values(state.validation).some(
(validation) => validation === 'error',
)}
isDisabled={
!!state.flow.apiError ||
Object.values(state.validation).some((validation) => validation === 'error')
}
isLoading={isLoading}
onClick={() => dispatch(startCreate())}
>
{t('Create and edit')}
</Button>
</FlexItem>
<FlexItem>
<Button variant="secondary" isDisabled={true} isLoading={isLoading}>
{t('Create and start')}
{t('Create')}
</Button>
</FlexItem>
<FlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export const EditableDescriptionItem: FC<{
content: ReactNode;
ariaEditLabel: string;
onEdit: () => void;
}> = ({ title, content, ariaEditLabel = 'Edit', onEdit }) => (
isDisabled?: boolean;
}> = ({ title, content, ariaEditLabel = 'Edit', onEdit, isDisabled = false }) => (
<DescriptionListGroup>
<DescriptionListTerm>{title}</DescriptionListTerm>
<DescriptionListDescription>
Expand All @@ -25,9 +26,9 @@ export const EditableDescriptionItem: FC<{
className="forklift-page-editable-description-item-button"
style={{ paddingTop: 0 }}
variant="plain"
icon={<PencilAltIcon />}
aria-label={ariaEditLabel}
onClick={onEdit}
{...(isDisabled
? {}
: { icon: <PencilAltIcon />, onClick: onEdit, 'aria-Label': ariaEditLabel })}
/>
</div>
</DescriptionListDescription>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import { StateAlerts } from './StateAlerts';

const buildNetworkMessages = (
t: (key: string) => string,
): { [key in NetworkAlerts]: { title: string; body: string } } => ({
): { [key in NetworkAlerts]: { title: string; body: string; blocker?: boolean } } => ({
NET_MAP_NAME_REGENERATED: {
title: t('Network Map name re-generated'),
body: t('New name was generated for the Network Map due to naming conflict.'),
Expand All @@ -63,17 +63,46 @@ const buildNetworkMessages = (
title: t('Network mappings have been re-generated'),
body: t('All discovered networks have been mapped to the default network.'),
},
MULTIPLE_NICS_ON_THE_SAME_NETWORK: {
title: t('Multiple NICs on the same network'),
body: t('VM(s) with multiple NICs on the same network were detected.'),
},
OVIRT_NICS_WITH_EMPTY_PROFILE: {
title: t('NICs with empty NIC profile'),
body: t('VM(s) with NICs that are not linked with a NIC profile were detected.'),
blocker: true,
},
UNMAPPED_NETWORKS: {
title: t('Incomplete mapping'),
body: t('All networks detected on the selected VMs require a mapping.'),
blocker: true,
},
MULTIPLE_NICS_MAPPED_TO_POD_NETWORKING: {
title: t('Multiple NICs mapped to Pod Networking '),
body: t('VM(s) with more than one interface mapped to Pod Networking were detected.'),
blocker: true,
},
});
const buildStorageMessages = (
t: (key: string) => string,
): { [key in StorageAlerts]: { title: string; body: string } } => ({
): { [key in StorageAlerts]: { title: string; body: string; blocker?: boolean } } => ({
STORAGE_MAPPING_REGENERATED: {
title: t('Storage mappings have been re-generated'),
body: t('All discovered storages have been mapped to the default storage.'),
},
STORAGE_MAP_NAME_REGENERATED: {
title: t('Storage Map name re-generated'),
body: t('New name was generated for the Storage Map due to naming conflict.'),
},
UNMAPPED_STORAGES: {
title: t('Incomplete mapping'),
body: t('All storages detected on the selected VMs require a mapping.'),
blocker: true,
},
});

export const PlansCreateForm = ({
children,
state: {
underConstruction: { plan, netMap, storageMap },
validation,
Expand All @@ -99,6 +128,7 @@ export const PlansCreateForm = ({
},
dispatch,
}: {
children?;
state: CreateVmMigrationPageState;
dispatch: (action: PageAction<unknown, unknown>) => void;
}) => {
Expand Down Expand Up @@ -137,6 +167,7 @@ export const PlansCreateForm = ({
}
>
<DrawerContentBody>
{children}
<DescriptionList
className="forklift-create-provider-edit-section"
columnModifier={{
Expand All @@ -160,6 +191,7 @@ export const PlansCreateForm = ({
id="planName"
value={plan.metadata.name}
validated={validation.planName}
isDisabled={flow.editingDone}
onChange={(value) => dispatch(setPlanName(value?.trim() ?? ''))}
/>
</FormGroup>
Expand All @@ -170,6 +202,7 @@ export const PlansCreateForm = ({
content={plan.metadata.name}
ariaEditLabel={t('Edit plan name')}
onEdit={() => setIsNameEdited(true)}
isDisabled={flow.editingDone}
/>
)}
<DetailsItem
Expand Down Expand Up @@ -211,6 +244,7 @@ export const PlansCreateForm = ({
onChange={(value) => dispatch(setPlanTargetProvider(value))}
validated={validation.targetProvider}
id="targetProvider"
isDisabled={flow.editingDone}
>
{[
<FormSelectOption
Expand Down Expand Up @@ -247,6 +281,7 @@ export const PlansCreateForm = ({
}
ariaEditLabel={t('Edit target provider')}
onEdit={() => setIsTargetProviderEdited(true)}
isDisabled={flow.editingDone}
/>
)}
{isTargetNamespaceEdited ||
Expand All @@ -264,6 +299,7 @@ export const PlansCreateForm = ({
onChange={(value) => dispatch(setPlanTargetNamespace(value))}
validated={validation.targetNamespace}
id="targetNamespace"
isDisabled={flow.editingDone}
>
{[
<FormSelectOption
Expand Down Expand Up @@ -302,6 +338,7 @@ export const PlansCreateForm = ({
}
ariaEditLabel={t('Edit target namespace')}
onEdit={() => setIsTargetNamespaceEdited(true)}
isDisabled={flow.editingDone}
/>
)}
<DescriptionListGroup>
Expand All @@ -318,6 +355,14 @@ export const PlansCreateForm = ({
</span>
</DescriptionListTerm>
<DescriptionListDescription className="forklift-page-mapping-list">
<StateAlerts
variant={AlertVariant.danger}
messages={Array.from(alerts.networkMappings.errors).map((key) => ({
key,
...networkMessages[key],
}))}
onClose={(key) => dispatch(removeAlert(key as NetworkAlerts))}
/>
<StateAlerts
variant={AlertVariant.warning}
messages={Array.from(alerts.networkMappings.warnings).map((key) => ({
Expand Down Expand Up @@ -356,6 +401,14 @@ export const PlansCreateForm = ({
</span>
</DescriptionListTerm>
<DescriptionListDescription className="forklift-page-mapping-list">
<StateAlerts
variant={AlertVariant.danger}
messages={Array.from(alerts.storageMappings.errors).map((key) => ({
key,
...storageMessages[key],
}))}
onClose={(key) => dispatch(removeAlert(key as StorageAlerts))}
/>
<StateAlerts
variant={AlertVariant.warning}
messages={Array.from(alerts.storageMappings.warnings).map((key) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ import { Alert, AlertActionCloseButton, AlertVariant } from '@patternfly/react-c

export const StateAlerts: FC<{
variant: AlertVariant;
messages: { key: string; title: string; body: string }[];
messages: { key: string; title: string; body: string; blocker?: boolean }[];
onClose?: (key: string) => void;
}> = ({ variant, messages, onClose }) => (
<>
{messages.map(({ key, title, body }) => (
{messages.map(({ key, title, body, blocker }) => (
<Alert
key={key}
isInline
actionClose={onClose ? <AlertActionCloseButton onClose={() => onClose(key)} /> : undefined}
actionClose={
onClose && !blocker ? <AlertActionCloseButton onClose={() => onClose(key)} /> : undefined
}
variant={variant}
title={title}
>
Expand Down
Loading
Loading