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 186a9868b..db9456736 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 @@ -168,6 +168,7 @@ "Edit StorageMap": "Edit StorageMap", "Edit URL": "Edit URL", "Edit VDDK init image": "Edit VDDK init image", + "Edit virtual machines": "Edit virtual machines", "Empty": "Empty", "Endpoint": "Endpoint", "Endpoint type": "Endpoint type", @@ -487,6 +488,7 @@ "The chosen provider is no longer available.": "The chosen provider is no longer available.", "The current certificate does not match the certificate fetched from URL. Manually validate the fingerprint before proceeding.": "The current certificate does not match the certificate fetched from URL. Manually validate the fingerprint before proceeding.", "The edit mappings button is disabled if the plan started running and at least one virtual machine was migrated successfully or when the plan status does not enable editing.": "The edit mappings button is disabled if the plan started running and at least one virtual machine was migrated successfully or when the plan status does not enable editing.", + "The following VMs do not exist on the source provider and will be removed from the plan": "The following VMs do not exist on the source provider and will be removed from the plan", "The interval in minutes for precopy. Default value is 60.": "The interval in minutes for precopy. Default value is 60.", "The interval in seconds for snapshot pooling. Default value is 10.": "The interval in seconds for snapshot pooling. Default value is 10.", "The limit for CPU usage by the controller, specified in milliCPU. Default value is 500m.": "The limit for CPU usage by the controller, specified in milliCPU. Default value is 500m.", @@ -526,7 +528,9 @@ "Update credentials": "Update credentials", "Update hooks": "Update hooks", "Update mappings": "Update mappings", + "Update migration plan": "Update migration plan", "Update providers": "Update providers", + "Update virtual machines": "Update virtual machines", "Updated": "Updated", "Upload": "Upload", "URL": "URL", diff --git a/packages/forklift-console-plugin/src/modules/Plans/utils/index.ts b/packages/forklift-console-plugin/src/modules/Plans/utils/index.ts index 6cc78e687..60c9dc565 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/utils/index.ts +++ b/packages/forklift-console-plugin/src/modules/Plans/utils/index.ts @@ -1,5 +1,7 @@ // @index(['./*', /style/g], f => `export * from '${f.path}';`) export * from './constants'; export * from './helpers'; +export * from './patchPlanMappingsData'; +export * from './patchPlanSpecVms'; export * from './types'; // @endindex diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/utils/patchPlanMappingsData.ts b/packages/forklift-console-plugin/src/modules/Plans/utils/patchPlanMappingsData.ts similarity index 97% rename from packages/forklift-console-plugin/src/modules/Plans/views/details/utils/patchPlanMappingsData.ts rename to packages/forklift-console-plugin/src/modules/Plans/utils/patchPlanMappingsData.ts index 7a254bfda..db9b489ff 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/utils/patchPlanMappingsData.ts +++ b/packages/forklift-console-plugin/src/modules/Plans/utils/patchPlanMappingsData.ts @@ -56,7 +56,7 @@ export async function patchPlanMappingsData( * @param {NetworkMap} networkMap - The network map object to update. * @returns {NetworkMap} The updated network map object. */ -export function updateNetworkMapSpecMapDestination( +function updateNetworkMapSpecMapDestination( networkMaps: V1beta1NetworkMapSpecMap[], ): V1beta1NetworkMapSpecMap[] { networkMaps?.forEach((entry) => { diff --git a/packages/forklift-console-plugin/src/modules/Plans/utils/patchPlanSpecVms.ts b/packages/forklift-console-plugin/src/modules/Plans/utils/patchPlanSpecVms.ts new file mode 100644 index 000000000..9fef48c9b --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/utils/patchPlanSpecVms.ts @@ -0,0 +1,21 @@ +import { PlanModel, V1beta1Plan } from '@kubev2v/types'; +import { k8sPatch } from '@openshift-console/dynamic-plugin-sdk'; + +/** + * Patch the plan VMs + * + * @param plan The V1beta1Plan object which will have its VMs patched + */ +export const patchPlanSpecVms = async (plan: V1beta1Plan) => { + await k8sPatch({ + model: PlanModel, + resource: plan, + data: [ + { + op: 'replace', + path: '/spec/vms', + value: plan.spec.vms, + }, + ], + }); +}; diff --git a/packages/forklift-console-plugin/src/modules/Plans/utils/types/PlanEditAction.ts b/packages/forklift-console-plugin/src/modules/Plans/utils/types/PlanEditAction.ts new file mode 100644 index 000000000..b9f1933d6 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/utils/types/PlanEditAction.ts @@ -0,0 +1 @@ +export type PlanEditAction = 'PLAN' | 'VMS'; diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/create/PlanCreatePage.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/create/PlanCreatePage.tsx index bbfd45fb2..509ff742a 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/create/PlanCreatePage.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/create/PlanCreatePage.tsx @@ -70,10 +70,7 @@ export const PlanCreatePage: FC<{ namespace: string }> = ({ namespace }) => { namespace: namespace || projectName, }); - const selectedProvider = - filterState.selectedProviderUID !== '' - ? findProviderByID(filterState.selectedProviderUID, providers) - : undefined; + const selectedProvider = findProviderByID(filterState.selectedProviderUID, providers); // Init Create migration plan form state const [state, dispatch, emptyContext] = useFetchEffects({ diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/create/components/createProviderCardItems.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/create/components/createProviderCardItems.tsx index 1ece5afac..60106903a 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/create/components/createProviderCardItems.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/create/components/createProviderCardItems.tsx @@ -42,5 +42,8 @@ export const findProviderByID = ( id: string, providers: V1beta1Provider[], ): V1beta1Provider | undefined => { + if (id === '') { + return undefined; + } return providers.find((provider) => provider.metadata.uid === id); }; diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/create/steps/SelectSourceProvider/SelectSourceProvider.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/create/steps/SelectSourceProvider/SelectSourceProvider.tsx index 23c19ef04..233059f6f 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/create/steps/SelectSourceProvider/SelectSourceProvider.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/create/steps/SelectSourceProvider/SelectSourceProvider.tsx @@ -19,6 +19,7 @@ export const SelectSourceProvider: React.FC<{ state: CreateVmMigrationPageState; dispatch: React.Dispatch>; filterDispatch: React.Dispatch; + hideProviderSection?: boolean; }> = ({ filterState, providers, @@ -27,6 +28,7 @@ export const SelectSourceProvider: React.FC<{ projectName, dispatch, filterDispatch, + hideProviderSection, }) => { const { t } = useForkliftTranslation(); @@ -45,16 +47,20 @@ export const SelectSourceProvider: React.FC<{ return ( <> - {t('Select source provider')} + {!hideProviderSection && ( + <> + {t('Select source provider')} - + + + )} {filterState.selectedProviderUID && ( <> diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/UpdateMappings/PlanMappingsInitSection.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/UpdateMappings/PlanMappingsInitSection.tsx new file mode 100644 index 000000000..683496568 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/UpdateMappings/PlanMappingsInitSection.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import { useOpenShiftNetworks, useSourceNetworks } from 'src/modules/Providers/hooks/useNetworks'; +import { useOpenShiftStorages, useSourceStorages } from 'src/modules/Providers/hooks/useStorages'; +import { useProviders } from 'src/utils/fetch'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { V1beta1NetworkMap, V1beta1Plan, V1beta1StorageMap } from '@kubev2v/types'; +import { Alert } from '@patternfly/react-core'; + +import { PlanMappingsSection } from './PlanMappingsSection'; +import { PlanMappingsSectionState } from './types'; + +type PlanMappingsInitSectionProps = { + plan: V1beta1Plan; + loaded?: boolean; + loadError?: unknown; + planMappingsState: PlanMappingsSectionState; + planMappingsDispatch: React.Dispatch<{ + type: string; + payload?; + }>; + planNetworkMaps: V1beta1NetworkMap; + planStorageMaps: V1beta1StorageMap; +}; + +export const PlanMappingsInitSection: React.FC = (props) => { + const { t } = useForkliftTranslation(); + const { plan, planMappingsState, planMappingsDispatch, planNetworkMaps, planStorageMaps } = props; + + // Retrieve all k8s Providers + const [providers, providersLoaded, providersLoadError] = useProviders({ + namespace: plan?.metadata?.namespace, + }); + + const sourceProvider = providers + ? providers.find((p) => p?.metadata?.name === plan?.spec?.provider?.source?.name) + : null; + const targetProvider = providers + ? providers.find((p) => p?.metadata?.name === plan?.spec?.provider?.destination?.name) + : null; + + // Retrieve source and target providers Mappings from the inventory + const [sourceNetworks, sourceNetworksLoading, sourceNetworksError] = + useSourceNetworks(sourceProvider); + const [targetNetworks, targetNetworksLoading, targetNetworksError] = + useOpenShiftNetworks(targetProvider); + const [sourceStorages, sourceStoragesLoading, sourceStoragesError] = + useSourceStorages(sourceProvider); + const [targetStorages, targetStoragesLoading, targetStoragesError] = + useOpenShiftStorages(targetProvider); + + if ( + !providersLoaded || + sourceNetworksLoading || + targetNetworksLoading || + sourceStoragesLoading || + targetStoragesLoading + ) { + return ( +
+ {t('Data is loading, please wait.')} +
+ ); + } + + if ( + providersLoadError || + sourceNetworksError || + targetNetworksError || + sourceStoragesError || + targetStoragesError + ) { + return ( +
+ + {t( + 'Something is wrong, the data was not loaded due to an error, please try to reload the page.', + )} + +
+ ); + } + + // Warn when missing inventory data, missing inventory will make + // some editing options missing. + const alerts = []; + + if (targetStorages.length == 0) { + // Note: target network can't be missing, we always have Pod network. + alerts.push('Missing target storage inventory.'); + } + + if (sourceStorages.length == 0 || sourceNetworks.length == 0) { + alerts.push('Missing storage inventory.'); + } + + return ( + <> + {alerts.map((alert) => ( +
+ +
+ ))} + + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Mappings/PlanMappingsSection.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/UpdateMappings/PlanMappingsSection.tsx similarity index 90% rename from packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Mappings/PlanMappingsSection.tsx rename to packages/forklift-console-plugin/src/modules/Plans/views/details/components/UpdateMappings/PlanMappingsSection.tsx index 5bf78ae11..7fc05c30e 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Mappings/PlanMappingsSection.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/UpdateMappings/PlanMappingsSection.tsx @@ -1,5 +1,6 @@ -import React, { ReactNode, useReducer, useState } from 'react'; +import React, { useState } from 'react'; import { isPlanEditable } from 'src/modules/Plans/utils'; +import { patchPlanMappingsData } from 'src/modules/Plans/utils'; import { InventoryNetwork } from 'src/modules/Providers/hooks/useNetworks'; import { InventoryStorage } from 'src/modules/Providers/hooks/useStorages'; import { useForkliftTranslation } from 'src/utils/i18n'; @@ -33,18 +34,18 @@ import { } from '@patternfly/react-core'; import Pencil from '@patternfly/react-icons/dist/esm/icons/pencil-alt-icon'; -import { Mapping, MappingList } from '../../components'; import { canDeleteAndPatchPlanHooks, - hasPlanMappingsChanged, hasSomeCompleteRunningVMs, mapSourceNetworksIdsToLabels, mapSourceStoragesIdsToLabels, mapTargetNetworksIdsToLabels, mapTargetStoragesLabelsToIds, - patchPlanMappingsData, POD_NETWORK, } from '../../utils'; +import { Mapping, MappingList } from '..'; + +import { PlanMappingsSectionState } from './types'; /** * Represents the state (edit/view) of the Plan mappings section. @@ -56,15 +57,8 @@ import { * @property {V1beta1NetworkMapSpecMap[]} updatedNetwork - The new version of the Plan Network Maps being edited. * @property {V1beta1StorageMapSpecMap[]} updatedStorage - The new version of the Plan Storage Maps being edited. */ -interface PlanMappingsSectionState { - edit: boolean; - dataChanged: boolean; - alertMessage: ReactNode; - updatedNetwork: V1beta1NetworkMapSpecMap[]; - updatedStorage: V1beta1StorageMapSpecMap[]; -} - -export type PlanMappingsSectionProps = { + +type PlanMappingsSectionProps = { plan: V1beta1Plan; planNetworkMaps: V1beta1NetworkMap; planStorageMaps: V1beta1StorageMap; @@ -72,6 +66,11 @@ export type PlanMappingsSectionProps = { targetNetworks: OpenShiftNetworkAttachmentDefinition[]; sourceStorages: InventoryStorage[]; targetStorages: OpenShiftStorageClass[]; + planMappingsState?: PlanMappingsSectionState; + planMappingsDispatch?: React.Dispatch<{ + type: string; + payload?; + }>; }; export const PlanMappingsSection: React.FC = ({ @@ -82,84 +81,14 @@ export const PlanMappingsSection: React.FC = ({ targetNetworks, sourceStorages, targetStorages, + planMappingsState: state, + planMappingsDispatch: dispatch, }) => { const { t } = useForkliftTranslation(); - const initialState: PlanMappingsSectionState = { - edit: false, - dataChanged: false, - alertMessage: null, - updatedNetwork: planNetworkMaps?.spec?.map, - updatedStorage: planStorageMaps?.spec?.map, - }; - const [isLoading, setIsLoading] = useState(false); const [isAddNetworkMapAvailable, setIsAddNetworkMapAvailable] = useState(true); const [isAddStorageMapAvailable, setIsAddStorageMapAvailable] = useState(true); - const [state, dispatch] = useReducer(reducer, initialState); - - function reducer( - state: PlanMappingsSectionState, - action: { type: string; payload? }, - ): PlanMappingsSectionState { - switch (action.type) { - case 'TOGGLE_EDIT': { - return { ...state, edit: !state.edit }; - } - case 'SET_CANCEL': { - const dataChanged = false; - - return { - ...state, - dataChanged, - alertMessage: null, - updatedNetwork: planNetworkMaps?.spec?.map, - updatedStorage: planStorageMaps?.spec?.map, - }; - } - case 'SET_ALERT_MESSAGE': { - return { ...state, alertMessage: action.payload }; - } - case 'ADD_NETWORK_MAPPING': - case 'DELETE_NETWORK_MAPPING': - case 'REPLACE_NETWORK_MAPPING': { - const updatedNetwork = action.payload.newState; - const dataChanged = hasPlanMappingsChanged( - planNetworkMaps?.spec?.map, - planStorageMaps?.spec?.map, - updatedNetwork, - state?.updatedStorage, - ); - - return { - ...state, - dataChanged, - alertMessage: null, - updatedNetwork, - }; - } - case 'ADD_STORAGE_MAPPING': - case 'DELETE_STORAGE_MAPPING': - case 'REPLACE_STORAGE_MAPPING': { - const updatedStorage = action.payload.newState; - const dataChanged = hasPlanMappingsChanged( - planNetworkMaps?.spec?.map, - planStorageMaps?.spec?.map, - state?.updatedNetwork, - updatedStorage, - ); - - return { - ...state, - dataChanged, - alertMessage: null, - updatedStorage, - }; - } - default: - return state; - } - } // Toggles between view and edit modes function onToggleEdit() { diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/UpdateMappings/index.ts b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/UpdateMappings/index.ts new file mode 100644 index 000000000..4d7407227 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/UpdateMappings/index.ts @@ -0,0 +1,7 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './initialPlanMappingsState'; +export * from './PlanMappingsInitSection'; +export * from './PlanMappingsSection'; +export * from './reducer'; +export * from './types'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/UpdateMappings/initialPlanMappingsState.ts b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/UpdateMappings/initialPlanMappingsState.ts new file mode 100644 index 000000000..c372e1969 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/UpdateMappings/initialPlanMappingsState.ts @@ -0,0 +1,28 @@ +import { PlanEditAction } from 'src/modules/Plans/utils/types/PlanEditAction'; + +import { V1beta1NetworkMap, V1beta1StorageMap } from '@kubev2v/types'; + +import { PlanMappingsSectionState } from './types'; + +export type InitialPlanMappingsStateProps = { + planNetworkMaps: V1beta1NetworkMap; + planStorageMaps: V1beta1StorageMap; + edit: boolean; + editAction?: PlanEditAction; +}; + +export const initialPlanMappingsState = ({ + planNetworkMaps, + planStorageMaps, + editAction, + edit, +}: InitialPlanMappingsStateProps): PlanMappingsSectionState => ({ + edit, + dataChanged: false, + alertMessage: null, + updatedNetwork: planNetworkMaps?.spec?.map || [], + updatedStorage: planStorageMaps?.spec?.map || [], + planNetworkMaps: planNetworkMaps, + planStorageMaps: planStorageMaps, + editAction, +}); diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/UpdateMappings/reducer.ts b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/UpdateMappings/reducer.ts new file mode 100644 index 000000000..1b26f511c --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/UpdateMappings/reducer.ts @@ -0,0 +1,76 @@ +import { hasPlanMappingsChanged } from '../../utils'; + +import { PlanMappingsSectionState } from './types'; + +export function planMappingsSectionReducer( + state: PlanMappingsSectionState, + action: { type: string; payload? }, +): PlanMappingsSectionState { + switch (action.type) { + case 'SET_PLAN_MAPS': { + const { planNetworkMaps, planStorageMaps } = action.payload; + return { + ...state, + planNetworkMaps, + planStorageMaps, + updatedNetwork: planNetworkMaps?.spec?.map, + updatedStorage: planStorageMaps?.spec?.map, + }; + } + case 'TOGGLE_EDIT': { + return { ...state, edit: !state.edit }; + } + case 'SET_CANCEL': { + const dataChanged = false; + + return { + ...state, + dataChanged, + alertMessage: null, + updatedNetwork: state.planNetworkMaps.spec.map, + updatedStorage: state.planStorageMaps.spec.map, + }; + } + case 'SET_ALERT_MESSAGE': { + return { ...state, alertMessage: action.payload }; + } + case 'ADD_NETWORK_MAPPING': + case 'DELETE_NETWORK_MAPPING': + case 'REPLACE_NETWORK_MAPPING': { + const updatedNetwork = action.payload.newState; + const dataChanged = hasPlanMappingsChanged( + state.planNetworkMaps.spec.map, + state.planStorageMaps.spec.map, + updatedNetwork, + state?.updatedStorage, + ); + + return { + ...state, + dataChanged, + alertMessage: null, + updatedNetwork, + }; + } + case 'ADD_STORAGE_MAPPING': + case 'DELETE_STORAGE_MAPPING': + case 'REPLACE_STORAGE_MAPPING': { + const updatedStorage = action.payload.newState; + const dataChanged = hasPlanMappingsChanged( + state.planNetworkMaps.spec.map, + state.planStorageMaps.spec.map, + state?.updatedNetwork, + updatedStorage, + ); + + return { + ...state, + dataChanged, + alertMessage: null, + updatedStorage, + }; + } + default: + return state; + } +} diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/UpdateMappings/types.ts b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/UpdateMappings/types.ts new file mode 100644 index 000000000..c8adc1c80 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/UpdateMappings/types.ts @@ -0,0 +1,20 @@ +import { ReactNode } from 'react'; +import { PlanEditAction } from 'src/modules/Plans/utils/types/PlanEditAction'; + +import { + V1beta1NetworkMap, + V1beta1NetworkMapSpecMap, + V1beta1StorageMap, + V1beta1StorageMapSpecMap, +} from '@kubev2v/types'; + +export interface PlanMappingsSectionState { + edit: boolean; + dataChanged: boolean; + alertMessage: ReactNode; + updatedNetwork: V1beta1NetworkMapSpecMap[]; + updatedStorage: V1beta1StorageMapSpecMap[]; + planNetworkMaps: V1beta1NetworkMap; + planStorageMaps: V1beta1StorageMap; + editAction?: PlanEditAction; +} diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Mappings/PlanMappings.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Mappings/PlanMappings.tsx index ba5b626ee..bc357b164 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Mappings/PlanMappings.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Mappings/PlanMappings.tsx @@ -1,36 +1,29 @@ -import React from 'react'; +import React, { useEffect, useReducer } from 'react'; import { SectionHeading } from 'src/components/headers/SectionHeading'; -import { useOpenShiftNetworks, useSourceNetworks } from 'src/modules/Providers/hooks/useNetworks'; -import { useOpenShiftStorages, useSourceStorages } from 'src/modules/Providers/hooks/useStorages'; import { useForkliftTranslation } from 'src/utils/i18n'; import { NetworkMapModelGroupVersionKind, PlanModelGroupVersionKind, - ProviderModelGroupVersionKind, StorageMapModelGroupVersionKind, V1beta1NetworkMap, V1beta1Plan, - V1beta1Provider, V1beta1StorageMap, } from '@kubev2v/types'; import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; -import { Alert, PageSection } from '@patternfly/react-core'; +import { PageSection } from '@patternfly/react-core'; -import { PlanMappingsSection } from './PlanMappingsSection'; - -export type PlanMappingsInitSectionProps = { - plan: V1beta1Plan; - loaded: boolean; - loadError: unknown; -}; +import { + initialPlanMappingsState, + PlanMappingsInitSection, + planMappingsSectionReducer, +} from '../../components/UpdateMappings'; export const PlanMappings: React.FC<{ name: string; namespace: string }> = ({ name, namespace, }) => { const { t } = useForkliftTranslation(); - const [plan, loaded, loadError] = useK8sWatchResource({ groupVersionKind: PlanModelGroupVersionKind, namespaced: true, @@ -38,30 +31,6 @@ export const PlanMappings: React.FC<{ name: string; namespace: string }> = ({ namespace, }); - return ( - <> -
- - - - -
- - ); -}; - -const PlanMappingsInitSection: React.FC = (props) => { - const { t } = useForkliftTranslation(); - const { plan } = props; - - // Retrieve all k8s Providers - const [providers, providersLoaded, providersLoadError] = useK8sWatchResource({ - groupVersionKind: ProviderModelGroupVersionKind, - namespaced: true, - isList: true, - namespace: plan?.metadata?.namespace, - }); - // Retrieve all k8s Network Mappings const [networkMaps, networkMapsLoaded, networkMapsError] = useK8sWatchResource< V1beta1NetworkMap[] @@ -88,95 +57,74 @@ const PlanMappingsInitSection: React.FC = (props) const planStorageMaps = storageMaps ? storageMaps.find((storage) => storage?.metadata?.name === plan.spec.map?.storage?.name) : null; - const sourceProvider: V1beta1Provider = providers - ? providers.find((p) => p?.metadata?.name === plan?.spec?.provider?.source?.name) - : null; - const targetProvider = providers - ? providers.find((p) => p?.metadata?.name === plan?.spec?.provider?.destination?.name) - : null; - - // Retrieve source and target providers Mappings from the inventory - const [sourceNetworks, sourceNetworksLoading, sourceNetworksError] = - useSourceNetworks(sourceProvider); - const [targetNetworks, targetNetworksLoading, targetNetworksError] = - useOpenShiftNetworks(targetProvider); - const [sourceStorages, sourceStoragesLoading, sourceStoragesError] = - useSourceStorages(sourceProvider); - const [targetStorages, targetStoragesLoading, targetStoragesError] = - useOpenShiftStorages(targetProvider); - - if ( - !networkMapsLoaded || - !storageMapsLoaded || - !providersLoaded || - sourceNetworksLoading || - targetNetworksLoading || - sourceStoragesLoading || - targetStoragesLoading - ) { - return ( -
- {t('Data is loading, please wait.')} -
- ); - } - if ( - networkMapsError || - storageMapsError || - providersLoadError || - sourceNetworksError || - targetNetworksError || - sourceStoragesError || - targetStoragesError - ) { - return ( -
- - {t( - 'Something is wrong, the data was not loaded due to an error, please try to reload the page.', - )} - -
- ); - } - - if (networkMaps.length == 0 || storageMaps.length == 0) - return ( -
- {t('No Mapping found.')} -
- ); + const [state, dispatch] = useReducer( + planMappingsSectionReducer, + initialPlanMappingsState({ + edit: false, + planNetworkMaps, + planStorageMaps, + }), + ); - // Warn when missing inventory data, missing inventory will make - // some editing options missing. - const alerts = []; + useEffect(() => { + if (planNetworkMaps && planStorageMaps) { + dispatch({ + type: 'SET_PLAN_MAPS', + payload: { planNetworkMaps, planStorageMaps }, + }); + } + }, [planNetworkMaps, planStorageMaps]); + + const checkResources = () => { + if (!networkMapsLoaded || !storageMapsLoaded) { + return ( +
+ {t('Data is loading, please wait.')} +
+ ); + } + + if (networkMapsError || storageMapsError) { + return ( +
+ + {t( + 'Something is wrong, the data was not loaded due to an error, please try to reload the page.', + )} + +
+ ); + } - if (targetStorages.length == 0) { - // Note: target network can't be missing, we always have Pod network. - alerts.push('Missing target storage inventory.'); - } + if (networkMaps.length == 0 || storageMaps.length == 0) + return ( +
+ {t('No Mapping found.')} +
+ ); - if (sourceStorages.length == 0 || sourceNetworks.length == 0) { - alerts.push('Missing storage inventory.'); - } + return null; + }; return ( <> - {alerts.map((alert) => ( -
- -
- ))} - +
+ + + {checkResources() ?? ( + + )} + +
); }; diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Mappings/index.ts b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Mappings/index.ts index d7200b8ea..2b98b8361 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Mappings/index.ts +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Mappings/index.ts @@ -1,4 +1,3 @@ // @index(['./*', /style/g], f => `export * from '${f.path}';`) export * from './PlanMappings'; -export * from './PlanMappingsSection'; // @endindex diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/Migration/MigrationVirtualMachinesList.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/Migration/MigrationVirtualMachinesList.tsx index abfc0ed6a..2a5b05bb3 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/Migration/MigrationVirtualMachinesList.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/Migration/MigrationVirtualMachinesList.tsx @@ -33,12 +33,12 @@ const vmStatuses = [ { id: 'Unknown', label: 'Unknown' }, ]; -const getVMMigrationStatus = (obj: VMData) => { - const isError = obj.statusVM?.conditions?.find((c) => c.type === 'Failed' && c.status === 'True'); - const isSuccess = obj.statusVM?.conditions?.find( +export const getVMMigrationStatus = (statusVM: V1beta1PlanStatusMigrationVms) => { + const isError = statusVM?.conditions?.find((c) => c.type === 'Failed' && c.status === 'True'); + const isSuccess = statusVM?.conditions?.find( (c) => c.type === 'Succeeded' && c.status === 'True', ); - const isRunning = obj.statusVM?.completed === undefined; + const isRunning = statusVM?.completed === undefined; if (isError) { return 'Failed'; diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/Plan/PlanVirtualMachinesList.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/Plan/PlanVirtualMachinesList.tsx index 4be029bea..7387ad49b 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/Plan/PlanVirtualMachinesList.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/Plan/PlanVirtualMachinesList.tsx @@ -1,8 +1,10 @@ import React, { FC } from 'react'; +import { StandardPage } from 'src/components/page/StandardPage'; import { GlobalActionWithSelection, StandardPageWithSelection, } from 'src/components/page/StandardPageWithSelection'; +import { getPlanPhase, PlanPhase } from 'src/modules/Plans/utils'; import { useForkliftTranslation } from 'src/utils/i18n'; import { loadUserSettings, ResourceFieldFactory } from '@kubev2v/common'; @@ -12,7 +14,7 @@ import { V1beta1PlanStatusMigrationVms, } from '@kubev2v/types'; -import { PlanVMsDeleteButton } from '../components'; +import { PlanVMsDeleteButton, PlanVMsEditButton } from '../components'; import { PlanData, VMData } from '../types'; import { PlanVirtualMachinesRow } from './PlanVirtualMachinesRow'; @@ -41,9 +43,6 @@ const fieldsMetadataFactory: ResourceFieldFactory = (t) => [ }, ]; -const PageWithSelection = StandardPageWithSelection; -type PageGlobalActions = FC>[]; - export const PlanVirtualMachinesList: FC<{ obj: PlanData }> = ({ obj }) => { const { t } = useForkliftTranslation(); @@ -84,24 +83,36 @@ export const PlanVirtualMachinesList: FC<{ obj: PlanData }> = ({ obj }) => { const onSelect = () => undefined; const initialSelectedIds = []; - const actions: PageGlobalActions = [ - ({ selectedIds }) => , - ]; - - return ( - >[]; + + const commonProps = { + title: t('Virtual Machines'), + dataSource: vmDataSource, + CellMapper: PlanVirtualMachinesRow, + fieldsMetadata: fieldsMetadataFactory(t), + userSettings: userSettings, + namespace: '', + page: 1, + toId: vmDataToId, + }; + + return phase === PlanPhase.Ready ? ( + + {...commonProps} + GlobalActionToolbarItems={[() => ]} + /> + ) : ( + + {...commonProps} + GlobalActionToolbarItems={ + [ + ({ selectedIds }) => , + ] as PageGlobalActions + } canSelect={canSelect} onSelect={onSelect} selectedIds={initialSelectedIds} - GlobalActionToolbarItems={actions} /> ); }; diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/components/PlanVMsEditButton.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/components/PlanVMsEditButton.tsx new file mode 100644 index 000000000..064280a5b --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/components/PlanVMsEditButton.tsx @@ -0,0 +1,25 @@ +import React, { FC } from 'react'; +import { useModal } from 'src/modules/Providers/modals'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { V1beta1Plan } from '@kubev2v/types'; +import { Button } from '@patternfly/react-core'; + +import { PlanVMsEditModal } from '../modals'; + +export const PlanVMsEditButton: FC<{ + plan: V1beta1Plan; +}> = ({ plan }) => { + const { t } = useForkliftTranslation(); + const { showModal } = useModal(); + + const onClick = () => { + showModal(); + }; + + return ( + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/components/index.ts b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/components/index.ts index db0547464..88a035dbf 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/components/index.ts +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/components/index.ts @@ -4,4 +4,5 @@ export * from './MigrationVMsCancelButton'; export * from './NameCellRenderer'; export * from './PlanVMsCellProps'; export * from './PlanVMsDeleteButton'; +export * from './PlanVMsEditButton'; // @endindex diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/modals/PlanVMsEditModal.style.css b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/modals/PlanVMsEditModal.style.css new file mode 100644 index 000000000..e88ce2baf --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/modals/PlanVMsEditModal.style.css @@ -0,0 +1,3 @@ +.forklift-edit-modal { + overflow: auto; +} diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/modals/PlanVMsEditModal.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/modals/PlanVMsEditModal.tsx new file mode 100644 index 000000000..3def4a3c4 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/modals/PlanVMsEditModal.tsx @@ -0,0 +1,120 @@ +import React, { FC, useMemo } from 'react'; +import { PlanEditAction } from 'src/modules/Plans/utils/types/PlanEditAction'; +import { PlanEditPage } from 'src/modules/Plans/views/edit/PlanEditPage'; +import { useModal } from 'src/modules/Providers/modals'; +import { useInventoryVms, VmData } from 'src/modules/Providers/views'; +import { isEmpty } from 'src/utils'; +import { useNetworkMaps, useProviders, useStorageMaps } from 'src/utils/fetch'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { V1beta1Plan, V1beta1PlanSpecVms } from '@kubev2v/types'; +import { Modal, ModalVariant } from '@patternfly/react-core'; + +import './PlanVMsDeleteModal.style.css'; +import './PlanVMsEditModal.style.css'; + +interface PlanVMsEditModalProps { + plan: V1beta1Plan; + editAction: PlanEditAction; +} + +export const PlanVMsEditModal: FC = ({ plan, editAction }) => { + const { toggleModal } = useModal(); + const { t } = useForkliftTranslation(); + const projectName = plan?.metadata?.namespace; + + // Retrieve all k8s Providers + const [providers, providersLoaded, providersLoadError] = useProviders({ + namespace: projectName, + }); + + const sourceProvider = providers + ? providers.find((p) => p?.metadata?.name === plan?.spec?.provider?.source?.name) + : null; + const targetProvider = providers + ? providers.find((p) => p?.metadata?.name === plan?.spec?.provider?.destination?.name) + : null; + + // Retrieve all k8s Network Mappings + const [networkMaps, networkMapsLoaded, networkMapsError] = useNetworkMaps({ + namespace: projectName, + }); + + // Retrieve all k8s Storage Mappings + const [storageMaps, storageMapsLoaded, storageMapsError] = useStorageMaps({ + namespace: projectName, + }); + + const [vmData] = useInventoryVms( + { provider: sourceProvider }, + providersLoaded, + providersLoadError, + ); + const selectedVMs: VmData[] = []; + const notFoundPlanVMs: V1beta1PlanSpecVms[] = []; + plan.spec.vms.forEach((planVm) => { + const providerVm = vmData.find((vm) => vm.vm.id === planVm.id); + if (providerVm) { + selectedVMs.push(providerVm); + } else { + // Edge case: plan VM not found in list of provider VMs + notFoundPlanVMs.push(planVm); + } + }); + + const planNetworkMaps = useMemo(() => { + return networkMaps + ? networkMaps.find((net) => net?.metadata?.name === plan?.spec?.map?.network?.name) + : null; + }, [networkMaps, plan]); + const planStorageMaps = useMemo(() => { + return storageMaps + ? storageMaps.find((storage) => storage?.metadata?.name === plan.spec.map?.storage?.name) + : null; + }, [storageMaps, plan]); + + const finishedLoading = + providersLoaded && networkMapsLoaded && storageMapsLoaded && !isEmpty(vmData.length); + const hasErrors = providersLoadError || networkMapsError || storageMapsError; + + return ( + + {hasErrors && ( +
+ + {t( + 'Something is wrong, the data was not loaded due to an error, please try to reload the page.', + )} + +
+ )} + {!hasErrors && finishedLoading ? ( + + ) : ( +
+ {t('Data is loading, please wait.')} +
+ )} +
+ ); +}; diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/modals/index.ts b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/modals/index.ts index d40f0e3ef..3af1d27c3 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/modals/index.ts +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/modals/index.ts @@ -2,4 +2,5 @@ export * from './MigrationVMsCancelModal'; export * from './PipelineTasksModal'; export * from './PlanVMsDeleteModal'; +export * from './PlanVMsEditModal'; // @endindex diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/utils/index.ts b/packages/forklift-console-plugin/src/modules/Plans/views/details/utils/index.ts index 46a7383f7..8673c057a 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/utils/index.ts +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/utils/index.ts @@ -8,5 +8,4 @@ export * from './hasPlanMappingsChanged'; export * from './hasSomeCompleteRunningVMs'; export * from './hasTaskCompleted'; export * from './mapMappingsIdsToLabels'; -export * from './patchPlanMappingsData'; // @endindex diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/edit/PlanEditPage.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/edit/PlanEditPage.tsx new file mode 100644 index 000000000..c0519e86a --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/edit/PlanEditPage.tsx @@ -0,0 +1,213 @@ +import React, { useEffect, useReducer, useRef } from 'react'; +import { PlanEditAction } from 'src/modules/Plans/utils/types/PlanEditAction'; +import { VmData } from 'src/modules/Providers/views/details/tabs/VirtualMachines/components/VMCellProps'; +import { setAPiError, startUpdate } from 'src/modules/Providers/views/migrate/reducer/actions'; +import { useFetchEffects } from 'src/modules/Providers/views/migrate/useFetchEffects'; +import { isEmpty } from 'src/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { + V1beta1NetworkMap, + V1beta1Plan, + V1beta1PlanSpecVms, + V1beta1Provider, + V1beta1StorageMap, +} from '@kubev2v/types'; +import { Alert, PageSection, Text, TextContent, Title } from '@patternfly/react-core'; +import { Wizard } from '@patternfly/react-core/deprecated'; + +import { patchPlanMappingsData, patchPlanSpecVms } from '../../utils'; +import { findProviderByID } from '../create/components'; +import { planCreatePageInitialState, planCreatePageReducer } from '../create/states'; +import { SelectSourceProvider } from '../create/steps'; +import { + initialPlanMappingsState, + planMappingsSectionReducer, +} from '../details/components/UpdateMappings'; + +import { PlanUpdateForm } from './steps/PlanUpdateForm'; + +import '../create/PlanCreatePage.style.css'; + +export const PlanEditPage: React.FC<{ + plan: V1beta1Plan; + providers: V1beta1Provider[]; + sourceProvider: V1beta1Provider; + targetProvider: V1beta1Provider; + projectName: string; + onClose: () => void; + selectedVMs: VmData[]; + notFoundPlanVMs: V1beta1PlanSpecVms[]; + editAction: PlanEditAction; + planNetworkMaps: V1beta1NetworkMap; + planStorageMaps: V1beta1StorageMap; +}> = ({ + plan, + providers, + sourceProvider, + targetProvider, + projectName, + onClose, + selectedVMs, + notFoundPlanVMs, + editAction, + planNetworkMaps, + planStorageMaps, +}) => { + const { t } = useForkliftTranslation(); + const startAtStep = 1; + + // Init Select source provider form state + const [filterState, filterDispatch] = useReducer(planCreatePageReducer, { + ...planCreatePageInitialState, + selectedProviderUID: sourceProvider.metadata.uid, + selectedVMs: selectedVMs, + }); + + const selectedProvider = findProviderByID(filterState.selectedProviderUID, providers); + + const [state, dispatch, emptyContext] = useFetchEffects({ + data: { + selectedVms: filterState.selectedVMs, + provider: selectedProvider, + targetProvider, + plan, + editAction, + }, + }); + + const [planMappingsState, planMappingsDispatch] = useReducer( + planMappingsSectionReducer, + initialPlanMappingsState({ + planNetworkMaps, + planStorageMaps, + editAction, + edit: true, + }), + ); + + useEffect(() => { + if (planNetworkMaps && planStorageMaps) { + planMappingsDispatch({ + type: 'SET_PLAN_MAPS', + payload: { planNetworkMaps, planStorageMaps }, + }); + } + }, [planNetworkMaps, planStorageMaps]); + + const mounted = useRef(true); + useEffect( + () => () => { + mounted.current = false; + }, + [], + ); + + useEffect(() => { + const { + flow, + underConstruction: { plan }, + } = state; + if (!flow.editingDone || !mounted.current) { + return; + } + + Promise.all([ + patchPlanSpecVms(plan), + patchPlanMappingsData( + planMappingsState.planNetworkMaps, + planMappingsState.planStorageMaps, + planMappingsState.updatedNetwork, + planMappingsState.updatedStorage, + ), + ]) + .then(() => onClose()) + .catch((error) => mounted.current && dispatch(setAPiError(error))); + }, [state.flow.editingDone]); + + const steps = [ + { + id: 'step-1', + name: editAction === 'VMS' ? t('Select virtual machines') : t('Select source provider'), + component: ( + <> + {!isEmpty(notFoundPlanVMs) && ( + + + {notFoundPlanVMs.map((vm) => `${vm.name} `)} + + + )} + + + ), + enableNext: filterState?.selectedVMs?.length > 0, + }, + { + id: 'step-2', + name: editAction === 'VMS' ? t('Update mappings') : t('Update migration plan'), + component: ( + + + {t('Update mappings')} + + + ), + enableNext: + !emptyContext && + !( + !!state?.flow?.apiError || + Object.values(state?.validation || []).some((validation) => validation === 'error') + ), + canJumpTo: filterState?.selectedVMs?.length > 0, + nextButtonText: + editAction === 'VMS' ? t('Update virtual machines') : t('Update migration plan'), + }, + ]; + + const title = t('Plans wizard'); + return ( + <> + + + {editAction === 'VMS' ? t('Update virtual machines') : t('Update migration plan')} + + + + + dispatch(startUpdate())} + onClose={onClose} + startAtStep={startAtStep} + /> + + + ); +}; + +export default PlanEditPage; diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/edit/steps/PlanUpdateForm.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/edit/steps/PlanUpdateForm.tsx new file mode 100644 index 000000000..c97cb4df8 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/edit/steps/PlanUpdateForm.tsx @@ -0,0 +1,97 @@ +import React, { Dispatch, FormEvent, ReactNode } from 'react'; +import { FormAlerts } from 'src/modules/Providers/views/migrate/components/FormAlerts'; +import { PlansProvidersFields } from 'src/modules/Providers/views/migrate/components/PlansProvidersFields'; +import { PlansTargetNamespaceField } from 'src/modules/Providers/views/migrate/components/PlansTargetNamespaceField'; +import { + PageAction, + setPlanTargetNamespace, + setPlanTargetProvider, +} from 'src/modules/Providers/views/migrate/reducer/actions'; +import { isDone } from 'src/modules/Providers/views/migrate/reducer/helpers'; +import { CreateVmMigrationPageState } from 'src/modules/Providers/views/migrate/types'; + +import { LoadingDots } from '@kubev2v/common'; +import { V1beta1NetworkMap, V1beta1StorageMap } from '@kubev2v/types'; +import { DescriptionList } from '@patternfly/react-core'; + +import { + PlanMappingsInitSection, + PlanMappingsSectionState, +} from '../../details/components/UpdateMappings'; + +type PlanUpdateFormProps = { + children?: ReactNode; + formActions?: ReactNode; + state: CreateVmMigrationPageState; + dispatch: (action: PageAction) => void; + planMappingsState: PlanMappingsSectionState; + planMappingsDispatch: Dispatch<{ + type: string; + payload?; + }>; + planNetworkMaps: V1beta1NetworkMap; + planStorageMaps: V1beta1StorageMap; +}; + +export const PlanUpdateForm = ({ + children, + state, + dispatch, + formActions, + planMappingsState, + planMappingsDispatch, + planNetworkMaps, + planStorageMaps, +}: PlanUpdateFormProps) => { + if (!isDone(state.flow.initialLoading) && !state.flow.apiError) { + return ; + } + + const { + underConstruction: { plan }, + flow, + } = state; + + const onChangeTargetProvider: (value: string, event: FormEvent) => void = ( + value, + ) => { + dispatch(setPlanTargetProvider(value)); + }; + + const onChangeTargetNamespace: (value: string) => void = (value) => { + dispatch(setPlanTargetNamespace(value)); + }; + + return ( + <> + {children} + + {flow.editAction !== 'VMS' && ( + <> + + + + )} + + + {state.flow.apiError && } +
{formActions}
+ + ); +}; + +export default PlanUpdateForm; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/ProvidersCreateVmMigrationContext.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/ProvidersCreateVmMigrationContext.tsx index e14d2d758..a3362e115 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/ProvidersCreateVmMigrationContext.tsx +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/ProvidersCreateVmMigrationContext.tsx @@ -8,8 +8,9 @@ import { useState, } from 'react'; import { produce } from 'immer'; +import { PlanEditAction } from 'src/modules/Plans/utils/types/PlanEditAction'; -import { V1beta1Provider } from '@kubev2v/types'; +import { V1beta1Plan, V1beta1Provider } from '@kubev2v/types'; import { VmData } from '../details'; @@ -18,6 +19,9 @@ export interface CreateVmMigrationContextData { provider?: V1beta1Provider; planName?: string; projectName?: string; + targetProvider?: V1beta1Provider; + plan?: V1beta1Plan; + editAction?: PlanEditAction; } export interface CreateVmMigrationContextType { diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/FormAlerts.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/FormAlerts.tsx new file mode 100644 index 000000000..25f35e264 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/FormAlerts.tsx @@ -0,0 +1,15 @@ +import React, { FC } from 'react'; + +import { Alert } from '@patternfly/react-core'; + +import { CreateVmMigrationPageState } from '../types'; + +interface FormAlertsProps { + state: CreateVmMigrationPageState; +} + +export const FormAlerts: FC = ({ state }) => ( + + {state?.flow?.apiError?.message || state?.flow?.apiError?.toString()} + +); diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/PlansCreateForm.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/PlansCreateForm.tsx index 4d581ad85..6c7d43415 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/PlansCreateForm.tsx +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/PlansCreateForm.tsx @@ -1,28 +1,16 @@ import React, { ReactNode } from 'react'; -import { FilterableSelect } from 'src/components'; import SectionHeading from 'src/components/headers/SectionHeading'; -import { ForkliftTrans, useForkliftTranslation } from 'src/utils/i18n'; +import { useForkliftTranslation } from 'src/utils/i18n'; -import { FormGroupWithHelpText, HelpIconPopover } from '@kubev2v/common'; -import { - NetworkMapModelGroupVersionKind, - ProviderModelGroupVersionKind, - StorageMapModelGroupVersionKind, -} from '@kubev2v/types'; +import { NetworkMapModelGroupVersionKind, StorageMapModelGroupVersionKind } from '@kubev2v/types'; import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; import { AlertVariant, DescriptionList, DescriptionListDescription, DescriptionListGroup, - Form, - FormSelect, - FormSelectOption, - Stack, - StackItem, } from '@patternfly/react-core'; -import { DetailsItem, getIsTarget } from '../../../utils'; import { addNetworkMapping, addStorageMapping, @@ -39,6 +27,8 @@ import { CreateVmMigrationPageState, NetworkAlerts, StorageAlerts } from '../typ import { MappingList } from './MappingList'; import { MappingListHeader } from './MappingListHeader'; +import { PlansProvidersFields } from './PlansProvidersFields'; +import { PlansTargetNamespaceField } from './PlansTargetNamespaceField'; import { StateAlerts } from './StateAlerts'; const buildNetworkMessages = ( @@ -120,13 +110,7 @@ export const PlansCreateForm = ({ const { t } = useForkliftTranslation(); const { - underConstruction: { plan, netMap, storageMap }, - validation, - calculatedOnce: { namespacesUsedBySelectedVms }, - existingResources: { - providers: availableProviders, - targetNamespaces: availableTargetNamespaces, - }, + underConstruction: { netMap, storageMap }, calculatedPerNamespace: { targetNetworks, targetStorages, @@ -149,6 +133,10 @@ export const PlansCreateForm = ({ dispatch(setPlanTargetProvider(value)); }; + const onChangeTargetNamespace: (value: string) => void = (value) => { + dispatch(setPlanTargetNamespace(value)); + }; + return ( <> {children} @@ -158,113 +146,13 @@ export const PlansCreateForm = ({ default: '1Col', }} > - + - - } + - - -
- - onChangeTargetProvider(v, e)} - id="targetProvider" - isDisabled={flow.editingDone} - > - {[ - , - ...availableProviders - .filter(getIsTarget) - .map((provider, index) => ( - - )), - ]} - - -
- -
- - - - - Namespaces, also known as projects, separate resources within clusters. - - - - - - The target namespace is the namespace within your selected target provider - that your virtual machines will be migrated to. This is different from the - namespace that your migration plan will be created in and where your provider - was created. - - - - - } - > - ({ - itemId: ns?.name, - isDisabled: - namespacesUsedBySelectedVms.includes(ns?.name) && - plan.spec.provider?.destination?.name === plan.spec.provider.source.name, - children: ns?.name, - }))} - value={plan.spec.targetNamespace} - onSelect={(value) => dispatch(setPlanTargetNamespace(value as string))} - isDisabled={flow.editingDone} - isScrollable - canCreate - createNewOptionLabel={t('Create new namespace:')} - /> - -
- ) => void; +} + +export const PlansProvidersFields: FC = ({ + onChangeTargetProvider, + state, +}) => { + const { t } = useForkliftTranslation(); + const { + underConstruction: { plan }, + validation, + existingResources: { providers: availableProviders }, + flow, + } = state; + return ( + <> + + + + } + /> + + + +
+ + onChangeTargetProvider(v, e)} + id="targetProvider" + isDisabled={flow.editingDone} + > + {[ + , + ...availableProviders + .filter(getIsTarget) + .map((provider, index) => ( + + )), + ]} + + +
+ + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/PlansTargetNamespaceField.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/PlansTargetNamespaceField.tsx new file mode 100644 index 000000000..d748b9faa --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/PlansTargetNamespaceField.tsx @@ -0,0 +1,74 @@ +import React, { FC } from 'react'; +import { FilterableSelect } from 'src/components'; +import { ForkliftTrans, useForkliftTranslation } from 'src/utils'; + +import { FormGroupWithHelpText, HelpIconPopover } from '@kubev2v/common'; +import { Form, Stack, StackItem } from '@patternfly/react-core'; + +import { CreateVmMigrationPageState } from '../types'; + +interface PlansTargetNamespaceFieldProps { + state: CreateVmMigrationPageState; + onChangeTargetNamespace: (value: string) => void; +} + +export const PlansTargetNamespaceField: FC = ({ + state, + onChangeTargetNamespace, +}) => { + const { t } = useForkliftTranslation(); + const { + underConstruction: { plan }, + validation, + calculatedOnce: { namespacesUsedBySelectedVms }, + existingResources: { targetNamespaces: availableTargetNamespaces }, + flow, + } = state; + + return ( +
+ + + + + Namespaces, also known as projects, separate resources within clusters. + + + + + + The target namespace is the namespace within your selected target provider that + your virtual machines will be migrated to. This is different from the namespace + that your migration plan will be created in and where your provider was created. + + + + + } + > + ({ + itemId: targetNamespace?.name, + isDisabled: + namespacesUsedBySelectedVms.includes(targetNamespace?.name) && + plan.spec.provider?.destination?.name === plan.spec.provider.source.name, + children: targetNamespace?.name, + }))} + value={plan.spec.targetNamespace} + onSelect={(value) => onChangeTargetNamespace(value as string)} + isDisabled={flow.editingDone} + isScrollable + canCreate + createNewOptionLabel={t('Create new namespace:')} + /> + +
+ ); +}; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/actions.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/actions.ts index 8cf37f852..20b0ef6dc 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/actions.ts +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/actions.ts @@ -44,6 +44,7 @@ export const SET_NICK_PROFILES = 'SET_NICK_PROFILES'; export const SET_DISKS = 'SET_DISKS'; export const SET_EXISTING_NET_MAPS = 'SET_EXISTING_NET_MAPS'; export const SET_EXISTING_STORAGE_MAPS = 'SET_EXISTING_STORAGE_MAPS'; +export const START_UPDATE = 'START_UPDATE'; export const START_CREATE = 'START_CREATE'; export const SET_API_ERROR = 'SET_API_ERROR'; export const REMOVE_ALERT = 'REMOVE_ALERT'; @@ -69,6 +70,7 @@ export type CreateVmMigration = | typeof SET_NICK_PROFILES | typeof SET_DISKS | typeof SET_EXISTING_NET_MAPS + | typeof START_UPDATE | typeof START_CREATE | typeof SET_API_ERROR | typeof SET_EXISTING_STORAGE_MAPS @@ -381,6 +383,11 @@ export const setDisks = ( payload: { disks, loading, error }, }); +export const startUpdate = (): PageAction => ({ + type: 'START_UPDATE', + payload: {}, +}); + export const startCreate = (): PageAction => ({ type: 'START_CREATE', payload: {}, @@ -404,6 +411,8 @@ export const initState = ( projectName, sourceProvider: V1beta1Provider, selectedVms: VmData[], + plan?: V1beta1Plan, + targetProvider?: V1beta1Provider, ): PageAction => ({ type: 'INIT', payload: { @@ -411,6 +420,8 @@ export const initState = ( planName, projectName, sourceProvider, + targetProvider, selectedVms, + plan, }, }); diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/createInitialState.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/createInitialState.ts index da3ebdefb..d489abd72 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/createInitialState.ts +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/createInitialState.ts @@ -1,6 +1,9 @@ +import { PlanEditAction } from 'src/modules/Plans/utils/types/PlanEditAction'; + import { ProviderModelGroupVersionKind as ProviderGVK, ProviderType, + V1beta1Plan, V1beta1Provider, } from '@kubev2v/types'; @@ -23,9 +26,12 @@ import { getObjectRef, resourceFieldsForType } from './helpers'; export type InitialStateParameters = { namespace: string; sourceProvider: V1beta1Provider; + targetProvider?: V1beta1Provider; selectedVms: VmData[]; planName: string; projectName: string; + plan?: V1beta1Plan; + editAction?: PlanEditAction; }; export const createInitialState = ({ @@ -37,25 +43,27 @@ export const createInitialState = ({ apiVersion: `${ProviderGVK.group}/${ProviderGVK.version}`, kind: ProviderGVK.kind, }, + targetProvider, selectedVms = [], + plan = planTemplate, + editAction, }: InitialStateParameters): CreateVmMigrationPageState => { const hasVmNicWithEmptyProfile = hasNicWithEmptyProfile(sourceProvider, selectedVms); - return { underConstruction: { projectName, plan: { - ...planTemplate, + ...plan, metadata: { - ...planTemplate?.metadata, - name: planName, + ...plan?.metadata, + name: planName || plan?.metadata?.name || '', namespace, }, spec: { - ...planTemplate?.spec, + ...plan?.spec, provider: { source: getObjectRef(sourceProvider), - destination: undefined, + destination: targetProvider ? getObjectRef(targetProvider) : undefined, }, targetNamespace: namespace, vms: selectedVms.map((data) => ({ @@ -76,7 +84,7 @@ export const createInitialState = ({ ...networkMapTemplate?.spec, provider: { source: getObjectRef(sourceProvider), - destination: undefined, + destination: targetProvider ? getObjectRef(targetProvider) : undefined, }, }, }, @@ -91,7 +99,7 @@ export const createInitialState = ({ ...storageMapTemplate?.spec, provider: { source: getObjectRef(sourceProvider), - destination: undefined, + destination: targetProvider ? getObjectRef(targetProvider) : undefined, }, }, }, @@ -114,6 +122,7 @@ export const createInitialState = ({ selectedVms, sourceProvider, namespace, + plan, }, validation: { planName: 'default', @@ -167,6 +176,7 @@ export const createInitialState = ({ [SET_DISKS]: !['ovirt', 'openstack'].includes(sourceProvider.spec?.type), [SET_NICK_PROFILES]: sourceProvider.spec?.type !== 'ovirt', }, + editAction, }, }; }; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/reducer.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/reducer.ts index 6105d85ba..d1a114ea4 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/reducer.ts +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer/reducer.ts @@ -51,6 +51,7 @@ import { SET_TARGET_NAMESPACE, SET_TARGET_PROVIDER, START_CREATE, + START_UPDATE, } from './actions'; import { addMapping, deleteMapping, replaceMapping } from './changeMapping'; import { createInitialState, InitialStateParameters } from './createInitialState'; @@ -290,6 +291,9 @@ const handlers: { // triggered from useEffect on any data change existingResources.storageMaps = existingStorageMaps; }, + [START_UPDATE]({ flow }) { + flow.editingDone = true; + }, [START_CREATE]({ flow, receivedAsParams: { sourceProvider }, @@ -473,13 +477,23 @@ const handlers: { [INIT]( draft, { - payload: { namespace, sourceProvider, selectedVms, planName, projectName }, + payload: { + namespace, + sourceProvider, + targetProvider, + selectedVms, + plan, + planName, + projectName, + }, }: PageAction, ) { const newDraft = createInitialState({ namespace, sourceProvider, + targetProvider, selectedVms, + plan, planName, projectName, }); diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/types.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/types.ts index b20b64f1d..fd5c8c0a7 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/types.ts +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/types.ts @@ -1,4 +1,5 @@ import { FC } from 'react'; +import { PlanEditAction } from 'src/modules/Plans/utils/types/PlanEditAction'; import { ResourceFieldFactory, RowProps } from '@kubev2v/common'; import { @@ -89,6 +90,7 @@ export interface CreateVmMigrationPageState { selectedVms: VmData[]; sourceProvider: V1beta1Provider; namespace: string; + plan?: V1beta1Plan; }; // placeholder for helper data workArea: { @@ -98,6 +100,7 @@ export interface CreateVmMigrationPageState { editingDone: boolean; apiError?: Error; initialLoading: { [keys in CreateVmMigration]?: boolean }; + editAction?: PlanEditAction; }; } export interface MappingSource { diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/useFetchEffects.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/useFetchEffects.ts index 8e468f753..a77d6e4de 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/useFetchEffects.ts +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/useFetchEffects.ts @@ -47,8 +47,11 @@ export const useFetchEffects = ( const { selectedVms, provider: sourceProvider, + targetProvider: tProvider, + plan, planName, projectName, + editAction, } = createVmMigrationContext?.data || {}; // error state - the page was entered directly without choosing the VMs @@ -57,7 +60,16 @@ export const useFetchEffects = ( const [state, dispatch] = useImmerReducer( reducer, - { namespace, sourceProvider, selectedVms, planName, projectName }, + { + namespace, + sourceProvider, + targetProvider: tProvider, + selectedVms, + plan, + planName, + projectName, + editAction, + }, createInitialState, ); @@ -80,7 +92,9 @@ export const useFetchEffects = ( useEffect( () => !editingDone && - dispatch(initState(namespace, planName, projectName, sourceProvider, selectedVms)), + dispatch( + initState(namespace, planName, projectName, sourceProvider, selectedVms, plan, tProvider), + ), [selectedVms], ); @@ -186,6 +200,7 @@ export const useFetchEffects = ( const [sourceNetworks, sourceNetworksLoading, sourceNetworksError] = useSourceNetworks(sourceProvider); + useEffect( () => !editingDone && diff --git a/packages/forklift-console-plugin/src/utils/fetch.ts b/packages/forklift-console-plugin/src/utils/fetch.ts index 5ba149404..72ead4870 100644 --- a/packages/forklift-console-plugin/src/utils/fetch.ts +++ b/packages/forklift-console-plugin/src/utils/fetch.ts @@ -1,10 +1,28 @@ -import { ProviderModelGroupVersionKind, V1beta1Provider } from '@kubev2v/types'; +import { + NetworkMapModelGroupVersionKind, + ProviderModelGroupVersionKind, + StorageMapModelGroupVersionKind, + V1beta1NetworkMap, + V1beta1Provider, + V1beta1StorageMap, +} from '@kubev2v/types'; import { useK8sWatchResource, WatchK8sResource, WatchK8sResult, } from '@openshift-console/dynamic-plugin-sdk'; +export const useProvider = ({ + namespace, + name, +}: WatchK8sResource): WatchK8sResult => + useK8sWatchResource({ + groupVersionKind: ProviderModelGroupVersionKind, + namespaced: true, + namespace, + name, + }); + export const useProviders = ({ namespace, name, @@ -38,3 +56,29 @@ export const useHasSufficientProviders = (namespace?: string) => { return hasSufficientProviders; }; + +export const useNetworkMaps = ({ + namespace, + name, + isList = true, +}: WatchK8sResource): WatchK8sResult => + useK8sWatchResource({ + groupVersionKind: NetworkMapModelGroupVersionKind, + isList, + namespaced: true, + namespace, + name, + }); + +export const useStorageMaps = ({ + namespace, + name, + isList = true, +}: WatchK8sResource): WatchK8sResult => + useK8sWatchResource({ + groupVersionKind: StorageMapModelGroupVersionKind, + isList, + namespaced: true, + namespace, + name, + }); diff --git a/packages/forklift-console-plugin/src/utils/index.ts b/packages/forklift-console-plugin/src/utils/index.ts index 47685e96f..41fe0d9ce 100644 --- a/packages/forklift-console-plugin/src/utils/index.ts +++ b/packages/forklift-console-plugin/src/utils/index.ts @@ -4,6 +4,7 @@ export * from './deepCopy'; export * from './enums'; export * from './fetch'; export * from './i18n'; +export * from './isEmpty'; export * from './resources'; export * from './types'; // @endindex diff --git a/packages/forklift-console-plugin/src/utils/isEmpty.ts b/packages/forklift-console-plugin/src/utils/isEmpty.ts new file mode 100644 index 000000000..c03036883 --- /dev/null +++ b/packages/forklift-console-plugin/src/utils/isEmpty.ts @@ -0,0 +1,3 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const isEmpty = (obj: any) => + [Array, Object].includes((obj || {}).constructor) && !Object.entries(obj || {}).length;