From 1c385d6f365f2cd69eaad80a584b13a6a82d51ba Mon Sep 17 00:00:00 2001 From: Radoslaw Szwajkowski Date: Mon, 8 Jan 2024 13:18:28 +0100 Subject: [PATCH] Add a fast Create Plan page - step 1 Changes: 1. transfer data from VM List screen to Create Plan page via console.context-provider 2. add immer library to keep data in the reducer immutable 3. add uuid library to generate random suffixes 4. use forms for the Create Plan page - similar to Create Provider Signed-off-by: Radoslaw Szwajkowski --- package-lock.json | 36 ++++ packages/eslint-plugin/cspell.wordlist.txt | 1 + .../en/plugin__forklift-console-plugin.json | 6 + packages/forklift-console-plugin/package.json | 5 +- .../src/modules/Providers/dynamic-plugin.ts | 31 +++- .../Providers/views/create/templates/index.ts | 1 + .../views/create/templates/planTemplate.ts | 14 ++ .../components/MigrationAction.tsx | 39 +++++ .../ProviderVirtualMachinesList.tsx | 18 +- .../views/migrate/PlansCreateForm.tsx | 70 ++++++++ .../ProvidersCreateVmMigrationContext.tsx | 64 +++++++ .../ProvidersCreateVmMigrationPage.tsx | 91 ++++++++++ .../Providers/views/migrate/actions.ts | 86 ++++++++++ .../modules/Providers/views/migrate/index.ts | 3 + .../Providers/views/migrate/reducer.ts | 160 ++++++++++++++++++ 15 files changed, 621 insertions(+), 4 deletions(-) create mode 100644 packages/forklift-console-plugin/src/modules/Providers/views/create/templates/planTemplate.ts create mode 100644 packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/components/MigrationAction.tsx create mode 100644 packages/forklift-console-plugin/src/modules/Providers/views/migrate/PlansCreateForm.tsx create mode 100644 packages/forklift-console-plugin/src/modules/Providers/views/migrate/ProvidersCreateVmMigrationContext.tsx create mode 100644 packages/forklift-console-plugin/src/modules/Providers/views/migrate/ProvidersCreateVmMigrationPage.tsx create mode 100644 packages/forklift-console-plugin/src/modules/Providers/views/migrate/actions.ts create mode 100644 packages/forklift-console-plugin/src/modules/Providers/views/migrate/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer.ts diff --git a/package-lock.json b/package-lock.json index 451a1c54a..89f77a629 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16549,6 +16549,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz", + "integrity": "sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/immutable": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", @@ -29721,6 +29731,16 @@ "node": ">=0.10.0" } }, + "node_modules/use-immer": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/use-immer/-/use-immer-0.9.0.tgz", + "integrity": "sha512-/L+enLi0nvuZ6j4WlyK0US9/ECUtV5v9RUbtxnn5+WbtaXYUaOBoKHDNL9I5AETdurQ4rIFIj/s+Z5X80ATyKw==", + "dev": true, + "peerDependencies": { + "immer": ">=2.0.0", + "react": "^16.8.0 || ^17.0.1 || ^18.0.0" + } + }, "node_modules/use-resize-observer": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-9.1.0.tgz", @@ -31417,6 +31437,7 @@ "@types/jsonpath": "^0.2.0", "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.7.1", + "immer": "^10.0.3", "mini-svg-data-uri": "^1.4.4", "react-linkify": "^1.0.0-alpha", "style-loader": "^3.3.1", @@ -31424,6 +31445,8 @@ "ts-loader": "^9.3.1", "tsconfig-paths-webpack-plugin": "^4.0.0", "typescript": "^4.7.4", + "use-immer": "^0.9.0", + "uuid": "^9.0.1", "webpack": "^5.79.0", "webpack-cli": "^5.0.2", "webpack-dev-server": "^4.7.4" @@ -31441,6 +31464,19 @@ "react-router-dom": "5.2.0" } }, + "packages/forklift-console-plugin/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "packages/legacy": { "name": "@kubev2v/legacy", "version": "0.0.1", diff --git a/packages/eslint-plugin/cspell.wordlist.txt b/packages/eslint-plugin/cspell.wordlist.txt index 7b60f32fc..291d06432 100644 --- a/packages/eslint-plugin/cspell.wordlist.txt +++ b/packages/eslint-plugin/cspell.wordlist.txt @@ -66,3 +66,4 @@ multiqueue filesystems bootloader typeahead +immer 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 7966d8464..109a3dcc4 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 @@ -6,6 +6,7 @@ "{{groupCount}} Groups": "{{groupCount}} Groups", "{{name}} Details": "{{name}} Details", "{{selectedLength}} hosts selected.": "{{selectedLength}} hosts selected.", + "{{vmCount}} VMs selected ": "{{vmCount}} VMs selected ", "{{vmDone}} of {{vmCount}} VMs migrated": "{{vmDone}} of {{vmCount}} VMs migrated", "24 hours": "24 hours", "404: Not Found": "404: Not Found", @@ -57,10 +58,13 @@ "Copied": "Copied", "Copy": "Copy", "Create a migration plan and select VMs from the source provider for migration.": "Create a migration plan and select VMs from the source provider for migration.", + "Create and edit": "Create and edit", + "Create and start": "Create and start", "Create by using the form or manually entering YAML or JSON definitions, Provider CR stores attributes that enable MTV to connect to and interact with the source and target providers.": "Create by using the form or manually entering YAML or JSON definitions, Provider CR stores attributes that enable MTV to connect to and interact with the source and target providers.", "Create NetworkMap": "Create NetworkMap", "Create new provider": "Create new provider", "Create plan": "Create plan", + "Create Plan": "Create Plan", "Create provider": "Create provider", "Create Provider": "Create Provider", "Create StorageMap": "Create StorageMap", @@ -191,6 +195,7 @@ "Maximum number of concurrent VM migrations. Default value is 20.": "Maximum number of concurrent VM migrations. Default value is 20.", "Message": "Message", "Message: {{message}}": "Message: {{message}}", + "Migrate": "Migrate", "Migrating virtualization workloads is a multi-step process. <2>Learn more.": "Migrating virtualization workloads is a multi-step process. <2>Learn more.", "Migration network maps are used to map network interfaces between source and target virtualization providers, at least one source and one target provider must be available in order to create a migration storage map, <2>Learn more.": "Migration network maps are used to map network interfaces between source and target virtualization providers, at least one source and one target provider must be available in order to create a migration storage map, <2>Learn more.", "Migration networks maps are used to map network interfaces between source and target workloads.": "Migration networks maps are used to map network interfaces between source and target workloads.", @@ -281,6 +286,7 @@ "Password": "Password", "Path": "Path", "Persistent TPM/EFI": "Persistent TPM/EFI", + "Plan name": "Plan name", "Plans": "Plans", "Plans for virtualization": "Plans for virtualization", "Please choose a NetworkAttachmentDefinition for default data transfer.": "Please choose a NetworkAttachmentDefinition for default data transfer.", diff --git a/packages/forklift-console-plugin/package.json b/packages/forklift-console-plugin/package.json index a4c5bc960..dc25ec825 100644 --- a/packages/forklift-console-plugin/package.json +++ b/packages/forklift-console-plugin/package.json @@ -52,6 +52,7 @@ "@types/jsonpath": "^0.2.0", "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.7.1", + "immer": "^10.0.3", "mini-svg-data-uri": "^1.4.4", "react-linkify": "^1.0.0-alpha", "style-loader": "^3.3.1", @@ -59,6 +60,8 @@ "ts-loader": "^9.3.1", "tsconfig-paths-webpack-plugin": "^4.0.0", "typescript": "^4.7.4", + "use-immer": "^0.9.0", + "uuid": "^9.0.1", "webpack": "^5.79.0", "webpack-cli": "^5.0.2", "webpack-dev-server": "^4.7.4" @@ -66,4 +69,4 @@ "msw": { "workerDirectory": "dist" } -} +} \ No newline at end of file diff --git a/packages/forklift-console-plugin/src/modules/Providers/dynamic-plugin.ts b/packages/forklift-console-plugin/src/modules/Providers/dynamic-plugin.ts index 8188cb3ad..6d2ad93ca 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/dynamic-plugin.ts +++ b/packages/forklift-console-plugin/src/modules/Providers/dynamic-plugin.ts @@ -1,6 +1,12 @@ -import { ProviderModel, ProviderModelGroupVersionKind } from '@kubev2v/types'; +import { + PlanModel, + PlanModelGroupVersionKind, + ProviderModel, + ProviderModelGroupVersionKind, +} from '@kubev2v/types'; import { EncodedExtension } from '@openshift/dynamic-plugin-sdk'; import { + ContextProvider, CreateResource, ModelMetadata, ResourceDetailsPage, @@ -13,6 +19,10 @@ export const exposedModules: ConsolePluginMetadata['exposedModules'] = { ProvidersListPage: './modules/Providers/views/list/ProvidersListPage', ProviderDetailsPage: './modules/Providers/views/details/ProviderDetailsPage', ProvidersCreatePage: './modules/Providers/views/create/ProvidersCreatePage', + ProvidersCreateVmMigrationContext: + './modules/Providers/views/migrate/ProvidersCreateVmMigrationContext', + ProvidersCreateVmMigrationPage: + './modules/Providers/views/migrate/ProvidersCreateVmMigrationPage', }; export const extensions: EncodedExtension[] = [ @@ -71,4 +81,23 @@ export const extensions: EncodedExtension[] = [ ...ProviderModel, }, } as EncodedExtension, + { + type: 'console.context-provider', + properties: { + provider: { $codeRef: 'ProvidersCreateVmMigrationContext.CreateVmMigrationProvider' }, + useValueHook: { + $codeRef: 'ProvidersCreateVmMigrationContext.useCreateVmMigrationContextValue', + }, + }, + } as EncodedExtension, + { + type: 'console.resource/create', + properties: { + component: { + $codeRef: 'ProvidersCreateVmMigrationPage', + }, + model: PlanModelGroupVersionKind, + ...PlanModel, + }, + } as EncodedExtension, ]; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/index.ts b/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/index.ts index 9408052f7..2f2ecff62 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/index.ts +++ b/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/index.ts @@ -1,4 +1,5 @@ // @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './planTemplate'; export * from './providerTemplate'; export * from './secretTemplate'; // @endindex diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/planTemplate.ts b/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/planTemplate.ts new file mode 100644 index 000000000..2e291d68a --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/planTemplate.ts @@ -0,0 +1,14 @@ +import { V1beta1Plan } from '@kubev2v/types'; + +export const planTemplate: V1beta1Plan = { + apiVersion: 'forklift.konveyor.io/v1beta1', + kind: 'Plan', + metadata: { + name: undefined, + namespace: undefined, + }, + spec: { + map: {}, + targetNamespace: '', + }, +}; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/components/MigrationAction.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/components/MigrationAction.tsx new file mode 100644 index 000000000..f50af3791 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/details/tabs/VirtualMachines/components/MigrationAction.tsx @@ -0,0 +1,39 @@ +import React, { FC } from 'react'; +import { useHistory } from 'react-router'; +import { getResourceUrl } from 'src/modules/Providers/utils'; +import { useCreateVmMigrationData } from 'src/modules/Providers/views/migrate'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { PlanModelRef, V1beta1Provider } from '@kubev2v/types'; +import { Button, ToolbarItem } from '@patternfly/react-core'; + +import { VmData } from './VMCellProps'; + +export const MigrationAction: FC<{ + selectedVms: VmData[]; + provider: V1beta1Provider; +}> = ({ selectedVms, provider }) => { + const { t } = useForkliftTranslation(); + const history = useHistory(); + const namespace = provider?.metadata?.namespace; + const planListURL = getResourceUrl({ + reference: PlanModelRef, + namespace, + namespaced: namespace !== undefined, + }); + const { setData } = useCreateVmMigrationData(); + return ( + + + + ); +}; 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 904053fda..e78d9cb54 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 @@ -1,6 +1,6 @@ import React, { FC, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { withIdBasedSelection } from 'src/components/page/withSelection'; +import { GlobalActionWithSelection, withIdBasedSelection } from 'src/components/page/withSelection'; import { ProviderData } from 'src/modules/Providers/utils'; import { useForkliftTranslation } from 'src/utils/i18n'; @@ -16,6 +16,7 @@ import { Concern } from '@kubev2v/types'; import { useInventoryVms } from '../utils/useInventoryVms'; +import { MigrationAction } from './MigrationAction'; import { VmData } from './VMCellProps'; export interface ProviderVirtualMachinesListProps extends RouteComponentProps { @@ -29,8 +30,10 @@ export interface ProviderVirtualMachinesListProps extends RouteComponentProps { pageId: string; } +export const toId = (item: VmData) => + item.vm.providerType === 'openshift' ? item.vm.uid : item.vm.id; const PageWithSelection = withIdBasedSelection({ - toId: (item: VmData) => (item.vm.providerType === 'openshift' ? item.vm.uid : item.vm.id), + toId, canSelect: (item: VmData) => !!item, }); @@ -47,6 +50,16 @@ export const ProviderVirtualMachinesList: FC = const [userSettings] = useState(() => loadUserSettings({ pageId })); const [vmData, loading] = useInventoryVms(obj, loaded, loadError); + const actions: FC>[] = [ + ({ selectedIds }) => ( + selectedIds.includes(toId(data))), + }} + /> + ), + ]; return ( = features: EnumFilter, }} extraSupportedMatchers={[concernsMatcher, featuresMatcher]} + GlobalActionToolbarItems={actions} /> ); }; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/PlansCreateForm.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/PlansCreateForm.tsx new file mode 100644 index 000000000..208d6b716 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/PlansCreateForm.tsx @@ -0,0 +1,70 @@ +import React, { useState } from 'react'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ProviderModelGroupVersionKind } from '@kubev2v/types'; +import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; +import { DescriptionList, Form, FormGroup, TextInput } from '@patternfly/react-core'; + +import { DetailsItem } from '../../utils'; + +import { PageAction, setPlanName } from './actions'; +import { CreateVmMigrationPageState } from './reducer'; + +export const PlansCreateForm = ({ + state: { newPlan: plan, validation }, + dispatch, +}: { + state: CreateVmMigrationPageState; + dispatch: (action: PageAction) => void; +}) => { + const { t } = useForkliftTranslation(); + const [isNameEdited, setIsNameEdited] = useState(false); + return ( + + {isNameEdited ? ( +
+ + dispatch(setPlanName(value?.trim() ?? '', []))} + /> + +
+ ) : ( + setIsNameEdited(true)} + /> + )} + + } + /> + +
+ ); +}; 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 new file mode 100644 index 000000000..7b3194b36 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/ProvidersCreateVmMigrationContext.tsx @@ -0,0 +1,64 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { produce } from 'immer'; + +import { V1beta1Provider } from '@kubev2v/types'; + +import { VmData } from '../details'; + +export interface CreateVmMigrationContextData { + selectedVms: VmData[]; + provider?: V1beta1Provider; +} + +export interface CreateVmMigrationContextType { + data?: CreateVmMigrationContextData; + setData: (data: CreateVmMigrationContextData) => void; +} + +export const CreateVmMigrationContext = createContext({ + setData: () => undefined, +}); + +export const CreateVmMigrationProvider = CreateVmMigrationContext.Provider; + +/* Provides value for the context via useValueHook extension point + */ +export const useCreateVmMigrationContextValue = (): CreateVmMigrationContextType => { + const [data, setData] = useState(); + + // use the same approach as useSafetyFirst() hook + // https://github.com/openshift/console/blob/9d4a9b0a01b2de64b308f8423a325f1fae5f8726/frontend/packages/console-dynamic-plugin-sdk/src/app/components/safety-first.tsx#L10 + const mounted = useRef(true); + useEffect( + () => () => { + mounted.current = false; + }, + [], + ); + + const setValueSafe = useCallback((newValue) => { + if (mounted.current) { + setData(newValue); + } + }, []); + + return useMemo( + () => ({ + data, + setData: (newState: CreateVmMigrationContextData) => setValueSafe(produce(() => newState)), + }), + [data, setData], + ); +}; + +/* Abstraction layer to separate the code from the current data passing implementation (context). + */ +export const useCreateVmMigrationData = () => useContext(CreateVmMigrationContext); diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/ProvidersCreateVmMigrationPage.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/ProvidersCreateVmMigrationPage.tsx new file mode 100644 index 000000000..3bce7203e --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/ProvidersCreateVmMigrationPage.tsx @@ -0,0 +1,91 @@ +import React, { FC, useEffect } from 'react'; +import { useHistory } from 'react-router'; +import { useForkliftTranslation } from 'src/utils/i18n'; +import { useImmerReducer } from 'use-immer'; + +import { ProviderModelGroupVersionKind, ProviderModelRef, V1beta1Provider } from '@kubev2v/types'; +import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { Button, Flex, FlexItem, PageSection, Title } from '@patternfly/react-core'; + +import { useToggle } from '../../hooks'; +import { getResourceUrl } from '../../utils'; + +import { setAvailableProviders } from './actions'; +import { PlansCreateForm } from './PlansCreateForm'; +import { useCreateVmMigrationData } from './ProvidersCreateVmMigrationContext'; +import { createInitialState, reducer } from './reducer'; + +const ProvidersCreateVmMigrationPage: FC<{ + namespace: string; +}> = ({ namespace }) => { + const { t } = useForkliftTranslation(); + const history = useHistory(); + + const { data: { selectedVms = [], provider: sourceProvider = undefined } = {} } = + useCreateVmMigrationData(); + // error state - the page was entered directly without choosing the VMs + const emptyContext = !selectedVms?.length || !sourceProvider; + // error recovery - redirect to provider list + useEffect(() => { + if (emptyContext) { + history.push( + getResourceUrl({ + reference: ProviderModelRef, + namespace: namespace, + }), + ); + } + }, [emptyContext]); + + const [state, dispatch] = useImmerReducer( + reducer, + { namespace, sourceProvider, selectedVms }, + createInitialState, + ); + + const [providers] = useK8sWatchResource({ + groupVersionKind: ProviderModelGroupVersionKind, + namespaced: true, + isList: true, + namespace, + }); + useEffect(() => dispatch(setAvailableProviders(providers ?? [])), [providers]); + + const [isLoading, toggleIsLoading] = useToggle(); + const onUpdate = toggleIsLoading; + + 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 <>; + } + + return ( + + {t('Create Plan')} + + + + + + + + + + + + + + + + ); +}; + +export default ProvidersCreateVmMigrationPage; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/actions.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/actions.ts new file mode 100644 index 000000000..89f660ead --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/actions.ts @@ -0,0 +1,86 @@ +import { V1beta1Provider } from '@kubev2v/types'; + +// action type names +export const SET_NAME = 'SET_NAME'; +export const SET_DESCRIPTION = 'SET_DESCRIPTION'; +export const SET_TARGET_PROVIDER = 'SET_TARGET_PROVIDER'; +export const SET_TARGET_NAMESPACE = 'SET_TARGET_NAMESPACE'; +export const SET_AVAILABLE_PROVIDERS = 'SET_AVAILABLE_PROVIDERS'; + +export type CreateVmMigration = + | typeof SET_NAME + | typeof SET_DESCRIPTION + | typeof SET_TARGET_PROVIDER + | typeof SET_TARGET_NAMESPACE + | typeof SET_AVAILABLE_PROVIDERS; + +export interface PageAction { + type: S; + payload: T; +} + +// action payload types + +export interface PlanName { + name: string; + existingPlanNames: string[]; +} + +export interface PlanDescription { + description: string; +} + +export interface PlanTargetProvider { + targetProvider: V1beta1Provider; +} + +export interface PlanTargetNamespace { + targetNamespace: string; +} + +export interface PlanAvailableProviders { + availableProviders: V1beta1Provider[]; +} + +// action creators + +export const setPlanTargetProvider = ( + targetProvider: V1beta1Provider, +): PageAction => ({ + type: 'SET_TARGET_PROVIDER', + payload: { targetProvider }, +}); + +export const setPlanTargetNamespace = ( + targetNamespace: string, +): PageAction => ({ + type: 'SET_TARGET_NAMESPACE', + payload: { targetNamespace }, +}); + +export const setPlanDescription = ( + description: string, +): PageAction => ({ + type: 'SET_DESCRIPTION', + payload: { description }, +}); + +export const setPlanName = ( + name: string, + existingPlanNames: string[], +): PageAction => ({ + type: 'SET_NAME', + payload: { + name, + existingPlanNames, + }, +}); + +export const setAvailableProviders = ( + availableProviders: V1beta1Provider[], +): PageAction => ({ + type: 'SET_AVAILABLE_PROVIDERS', + payload: { + availableProviders, + }, +}); diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/index.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/index.ts new file mode 100644 index 000000000..be02a5b5f --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/index.ts @@ -0,0 +1,3 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './ProvidersCreateVmMigrationContext'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer.ts new file mode 100644 index 000000000..488018f10 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer.ts @@ -0,0 +1,160 @@ +import { Draft } from 'immer'; +import { isProviderLocalTarget } from 'src/utils/resources'; +import { v4 as randomId } from 'uuid'; + +import { V1beta1Plan, V1beta1Provider } from '@kubev2v/types'; + +import { validateK8sName, Validation } from '../../utils'; +import { planTemplate } from '../create/templates'; +import { toId, VmData } from '../details'; + +import { + CreateVmMigration, + PageAction, + PlanAvailableProviders, + PlanDescription, + PlanName, + PlanTargetNamespace, + PlanTargetProvider, + SET_AVAILABLE_PROVIDERS, + SET_DESCRIPTION, + SET_NAME, + SET_TARGET_NAMESPACE, + SET_TARGET_PROVIDER, +} from './actions'; + +export interface CreateVmMigrationPageState { + newPlan: V1beta1Plan; + validationError: Error | null; + apiError: Error | null; + validation: { + name: Validation; + targetNamespace: Validation; + }; + availableProviders: V1beta1Provider[]; + selectedVms: VmData[]; +} + +const validateUniqueName = (name: string, existingPlanNames: string[]) => + existingPlanNames.every((existingName) => existingName !== name); + +const actions: { + [name: string]: ( + draft: Draft, + action: PageAction, + ) => CreateVmMigrationPageState; +} = { + [SET_NAME]( + draft, + { payload: { name, existingPlanNames } }: PageAction, + ) { + draft.newPlan.metadata.name = name; + draft.validation.name = + validateK8sName(name) && validateUniqueName(name, existingPlanNames) ? 'success' : 'error'; + return draft; + }, + [SET_DESCRIPTION]( + draft, + { payload: { description } }: PageAction, + ) { + draft.newPlan.spec.description = description; + return draft; + }, + [SET_TARGET_NAMESPACE]( + draft, + { payload: { targetNamespace } }: PageAction, + ) { + draft.newPlan.spec.targetNamespace = targetNamespace; + draft.validation.targetNamespace = validateK8sName(targetNamespace) ? 'success' : 'error'; + return draft; + }, + [SET_TARGET_PROVIDER]( + draft, + { payload: { targetProvider } }: PageAction, + ) { + draft.newPlan.spec.provider.destination = getObjectRef(targetProvider); + draft.newPlan.spec.targetNamespace = undefined; + draft.validation.targetNamespace = 'default'; + return draft; + }, + [SET_AVAILABLE_PROVIDERS]( + draft, + { payload: { availableProviders } }: PageAction, + ) { + const targetProvider = draft.newPlan.spec.provider.destination; + if ( + !targetProvider || + !availableProviders.find((p) => p?.metadata?.name === targetProvider.name) + ) { + // set the default provider if none is set + // reset the provider if provider was removed + const firstHostProvider = availableProviders.find((p) => isProviderLocalTarget(p)); + draft.newPlan.spec.provider.destination = + firstHostProvider && getObjectRef(firstHostProvider); + draft.newPlan.spec.targetNamespace = undefined; + draft.validation.targetNamespace = 'default'; + } + draft.availableProviders = availableProviders; + return draft; + }, +}; + +export const reducer = ( + draft: Draft, + action: PageAction, +) => { + return actions?.[action?.type]?.(draft, action) ?? draft; +}; + +// based on the method used in legacy/src/common/helpers +// and mocks/src/definitions/utils +export const getObjectRef = ( + { apiVersion, kind, metadata: { name, namespace, uid } = {} }: V1beta1Provider = { + apiVersion: undefined, + kind: undefined, + }, +) => ({ + apiVersion, + kind, + name, + namespace, + uid, +}); + +export const createInitialState = ({ + namespace, + sourceProvider, + selectedVms, +}: { + namespace: string; + sourceProvider: V1beta1Provider; + selectedVms: VmData[]; +}): CreateVmMigrationPageState => ({ + newPlan: { + ...planTemplate, + metadata: { + ...planTemplate?.metadata, + name: sourceProvider?.metadata?.name + ? `${sourceProvider?.metadata?.name}-${randomId().substring(0, 8)}` + : undefined, + namespace: namespace || process?.env?.DEFAULT_NAMESPACE || 'default', + }, + spec: { + ...planTemplate?.spec, + provider: { + source: getObjectRef(sourceProvider), + destination: undefined, + }, + targetNamespace: namespace, + vms: selectedVms.map((data) => ({ name: data.name, id: toId(data) })), + }, + }, + validationError: null, + apiError: null, + availableProviders: [], + selectedVms, + validation: { + name: 'default', + targetNamespace: 'default', + }, +});