Skip to content

Commit c1c1c83

Browse files
committed
[MTV-2247] Initial new create plan wizard setup
Signed-off-by: Jeff Puzzo <[email protected]>
1 parent c2811e2 commit c1c1c83

File tree

14 files changed

+416
-4
lines changed

14 files changed

+416
-4
lines changed

packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json

+16
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"Add mapping": "Add mapping",
7171
"Add passphrase": "Add passphrase",
7272
"Add source and target providers for the migration.": "Add source and target providers for the migration.",
73+
"Additional set up": "Additional set up",
7374
"All discovered networks have been mapped to the default network.": "All discovered networks have been mapped to the default network.",
7475
"All discovered storages have been mapped to the default storage.": "All discovered storages have been mapped to the default storage.",
7576
"All networks detected on the selected VMs require a mapping.": "All networks detected on the selected VMs require a mapping.",
@@ -90,7 +91,9 @@
9091
"At least one source and one target provider in the {{name}} project must be available.": "At least one source and one target provider in the {{name}} project must be available.",
9192
"At least one source and one target provider must be available.": "At least one source and one target provider must be available.",
9293
"Authentication type": "Authentication type",
94+
"Back": "Back",
9395
"Bandwidth": "Bandwidth",
96+
"Basic set up": "Basic set up",
9497
"Boot from first root device": "Boot from first root device",
9598
"Boot from the first hard drive": "Boot from the first hard drive",
9699
"Boot from the first partition on the first hard drive": "Boot from the first partition on the first hard drive",
@@ -142,6 +145,7 @@
142145
"Create NetworkMap": "Create NetworkMap",
143146
"Create new namespace:": "Create new namespace:",
144147
"Create new provider": "Create new provider",
148+
"Create plan": "Create plan",
145149
"Create Plan": "Create Plan",
146150
"Create provider": "Create provider",
147151
"Create Provider": "Create Provider",
@@ -253,12 +257,14 @@
253257
"First root device": "First root device",
254258
"Flavor": "Flavor",
255259
"Folder": "Folder",
260+
"General": "General",
256261
"Go to providers list": "Go to providers list",
257262
"GPUs/Host Devices": "GPUs/Host Devices",
258263
"Hide from view": "Hide from view",
259264
"Hide values": "Hide values",
260265
"Hide variables": "Hide variables",
261266
"Hooks": "Hooks",
267+
"Hooks (optional)": "Hooks (optional)",
262268
"Hooks for virtualization": "Hooks for virtualization",
263269
"Host": "Host",
264270
"Host cluster": "Host cluster",
@@ -335,6 +341,7 @@
335341
"Name": "Name",
336342
"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.",
337343
"Name is required and must be a unique within a namespace and valid Kubernetes name.": "Name is required and must be a unique within a namespace and valid Kubernetes name.",
344+
"Name your plan and choose the project you would like it to be created in.": "Name your plan and choose the project you would like it to be created in.",
338345
"Namespace": "Namespace",
339346
"Namespace defines the space within which each name must be unique.\n An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation.\n Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.": "Namespace defines the space within which each name must be unique.\n An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation.\n Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.",
340347
"Namespace defines the space within which each name must be unique.\n An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation.\n Not all objects are required to be scoped to a namespace -\n the value of this field for those objects will be empty.": "Namespace defines the space within which each name must be unique.\n An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation.\n Not all objects are required to be scoped to a namespace -\n the value of this field for those objects will be empty.",
@@ -344,6 +351,7 @@
344351
"Network interfaces": "Network interfaces",
345352
"Network Map name re-generated": "Network Map name re-generated",
346353
"Network map:": "Network map:",
354+
"Network mapping": "Network mapping",
347355
"Network mapping is empty, make sure no mappings are required for this migration plan.": "Network mapping is empty, make sure no mappings are required for this migration plan.",
348356
"Network mappings": "Network mappings",
349357
"Network mappings have been re-generated": "Network mappings have been re-generated",
@@ -359,6 +367,7 @@
359367
"Networks used by the selected VMs": "Networks used by the selected VMs",
360368
"New name was generated for the Network Map due to naming conflict.": "New name was generated for the Network Map due to naming conflict.",
361369
"New name was generated for the Storage Map due to naming conflict.": "New name was generated for the Storage Map due to naming conflict.",
370+
"Next": "Next",
362371
"NICs with empty NIC profile": "NICs with empty NIC profile",
363372
"No concerns found for this virtual machine.": "No concerns found for this virtual machine.",
364373
"No credentials found.": "No credentials found.",
@@ -417,6 +426,8 @@
417426
"Operator": "Operator",
418427
"Operator conditions define the current state of the controller": "Operator conditions define the current state of the controller",
419428
"Other networks present on the source provider ": "Other networks present on the source provider ",
429+
"Other settings": "Other settings",
430+
"Other settings (optional)": "Other settings (optional)",
420431
"Other storages present on the source provider ": "Other storages present on the source provider ",
421432
"OvaPath": "OvaPath",
422433
"Overview": "Overview",
@@ -428,7 +439,9 @@
428439
"Phase": "Phase",
429440
"Pipeline status": "Pipeline status",
430441
"Plan details": "Plan details",
442+
"Plan information": "Plan information",
431443
"Plan name": "Plan name",
444+
"Plan name is required.": "Plan name is required.",
432445
"Plans": "Plans",
433446
"Plans for virtualization": "Plans for virtualization",
434447
"Plans wizard": "Plans wizard",
@@ -487,6 +500,7 @@
487500
"Restore default columns": "Restore default columns",
488501
"Return to the providers list page": "Return to the providers list page",
489502
"Reveal values": "Reveal values",
503+
"Review and create": "Review and create",
490504
"Root device": "Root device",
491505
"Run the migration plan.": "Run the migration plan.",
492506
"Running": "Running",
@@ -520,6 +534,7 @@
520534
"Show the welcome card": "Show the welcome card",
521535
"Show variables": "Show variables",
522536
"Skip certificate validation": "Skip certificate validation",
537+
"Skip to review": "Skip to review",
523538
"Skip VMware Virtual Disk Development Kit (VDDK) SDK acceleration (not recommended).": "Skip VMware Virtual Disk Development Kit (VDDK) SDK acceleration (not recommended).",
524539
"Snapshot polling interval (seconds)": "Snapshot polling interval (seconds)",
525540
"Some VMs Failed": "Some VMs Failed",
@@ -543,6 +558,7 @@
543558
"Storage domains": "Storage domains",
544559
"Storage Map name re-generated": "Storage Map name re-generated",
545560
"Storage map:": "Storage map:",
561+
"Storage mapping": "Storage mapping",
546562
"Storage mapping is empty, make sure no mappings are required for this migration plan.": "Storage mapping is empty, make sure no mappings are required for this migration plan.",
547563
"Storage mappings": "Storage mappings",
548564
"Storage mappings have been re-generated": "Storage mappings have been re-generated",

packages/forklift-console-plugin/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"node-forge": "^1",
4545
"react": "17.0.2",
4646
"react-dom": "17.0.2",
47+
"react-hook-form": "^7.54.2",
4748
"react-i18next": "^11.14.3",
4849
"react-linkify": "^1.0.0-alpha",
4950
"react-router": "5.3.x",
@@ -53,6 +54,7 @@
5354
"devDependencies": {
5455
"@openshift-console/dynamic-plugin-sdk-webpack": "1.3.0",
5556
"@types/ejs": "^3.1.5",
57+
"@types/i18next": "^11.9.3",
5658
"@types/jsonpath": "^0.2.4",
5759
"@types/jsrsasign": "10.5.14",
5860
"@types/luxon": "^3.4.2",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React, { FC } from 'react';
2+
import { FieldError } from 'react-hook-form';
3+
4+
import { FormHelperText, HelperText, HelperTextItem } from '@patternfly/react-core';
5+
import { ExclamationCircleIcon } from '@patternfly/react-icons';
6+
7+
type FormErrorHelperTextProps = {
8+
error: Partial<FieldError>;
9+
};
10+
11+
export const FormErrorHelperText: FC<FormErrorHelperTextProps> = ({ error }) => {
12+
return error ? (
13+
<FormHelperText>
14+
<HelperText>
15+
<HelperTextItem icon={<ExclamationCircleIcon />} variant="error">
16+
{error?.message?.toString()}
17+
</HelperTextItem>
18+
</HelperText>
19+
</FormHelperText>
20+
) : null;
21+
};

packages/forklift-console-plugin/src/modules/Plans/dynamic-plugin.ts

+12
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { ConsolePluginBuildMetadata } from '@openshift-console/dynamic-plug
1212
export const exposedModules: ConsolePluginBuildMetadata['exposedModules'] = {
1313
PlansListPage: './modules/Plans/views/list/PlansListPage',
1414
PlanCreatePage: './modules/Plans/views/create/PlanCreatePage',
15+
PlanCreatePageV2: './plans/create/PlanCreatePage',
1516
PlanDetailsPage: './modules/Plans/views/details/PlanDetailsPage',
1617
};
1718

@@ -64,6 +65,17 @@ export const extensions: EncodedExtension[] = [
6465
},
6566
} as EncodedExtension<CreateResource>,
6667

68+
{
69+
type: 'console.page/route',
70+
properties: {
71+
exact: false,
72+
path: ['/mtv/create/plan'],
73+
component: {
74+
$codeRef: 'PlanCreatePageV2',
75+
},
76+
},
77+
},
78+
6779
{
6880
type: 'console.model-metadata',
6981
properties: {

packages/forklift-console-plugin/src/modules/Plans/views/create/steps/CreateMigrationPlan/index.ts

-3
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.create-plan-wizard__form {
2+
width: 66%;
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import React, { FC, useState } from 'react';
2+
import { FormProvider, useForm } from 'react-hook-form';
3+
import { useForkliftTranslation } from 'src/utils';
4+
5+
import { Form, Title, Wizard, WizardStep, WizardStepType } from '@patternfly/react-core';
6+
7+
import { GeneralInformationForm } from './steps/GeneralInformationForm';
8+
import { defaultCurrentStep, planStepIndexes, planStepNames, PlanWizardStepId } from './constants';
9+
import { CreatePlanWizardFooter } from './CreatePlanWizardFooter';
10+
11+
import './CreatePlanWizard.style.scss';
12+
13+
export const CreatePlanWizard: FC = () => {
14+
const { t } = useForkliftTranslation();
15+
const form = useForm({ mode: 'onChange' });
16+
const [currentStep, setCurrentStep] = useState<WizardStepType>(defaultCurrentStep);
17+
const { formState, getValues } = form;
18+
const formValues = getValues();
19+
20+
const onSubmit = () => console.log('SUBMITTED: ', formValues);
21+
22+
const getStepProps = (id: PlanWizardStepId) => ({
23+
id,
24+
name: planStepNames[id],
25+
isDisabled:
26+
currentStep?.index < planStepIndexes[id] && Object.keys(formState?.errors).length > 0,
27+
});
28+
29+
return (
30+
<FormProvider {...form}>
31+
<Wizard
32+
isVisitRequired
33+
title={t('Create migration plan')}
34+
footer={<CreatePlanWizardFooter />}
35+
onStepChange={(_event, currentStep) => setCurrentStep(currentStep)}
36+
>
37+
<WizardStep
38+
{...getStepProps(PlanWizardStepId.BasicSetUp)}
39+
steps={[
40+
<WizardStep key={PlanWizardStepId.General} {...getStepProps(PlanWizardStepId.General)}>
41+
<GeneralInformationForm />
42+
</WizardStep>,
43+
<WizardStep
44+
key={PlanWizardStepId.VirtualMachines}
45+
{...getStepProps(PlanWizardStepId.VirtualMachines)}
46+
>
47+
<Form>
48+
<Title headingLevel="h2">{t('Virtual machines')}</Title>
49+
</Form>
50+
</WizardStep>,
51+
<WizardStep
52+
key={PlanWizardStepId.NetworkMapping}
53+
{...getStepProps(PlanWizardStepId.NetworkMapping)}
54+
>
55+
<Form>
56+
<Title headingLevel="h2">{t('Network mappings')}</Title>
57+
</Form>
58+
</WizardStep>,
59+
<WizardStep
60+
key={PlanWizardStepId.StorageMapping}
61+
{...getStepProps(PlanWizardStepId.StorageMapping)}
62+
>
63+
<Form>
64+
<Title headingLevel="h2">{t('Storage mappings')}</Title>
65+
</Form>
66+
</WizardStep>,
67+
<WizardStep
68+
key={PlanWizardStepId.MigrationType}
69+
{...getStepProps(PlanWizardStepId.MigrationType)}
70+
>
71+
<Form>
72+
<Title headingLevel="h2">{t('Migration type')}</Title>
73+
</Form>
74+
</WizardStep>,
75+
]}
76+
/>
77+
78+
<WizardStep
79+
{...getStepProps(PlanWizardStepId.AdditionalSetUp)}
80+
steps={[
81+
<WizardStep
82+
key={PlanWizardStepId.OtherSettings}
83+
{...getStepProps(PlanWizardStepId.OtherSettings)}
84+
>
85+
<Form>
86+
<Title headingLevel="h2">{t('Other settings')}</Title>
87+
</Form>
88+
</WizardStep>,
89+
<WizardStep key={PlanWizardStepId.Hooks} {...getStepProps(PlanWizardStepId.Hooks)}>
90+
<Form>
91+
<Title headingLevel="h2">{t('Hooks')}</Title>
92+
</Form>
93+
</WizardStep>,
94+
]}
95+
/>
96+
97+
<WizardStep
98+
footer={<CreatePlanWizardFooter nextButtonText={t('Create plan')} onNext={onSubmit} />}
99+
{...getStepProps(PlanWizardStepId.ReviewAndCreate)}
100+
>
101+
<pre>{JSON.stringify(formValues, null, 2)}</pre>
102+
</WizardStep>
103+
</Wizard>
104+
</FormProvider>
105+
);
106+
};
107+
108+
export default CreatePlanWizard;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import React, { FC, MouseEvent } from 'react';
2+
import { useFormContext } from 'react-hook-form';
3+
import { useHistory } from 'react-router';
4+
import { getResourceUrl } from 'src/modules';
5+
import { useForkliftTranslation } from 'src/utils';
6+
7+
import { PlanModelRef } from '@kubev2v/types';
8+
import { useActiveNamespace } from '@openshift-console/dynamic-plugin-sdk';
9+
import {
10+
Button,
11+
ButtonVariant,
12+
useWizardContext,
13+
WizardFooterProps,
14+
WizardFooterWrapper,
15+
} from '@patternfly/react-core';
16+
17+
import { PlanWizardStepId } from './constants';
18+
19+
type CreatePlanWizardFooterProps = Partial<Pick<WizardFooterProps, 'nextButtonText' | 'onNext'>>;
20+
21+
export const CreatePlanWizardFooter: FC<CreatePlanWizardFooterProps> = ({
22+
nextButtonText,
23+
onNext,
24+
}) => {
25+
const history = useHistory();
26+
const { t } = useForkliftTranslation();
27+
const [activeNamespace] = useActiveNamespace();
28+
const { trigger } = useFormContext();
29+
const { activeStep, goToNextStep, goToPrevStep, goToStepById } = useWizardContext();
30+
const canSkipToReview =
31+
activeStep.id === PlanWizardStepId.MigrationType ||
32+
activeStep.id === PlanWizardStepId.OtherSettings;
33+
34+
const onNextClick =
35+
(event: MouseEvent<HTMLButtonElement>) => async (goToStep: () => void | Promise<void>) => {
36+
if (onNext) {
37+
return onNext(event);
38+
}
39+
40+
const isValid = await trigger(null, { shouldFocus: true });
41+
if (isValid) {
42+
goToStep();
43+
}
44+
};
45+
46+
const onCancel = () => {
47+
const plansListURL = getResourceUrl({
48+
reference: PlanModelRef,
49+
namespace: activeNamespace,
50+
});
51+
52+
history.push(plansListURL);
53+
};
54+
55+
return (
56+
<WizardFooterWrapper>
57+
<Button
58+
variant={ButtonVariant.secondary}
59+
onClick={goToPrevStep}
60+
isDisabled={activeStep.id === PlanWizardStepId.General}
61+
>
62+
{t('Back')}
63+
</Button>
64+
<Button
65+
variant={ButtonVariant.primary}
66+
onClick={(event) => onNextClick(event)(() => goToNextStep())}
67+
>
68+
{nextButtonText || t('Next')}
69+
</Button>
70+
{canSkipToReview && (
71+
<Button
72+
variant={ButtonVariant.tertiary}
73+
onClick={(event) =>
74+
onNextClick(event)(() => goToStepById(PlanWizardStepId.ReviewAndCreate))
75+
}
76+
>
77+
{t('Skip to review')}
78+
</Button>
79+
)}
80+
<Button variant={ButtonVariant.link} onClick={onCancel}>
81+
{t('Cancel')}
82+
</Button>
83+
</WizardFooterWrapper>
84+
);
85+
};
86+
87+
export default CreatePlanWizardFooter;

0 commit comments

Comments
 (0)