diff --git a/packages/eslint-plugin/cspell.wordlist.txt b/packages/eslint-plugin/cspell.wordlist.txt
index 291d06432..669f57a7d 100644
--- a/packages/eslint-plugin/cspell.wordlist.txt
+++ b/packages/eslint-plugin/cspell.wordlist.txt
@@ -67,3 +67,5 @@ filesystems
bootloader
typeahead
immer
+networkattachmentdefinitions
+nicprofiles
\ No newline at end of file
diff --git a/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json b/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json
index ede8db2dc..d469e2329 100644
--- a/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json
+++ b/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json
@@ -19,6 +19,7 @@
"A user password for connecting to the Red Hat Virtualization Manager (RHVM) API endpoint.": "A user password for connecting to the Red Hat Virtualization Manager (RHVM) API endpoint.",
"A user password for connecting to the vCenter API endpoint.": "A user password for connecting to the vCenter API endpoint.",
"Actions": "Actions",
+ "Add mapping": "Add mapping",
"Add source and target providers for the migration.": "Add source and target providers for the migration.",
"Application credential ID": "Application credential ID",
"Application credential name": "Application credential name",
@@ -85,6 +86,7 @@
"Defines the CPU limits allocated to the main container in the controller pod. The default value is 500 milliCPU.": "Defines the CPU limits allocated to the main container in the controller pod. The default value is 500 milliCPU.",
"Delete": "Delete",
"Delete {{model.label}}": "Delete {{model.label}}",
+ "Delete mapping": "Delete mapping",
"Delete Mapping": "Delete Mapping",
"Delete NetworkMap?": "Delete NetworkMap?",
"Delete Plan?": "Delete Plan?",
@@ -229,14 +231,17 @@
"Namespace is not defined": "Namespace is not defined",
"Network for data transfer": "Network for data transfer",
"Network interfaces": "Network interfaces",
+ "Network map:": "Network map:",
"NetworkAttachmentDefinitions": "NetworkAttachmentDefinitions",
"NetworkMaps": "NetworkMaps",
"NetworkMaps for virtualization": "NetworkMaps for virtualization",
"Networks": "Networks",
+ "Networks used by the selected VMs": "Networks used by the selected VMs",
"No credentials found.": "No credentials found.",
"No inventory data available.": "No inventory data available.",
"No NetworkMaps found in namespace <1>{namespace}1>.": "No NetworkMaps found in namespace <1>{namespace}1>.",
"No NetworkMaps found.": "No NetworkMaps found.",
+ "No networks in this category": "No networks in this category",
"No owner": "No owner",
"No Plans found in namespace <1>{namespace}1>.": "No Plans found in namespace <1>{namespace}1>.",
"No Plans found.": "No Plans found.",
@@ -249,6 +254,7 @@
"No secret.": "No secret.",
"No StorageMaps found in namespace <1>{namespace}1>.": "No StorageMaps found in namespace <1>{namespace}1>.",
"No StorageMaps found.": "No StorageMaps found.",
+ "No storages in this category": "No storages in this category",
"Not Ready": "Not Ready",
"Note: If 'Skip certificate validation' is selected, migrations from this provider will not be secure. Insecure migration means that the transferred data is sent over an insecure connection and potentially sensitive data could be exposed.": "Note: If 'Skip certificate validation' is selected, migrations from this provider will not be secure. Insecure migration means that the transferred data is sent over an insecure connection and potentially sensitive data could be exposed.",
"Note: It is strongly recommended to specify a VDDK init image to accelerate migrations.": "Note: It is strongly recommended to specify a VDDK init image to accelerate migrations.",
@@ -287,6 +293,8 @@
"OpenStack REST API user name.": "OpenStack REST API user name.",
"Operator": "Operator",
"Operator conditions define the current state of the controller": "Operator conditions define the current state of the controller",
+ "Other networks present on the source provider ": "Other networks present on the source provider ",
+ "Other storages present on the source provider ": "Other storages present on the source provider ",
"OvaPath": "OvaPath",
"Overview": "Overview",
"Owner": "Owner",
@@ -365,8 +373,10 @@
"Storage": "Storage",
"Storage classes": "Storage classes",
"Storage domains": "Storage domains",
+ "Storage map:": "Storage map:",
"StorageMaps": "StorageMaps",
"StorageMaps for virtualization": "StorageMaps for virtualization",
+ "Storages used by the selected VMs": "Storages used by the selected VMs",
"Succeeded": "Succeeded",
"Target and Source": "Target and Source",
"Target namespace": "Target namespace",
diff --git a/packages/forklift-console-plugin/src/modules/Providers/hooks/useNetworks.ts b/packages/forklift-console-plugin/src/modules/Providers/hooks/useNetworks.ts
new file mode 100644
index 000000000..d60098d31
--- /dev/null
+++ b/packages/forklift-console-plugin/src/modules/Providers/hooks/useNetworks.ts
@@ -0,0 +1,64 @@
+import { useMemo } from 'react';
+
+import {
+ OpenShiftNetworkAttachmentDefinition,
+ OpenstackNetwork,
+ OVirtNetwork,
+ ProviderType,
+ V1beta1Provider,
+ VSphereNetwork,
+} from '@kubev2v/types';
+
+import useProviderInventory from './useProviderInventory';
+
+export type InventoryNetwork =
+ | OpenShiftNetworkAttachmentDefinition
+ | OpenstackNetwork
+ | OVirtNetwork
+ | VSphereNetwork;
+
+export const useSourceNetworks = (
+ provider: V1beta1Provider,
+): [InventoryNetwork[], boolean, Error] => {
+ const providerType: ProviderType = provider?.spec?.type as ProviderType;
+ const {
+ inventory: networks,
+ loading,
+ error,
+ } = useProviderInventory({
+ provider,
+ subPath: providerType === 'openshift' ? '/networkattachmentdefinitions' : '/networks',
+ });
+
+ const typedNetworks = useMemo(
+ () =>
+ Array.isArray(networks)
+ ? networks.map((net) => ({ ...net, providerType } as InventoryNetwork))
+ : [],
+ [networks],
+ );
+
+ return [typedNetworks, loading, error];
+};
+
+export const useOpenShiftNetworks = (
+ provider: V1beta1Provider,
+): [OpenShiftNetworkAttachmentDefinition[], boolean, Error] => {
+ const isOpenShift = provider?.spec?.type === 'openshift';
+ const {
+ inventory: networks,
+ loading,
+ error,
+ } = useProviderInventory({
+ provider,
+ subPath: isOpenShift ? '/networkattachmentdefinitions' : '',
+ });
+
+ const typedNetworks: OpenShiftNetworkAttachmentDefinition[] = useMemo(
+ () =>
+ Array.isArray(networks) ? networks.map((net) => ({ ...net, providerType: 'openshift' })) : [],
+ [networks],
+ );
+
+ return [typedNetworks, loading, error];
+};
diff --git a/packages/forklift-console-plugin/src/modules/Providers/hooks/useNicProfiles.ts b/packages/forklift-console-plugin/src/modules/Providers/hooks/useNicProfiles.ts
new file mode 100644
index 000000000..6693f8271
--- /dev/null
+++ b/packages/forklift-console-plugin/src/modules/Providers/hooks/useNicProfiles.ts
@@ -0,0 +1,29 @@
+import { useMemo } from 'react';
+
+import { OVirtNicProfile, V1beta1Provider } from '@kubev2v/types';
+
+import useProviderInventory from './useProviderInventory';
+
+/**
+ * Works only for oVirt
+ */
+export const useNicProfiles = (provider: V1beta1Provider): [OVirtNicProfile[], boolean, Error] => {
+ const isOVirt = provider?.spec?.type === 'ovirt';
+ const {
+ inventory: nicProfiles,
+ loading,
+ error,
+ } = useProviderInventory({
+ provider,
+ subPath: isOVirt ? '/nicprofiles?detail=1' : '',
+ });
+
+ const stable = useMemo(() => {
+ if (!isOVirt) {
+ return [];
+ }
+ return Array.isArray(nicProfiles) ? nicProfiles : [];
+ }, [isOVirt, nicProfiles]);
+
+ return [stable, loading, error];
+};
diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/index.ts b/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/index.ts
index 2f2ecff62..506828835 100644
--- a/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/index.ts
+++ b/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/index.ts
@@ -1,5 +1,7 @@
// @index(['./*', /style/g], f => `export * from '${f.path}';`)
+export * from './networkMapTemplate';
export * from './planTemplate';
export * from './providerTemplate';
export * from './secretTemplate';
+export * from './storageMapTemplate';
// @endindex
diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/networkMapTemplate.ts b/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/networkMapTemplate.ts
new file mode 100644
index 000000000..2106c7fa0
--- /dev/null
+++ b/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/networkMapTemplate.ts
@@ -0,0 +1,10 @@
+import { V1beta1NetworkMap } from '@kubev2v/types';
+
+export const networkMapTemplate: V1beta1NetworkMap = {
+ apiVersion: 'forklift.konveyor.io/v1beta1',
+ kind: 'NetworkMap',
+ metadata: {
+ name: undefined,
+ namespace: undefined,
+ },
+};
diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/storageMapTemplate.ts b/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/storageMapTemplate.ts
new file mode 100644
index 000000000..6494a930a
--- /dev/null
+++ b/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/storageMapTemplate.ts
@@ -0,0 +1,10 @@
+import { V1beta1StorageMap } from '@kubev2v/types';
+
+export const storageMapTemplate: V1beta1StorageMap = {
+ apiVersion: 'forklift.konveyor.io/v1beta1',
+ kind: 'StorageMap',
+ metadata: {
+ name: undefined,
+ namespace: undefined,
+ },
+};
diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/MappingList.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/MappingList.tsx
new file mode 100644
index 000000000..300ebe662
--- /dev/null
+++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/MappingList.tsx
@@ -0,0 +1,224 @@
+import React, { FC } from 'react';
+import { useForkliftTranslation } from 'src/utils/i18n';
+
+import {
+ Button,
+ DataList,
+ DataListAction,
+ DataListCell,
+ DataListItem,
+ DataListItemCells,
+ DataListItemRow,
+ Select,
+ SelectGroup,
+ SelectOption,
+ SelectVariant,
+} from '@patternfly/react-core';
+import { MinusCircleIcon, PlusCircleIcon } from '@patternfly/react-icons';
+
+import { useToggle } from '../../hooks';
+
+import './ProvidersCreateVmMigration.style.css';
+
+export interface Mapping {
+ source: string;
+ destination: string;
+}
+
+interface MappingListProps {
+ mappings: Mapping[];
+ sources: {
+ label: string;
+ usedBySelectedVms: boolean;
+ isMapped: boolean;
+ }[];
+ availableDestinations: string[];
+ replaceMapping: (val: { current: Mapping; next: Mapping }) => void;
+ deleteMapping: (mapping: Mapping) => void;
+ addMapping: (mapping: Mapping) => void;
+ usedSourcesLabel: string;
+ generalSourcesLabel: string;
+ noSourcesLabel: string;
+ isDisabled: boolean;
+}
+
+export const MappingList: FC = ({
+ mappings,
+ sources,
+ availableDestinations,
+ replaceMapping,
+ deleteMapping,
+ addMapping,
+ usedSourcesLabel,
+ generalSourcesLabel,
+ noSourcesLabel,
+ isDisabled,
+}) => {
+ const { t } = useForkliftTranslation();
+ const usedSources = sources.filter(({ usedBySelectedVms }) => usedBySelectedVms);
+ const generalSources = sources.filter(({ usedBySelectedVms }) => !usedBySelectedVms);
+ const allMapped = sources.every(({ isMapped }) => isMapped);
+ return (
+ <>
+
+ {mappings.map(({ source, destination }, index) => (
+
+ ))}
+
+
+ addMapping({
+ source: usedSources?.[0]?.label ?? generalSources?.[0]?.label,
+ // assume that the default exists and is first in the list
+ destination: availableDestinations?.[0],
+ })
+ }
+ type="button"
+ variant="link"
+ isDisabled={allMapped || isDisabled}
+ icon={ }
+ >
+ {t('Add mapping')}
+
+ >
+ );
+};
+
+interface MappingItemProps {
+ source: string;
+ destination: string;
+ destinations: string[];
+ generalSources: {
+ label: string;
+ usedBySelectedVms: boolean;
+ isMapped: boolean;
+ }[];
+ usedSources: {
+ label: string;
+ usedBySelectedVms: boolean;
+ isMapped: boolean;
+ }[];
+ usedSourcesLabel: string;
+ generalSourcesLabel: string;
+ noSourcesLabel: string;
+ index: number;
+ replaceMapping: (val: { current: Mapping; next: Mapping }) => void;
+ deleteMapping: (mapping: Mapping) => void;
+ isDisabled: boolean;
+}
+const MappingItem: FC = ({
+ source,
+ destination,
+ destinations,
+ generalSources,
+ usedSources,
+ usedSourcesLabel,
+ generalSourcesLabel,
+ noSourcesLabel,
+ index,
+ replaceMapping,
+ deleteMapping,
+ isDisabled,
+}) => {
+ const { t } = useForkliftTranslation();
+ const [isSrcOpen, setToggleSrcOpen] = useToggle(false);
+ const [isTrgOpen, setToggleTrgOpen] = useToggle(false);
+ return (
+
+
+
+
+ !isPlaceholder &&
+ replaceMapping({
+ current: { source, destination },
+ next: { source: value, destination },
+ })
+ }
+ selections={source}
+ isOpen={isSrcOpen}
+ isDisabled={isDisabled}
+ aria-labelledby=""
+ isGrouped
+ >
+
+ {toGroup(usedSources, noSourcesLabel, source)}
+
+
+ {toGroup(generalSources, noSourcesLabel, source)}
+
+
+ ,
+
+
+ replaceMapping({
+ current: { source, destination },
+ next: { source, destination: value },
+ })
+ }
+ selections={destination}
+ isOpen={isTrgOpen}
+ isDisabled={isDisabled}
+ aria-labelledby=""
+ >
+ {destinations.map((label) => (
+
+ ))}
+
+ ,
+ ]}
+ />
+
+ deleteMapping({ source, destination })}
+ variant="plain"
+ aria-label={t('Delete mapping')}
+ key="delete-action"
+ icon={ }
+ isDisabled={isDisabled}
+ />
+
+
+
+ );
+};
+
+const toGroup = (
+ sources: MappingListProps['sources'],
+ noSourcesLabel: string,
+ selectedSource: string,
+) =>
+ sources.length !== 0 ? (
+ sources.map(({ label, isMapped }) => (
+
+ ))
+ ) : (
+
+ );
diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/PlansCreateForm.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/PlansCreateForm.tsx
index 3008915d8..ad648c89c 100644
--- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/PlansCreateForm.tsx
+++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/PlansCreateForm.tsx
@@ -4,7 +4,11 @@ import { useForkliftTranslation } from 'src/utils/i18n';
import { isProviderLocalOpenshift } from 'src/utils/resources';
import { EnumFilter, SearchableGroupedEnumFilter } from '@kubev2v/common';
-import { ProviderModelGroupVersionKind } from '@kubev2v/types';
+import {
+ NetworkMapModelGroupVersionKind,
+ ProviderModelGroupVersionKind,
+ StorageMapModelGroupVersionKind,
+} from '@kubev2v/types';
import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk';
import {
Button,
@@ -29,18 +33,38 @@ import {
import { DetailsItem, getIsTarget } from '../../utils';
import { concernsMatcher, featuresMatcher, VmData } from '../details';
-import { PageAction, setPlanName, setPlanTargetNamespace, setPlanTargetProvider } from './actions';
+import {
+ PageAction,
+ replaceNetworkMapping,
+ replaceStorageMapping,
+ setPlanName,
+ setPlanTargetNamespace,
+ setPlanTargetProvider,
+} from './actions';
import { EditableDescriptionItem } from './EditableDescriptionItem';
+import { MappingList } from './MappingList';
import { CreateVmMigrationPageState } from './reducer';
export const PlansCreateForm = ({
state: {
- newPlan: plan,
+ underConstruction: { plan, netMap, storageMap },
validation,
- selectedVms,
- vmFieldsFactory: [vmFieldsFactory, RowMapper],
- availableProviders,
- availableTargetNamespaces,
+ receivedAsParams: { selectedVms },
+ calculatedOnce: {
+ vmFieldsFactory: [vmFieldsFactory, RowMapper],
+ },
+ existingResources: {
+ providers: availableProviders,
+ targetNamespaces: availableTargetNamespaces,
+ },
+ calculatedPerNamespace: {
+ targetNetworks,
+ targetStorages,
+ sourceNetworks,
+ networkMappings,
+ storageMappings,
+ },
+ flow,
},
dispatch,
}: {
@@ -86,20 +110,20 @@ export const PlansCreateForm = ({
default: '1Col',
}}
>
- {isNameEdited || validation.name === 'error' ? (
+ {isNameEdited || validation.planName === 'error' ? (