Skip to content

Commit

Permalink
Add volume mode and access mode selection for
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
awels committed Dec 9, 2024
1 parent dac2ebd commit 590143c
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 42 deletions.
5 changes: 3 additions & 2 deletions src/app/common/components/SimpleSelect.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -51,6 +51,7 @@ const SimpleSelect: React.FunctionComponent<ISimpleSelectProps> = ({
<SelectOption
key={option.toString()}
value={option}
description={(option as OptionWithValue)?.props?.description}
{...(typeof option === 'object' && (option as OptionWithValue).props)}
/>
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
18 changes: 9 additions & 9 deletions src/app/home/pages/PlansPage/components/Wizard/GeneralForm.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IPVAccessModeSelectProps> = ({
pv,
currentPV,
storageClasses,
}: IPVAccessModeSelectProps) => {
const { values, setFieldValue } = useFormikContext<IFormValues>();

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 (
<SimpleSelect
id="select-storage-class"
aria-label="Select storage class"
className={styles.copySelectStyle}
onChange={(option: any) => onAccessModeChange(currentPV, option.value)}
options={accessModeOptions}
placeholderText="Select volume mode..."
value={
accessModeOptions.find(
(option) => currentStorageClass && option.value === currentStorageClass.accessMode
) || accessModeOptions[0]
}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,26 @@ export const PVStorageClassSelect: React.FunctionComponent<IPVStorageClassSelect
}: IPVStorageClassSelectProps) => {
const { values, setFieldValue } = useFormikContext<IFormValues>();

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 (
Expand All @@ -48,11 +50,9 @@ export const PVStorageClassSelect: React.FunctionComponent<IPVStorageClassSelect
onChange={(option: any) => 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..."
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IPVVolumeModeSelectProps> = ({
pv,
currentPV,
storageClasses,
}: IPVVolumeModeSelectProps) => {
const { values, setFieldValue } = useFormikContext<IFormValues>();

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 (
<SimpleSelect
id="select-storage-class"
aria-label="Select storage class"
className={styles.copySelectStyle}
onChange={(option: any) => onVolumeModeChange(currentPV, option.value)}
options={volumeModeOptions}
placeholderText="Select volume mode..."
value={
volumeModeOptions.find(
(option) => currentStorageClass && option.value === currentStorageClass.volumeMode
) || volumeModeOptions[0]
}
/>
);
};
33 changes: 29 additions & 4 deletions src/app/home/pages/PlansPage/components/Wizard/VolumesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -122,12 +126,13 @@ const VolumesTable: React.FunctionComponent<IVolumesTableProps> = ({
const columns = isSCC
? [
// Columns for storage class conversion
{ title: 'PV name', transforms: [sortable] },
{ title: 'Claim', transforms: [sortable] }, // TODO should this be renamed PVC? if so, just here or everywhere?
{ title: 'Namespace', transforms: [sortable] }, // TODO should namespace come before Claim? here or everywhere?
{ 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: (
<React.Fragment>
Expand Down Expand Up @@ -166,12 +171,13 @@ const VolumesTable: React.FunctionComponent<IVolumesTableProps> = ({
const getSortValues = (pv: IPlanPersistentVolume) =>
isSCC
? [
pv.name,
pvcNameToString(pv.pvc),
pv.pvc.namespace,
pv.storageClass,
pv.capacity,
pv.selection.storageClass,
pv.pvc.volumeMode,
pv.pvc.accessModes[0],
pv.selection.verify,
]
: [
Expand Down Expand Up @@ -219,6 +225,20 @@ const VolumesTable: React.FunctionComponent<IVolumesTableProps> = ({
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,
Expand Down Expand Up @@ -308,7 +328,7 @@ const VolumesTable: React.FunctionComponent<IVolumesTableProps> = ({
newSelected = [...new Set([...props.meta.selectedPVs, props.cells[0]])];
} else {
newSelected = props.meta.selectedPVs.filter(
(selected: string) => selected !== props.cells[0]
(selected: string, index: number) => index != rowIndex
);
}
}
Expand Down Expand Up @@ -389,7 +409,6 @@ const VolumesTable: React.FunctionComponent<IVolumesTableProps> = ({
};
const cells = isSCC
? [
pv.name,
pvcNameToString(pv.pvc),
pv.pvc.namespace,
// Storage class can be empty here if none exists/ none selected initially
Expand All @@ -400,6 +419,12 @@ const VolumesTable: React.FunctionComponent<IVolumesTableProps> = ({
<PVStorageClassSelect {...{ pv, currentPV, storageClasses, currentStorageClass }} />
),
},
{
title: <PVVolumeModeSelect {...{ pv, currentPV, storageClasses }} />,
},
{
title: <PVAccessModeSelect {...{ pv, currentPV, storageClasses }} />,
},
{
title: (
<VerifyCopyCheckbox
Expand Down
15 changes: 12 additions & 3 deletions src/app/home/pages/PlansPage/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,13 @@ export const getElapsedTime = (step: IStep, migration: IMigration): string => {
export type IPlanInfo = ReturnType<typeof getPlanInfo>;

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(':');
Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 590143c

Please sign in to comment.