From 67678be95af363604ac6692353fd62c42a95b1b6 Mon Sep 17 00:00:00 2001 From: Alexander Wels Date: Fri, 6 Dec 2024 07:36:31 -0600 Subject: [PATCH] Add volume mode and access mode selection for VM related volumes in the Persistent Volume grid. This allows the user to change the volume mode and access mode for volumes that are part of a VM. Signed-off-by: Alexander Wels --- src/app/common/components/SimpleSelect.tsx | 5 +- .../PlanActions/PlanActionsComponent.tsx | 15 ++-- .../components/Wizard/GeneralForm.tsx | 18 ++--- .../components/Wizard/PVAccessModeSelect.tsx | 66 +++++++++++++++++ .../Wizard/PVStorageClassSelect.tsx | 22 +++--- .../components/Wizard/PVVolumeModeSelect.tsx | 61 +++++++++++++++ .../components/Wizard/VolumesTable.tsx | 74 +++++++++++++------ src/app/home/pages/PlansPage/helpers.ts | 15 +++- src/app/plan/duck/types.ts | 13 +++- src/client/resources/conversions.ts | 7 +- 10 files changed, 237 insertions(+), 59 deletions(-) create mode 100644 src/app/home/pages/PlansPage/components/Wizard/PVAccessModeSelect.tsx create mode 100644 src/app/home/pages/PlansPage/components/Wizard/PVVolumeModeSelect.tsx diff --git a/src/app/common/components/SimpleSelect.tsx b/src/app/common/components/SimpleSelect.tsx index a0e57d9c0..1d013e381 100644 --- a/src/app/common/components/SimpleSelect.tsx +++ b/src/app/common/components/SimpleSelect.tsx @@ -1,11 +1,11 @@ -import React, { useState } from 'react'; import { Select, SelectOption, SelectOptionObject, - SelectProps, SelectOptionProps, + SelectProps, } from '@patternfly/react-core'; +import React, { useState } from 'react'; import './SimpleSelect.css'; @@ -51,6 +51,7 @@ const SimpleSelect: React.FunctionComponent = ({ ))} diff --git a/src/app/home/pages/PlansPage/components/PlanActions/PlanActionsComponent.tsx b/src/app/home/pages/PlansPage/components/PlanActions/PlanActionsComponent.tsx index 675770199..2551c62d1 100644 --- a/src/app/home/pages/PlansPage/components/PlanActions/PlanActionsComponent.tsx +++ b/src/app/home/pages/PlansPage/components/PlanActions/PlanActionsComponent.tsx @@ -1,21 +1,20 @@ -import React from 'react'; -import { useState } from 'react'; import { Dropdown, + DropdownGroup, DropdownItem, DropdownPosition, - KebabToggle, Flex, FlexItem, - DropdownGroup, + KebabToggle, } from '@patternfly/react-core'; -import { useOpenModal } from '../../../../duck'; +import React, { useState } from 'react'; +import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import WizardContainer from '../Wizard/WizardContainer'; import ConfirmModal from '../../../../../common/components/ConfirmModal'; -import { IPlan } from '../../../../../plan/duck/types'; -import { useDispatch } from 'react-redux'; import { PlanActions } from '../../../../../plan/duck'; +import { IPlan } from '../../../../../plan/duck/types'; +import { useOpenModal } from '../../../../duck'; +import WizardContainer from '../Wizard/WizardContainer'; import { MigrationActionsDropdownGroup } from './MigrationActionsDropdownGroup'; import { MigrationConfirmModals, useMigrationConfirmModalState } from './MigrationConfirmModals'; interface IPlanActionsProps { diff --git a/src/app/home/pages/PlansPage/components/Wizard/GeneralForm.tsx b/src/app/home/pages/PlansPage/components/Wizard/GeneralForm.tsx index 3ef5e338e..6745179fd 100644 --- a/src/app/home/pages/PlansPage/components/Wizard/GeneralForm.tsx +++ b/src/app/home/pages/PlansPage/components/Wizard/GeneralForm.tsx @@ -1,18 +1,18 @@ -import React, { useEffect, useRef } from 'react'; -import { useFormikContext } from 'formik'; -import { IFormValues } from './WizardContainer'; -import { Form, FormGroup, TextContent, Text, TextInput, Tooltip } from '@patternfly/react-core'; +import { Form, FormGroup, Text, TextContent, TextInput, Tooltip } from '@patternfly/react-core'; +import { ExclamationTriangleIcon } from '@patternfly/react-icons/dist/js/icons/exclamation-triangle-icon'; import spacing from '@patternfly/react-styles/css/utilities/Spacing/spacing'; +import { useFormikContext } from 'formik'; +import React, { useEffect, useRef } from 'react'; +import { useSelector } from 'react-redux'; +import { DefaultRootState } from '../../../../../../configureStore'; +import { ICluster } from '../../../../../cluster/duck/types'; import SimpleSelect, { OptionWithValue } from '../../../../../common/components/SimpleSelect'; +import { usePausedPollingEffect } from '../../../../../common/context'; import { useForcedValidationOnChange } from '../../../../../common/duck/hooks'; import { validatedState } from '../../../../../common/helpers'; -import { ICluster } from '../../../../../cluster/duck/types'; -import { ExclamationTriangleIcon } from '@patternfly/react-icons/dist/js/icons/exclamation-triangle-icon'; -import { usePausedPollingEffect } from '../../../../../common/context'; import { IStorage } from '../../../../../storage/duck/types'; import { MigrationType } from '../../types'; -import { useSelector } from 'react-redux'; -import { DefaultRootState } from '../../../../../../configureStore'; +import { IFormValues } from './WizardContainer'; export type IGeneralFormProps = { isEdit: boolean; diff --git a/src/app/home/pages/PlansPage/components/Wizard/PVAccessModeSelect.tsx b/src/app/home/pages/PlansPage/components/Wizard/PVAccessModeSelect.tsx new file mode 100644 index 000000000..6c32ecbd9 --- /dev/null +++ b/src/app/home/pages/PlansPage/components/Wizard/PVAccessModeSelect.tsx @@ -0,0 +1,66 @@ +import { useFormikContext } from 'formik'; +import React from 'react'; +import SimpleSelect, { OptionWithValue } from '../../../../../common/components/SimpleSelect'; +import { + IMigPlanStorageClass, + IPlanPersistentVolume, + IVolumeAccessModes, +} from '../../../../../plan/duck/types'; +import { IFormValues } from './WizardContainer'; + +const styles = require('./PVStorageClassSelect.module').default; + +interface IPVAccessModeSelectProps { + pv: IPlanPersistentVolume; + currentPV: IPlanPersistentVolume; + storageClasses: IMigPlanStorageClass[]; +} + +export const PVAccessModeSelect: React.FunctionComponent = ({ + pv, + currentPV, + storageClasses, +}: IPVAccessModeSelectProps) => { + const { values, setFieldValue } = useFormikContext(); + + const currentStorageClass = values.pvStorageClassAssignment[currentPV.name]; + const volumeAccessModes = currentStorageClass.volumeAccessModes; + const currentVolumeMode = currentStorageClass.volumeMode; + const possibleAccessModes = volumeAccessModes.find( + (volumeAccessMode: IVolumeAccessModes) => volumeAccessMode.volumeMode === currentVolumeMode + ) || { accessModes: [] as string[] }; + + const onAccessModeChange = (currentPV: IPlanPersistentVolume, value: string) => { + currentStorageClass.accessMode = value; + const updatedAssignment = { + ...values.pvStorageClassAssignment, + [currentPV.name]: currentStorageClass, + }; + setFieldValue('pvStorageClassAssignment', updatedAssignment); + }; + + const accessModeOptions: OptionWithValue[] = [ + ...possibleAccessModes.accessModes.map((value: string) => ({ + value: value, + toString: () => value, + })), + ]; + accessModeOptions.splice(1, 1); // remove ReadOnly option + accessModeOptions.splice(0, 0, { value: 'auto', toString: () => 'Auto' }); + + return ( + onAccessModeChange(currentPV, option.value)} + options={accessModeOptions} + placeholderText="Select volume mode..." + value={ + accessModeOptions.find( + (option) => currentStorageClass && option.value === currentStorageClass.accessMode + ) || accessModeOptions[0] + } + /> + ); +}; diff --git a/src/app/home/pages/PlansPage/components/Wizard/PVStorageClassSelect.tsx b/src/app/home/pages/PlansPage/components/Wizard/PVStorageClassSelect.tsx index 49b26d2c5..c5e01307f 100644 --- a/src/app/home/pages/PlansPage/components/Wizard/PVStorageClassSelect.tsx +++ b/src/app/home/pages/PlansPage/components/Wizard/PVStorageClassSelect.tsx @@ -20,24 +20,26 @@ export const PVStorageClassSelect: React.FunctionComponent { const { values, setFieldValue } = useFormikContext(); - const currentStorageClass = values.pvStorageClassAssignment[pv.name]; + const currentStorageClass = values.pvStorageClassAssignment[currentPV.name]; const onStorageClassChange = (currentPV: IPlanPersistentVolume, value: string) => { - const newSc = storageClasses.find((sc) => sc !== '' && sc.name === value) || ''; + const newSc = storageClasses.find((sc) => sc.name === value) || ''; + const copy = JSON.parse(JSON.stringify(newSc)); + copy.volumeMode = 'auto'; + copy.accessMode = 'auto'; const updatedAssignment = { ...values.pvStorageClassAssignment, - [currentPV.name]: newSc, + [currentPV.name]: copy, }; setFieldValue('pvStorageClassAssignment', updatedAssignment); }; - const noneOption = { value: '', toString: () => 'None' }; const storageClassOptions: OptionWithValue[] = [ ...storageClasses.map((storageClass) => ({ - value: storageClass !== '' && storageClass.name, + value: storageClass.name, toString: () => targetStorageClassToString(storageClass), + props: { description: storageClass.provisioner }, })), - noneOption, ]; return ( @@ -48,11 +50,9 @@ export const PVStorageClassSelect: React.FunctionComponent onStorageClassChange(currentPV, option.value)} options={storageClassOptions} value={ - currentStorageClass === '' - ? noneOption - : storageClassOptions.find( - (option) => currentStorageClass && option.value === currentStorageClass.name - ) || undefined + storageClassOptions.find( + (option) => currentStorageClass && option.value === currentStorageClass.name + ) || undefined } placeholderText="Select a storage class..." /> diff --git a/src/app/home/pages/PlansPage/components/Wizard/PVVolumeModeSelect.tsx b/src/app/home/pages/PlansPage/components/Wizard/PVVolumeModeSelect.tsx new file mode 100644 index 000000000..5cd43fbda --- /dev/null +++ b/src/app/home/pages/PlansPage/components/Wizard/PVVolumeModeSelect.tsx @@ -0,0 +1,61 @@ +import { useFormikContext } from 'formik'; +import React from 'react'; +import SimpleSelect, { OptionWithValue } from '../../../../../common/components/SimpleSelect'; +import { + IMigPlanStorageClass, + IPlanPersistentVolume, + IVolumeAccessModes, +} from '../../../../../plan/duck/types'; +import { IFormValues } from './WizardContainer'; + +const styles = require('./PVStorageClassSelect.module').default; + +interface IPVVolumeModeSelectProps { + pv: IPlanPersistentVolume; + currentPV: IPlanPersistentVolume; + storageClasses: IMigPlanStorageClass[]; +} + +export const PVVolumeModeSelect: React.FunctionComponent = ({ + pv, + currentPV, + storageClasses, +}: IPVVolumeModeSelectProps) => { + const { values, setFieldValue } = useFormikContext(); + + const currentStorageClass = values.pvStorageClassAssignment[currentPV.name]; + const volumeAccessModes = currentStorageClass.volumeAccessModes; + + const onVolumeModeChange = (currentPV: IPlanPersistentVolume, value: string) => { + currentStorageClass.volumeMode = value; + const updatedAssignment = { + ...values.pvStorageClassAssignment, + [currentPV.name]: currentStorageClass, + }; + setFieldValue('pvStorageClassAssignment', updatedAssignment); + }; + + const volumeModeOptions: OptionWithValue[] = [ + ...volumeAccessModes.map((volumeAccessMode: IVolumeAccessModes) => ({ + value: volumeAccessMode.volumeMode, + toString: () => volumeAccessMode.volumeMode, + })), + ]; + volumeModeOptions.splice(0, 0, { value: 'auto', toString: () => 'Auto' }); + + return ( + onVolumeModeChange(currentPV, option.value)} + options={volumeModeOptions} + placeholderText="Select volume mode..." + value={ + volumeModeOptions.find( + (option) => currentStorageClass && option.value === currentStorageClass.volumeMode + ) || volumeModeOptions[0] + } + /> + ); +}; diff --git a/src/app/home/pages/PlansPage/components/Wizard/VolumesTable.tsx b/src/app/home/pages/PlansPage/components/Wizard/VolumesTable.tsx index 60caa6817..103522c19 100644 --- a/src/app/home/pages/PlansPage/components/Wizard/VolumesTable.tsx +++ b/src/app/home/pages/PlansPage/components/Wizard/VolumesTable.tsx @@ -58,9 +58,13 @@ import { import { getSuggestedPvStorageClasses, pvcNameToString, + targetAccessModeToString, targetStorageClassToString, + targetVolumeModeToString, } from '../../helpers'; +import { PVAccessModeSelect } from './PVAccessModeSelect'; import { PVStorageClassSelect } from './PVStorageClassSelect'; +import { PVVolumeModeSelect } from './PVVolumeModeSelect'; import { VerifyCopyCheckbox } from './VerifyCopyCheckbox'; import { VerifyCopyWarningModal, VerifyWarningState } from './VerifyCopyWarningModal'; import { IFormValues, IOtherProps } from './WizardContainer'; @@ -128,6 +132,8 @@ const VolumesTable: React.FunctionComponent = ({ { title: 'Source storage class', transforms: [sortable] }, { title: 'Size', transforms: [sortable] }, { title: 'Target storage class', transforms: [sortable] }, + { title: 'Target volume mode', transforms: [sortable] }, + { title: 'Target access mode', transforms: [sortable] }, { title: ( @@ -172,6 +178,8 @@ const VolumesTable: React.FunctionComponent = ({ pv.storageClass, pv.capacity, pv.selection.storageClass, + pv.pvc.volumeMode, + pv.pvc.accessModes[0], pv.selection.verify, ] : [ @@ -219,6 +227,20 @@ const VolumesTable: React.FunctionComponent = ({ getItemValue: (pv) => targetStorageClassToString(values.pvStorageClassAssignment[pv.name]), }, + { + key: 'volumeMode', + title: 'Target volume mode', + type: FilterType.search, + placeholderText: 'Filter by volume mode...', + getItemValue: (pv) => targetVolumeModeToString(values.pvStorageClassAssignment[pv.name]), + }, + { + key: 'accessMode', + title: 'Target access mode', + type: FilterType.search, + placeholderText: 'Filter by access mode...', + getItemValue: (pv) => targetAccessModeToString(values.pvStorageClassAssignment[pv.name]), + }, ] : [ ...commonFilterCategories, @@ -400,6 +422,12 @@ const VolumesTable: React.FunctionComponent = ({ ), }, + { + title: , + }, + { + title: , + }, { title: ( = ({ isSelected: allRowsSelected, }} /> - {columns.map((column, columnIndex) => ( - - {column.title} - - ))} + {columns + .filter((column, columnIndex) => columnIndex !== 0) + .map((column, columnIndex) => ( + + {column.title} + + ))} @@ -577,22 +607,24 @@ const VolumesTable: React.FunctionComponent = ({ props: row, }} /> - {row.cells.map((cell, cellIndex) => { - const shiftedIndex = cellIndex + 1; - console.log('cell', cell); - return ( - - {typeof cell !== 'string' ? cell.title : cell} - - ); - })} + {row.cells + .filter((column, columnIndex) => columnIndex !== 0) + .map((cell, cellIndex) => { + const shiftedIndex = cellIndex + 1; + console.log('cell', cell); + return ( + + {typeof cell !== 'string' ? cell.title : cell} + + ); + })} ); })} diff --git a/src/app/home/pages/PlansPage/helpers.ts b/src/app/home/pages/PlansPage/helpers.ts index ab0792938..be9d9d802 100644 --- a/src/app/home/pages/PlansPage/helpers.ts +++ b/src/app/home/pages/PlansPage/helpers.ts @@ -401,7 +401,13 @@ export const getElapsedTime = (step: IStep, migration: IMigration): string => { export type IPlanInfo = ReturnType; export const targetStorageClassToString = (storageClass: IMigPlanStorageClass) => - storageClass && `${storageClass.name}:${storageClass.provisioner}`; + storageClass && `${storageClass.name}`; + +export const targetVolumeModeToString = (storageClass: IMigPlanStorageClass) => + storageClass && `${storageClass.volumeMode}`; + +export const targetAccessModeToString = (storageClass: IMigPlanStorageClass) => + storageClass && `${storageClass.accessMode}`; export const pvcNameToString = (pvc: IPlanPersistentVolume['pvc']) => { const includesMapping = pvc.name.includes(':'); @@ -491,11 +497,14 @@ export const getSuggestedPvStorageClasses = (migPlan?: IMigPlan) => { const storageClasses = migPlan?.status?.destStorageClasses || []; pvStorageClassAssignment = migPlanPvs.reduce((assignedScs, pv) => { const suggestedStorageClass = storageClasses.find( - (sc) => (sc !== '' && sc.name) === pv.selection.storageClass + (sc) => sc.name === pv.selection.storageClass ); + const copy = JSON.parse(JSON.stringify(suggestedStorageClass)); + copy.volumeMode = pv.pvc.volumeMode; + copy.accessMode = pv.pvc.accessModes[0] || 'ReadWriteOnce'; return { ...assignedScs, - [pv.name]: suggestedStorageClass ? suggestedStorageClass : '', + [pv.name]: copy || '', }; }, {}); return pvStorageClassAssignment; diff --git a/src/app/plan/duck/types.ts b/src/app/plan/duck/types.ts index 336ede2d8..5e5a286d7 100644 --- a/src/app/plan/duck/types.ts +++ b/src/app/plan/duck/types.ts @@ -12,6 +12,8 @@ export interface IPlanPersistentVolume { pvc: { namespace: string; name: string; + volumeMode: string; + accessModes: string[]; }; storageClass?: string; capacity: string; @@ -27,10 +29,17 @@ export interface IPlanPersistentVolume { }; } -export type IMigPlanStorageClass = IMigPlanStorageClassPopulated | ''; -type IMigPlanStorageClassPopulated = { +export type IVolumeAccessModes = { + volumeMode: string; + accessModes: string[]; +}; + +export type IMigPlanStorageClass = { name: string; provisioner: string; + volumeMode: string; + volumeAccessModes: IVolumeAccessModes[]; + accessMode: string; }; export interface IPlanSpecHook { diff --git a/src/client/resources/conversions.ts b/src/client/resources/conversions.ts index ef70f698d..9ef4af8f0 100644 --- a/src/client/resources/conversions.ts +++ b/src/client/resources/conversions.ts @@ -465,9 +465,10 @@ export function updateMigPlanFromValues( planValues.pvVerifyFlagAssignment[updatedPV.name]; const selectedStorageClassObj = planValues.pvStorageClassAssignment[updatedPV.name]; - if (selectedStorageClassObj || selectedStorageClassObj === '') { - updatedPV.selection.storageClass = - selectedStorageClassObj !== '' ? selectedStorageClassObj.name : ''; + if (selectedStorageClassObj !== undefined) { + updatedPV.selection.storageClass = selectedStorageClassObj.name; + updatedPV.pvc.volumeMode = selectedStorageClassObj.volumeMode; + updatedPV.pvc.accessModes = [selectedStorageClassObj.accessMode]; } const isPVSelected = planValues.selectedPVs.includes(pvItem.name); if (!isPVSelected) updatedPV.selection.action = 'skip';