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 65463ecf8..cbcabfffd 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 @@ -169,6 +169,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", @@ -486,7 +487,8 @@ "The certificate is not a valid PEM-encoded X.509 certificate": "The certificate is not a valid PEM-encoded X.509 certificate", "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 edit mappings button is disabled when the plan status does not enable editing.": "The edit mappings button is disabled when the plan status does not enable editing.", + "The edit virtual machines button is disabled when the plan status does not enable editing.": "The edit virtual machines button is disabled when the plan status does not enable editing.", "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.", @@ -504,6 +506,7 @@ "The URL of the Red Hat Virtualization Manager (RHVM) API endpoint, for example: https://rhv-host-example.com/ovirt-engine/api .": "The URL of the Red Hat Virtualization Manager (RHVM) API endpoint, for example: https://rhv-host-example.com/ovirt-engine/api .", "The URL of the vCenter API endpoint, for example: https://vCenter-host-example.com/sdk .": "The URL of the vCenter API endpoint, for example: https://vCenter-host-example.com/sdk .", "The username for the ESXi host admin": "The username for the ESXi host admin", + "This mapping's associated VM has been migrated and can no longer be modified.": "This mapping's associated VM has been migrated and can no longer be modified.", "To troubleshoot, check the Forklift controller pod events and logs.": "To troubleshoot, check the Forklift controller pod events and logs.", "To troubleshoot, check the Forklift controller pod logs.": "To troubleshoot, check the Forklift controller pod logs.", "To troubleshoot, view the network map details page\n and check the Forklift controller pod logs.": "To troubleshoot, view the network map details page\n and check the Forklift controller pod logs.", @@ -526,7 +529,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/components/page/StandardPageWithExpansion.tsx b/packages/forklift-console-plugin/src/components/page/StandardPageWithExpansion.tsx new file mode 100644 index 000000000..b4bbe1dfd --- /dev/null +++ b/packages/forklift-console-plugin/src/components/page/StandardPageWithExpansion.tsx @@ -0,0 +1,149 @@ +import React, { useState } from 'react'; + +import { RowProps, withTr } from '@kubev2v/common'; +import { Td, Th } from '@patternfly/react-table'; + +import StandardPage, { StandardPageProps } from './StandardPage'; + +export function withRowExpansion({ CellMapper, isExpanded, toggleExpandFor }) { + const Enhanced = (props: RowProps) => ( + <> + {isExpanded && ( + toggleExpandFor([props.resourceData]), + }} + /> + )} + + + ); + Enhanced.displayName = `${CellMapper.displayName || 'CellMapper'}WithExpansion`; + return Enhanced; +} + +export interface IdBasedExpansionProps { + /** + * @returns string that can be used as an unique identifier + */ + toId?: (item: T) => string; + + /** + * onExpand is called when expand changes + */ + onExpand?: (expandedIds: string[]) => void; + + /** + * Expanded ids + */ + expandedIds?: string[]; +} + +/** + * Adds ID based expansion to StandardPage component. + * Contract: + * 1. IDs provided with toId() function are unique and constant in time + */ +export function withIdBasedExpansion({ + toId, + onExpand, + expandedIds: initialExpandedIds, +}: IdBasedExpansionProps) { + const Enhanced = (props: StandardPageProps) => { + const [expandedIds, setExpandedIds] = useState(initialExpandedIds); + + const isExpanded = + onExpand || expandedIds ? (item: T) => expandedIds.includes(toId(item)) : undefined; + + const toggleExpandFor = (items: T[]) => { + const ids = items.map(toId); + const allExpanded = ids.every((id) => expandedIds?.includes(id)); + const newExpandedIds = [ + ...(expandedIds || []).filter((it) => !ids.includes(it)), + ...(allExpanded ? [] : ids), + ]; + + setExpandedIds(newExpandedIds); + if (onExpand) { + onExpand(newExpandedIds); + } + }; + + const { CellMapper, ExpandedComponent, ...rest } = props; + + const RowMapper = withTr( + withRowExpansion({ + CellMapper: CellMapper, + isExpanded, + toggleExpandFor, + }), + ExpandedComponent, + ); + + return ( + } + GlobalActionToolbarItems={props.GlobalActionToolbarItems} + /> + ); + }; + Enhanced.displayName = 'StandardPageWithExpansion'; + return Enhanced; +} + +/** + * Properties for the `StandardPageWithExpansion` component. + * These properties extend the base `StandardPageProps` and add additional ones related to expansion. + * + * @typedef {Object} StandardPageWithExpansionProps + * @property {Function} toId - A function that returns a unique identifier for each item. + * @property {Function} onExpand - A callback function that is triggered when row is expanded or un expanded. + * @property {string[]} expandedIds - An array of identifiers for the currently expanded items. + * @property {...StandardPageProps} - Other props that are passed through to the `StandardPage` component. + * + * @template T - The type of the items being displayed in the table. + */ +export interface StandardPageWithExpansionProps extends StandardPageProps { + toId?: (item: T) => string; + onExpand?: (expandedIds: string[]) => void; + expandedIds?: string[]; +} + +/** + * Renders a standard page with expansion capabilities. + * This component wraps the `StandardPage` component and adds support for row expansion. + * It uses the provided `toId`, `onExpand`, and `expandedIds` props to manage the expansion state. + * + * @param {Object} props - The properties passed to the component. + * @param {Function} props.toId - A function that returns a unique identifier for each item. + * @param {...StandardPageProps} props - Other props that are passed through to the `StandardPage` component. + * + * @template T - The type of the items being displayed in the table. + * + * @example + * item.id} + * // ...other props + * /> + */ +export function StandardPageWithExpansion(props: StandardPageWithExpansionProps) { + const { toId, onExpand, expandedIds, ...rest } = props; + + if (onExpand && (!toId || !expandedIds)) { + throw new Error('Missing required properties: toId, expandedIds'); + } + + const EnhancedStandardPage = withIdBasedExpansion({ + toId, + onExpand, + expandedIds, + }); + + return ; +} 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/components/ProvidersVirtualMachinesList.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/create/components/ProvidersVirtualMachinesList.tsx index e1b730b7d..7b90240ed 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/create/components/ProvidersVirtualMachinesList.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/create/components/ProvidersVirtualMachinesList.tsx @@ -14,9 +14,19 @@ export const ProviderVirtualMachinesList: React.FC<{ namespace: string; onSelect?: (selectedIds: VmData[]) => void; initialSelectedIds?: string[]; + disabledVmIds?: string[]; showActions: boolean; className?: string; -}> = ({ title, name, namespace, onSelect, initialSelectedIds, showActions, className }) => { +}> = ({ + title, + name, + namespace, + onSelect, + initialSelectedIds, + disabledVmIds, + showActions, + className, +}) => { const [provider, providerLoaded, providerLoadError] = useK8sWatchResource({ groupVersionKind: ProviderModelGroupVersionKind, namespaced: true, @@ -35,6 +45,7 @@ export const ProviderVirtualMachinesList: React.FC<{ loadError={providerLoadError} onSelect={onSelect} initialSelectedIds={initialSelectedIds} + disabledVmIds={disabledVmIds} showActions={showActions} className={className} /> diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/create/steps/SelectSourceProvider/MemoizedProviderVirtualMachinesList.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/create/steps/SelectSourceProvider/MemoizedProviderVirtualMachinesList.tsx index ef557cbcc..b95777059 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/create/steps/SelectSourceProvider/MemoizedProviderVirtualMachinesList.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/create/steps/SelectSourceProvider/MemoizedProviderVirtualMachinesList.tsx @@ -9,6 +9,7 @@ export interface ProviderVirtualMachinesListProps { namespace: string; onSelect: (selectedVms: VmData[]) => void; initialSelectedIds: string[]; + disabledVmIds?: string[]; showActions: boolean; } @@ -19,6 +20,7 @@ export const MemoizedProviderVirtualMachinesList = memo( namespace, onSelect, initialSelectedIds, + disabledVmIds, showActions, }: ProviderVirtualMachinesListProps) => { return ( @@ -28,6 +30,7 @@ export const MemoizedProviderVirtualMachinesList = memo( namespace={namespace} onSelect={onSelect} initialSelectedIds={initialSelectedIds} + disabledVmIds={disabledVmIds} showActions={showActions} /> ); 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 88023b47d..4fd49afca 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,8 @@ export const SelectSourceProvider: React.FC<{ state: CreateVmMigrationPageState; dispatch: React.Dispatch>; filterDispatch: React.Dispatch; + hideProviderSection?: boolean; + disabledVmIds?: string[]; }> = ({ filterState, providers, @@ -27,6 +29,8 @@ export const SelectSourceProvider: React.FC<{ projectName, dispatch, filterDispatch, + hideProviderSection, + disabledVmIds, }) => { const { t } = useForkliftTranslation(); @@ -45,16 +49,20 @@ export const SelectSourceProvider: React.FC<{ return ( <> - {t('Select source provider')} + {!hideProviderSection && ( + <> + {t('Select source provider')} - + + + )} {filterState.selectedProviderUID && ( <> @@ -70,6 +78,7 @@ export const SelectSourceProvider: React.FC<{ filterDispatch({ type: 'UPDATE_SELECTED_VMS', payload: selectedVms }) } initialSelectedIds={filterState.selectedVMs.map((vm) => vm.vm.id)} + disabledVmIds={disabledVmIds} showActions={false} /> diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/MappingList.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/MappingList.tsx index 9fb6a436e..910c328d8 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/MappingList.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/MappingList.tsx @@ -32,6 +32,13 @@ interface MappingListProps { * Is in edit/view mode? In case of view mode, the DataListCells are disabled and buttons are hidden */ isEditable?: boolean; + /** + * A function that determines whether a mapping list item is editable + * + * @param source The mapping source + * @returns boolean + */ + canEditItem?: (source: string) => boolean; } export const MappingList: FC = ({ @@ -45,9 +52,9 @@ export const MappingList: FC = ({ noSourcesLabel, isDisabled = false, isEditable = true, + canEditItem, }) => { const { t } = useForkliftTranslation(); - return ( <> @@ -64,6 +71,7 @@ export const MappingList: FC = ({ generalSourcesLabel={generalSourcesLabel} noSourcesLabel={noSourcesLabel} isEditable={isEditable} + canEditItem={(source) => canEditItem?.(source)} /> ))} diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/MappingListItem.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/MappingListItem.tsx index 15a231151..b210f2a3c 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/MappingListItem.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/MappingListItem.tsx @@ -10,6 +10,7 @@ import { DataListItem, DataListItemCells, DataListItemRow, + Tooltip, } from '@patternfly/react-core'; import { Select, @@ -35,6 +36,7 @@ interface MappingListItemProps { replaceMapping: (val: { current: Mapping; next: Mapping }) => void; deleteMapping: (mapping: Mapping) => void; isEditable: boolean; + canEditItem?: (source: string) => boolean; } export const MappingListItem: FC = ({ @@ -48,11 +50,14 @@ export const MappingListItem: FC = ({ replaceMapping, deleteMapping, isEditable, + canEditItem, }) => { const { t } = useForkliftTranslation(); const [isSrcOpen, setToggleSrcOpen] = useToggle(false); const [isTrgOpen, setToggleTrgOpen] = useToggle(false); + const canEditMapping = canEditItem?.(source); + const onClick = () => { deleteMapping({ source, destination }); }; @@ -93,7 +98,7 @@ export const MappingListItem: FC = ({ onSelect={onSelectSource} selections={source} isOpen={isSrcOpen} - isDisabled={!isEditable} + isDisabled={!isEditable || !canEditMapping} aria-labelledby="" isGrouped menuAppendTo={() => document.body} @@ -112,7 +117,7 @@ export const MappingListItem: FC = ({ onSelect={onSelectDestination} selections={destination} isOpen={isTrgOpen} - isDisabled={!isEditable} + isDisabled={!isEditable || !canEditMapping} aria-labelledby="" menuAppendTo={() => document.body} > @@ -129,13 +134,30 @@ export const MappingListItem: FC = ({ aria-label={t('Actions')} aria-labelledby="" > - - - - - - - - - {t( - 'Click the update mappings button to save your changes, button is disabled until a change is detected.', - )} - - - + {!state.editAction && ( + <> + + + + + + + + + + + {t( + 'Click the update mappings button to save your changes, button is disabled until a change is detected.', + )} + + + + + )} {state.alertMessage ? ( <> { - 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'; @@ -146,6 +147,7 @@ const fieldsMetadataFactory: ResourceFieldFactory = (t) => [ ]; const PageWithSelection = StandardPageWithSelection; +const PageWithExpansion = StandardPageWithExpansion; type PageWithSelectionProps = StandardPageWithSelectionProps; type PageGlobalActions = FC>[]; @@ -255,9 +257,7 @@ export const MigrationVirtualMachinesList: FC<{ obj: PlanData }> = ({ obj }) => ), ]; } else { - actions = [ - ({ selectedIds }) => , - ]; + actions = [() => ]; } const canSelectWhenExecuting = (item: VMData) => @@ -288,5 +288,9 @@ export const MigrationVirtualMachinesList: FC<{ obj: PlanData }> = ({ obj }) => GlobalActionToolbarItems: actions, }; - return ; + return isExecuting ? ( + + ) : ( + + ); }; 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..4cd25e85f 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,18 +1,16 @@ import React, { FC } from 'react'; -import { - GlobalActionWithSelection, - StandardPageWithSelection, -} from 'src/components/page/StandardPageWithSelection'; +import { StandardPage } from 'src/components/page/StandardPage'; import { useForkliftTranslation } from 'src/utils/i18n'; import { loadUserSettings, ResourceFieldFactory } from '@kubev2v/common'; +import { GlobalActionToolbarProps } from '@kubev2v/common'; import { V1beta1PlanSpecVms, V1beta1PlanStatusConditions, V1beta1PlanStatusMigrationVms, } from '@kubev2v/types'; -import { PlanVMsDeleteButton } from '../components'; +import { PlanVMsEditButton } from '../components'; import { PlanData, VMData } from '../types'; import { PlanVirtualMachinesRow } from './PlanVirtualMachinesRow'; @@ -41,8 +39,8 @@ const fieldsMetadataFactory: ResourceFieldFactory = (t) => [ }, ]; -const PageWithSelection = StandardPageWithSelection; -type PageGlobalActions = FC>[]; +const PageWithNoSelection = StandardPage; +type PageGlobalActions = FC>[]; export const PlanVirtualMachinesList: FC<{ obj: PlanData }> = ({ obj }) => { const { t } = useForkliftTranslation(); @@ -79,17 +77,11 @@ export const PlanVirtualMachinesList: FC<{ obj: PlanData }> = ({ obj }) => { })); const vmDataSource: [VMData[], boolean, unknown] = [vmData || [], true, undefined]; const vmDataToId = (item: VMData) => item?.specVM?.id; - const canSelect = (item: VMData) => - item?.statusVM?.started === undefined || item?.statusVM?.error !== undefined; - const onSelect = () => undefined; - const initialSelectedIds = []; - const actions: PageGlobalActions = [ - ({ selectedIds }) => , - ]; + const actions: PageGlobalActions = [() => ]; return ( - = ({ obj }) => { namespace={''} page={1} toId={vmDataToId} - 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..e140c38f9 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/components/PlanVMsEditButton.tsx @@ -0,0 +1,37 @@ +import React, { FC } from 'react'; +import { isPlanEditable } from 'src/modules/Plans/utils'; +import { useModal } from 'src/modules/Providers/modals'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { V1beta1Plan } from '@kubev2v/types'; +import { Button, Tooltip } from '@patternfly/react-core'; + +import { PlanVMsEditModal } from '../modals'; + +export const PlanVMsEditButton: FC<{ + plan: V1beta1Plan; +}> = ({ plan }) => { + const { t } = useForkliftTranslation(); + const { showModal } = useModal(); + const planEditable = isPlanEditable(plan); + + const onClick = () => { + showModal(); + }; + + return !planEditable ? ( + + + + ) : ( + + ); +}; 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..048913925 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/modals/PlanVMsEditModal.tsx @@ -0,0 +1,146 @@ +import React 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 } from 'src/modules/Providers/views'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { + NetworkMapModelGroupVersionKind, + ProviderModelGroupVersionKind, + StorageMapModelGroupVersionKind, + V1beta1NetworkMap, + V1beta1Plan, + V1beta1Provider, + V1beta1StorageMap, +} from '@kubev2v/types'; +import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { Modal, ModalVariant } from '@patternfly/react-core'; + +import './PlanVMsDeleteModal.style.css'; +import './PlanVMsEditModal.style.css'; + +export interface PlanVMsEditModalProps { + plan: V1beta1Plan; + editAction: PlanEditAction; +} + +export const PlanVMsEditModal: React.FC = ({ plan, editAction }) => { + const { toggleModal } = useModal(); + const { t } = useForkliftTranslation(); + const projectName = plan?.metadata?.namespace; + + // Retrieve k8s source provider + const [sourceProvider, sourceProviderLoaded, sourceProviderLoadError] = + useK8sWatchResource({ + groupVersionKind: ProviderModelGroupVersionKind, + namespaced: true, + name: plan?.spec?.provider?.source?.name, + namespace: plan?.spec?.provider?.source?.namespace, + }); + + // Retrieve k8s target provider + const [targetProvider, targetProviderLoaded, targetProviderLoadError] = + useK8sWatchResource({ + groupVersionKind: ProviderModelGroupVersionKind, + namespaced: true, + name: plan?.spec?.provider?.destination?.name, + namespace: plan?.spec?.provider?.destination?.namespace, + }); + + 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[] + >({ + groupVersionKind: NetworkMapModelGroupVersionKind, + namespaced: true, + isList: true, + namespace: plan?.metadata?.namespace, + }); + + // Retrieve all k8s Storage Mappings + const [storageMaps, storageMapsLoaded, storageMapsError] = useK8sWatchResource< + V1beta1StorageMap[] + >({ + groupVersionKind: StorageMapModelGroupVersionKind, + namespaced: true, + isList: true, + namespace: plan?.metadata?.namespace, + }); + + const [vmData] = useInventoryVms( + { provider: sourceProvider }, + sourceProviderLoaded, + sourceProviderLoadError, + ); + const initialSelectedIds = plan.spec.vms.map((specVm) => specVm.id); + const selectedVMs = vmData.filter((vm) => initialSelectedIds.includes(vm.vm.id)); + + const planNetworkMaps = networkMaps + ? networkMaps.find((net) => net?.metadata?.name === plan?.spec?.map?.network?.name) + : null; + const planStorageMaps = storageMaps + ? storageMaps.find((storage) => storage?.metadata?.name === plan.spec.map?.storage?.name) + : null; + + const finishedLoading = + providersLoaded && + sourceProviderLoaded && + targetProviderLoaded && + networkMapsLoaded && + storageMapsLoaded && + vmData.length > 0; + const hasErrors = + providersLoadError || + sourceProviderLoadError || + targetProviderLoadError || + 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/edit/PlanEditPage.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/edit/PlanEditPage.tsx new file mode 100644 index 000000000..e8da7e127 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/edit/PlanEditPage.tsx @@ -0,0 +1,177 @@ +import React, { useEffect, useReducer } from 'react'; +import { PlanEditAction } from 'src/modules/Plans/utils/types/PlanEditAction'; +import { + planMappingsSectionReducer, + PlanMappingsSectionState, +} from 'src/modules/Plans/views/details/tabs/Mappings/PlanMappingsSection'; +import { getVMMigrationStatus } from 'src/modules/Plans/views/details/tabs/VirtualMachines/Migration/MigrationVirtualMachinesList'; +import { VmData } from 'src/modules/Providers/views/details/tabs/VirtualMachines/components/VMCellProps'; +import ProvidersUpdateVmMigrationPage from 'src/modules/Providers/views/migrate/ProvidersUpdateVmMigrationPage'; +import { startUpdate } from 'src/modules/Providers/views/migrate/reducer/actions'; +import { useFetchEffects } from 'src/modules/Providers/views/migrate/useFetchEffects'; +import { useUpdateEffect } from 'src/modules/Providers/views/migrate/useUpdateEffect'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { V1beta1NetworkMap, V1beta1Plan, V1beta1Provider, V1beta1StorageMap } from '@kubev2v/types'; +import { PageSection, Title } from '@patternfly/react-core'; +import { Wizard } from '@patternfly/react-core/deprecated'; + +import { findProviderByID } from '../create/components'; +import { planCreatePageInitialState, planCreatePageReducer } from '../create/states'; +import { SelectSourceProvider } from '../create/steps'; + +import '../create/PlanCreatePage.style.css'; + +export const PlanEditPage: React.FC<{ + plan: V1beta1Plan; + providers: V1beta1Provider[]; + sourceProvider: V1beta1Provider; + targetProvider: V1beta1Provider; + projectName: string; + onClose: () => void; + selectedVMs?: VmData[]; + editAction: PlanEditAction; + planNetworkMaps: V1beta1NetworkMap; + planStorageMaps: V1beta1StorageMap; +}> = ({ + plan, + providers, + sourceProvider, + targetProvider, + projectName, + onClose, + selectedVMs, + editAction, + planNetworkMaps, + planStorageMaps, +}) => { + const { t } = useForkliftTranslation(); + const startAtStep = 1; + + const migrationVms = plan?.status?.migration?.vms; + const migratedVmIds = migrationVms?.reduce((migrated, vm) => { + if (getVMMigrationStatus(vm) === 'Succeeded') { + migrated.push(vm.id); + } + return migrated; + }, []); + + // Init Select source provider form state + const [filterState, filterDispatch] = useReducer(planCreatePageReducer, { + ...planCreatePageInitialState, + selectedProviderUID: sourceProvider.metadata.uid, + selectedVMs: selectedVMs, + }); + + const selectedProvider = + filterState.selectedProviderUID !== '' + ? findProviderByID(filterState.selectedProviderUID, providers) + : undefined; + + const [state, dispatch, emptyContext] = useFetchEffects({ + data: { + selectedVms: filterState.selectedVMs, + provider: selectedProvider, + targetProvider, + plan, + editAction, + }, + }); + + const initialPlanMappingsState: PlanMappingsSectionState = { + edit: true, + dataChanged: false, + alertMessage: null, + updatedNetwork: planNetworkMaps?.spec?.map || [], + updatedStorage: planStorageMaps?.spec?.map || [], + planNetworkMaps: planNetworkMaps, + planStorageMaps: planStorageMaps, + editAction, + }; + + const [planMappingsState, planMappingsDispatch] = useReducer( + planMappingsSectionReducer, + initialPlanMappingsState, + ); + + useEffect(() => { + if (planNetworkMaps && planStorageMaps) { + planMappingsDispatch({ + type: 'SET_PLAN_MAPS', + payload: { planNetworkMaps, planStorageMaps }, + }); + } + }, [planNetworkMaps, planStorageMaps]); + + useUpdateEffect(state, dispatch, planMappingsState, onClose); + + const steps = [ + { + id: 'step-1', + name: editAction === 'VMS' ? t('Select virtual machines') : t('Select source provider'), + component: ( + + ), + enableNext: filterState?.selectedVMs?.length > 0, + }, + { + id: 'step-2', + name: editAction === 'VMS' ? t('Update mappings') : t('Update migration plan'), + component: ( + + ), + 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 = 'Plans wizard'; + return ( + <> + + + {editAction === 'VMS' ? 'Update virtual machines' : 'Update migration plan'} + + + + + dispatch(startUpdate())} + onClose={onClose} + startAtStep={startAtStep} + /> + + + ); +}; + +export default PlanEditPage; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/OVirtVirtualMachinesList.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/OVirtVirtualMachinesList.tsx index b8da16762..f07bff210 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/OVirtVirtualMachinesList.tsx +++ b/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/OVirtVirtualMachinesList.tsx @@ -92,6 +92,7 @@ export const OVirtVirtualMachinesList: React.FC = loadError, onSelect, initialSelectedIds, + disabledVmIds, showActions, className, }) => ( @@ -105,6 +106,7 @@ export const OVirtVirtualMachinesList: React.FC = pageId="OVirtVirtualMachinesList" onSelect={onSelect} initialSelectedIds={initialSelectedIds} + disabledVmIds={disabledVmIds} showActions={showActions} className={className} /> diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/OpenShiftVirtualMachinesList.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/OpenShiftVirtualMachinesList.tsx index ffe854cc6..1977b0963 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/OpenShiftVirtualMachinesList.tsx +++ b/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/OpenShiftVirtualMachinesList.tsx @@ -80,6 +80,7 @@ export const OpenShiftVirtualMachinesList: React.FC ( @@ -93,6 +94,7 @@ export const OpenShiftVirtualMachinesList: React.FC diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/OpenStackVirtualMachinesList.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/OpenStackVirtualMachinesList.tsx index 7da4ae04d..6d83d22d2 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/OpenStackVirtualMachinesList.tsx +++ b/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/OpenStackVirtualMachinesList.tsx @@ -111,6 +111,7 @@ export const OpenStackVirtualMachinesList: React.FC ( @@ -124,6 +125,7 @@ export const OpenStackVirtualMachinesList: React.FC diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/OvaVirtualMachinesList.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/OvaVirtualMachinesList.tsx index 7310c67f5..186126fce 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/OvaVirtualMachinesList.tsx +++ b/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/OvaVirtualMachinesList.tsx @@ -49,6 +49,7 @@ export const OvaVirtualMachinesList: React.FC = ({ loadError, onSelect, initialSelectedIds, + disabledVmIds, showActions, className, }) => ( @@ -62,6 +63,7 @@ export const OvaVirtualMachinesList: React.FC = ({ pageId="OvaVirtualMachinesList" onSelect={onSelect} initialSelectedIds={initialSelectedIds} + disabledVmIds={disabledVmIds} showActions={showActions} className={className} /> diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/ProviderVirtualMachines.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/ProviderVirtualMachines.tsx index f8300ab91..3ce9d10a4 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/ProviderVirtualMachines.tsx +++ b/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/ProviderVirtualMachines.tsx @@ -22,6 +22,7 @@ export interface ProviderVirtualMachinesProps { loadError?: unknown; onSelect?: (selectedVMs: VmData[]) => void; initialSelectedIds?: string[]; + disabledVmIds?: string[]; showActions: boolean; className?: string; } @@ -77,6 +78,7 @@ export const ProviderVirtualMachinesListWrapper: React.FC { @@ -90,6 +92,7 @@ export const ProviderVirtualMachinesListWrapper: React.FC @@ -103,6 +106,7 @@ export const ProviderVirtualMachinesListWrapper: React.FC @@ -116,6 +120,7 @@ export const ProviderVirtualMachinesListWrapper: React.FC @@ -129,6 +134,7 @@ export const ProviderVirtualMachinesListWrapper: React.FC @@ -142,6 +148,7 @@ export const ProviderVirtualMachinesListWrapper: React.FC diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/VSphereVirtualMachinesList.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/VSphereVirtualMachinesList.tsx index f1bde2e04..1b8cb5065 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/VSphereVirtualMachinesList.tsx +++ b/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/VSphereVirtualMachinesList.tsx @@ -93,6 +93,7 @@ export const VSphereVirtualMachinesList: React.FC loadError, onSelect, initialSelectedIds, + disabledVmIds, showActions, className, }) => { @@ -128,6 +129,7 @@ export const VSphereVirtualMachinesList: React.FC pageId="VSphereVirtualMachinesList" onSelect={onSelect} initialSelectedIds={initialSelectedIds} + disabledVmIds={disabledVmIds} showActions={showActions} className={className} /> diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/components/ProviderVirtualMachinesList.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/components/ProviderVirtualMachinesList.tsx index d306dbec3..dc1ddd118 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/components/ProviderVirtualMachinesList.tsx +++ b/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/components/ProviderVirtualMachinesList.tsx @@ -31,6 +31,7 @@ export interface ProviderVirtualMachinesListProps { pageId: string; onSelect?: (selectedVMs: VmData[]) => void; initialSelectedIds?: string[]; + disabledVmIds?: string[]; showActions: boolean; className?: string; } @@ -45,6 +46,7 @@ export const ProviderVirtualMachinesList: FC = pageId, onSelect, initialSelectedIds, + disabledVmIds, showActions, className, }) => { @@ -97,6 +99,7 @@ export const ProviderVirtualMachinesList: FC = page={1} expandedIds={initialExpandedIds_} ExpandedComponent={ConcernsTable} + canSelect={(item) => !disabledVmIds?.includes(item.vm.id)} /> ); }; 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/ProvidersUpdateVmMigrationPage.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/ProvidersUpdateVmMigrationPage.tsx new file mode 100644 index 000000000..ccf5a7a19 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/ProvidersUpdateVmMigrationPage.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { PlanMappingsSectionState } from 'src/modules/Plans/views/details/tabs/Mappings/PlanMappingsSection'; + +import { LoadingDots } from '@kubev2v/common'; +import { V1beta1NetworkMap, V1beta1StorageMap } from '@kubev2v/types'; +import { Alert } from '@patternfly/react-core'; + +import { PlansUpdateForm } from './components/PlansUpdateForm'; +import { CreateVmMigration, PageAction } from './reducer/actions'; +import { isDone } from './reducer/helpers'; +import { CreateVmMigrationPageState } from './types'; + +const ProvidersUpdateVmMigrationPage: React.FC<{ + state: CreateVmMigrationPageState; + dispatch: React.Dispatch>; + emptyContext: boolean; + planMappingsState: PlanMappingsSectionState; + planMappingsDispatch: React.Dispatch<{ + type: string; + payload?; + }>; + planNetworkMaps: V1beta1NetworkMap; + planStorageMaps: V1beta1StorageMap; +}> = ({ + state, + dispatch, + emptyContext, + planMappingsState, + planMappingsDispatch, + planNetworkMaps, + planStorageMaps, +}) => { + if (emptyContext) { + // display empty node and wait for redirect triggered from useEffect + // the redirect should be triggered right after the first render() + // so any "empty page" would only "blink" + return <>; + } + + if (!isDone(state.flow.initialLoading) && !state.flow.apiError) { + return ; + } + + const FormAlerts = state.flow.apiError && ( + + {state?.flow?.apiError?.message || state?.flow?.apiError?.toString()} + + ); + + return ( + + ); +}; + +export default ProvidersUpdateVmMigrationPage; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/PlansUpdateForm.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/PlansUpdateForm.tsx new file mode 100644 index 000000000..80f3c918f --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/components/PlansUpdateForm.tsx @@ -0,0 +1,205 @@ +import React, { ReactNode } from 'react'; +import { FilterableSelect } from 'src/components'; +import SectionHeading from 'src/components/headers/SectionHeading'; +import { PlanMappingsInitSection } from 'src/modules/Plans/views/details/tabs/Mappings/PlanMappings'; +import { PlanMappingsSectionState } from 'src/modules/Plans/views/details/tabs/Mappings/PlanMappingsSection'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { FormGroupWithHelpText } from '@kubev2v/common'; +import { + ProviderModelGroupVersionKind, + V1beta1NetworkMap, + V1beta1StorageMap, +} from '@kubev2v/types'; +import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; +import { + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Form, + FormSelect, + FormSelectOption, +} from '@patternfly/react-core'; + +import { DetailsItem, getIsTarget } from '../../../utils'; +import { PageAction, setPlanTargetNamespace, setPlanTargetProvider } from '../reducer/actions'; +import { CreateVmMigrationPageState } from '../types'; + +export type PlansUpdateFormProps = { + children?: ReactNode; + formAlerts?: ReactNode; + formActions?: ReactNode; + state: CreateVmMigrationPageState; + dispatch: (action: PageAction) => void; + planMappingsState: PlanMappingsSectionState; + planMappingsDispatch: React.Dispatch<{ + type: string; + payload?; + }>; + planNetworkMaps: V1beta1NetworkMap; + planStorageMaps: V1beta1StorageMap; +}; + +export const PlansUpdateForm = ({ + children, + state, + dispatch, + formAlerts, + formActions, + planMappingsState, + planMappingsDispatch, + planNetworkMaps, + planStorageMaps, +}: PlansUpdateFormProps) => { + const { t } = useForkliftTranslation(); + + const { + underConstruction: { plan }, + validation, + calculatedOnce: { namespacesUsedBySelectedVms }, + existingResources: { + providers: availableProviders, + targetNamespaces: availableTargetNamespaces, + }, + flow, + } = state; + + const onChangeTargetProvider: ( + value: string, + event: React.FormEvent, + ) => void = (value) => { + dispatch(setPlanTargetProvider(value)); + }; + + const mappingsSection = ( + + ); + + return ( + <> + {children} + + {flow.editAction !== 'VMS' && ( + <> + + {t('Plan name')} + +
{plan.metadata.name}
+
+
+ + + + + } + /> + + {t('Selected VMs')} + + {t('{{vmCount}} VMs selected ', { vmCount: plan.spec.vms?.length ?? 0 })} + + + + + +
+ + onChangeTargetProvider(v, e)} + id="targetProvider" + isDisabled={flow.editingDone} + > + {[ + , + ...availableProviders + .filter(getIsTarget) + .map((provider, index) => ( + + )), + ]} + + +
+ +
+ + ({ + 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:')} + /> + +
+ + )} + {mappingsSection} +
+ {formAlerts} +
{formActions}
+ + ); +}; 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/modules/Providers/views/migrate/useUpdateEffect.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/useUpdateEffect.ts new file mode 100644 index 000000000..c8199d5a0 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/useUpdateEffect.ts @@ -0,0 +1,76 @@ +import { useEffect, useRef } from 'react'; +import { PlanMappingsSectionState } from 'src/modules/Plans/views/details/tabs/Mappings/PlanMappingsSection'; +import { patchPlanMappingsData } from 'src/modules/Plans/views/details/utils/patchPlanMappingsData'; + +import { + PlanModel, + V1beta1NetworkMap, + V1beta1NetworkMapSpecMap, + V1beta1Plan, + V1beta1StorageMap, + V1beta1StorageMapSpecMap, +} from '@kubev2v/types'; +import { k8sPatch } from '@openshift-console/dynamic-plugin-sdk'; + +import { setAPiError } from './reducer/actions'; +import { CreateVmMigrationPageState } from './types'; + +const updatePlan = async (plan: V1beta1Plan) => { + await k8sPatch({ + model: PlanModel, + resource: plan, + data: [ + { + op: 'replace', + path: '/spec/vms', + value: plan.spec.vms, + }, + ], + }); +}; + +export const useUpdateEffect = ( + state: CreateVmMigrationPageState, + dispatch, + planMappingsState: PlanMappingsSectionState, + onClose, +) => { + const mounted = useRef(true); + useEffect( + () => () => { + mounted.current = false; + }, + [], + ); + + useEffect(() => { + const { + flow, + underConstruction: { plan }, + } = state; + if (!flow.editingDone || !mounted.current) { + return; + } + + Promise.all([ + updatePlan(plan), + updateMappings( + planMappingsState.planNetworkMaps, + planMappingsState.planStorageMaps, + planMappingsState.updatedNetwork, + planMappingsState.updatedStorage, + ), + ]) + .then(() => onClose()) + .catch((error) => mounted.current && dispatch(setAPiError(error))); + }, [state.flow.editingDone]); +}; + +async function updateMappings( + planNetworkMaps: V1beta1NetworkMap, + planStorageMaps: V1beta1StorageMap, + updatedNetwork: V1beta1NetworkMapSpecMap[], + updatedStorage: V1beta1StorageMapSpecMap[], +) { + await patchPlanMappingsData(planNetworkMaps, planStorageMaps, updatedNetwork, updatedStorage); +}