From 02db59b9e580ad35b773c6ced42950615c50a49f Mon Sep 17 00:00:00 2001 From: yaacov Date: Tue, 18 Jun 2024 11:41:58 +0300 Subject: [PATCH] Add a plan details item to edit boot disk Signed-off-by: yaacov --- .../en/plugin__forklift-console-plugin.json | 11 ++ .../views/details/PlanDetailsPage.style.css | 6 +- .../SettingsSection/SettingsSection.tsx | 5 + .../components/RootDiskDetailsItem.tsx | 79 ++++++++++++ .../SettingsSection/components/index.ts | 1 + .../modals/EditRootDisk/EditRootDisk.tsx | 117 ++++++++++++++++++ .../EditRootDisk/editRootDiskModalAlert.tsx | 15 +++ .../EditRootDisk/editRootDiskModalBody.tsx | 30 +++++ .../EditRootDisk/getRootDiskLabelByKey.ts | 84 +++++++++++++ .../modals/EditRootDisk/index.ts | 6 + 10 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/components/RootDiskDetailsItem.tsx create mode 100644 packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/modals/EditRootDisk/EditRootDisk.tsx create mode 100644 packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/modals/EditRootDisk/editRootDiskModalAlert.tsx create mode 100644 packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/modals/EditRootDisk/editRootDiskModalBody.tsx create mode 100644 packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/modals/EditRootDisk/getRootDiskLabelByKey.ts create mode 100644 packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/modals/EditRootDisk/index.ts 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 ae8fb3b11..098097d61 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 @@ -48,6 +48,13 @@ "Assessment": "Assessment", "Authentication type": "Authentication type", "Bandwidth": "Bandwidth", + "Boot from first root device": "Boot from first root device", + "Boot from the first hard drive": "Boot from the first hard drive", + "Boot from the first partition on the first hard drive": "Boot from the first partition on the first hard drive", + "Boot from the first partition on the second hard drive": "Boot from the first partition on the second hard drive", + "Boot from the second hard drive": "Boot from the second hard drive", + "Boot from the second partition on the first hard drive": "Boot from the second partition on the first hard drive", + "Boot from the second partition on the second hard drive": "Boot from the second partition on the second hard drive", "CA certificate": "CA certificate", "CA certificate - disabled when 'Skip certificate validation' is selected": "CA certificate - disabled when 'Skip certificate validation' is selected", "CA certificate - leave empty to use system CA certificates": "CA certificate - leave empty to use system CA certificates", @@ -60,6 +67,7 @@ "Cannot retrieve certificate": "Cannot retrieve certificate", "Category": "Category", "Certificate change detected": "Certificate change detected", + "Choose the root filesystem to be converted.": "Choose the root filesystem to be converted.", "Clear all filters": "Clear all filters", "Click the pencil for setting provider web UI link": "Click the pencil for setting provider web UI link", "Click the update credentials button to save your changes, button is disabled until a change is detected.": "Click the update credentials button to save your changes, button is disabled until a change is detected.", @@ -142,6 +150,7 @@ "Edit Provider Credentials": "Edit Provider Credentials", "Edit provider credentials.\n Use this link to edit the providers credentials instead of editing the secret directly.": "Edit provider credentials.\n Use this link to edit the providers credentials instead of editing the secret directly.", "Edit provider web UI link": "Edit provider web UI link", + "Edit root device": "Edit root device", "Edit Snapshot polling interval (seconds)": "Edit Snapshot polling interval (seconds)", "Edit StorageMap": "Edit StorageMap", "Edit target namespace": "Edit target namespace", @@ -179,6 +188,7 @@ "Filter by template": "Filter by template", "Filter by tenant": "Filter by tenant", "Filter provider": "Filter provider", + "First root device": "First root device", "Flavor": "Flavor", "Folder": "Folder", "GPUs/Host Devices": "GPUs/Host Devices", @@ -382,6 +392,7 @@ "Restore default columns": "Restore default columns", "Return to the providers list page": "Return to the providers list page", "Reveal values": "Reveal values", + "Root device": "Root device", "Run the migration plan.": "Run the migration plan.", "Running": "Running", "Running virtual machines": "Running virtual machines", diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/PlanDetailsPage.style.css b/packages/forklift-console-plugin/src/modules/Plans/views/details/PlanDetailsPage.style.css index 658f502fd..279b6379f 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/PlanDetailsPage.style.css +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/PlanDetailsPage.style.css @@ -147,4 +147,8 @@ .forklift-page-plan-resources-td-fractional { text-align: left; display: inline-block; -} \ No newline at end of file +} + +.forklift-page-plan-settings-icon { + padding-right: var(--pf-global--spacer--sm); +} diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/SettingsSection.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/SettingsSection.tsx index 13ce26d9b..cc8ceb860 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/SettingsSection.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/SettingsSection.tsx @@ -9,6 +9,7 @@ import { DescriptionList } from '@patternfly/react-core'; import { PreserveClusterCpuModelDetailsItem, PreserveStaticIPsDetailsItem, + RootDiskDetailsItem, SetLUKSEncryptionPasswordsDetailsItem, TargetNamespaceDetailsItem, TransferNetworkDetailsItem, @@ -75,6 +76,10 @@ export const SettingsSectionInternal: React.FC = ({ obj, p {['vsphere'].includes(sourceProvider?.spec?.type) && ( )} + + {['vsphere'].includes(sourceProvider?.spec?.type) && ( + + )} ); diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/components/RootDiskDetailsItem.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/components/RootDiskDetailsItem.tsx new file mode 100644 index 000000000..66ad170f0 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/components/RootDiskDetailsItem.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { useModal } from 'src/modules/Providers/modals'; +import { DetailsItem } from 'src/modules/Providers/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { Label, Tooltip } from '@patternfly/react-core'; +import { ExclamationTriangleIcon } from '@patternfly/react-icons'; + +import { PlanDetailsItemProps } from '../../DetailsSection'; +import { VIRT_V2V_HELP_LINK } from '../modals'; +import { getRootDiskLabelByKey } from '../modals/EditRootDisk'; +import { EditRootDisk } from '../modals/EditRootDisk/EditRootDisk'; + +export const RootDiskDetailsItem: React.FC = ({ + resource, + canPatch, + helpContent, +}) => { + const { t } = useForkliftTranslation(); + const { showModal } = useModal(); + + const defaultHelpContent = t(`Choose the root filesystem to be converted.`); + + const rootDisk = resource?.spec?.vms?.[0].rootDisk; + + return ( + showModal())} + /> + ); +}; + +/** + * Generates a label component for the given disk key. + * @param {string} diskKey - The key representing the disk option. + * @returns {JSX.Element} The label component for the disk. + */ +const getDiskLabel = (diskKey: string) => { + const diskLabel = getRootDiskLabelByKey(diskKey); + + // First boot disk, color green + if (!diskKey) { + return ( + + ); + } + + // Known boot disk format, color grey. + if (diskKey.startsWith('/dev/sd')) { + return ( + + ); + } + + // Unknown boot disk format. + return ( + + + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/components/index.ts b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/components/index.ts index 0425c7ac1..a55ddae0e 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/components/index.ts +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/components/index.ts @@ -1,6 +1,7 @@ // @index(['./*', /style/g], f => `export * from '${f.path}';`) export * from './PreserveClusterCpuModelDetailsItem'; export * from './PreserveStaticIPsDetailsItem'; +export * from './RootDiskDetailsItem'; export * from './SetLUKSEncryptionPasswordsDetailsItem'; export * from './TargetNamespaceDetailsItem'; export * from './TransferNetworkDetailsItem'; diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/modals/EditRootDisk/EditRootDisk.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/modals/EditRootDisk/EditRootDisk.tsx new file mode 100644 index 000000000..0e8c67971 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/modals/EditRootDisk/EditRootDisk.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import { FilterableSelect } from 'src/components/FilterableSelect/FilterableSelect'; +import { + EditModal, + EditModalProps, + ModalInputComponentType, + OnConfirmHookType, +} from 'src/modules/Providers/modals'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { Modify, PlanModel, V1beta1Plan } from '@kubev2v/types'; +import { K8sModel, k8sPatch } from '@openshift-console/dynamic-plugin-sdk'; +import { HelperText, HelperTextItem, Text } from '@patternfly/react-core'; + +import { editRootDiskModalAlert } from './editRootDiskModalAlert'; +import { editRootDiskModalBody } from './editRootDiskModalBody'; +import { diskOptions, getRootDiskLabelByKey } from './getRootDiskLabelByKey'; + +const onConfirm: OnConfirmHookType = async ({ resource, model, newValue }) => { + const plan = resource as V1beta1Plan; + + const resourceValue = plan?.spec?.vms; + const op = resourceValue ? 'replace' : 'add'; + const newVMs = resourceValue.map((vm) => ({ + ...vm, + rootDisk: newValue || undefined, + })); + + const obj = await k8sPatch({ + model: model, + resource: resource, + data: [ + { + op, + path: '/spec/vms', + value: newVMs || undefined, + }, + ], + }); + + return obj; +}; + +interface DropdownRendererProps { + value: string | number; + onChange: (string) => void; +} + +const RootDiskInputFactory: () => ModalInputComponentType = () => { + const DropdownRenderer: React.FC = ({ value, onChange }) => { + const { t } = useForkliftTranslation(); + const options = diskOptions(t); + + const dropdownItems = options.map((option) => ({ + itemId: option.key, + children: ( + <> + {getRootDiskLabelByKey(option.key)} + {option.description && ( + + {option.description} + + )} + + ), + })); + + return ( + + ); + }; + + return DropdownRenderer; +}; + +export const EditRootDisk: React.FC = (props) => { + const { t } = useForkliftTranslation(); + + const plan = props.resource; + const rootDisk = plan.spec.vms?.[0]?.rootDisk; + const allVMsHasMatchingRootDisk = plan.spec.vms.every((vm) => vm?.rootDisk === rootDisk); + + return ( + obj?.spec?.vms?.[0]?.rootDisk} + title={props?.title || t('Edit root device')} + label={props?.label || t('Root device')} + model={PlanModel} + onConfirmHook={onConfirm} + body={ + <> + {editRootDiskModalBody} + {!allVMsHasMatchingRootDisk && editRootDiskModalAlert} + + } + InputComponent={RootDiskInputFactory()} + /> + ); +}; + +export type EditRootDiskProps = Modify< + EditModalProps, + { + resource: V1beta1Plan; + title?: string; + label?: string; + model?: K8sModel; + jsonPath?: string | string[]; + } +>; diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/modals/EditRootDisk/editRootDiskModalAlert.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/modals/EditRootDisk/editRootDiskModalAlert.tsx new file mode 100644 index 000000000..2280692c9 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/modals/EditRootDisk/editRootDiskModalAlert.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { AlertMessageForModals } from 'src/modules/Providers/modals'; + +export const editRootDiskModalAlert = ( + +

Warning: not all virtual machines are configures using the same root disk number,

+

updating the root disk number will override the current configuration.

+ + } + /> +); diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/modals/EditRootDisk/editRootDiskModalBody.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/modals/EditRootDisk/editRootDiskModalBody.tsx new file mode 100644 index 000000000..13779086a --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/modals/EditRootDisk/editRootDiskModalBody.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { ForkliftTrans } from 'src/utils'; + +import { ExternalLink } from '@kubev2v/common'; + +import { VIRT_V2V_HELP_LINK } from '../EditLUKSEncryptionPasswords'; + +export const editRootDiskModalBody = ( + <> + +

Choose the root filesystem to be converted.

+
+

+ Default behavior is to choose the first root device in the case of a multi-boot operating + system. Since this is a heuristic, it may sometimes choose the wrong one. +

+
+

+ When using a multi-boot VM, you can also name a specific root device, eg.{' '} + /dev/sda2 would mean to use the second partition on the first hard drive. + If the named root device does not exist or was not detected as a root device, the migration + will fail.{' '} + + Learn more + + . +

+
+ +); diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/modals/EditRootDisk/getRootDiskLabelByKey.ts b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/modals/EditRootDisk/getRootDiskLabelByKey.ts new file mode 100644 index 000000000..27f2911e8 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/modals/EditRootDisk/getRootDiskLabelByKey.ts @@ -0,0 +1,84 @@ +/** + * Type definition for DiskOption. + * @typedef {Object} DiskOption + * @property {string} key - The key representing the disk option. + * @property {string} description - The description of the disk option. + */ +export type DiskOption = { + key: string; + description: string; +}; + +/** + * Generates an array of disk options. + * @param {Function} [t=(text: string) => text] - Translation function. + * @returns {DiskOption[]} Array of disk options. + */ +export const diskOptions = (t = (text: string) => text): DiskOption[] => [ + { key: '', description: t('Boot from first root device') }, + { key: '/dev/sda', description: t('Boot from the first hard drive') }, + { + key: '/dev/sda1', + description: t('Boot from the first partition on the first hard drive'), + }, + { + key: '/dev/sda2', + description: t('Boot from the second partition on the first hard drive'), + }, + { key: '/dev/sdb', description: t('Boot from the second hard drive') }, + { + key: '/dev/sdb1', + description: t('Boot from the first partition on the second hard drive'), + }, + { + key: '/dev/sdb2', + description: t('Boot from the second partition on the second hard drive'), + }, +]; + +/** + * Gets the label for a root disk by its key. + * @param {string | number} key_ - The key representing the disk option. + * @returns {string} The label for the root disk. + */ +export const getRootDiskLabelByKey = (key_: string | number): string => { + const diskLetters = 'abcdefghijklmnopqrstuvwxyz'; + const partitionNumbers = '0123456789'; + + // Default is first root disk + if (!key_) { + return 'First root device'; + } + + const key = key_.toString(); + + if (key.startsWith('/dev/sd') && key.length >= 8) { + const diskLetter = key[7]; + const partitionNumber = key.length > 8 ? key.slice(8) : ''; + + const diskIndex = diskLetters.indexOf(diskLetter); + if (diskIndex === -1 || (partitionNumber && !partitionNumbers.includes(partitionNumber[0]))) { + // If format is unrecognized, just return the key as label + return key; + } + + const diskPosition = [ + 'First', + 'Second', + 'Third', + 'Fourth', + 'Fifth', + 'Sixth', + 'Seventh', + 'Eighth', + 'Ninth', + 'Tenth', + ][diskIndex]; + const partitionPosition = partitionNumber ? `${partitionNumber} partition` : ''; + + return `${diskPosition} HD${partitionPosition ? ` ${partitionPosition}` : ''} (${key})`; + } else { + // If format is unrecognized, just return the key as label + return key; + } +}; diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/modals/EditRootDisk/index.ts b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/modals/EditRootDisk/index.ts new file mode 100644 index 000000000..45e6fe75e --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/SettingsSection/modals/EditRootDisk/index.ts @@ -0,0 +1,6 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './EditRootDisk'; +export * from './editRootDiskModalAlert'; +export * from './editRootDiskModalBody'; +export * from './getRootDiskLabelByKey'; +// @endindex