Skip to content

Commit

Permalink
Validate that PV selection is valid (#1525)
Browse files Browse the repository at this point in the history
Instead of validating that Migration Plans don't
share the same namespace, validate that the PVs
selected are not shared between Migration Plans.

This validation happens on the backend and these
modification are to allow the UI to display the
proper progress and error messages in the PV
selection table.

Signed-off-by: Alexander Wels <[email protected]>
  • Loading branch information
awels authored Oct 16, 2024
1 parent 59f1270 commit f78ff94
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 120 deletions.
55 changes: 9 additions & 46 deletions src/app/home/pages/PlansPage/components/Wizard/VolumesForm.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
import React, { useEffect } from 'react';
import { StatusIcon } from '@konveyor/lib-ui';
import { Grid, GridItem } from '@patternfly/react-core';
import { useFormikContext } from 'formik';
import { IFormValues, IOtherProps } from './WizardContainer';
import VolumesTable from './VolumesTable';
import {
Grid,
GridItem,
Bullseye,
EmptyState,
Spinner,
Title,
Alert,
} from '@patternfly/react-core';
import { StatusIcon, StatusType } from '@konveyor/lib-ui';
import { IPlanPersistentVolume } from '../../../../../plan/duck/types';
import { usePausedPollingEffect } from '../../../../../common/context';
import { OptionLike, OptionWithValue } from '../../../../../common/components/SimpleSelect';
import { isEmpty } from 'lodash';
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { PlanActions } from '../../../../../plan/duck/actions';
import { DefaultRootState } from '../../../../../../configureStore';
import { isEmpty } from 'lodash';
import { usePausedPollingEffect } from '../../../../../common/context';
import { PlanActions } from '../../../../../plan/duck/actions';
import { IPlanPersistentVolume } from '../../../../../plan/duck/types';
import VolumesTable from './VolumesTable';
import { IFormValues, IOtherProps } from './WizardContainer';

const styles = require('./VolumesTable.module').default;

Expand Down Expand Up @@ -110,34 +101,6 @@ const VolumesForm: React.FunctionComponent<IOtherProps> = (props) => {
</Grid>
);
}
if (
planState.isFetchingPVResources ||
planState.isPollingStatus ||
planState.currentPlanStatus.state === 'Pending'
) {
return (
<Bullseye>
<EmptyState variant="large">
<div className="pf-c-empty-state__icon">
<Spinner size="xl" />
</div>
<Title headingLevel="h2" size="xl">
Discovering persistent volumes attached to source projects...
</Title>
</EmptyState>
</Bullseye>
);
}
if (planState.currentPlanStatus.state === 'Critical') {
return (
<Bullseye>
<EmptyState variant="large">
<Alert variant="danger" isInline title={planState.currentPlanStatus.errorMessage} />
</EmptyState>
</Bullseye>
);
}

return (
<VolumesTable
isEdit={props.isEdit}
Expand Down
125 changes: 89 additions & 36 deletions src/app/home/pages/PlansPage/components/Wizard/VolumesTable.tsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,69 @@
import React, { useState, useEffect } from 'react';
import {
TextContent,
Text,
TextVariants,
Grid,
GridItem,
Pagination,
PaginationVariant,
Level,
LevelItem,
Tooltip,
Popover,
Alert,
AlertActionLink,
Bullseye,
Button,
EmptyState,
EmptyStateBody,
EmptyStateIcon,
EmptyStateVariant,
Grid,
GridItem,
Level,
LevelItem,
Pagination,
PaginationVariant,
Popover,
PopoverPosition,
Spinner,
Text,
TextContent,
TextVariants,
Title,
Alert,
AlertActionLink,
Tooltip,
WizardContext,
} from '@patternfly/react-core';
import { ExclamationTriangleIcon, QuestionCircleIcon } from '@patternfly/react-icons';
import spacing from '@patternfly/react-styles/css/utilities/Spacing/spacing';
import {
IRowData,
sortable,
TableComposable,
Tbody,
Td,
Th,
Thead,
Tr,
IRowData,
} from '@patternfly/react-table';
import spacing from '@patternfly/react-styles/css/utilities/Spacing/spacing';
import SimpleSelect, { OptionWithValue } from '../../../../../common/components/SimpleSelect';
import { useFilterState, useSortState } from '../../../../../common/duck/hooks';
import { IFormValues, IOtherProps } from './WizardContainer';
import { useFormikContext } from 'formik';
import React, { useEffect, useState } from 'react';
import ReactJson from 'react-json-view';
import { useSelector } from 'react-redux';
import { DefaultRootState } from '../../../../../../configureStore';
import {
FilterToolbar,
FilterCategory,
FilterToolbar,
FilterType,
} from '../../../../../common/components/FilterToolbar';
import { capitalize } from '../../../../../common/duck/utils';
import SimpleSelect, { OptionWithValue } from '../../../../../common/components/SimpleSelect';
import TableEmptyState from '../../../../../common/components/TableEmptyState';
import { useFilterState, useSortState } from '../../../../../common/duck/hooks';
import { usePaginationState } from '../../../../../common/duck/hooks/usePaginationState';
import { capitalize } from '../../../../../common/duck/utils';
import {
IMigPlanStorageClass,
IPlanPersistentVolume,
PvCopyMethod,
} from '../../../../../plan/duck/types';
import { usePaginationState } from '../../../../../common/duck/hooks/usePaginationState';
import { useFormikContext } from 'formik';
import { useSelector } from 'react-redux';
import { DefaultRootState } from '../../../../../../configureStore';
import {
getSuggestedPvStorageClasses,
pvcNameToString,
targetStorageClassToString,
} from '../../helpers';
import { VerifyCopyWarningModal, VerifyWarningState } from './VerifyCopyWarningModal';
import { ExclamationTriangleIcon, QuestionCircleIcon } from '@patternfly/react-icons';
import { PVStorageClassSelect } from './PVStorageClassSelect';
import { VerifyCopyCheckbox } from './VerifyCopyCheckbox';
import ReactJson from 'react-json-view';
import { VerifyCopyWarningModal, VerifyWarningState } from './VerifyCopyWarningModal';
import { IFormValues, IOtherProps } from './WizardContainer';

const styles = require('./VolumesTable.module').default;

Expand All @@ -74,7 +76,7 @@ const VolumesTable: React.FunctionComponent<IVolumesTableProps> = ({
}: IVolumesTableProps) => {
const planState = useSelector((state: DefaultRootState) => state.plan);

const { setFieldValue, values } = useFormikContext<IFormValues>();
const { submitForm, setFieldValue, values } = useFormikContext<IFormValues>();
const isSCC = values.migrationType.value === 'scc';

// Initialize target storage class form selection from currentPlan if we're ready
Expand Down Expand Up @@ -242,30 +244,57 @@ const VolumesTable: React.FunctionComponent<IVolumesTableProps> = ({
useEffect(() => setPageNumber(1), [filterValues, sortBy]);

useEffect(() => {
const newSelected = filteredItems
.filter((pv) => pv.selection.action !== 'skip')
.map((pv) => pv.name);
//select all pvs on load
setAllRowsSelected(true);
const newSelected = filteredItems.map((pv) => pv.name);
setAllRowsSelected(newSelected.length === values.persistentVolumes.length);
setFieldValue('selectedPVs', newSelected);
}, []);

const skipUnSelectedPVs = (newSelected: any) => {
const newPVs = values.persistentVolumes.map((currentPV) => {
const isSelected = newSelected.find((selectedPV: string) => selectedPV === currentPV.name);
if (!isSelected) {
return {
...currentPV,
selection: {
...currentPV.selection,
action: 'skip',
},
};
} else {
//If the PV is selected and the action is not set to move, the PV needs to have a copy action set
return {
...currentPV,
selection: {
...currentPV.selection,
...(currentPV.selection.action !== 'move' && {
action: 'copy',
}),
},
};
}
});
return newPVs;
};
const [allRowsSelected, setAllRowsSelected] = React.useState(false);

const onSelectAll = (event: any, isSelected: boolean, rowIndex: number, rowData: IRowData) => {
setAllRowsSelected(isSelected);

let newSelected;
if (isSelected) {
newSelected = filteredItems.map((pv) => pv.name); // Select all (filtered)
} else {
newSelected = []; // Deselect all
}
setFieldValue('selectedPVs', newSelected);
const newPVs = skipUnSelectedPVs(newSelected);
setAllRowsSelected(newSelected.length === values.persistentVolumes.length);
setFieldValue('persistentVolumes', newPVs);
submitForm();
};

const onSelect = (event: any, isSelected: boolean, rowIndex: number, rowData: IRowData) => {
if (allRowsSelected) {
setAllRowsSelected(false);
}
let newSelected;
if (rowIndex === -1) {
if (isSelected) {
Expand All @@ -284,6 +313,10 @@ const VolumesTable: React.FunctionComponent<IVolumesTableProps> = ({
}
}
setFieldValue('selectedPVs', newSelected);
const newPVs = skipUnSelectedPVs(newSelected);
setAllRowsSelected(newSelected.length === values.persistentVolumes.length);
setFieldValue('persistentVolumes', newPVs);
submitForm();
};

const rows = currentPageItems.map((pv: IPlanPersistentVolume) => {
Expand Down Expand Up @@ -427,6 +460,26 @@ const VolumesTable: React.FunctionComponent<IVolumesTableProps> = ({
: 'Choose to move or copy persistent volumes associated with selected namespaces.'}
</Text>
</TextContent>
{planState.currentPlanStatus.state === 'Critical' && !planState.currentPlan.spec.refresh ? (
<Bullseye>
<EmptyState variant="large">
<Alert variant="danger" isInline title={planState.currentPlanStatus.errorMessage} />
</EmptyState>
</Bullseye>
) : null}
{planState.isFetchingPVResources ||
planState.isPollingStatus ||
planState.currentPlanStatus.state === 'Pending' ||
planState.currentPlan.spec.refresh ? (
<Bullseye>
<EmptyState variant="large">
<Spinner size="md" />
<Title headingLevel="h4" size="md">
Validating selected persistent volumes against other migration plans...
</Title>
</EmptyState>
</Bullseye>
) : null}
</GridItem>
{isSCC && values.persistentVolumes.length === 0 ? (
<GridItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ const WizardComponent = (props: IOtherProps) => {
!isFetchingPVList &&
currentPlanStatus.state !== 'Pending' &&
currentPlanStatus.state !== 'Critical' &&
!planState.currentPlan.spec.refresh &&
(values.migrationType.value !== 'scc' ||
(values.selectedPVs.length > 0 && storageClasses.length > 1))
);
Expand Down
73 changes: 35 additions & 38 deletions src/app/plan/duck/sagas.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,51 @@
import { ClientFactory, IClusterClient } from '@konveyor/lib-ui';
import Q from 'q';
import {
takeEvery,
takeLatest,
select,
retry,
race,
call,
delay,
put,
take,
race,
retry,
select,
StrictEffect,
take,
takeEvery,
takeLatest,
} from 'redux-saga/effects';
import { DiscoveryFactory } from '../../../client/discovery_factory';
import { IDiscoveryClient } from '../../../client/discoveryClient';
import { MigResource, MigResourceKind } from '../../../client/helpers';
import {
updateMigPlanFromValues,
createInitialMigPlan,
createInitialMigAnalytic,
createMigMigration,
createInitialMigPlan,
createMigHook,
createMigMigration,
updateMigHook,
updateMigPlanFromValues,
updatePlanHookList,
} from '../../../client/resources/conversions';
import { PlanActions, PlanActionTypes } from './actions';
import { CurrentPlanState } from './reducers';
import utils from '../../common/duck/utils';
import planUtils from './utils';
import { createAddEditStatus, AddEditState, AddEditMode } from '../../common/add_edit_state';
import Q from 'q';
import {
alertErrorTimeout,
DiscoveryResource,
NamespaceDiscovery,
PersistentVolumeDiscovery,
} from '../../../client/resources/discovery';
import { DefaultRootState } from '../../../configureStore';
import { certErrorOccurred } from '../../auth/duck/slice';
import { AddEditMode, AddEditState, createAddEditStatus } from '../../common/add_edit_state';
import {
alertErrorModal,
alertSuccessTimeout,
alertErrorTimeout,
alertProgressTimeout,
alertSuccessTimeout,
alertWarn,
} from '../../common/duck/slice';
import { certErrorOccurred, IAuthReducerState } from '../../auth/duck/slice';
import { DefaultRootState } from '../../../configureStore';
import {
IMigPlan,
IMigration,
IPersistentVolumeResource,
IPlan,
IPlanPersistentVolume,
IPlanSpecHook,
} from './types';
import utils from '../../common/duck/utils';
import { IMigHook } from '../../home/pages/HooksPage/types';
import { MigResource, MigResourceKind } from '../../../client/helpers';
import { ClientFactory, IClusterClient } from '@konveyor/lib-ui';
import { IDiscoveryClient } from '../../../client/discoveryClient';
import {
DiscoveryResource,
NamespaceDiscovery,
PersistentVolumeDiscovery,
} from '../../../client/resources/discovery';
import { DiscoveryFactory } from '../../../client/discovery_factory';
import { getPlanInfo } from '../../home/pages/PlansPage/helpers';
import { PlanActions, PlanActionTypes } from './actions';
import { CurrentPlanState } from './reducers';
import { IMigPlan, IMigration, IPersistentVolumeResource, IPlan, IPlanSpecHook } from './types';
import planUtils from './utils';

const uuidv1 = require('uuid/v1');
const PlanMigrationPollingInterval = 5000;
Expand Down Expand Up @@ -390,13 +383,17 @@ function* validatePlanPoll(action: any): any {
yield put(PlanActions.setCurrentPlan(updatedPlan));
yield put(PlanActions.updatePlanList(updatedPlan));
yield put(PlanActions.startPlanStatusPolling(updatedPlan.metadata.name));
yield put(PlanActions.validatePlanPollStop());
if (!updatedPlan.spec.refresh) {
yield put(PlanActions.validatePlanPollStop());
}
}
} else {
yield put(PlanActions.setCurrentPlan(updatedPlan));
yield put(PlanActions.updatePlanList(updatedPlan));
yield put(PlanActions.startPlanStatusPolling(updatedPlan.metadata.name));
yield put(PlanActions.validatePlanPollStop());
if (!updatedPlan.spec.refresh) {
yield put(PlanActions.validatePlanPollStop());
}
}
yield delay(params.delay);
}
Expand Down

0 comments on commit f78ff94

Please sign in to comment.