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';