Skip to content

Commit

Permalink
Merge pull request #1963 from upalatucci/delete-vm-modal-checkboxes
Browse files Browse the repository at this point in the history
CNV-41205: Add checkboxes for each disk to delete
  • Loading branch information
openshift-merge-bot[bot] authored May 23, 2024
2 parents 10ad594 + 2897ce3 commit 6b66c15
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 82 deletions.
2 changes: 1 addition & 1 deletion locales/en/plugin__kubevirt-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@
"Delete Data Volume: {{pvcName}}": "Delete Data Volume: {{pvcName}}",
"Delete DataImportCron?": "Delete DataImportCron?",
"Delete DataSource?": "Delete DataSource?",
"Delete disks ({{diskCount}}x)": "Delete disks ({{diskCount}}x)",
"Delete disk {{resourceName}} ({{kindAbbr}})": "Delete disk {{resourceName}} ({{kindAbbr}})",
"Delete MigrationPolicy?": "Delete MigrationPolicy?",
"Delete NIC?": "Delete NIC?",
"Delete Resource?": "Delete Resource?",
Expand Down
6 changes: 5 additions & 1 deletion src/utils/components/GracePeriodInput/GracePeriodInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ export const GracePeriodInput: FC<GracePeriodInputProps> = ({

return (
<StackItem>
<Flex alignItems={{ default: 'alignItemsCenter' }} className="grace-period-input">
<Flex
alignItems={{ default: 'alignItemsCenter' }}
className="grace-period-input"
spaceItems={{ default: 'spaceItemsXs' }}
>
<FlexItem>
<Checkbox
id="grace-period-checkbox"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import React, { FC, useState } from 'react';
import { useNavigate } from 'react-router-dom-v5-compat';

import { PersistentVolumeClaimModel } from '@kubevirt-ui/kubevirt-api/console';
import DataVolumeModel from '@kubevirt-ui/kubevirt-api/console/models/DataVolumeModel';
import VirtualMachineModel, {
VirtualMachineModelRef,
} from '@kubevirt-ui/kubevirt-api/console/models/VirtualMachineModel';
import { V1beta1DataVolume } from '@kubevirt-ui/kubevirt-api/containerized-data-importer/models';
import { IoK8sApiCoreV1PersistentVolumeClaim } from '@kubevirt-ui/kubevirt-api/kubernetes';
import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt';
import ConfirmActionMessage from '@kubevirt-utils/components/ConfirmActionMessage/ConfirmActionMessage';
import { GracePeriodInput } from '@kubevirt-utils/components/GracePeriodInput/GracePeriodInput';
import TabModal from '@kubevirt-utils/components/TabModal/TabModal';
import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation';
import { useLastNamespacePath } from '@kubevirt-utils/hooks/useLastNamespacePath';
import { buildOwnerReference, compareOwnerReferences } from '@kubevirt-utils/resources/shared';
import { k8sDelete, k8sPatch } from '@openshift-console/dynamic-plugin-sdk';
import { buildOwnerReference } from '@kubevirt-utils/resources/shared';
import { k8sDelete } from '@openshift-console/dynamic-plugin-sdk';
import { ButtonVariant, Stack, StackItem } from '@patternfly/react-core';

import DeleteOwnedResourcesMessage from './components/DeleteOwnedResourcesMessage';
import useDeleteVMResources from './hooks/useDeleteVMResources';
import { removeDataVolumeTemplatesToVM, updateVolumeResources } from './utils/helpers';
import { DEFAULT_GRACE_PERIOD } from './constants';

type DeleteVMModalProps = {
Expand All @@ -29,65 +31,27 @@ type DeleteVMModalProps = {
const DeleteVMModal: FC<DeleteVMModalProps> = ({ isOpen, onClose, vm }) => {
const { t } = useKubevirtTranslation();
const navigate = useNavigate();
const [deleteOwnedResource, setDeleteOwnedResource] = useState<boolean>(true);
const [gracePeriodCheckbox, setGracePeriodCheckbox] = useState<boolean>(false);
const [gracePeriodSeconds, setGracePeriodSeconds] = useState<number>(
vm?.spec?.template?.spec?.terminationGracePeriodSeconds || DEFAULT_GRACE_PERIOD,
);

const [volumesToSave, setVolumesToSave] = useState<
(IoK8sApiCoreV1PersistentVolumeClaim | V1beta1DataVolume)[]
>([]);

const { dataVolumes, loaded, pvcs, snapshots } = useDeleteVMResources(vm);
const lastNamespacePath = useLastNamespacePath();

const onDelete = async (updatedVM: V1VirtualMachine) => {
if (!deleteOwnedResource) {
const vmOwnerRef = buildOwnerReference(updatedVM);

await k8sPatch({
data: [
{
op: 'remove',
path: '/spec/dataVolumeTemplates',
},
],
model: VirtualMachineModel,
resource: updatedVM,
});

const pvcPromises = (pvcs || [])?.map((pvc) => {
const pvcFilteredOwnerReference = pvc?.metadata?.ownerReferences?.filter(
(pvcRef) => !compareOwnerReferences(pvcRef, vmOwnerRef),
);
return k8sPatch({
data: [
{
op: 'replace',
path: '/metadata/ownerReferences',
value: pvcFilteredOwnerReference,
},
],
model: PersistentVolumeClaimModel,
resource: pvc,
});
});
const vmOwnerRef = buildOwnerReference(updatedVM);

const dvPromises = (dataVolumes || [])?.map((dv) => {
const dvFilteredOwnerReference = dv?.metadata?.ownerReferences?.filter(
(dvRef) => !compareOwnerReferences(dvRef, vmOwnerRef),
);
return k8sPatch({
data: [
{
op: 'replace',
path: '/metadata/ownerReferences',
value: dvFilteredOwnerReference,
},
],
model: DataVolumeModel,
resource: dv,
});
});
await removeDataVolumeTemplatesToVM(
vm,
volumesToSave.filter((volume) => volume.kind === DataVolumeModel.kind) as V1beta1DataVolume[],
);

await Promise.allSettled([...pvcPromises, ...dvPromises]);
}
await Promise.allSettled(updateVolumeResources(volumesToSave, vmOwnerRef));

await k8sDelete({
json: gracePeriodCheckbox
Expand Down Expand Up @@ -121,11 +85,11 @@ const DeleteVMModal: FC<DeleteVMModalProps> = ({ isOpen, onClose, vm }) => {
/>
<DeleteOwnedResourcesMessage
dataVolumes={dataVolumes}
deleteOwnedResource={deleteOwnedResource}
loaded={loaded}
pvcs={pvcs}
setDeleteOwnedResource={setDeleteOwnedResource}
setVolumesToSave={setVolumesToSave}
snapshots={snapshots}
volumesToSave={volumesToSave}
/>
</Stack>
</TabModal>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,31 @@ import { IoK8sApiCoreV1PersistentVolumeClaim } from '@kubevirt-ui/kubevirt-api/k
import { V1alpha1VirtualMachineSnapshot } from '@kubevirt-ui/kubevirt-api/kubevirt';
import Loading from '@kubevirt-utils/components/Loading/Loading';
import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation';
import { Bullseye, Checkbox, StackItem } from '@patternfly/react-core';
import { getName } from '@kubevirt-utils/resources/shared';
import { Bullseye, StackItem } from '@patternfly/react-core';

import { findPVCOwner } from '../utils/helpers';

import DeleteVolumeCheckbox from './DeleteVolumeCheckbox';

type DeleteOwnedResourcesMessageProps = {
dataVolumes: V1beta1DataVolume[];
deleteOwnedResource: boolean;
loaded: boolean;
pvcs: IoK8sApiCoreV1PersistentVolumeClaim[];
setDeleteOwnedResource: Dispatch<SetStateAction<boolean>>;
setVolumesToSave: Dispatch<
SetStateAction<(IoK8sApiCoreV1PersistentVolumeClaim | V1beta1DataVolume)[]>
>;
snapshots: V1alpha1VirtualMachineSnapshot[];
volumesToSave: (IoK8sApiCoreV1PersistentVolumeClaim | V1beta1DataVolume)[];
};

const DeleteOwnedResourcesMessage: FC<DeleteOwnedResourcesMessageProps> = ({
dataVolumes,
deleteOwnedResource,
loaded,
pvcs,
setDeleteOwnedResource,
setVolumesToSave,
snapshots,
volumesToSave,
}) => {
const { t } = useKubevirtTranslation();

Expand All @@ -36,7 +41,7 @@ const DeleteOwnedResourcesMessage: FC<DeleteOwnedResourcesMessageProps> = ({
);
}

const pvcsWithNoDataVolumes = pvcs?.filter((pvc) => !findPVCOwner(pvc, dataVolumes));
const pvcsWithNoDataVolumes = pvcs?.filter((pvc) => !findPVCOwner(pvc, dataVolumes)) || [];

const diskCount = dataVolumes?.length + pvcsWithNoDataVolumes?.length || 0;
const hasSnapshots = snapshots?.length > 0;
Expand All @@ -50,16 +55,16 @@ const DeleteOwnedResourcesMessage: FC<DeleteOwnedResourcesMessageProps> = ({
)}
</StackItem>
)}
{!!diskCount && (
<StackItem>
<Checkbox
id="delete-owned-resources"
isChecked={deleteOwnedResource}
label={t('Delete disks ({{diskCount}}x)', { diskCount })}
onChange={(_, checked: boolean) => setDeleteOwnedResource(checked)}
/>
</StackItem>
)}

{[...(dataVolumes || []), ...pvcsWithNoDataVolumes].map((resource) => (
<DeleteVolumeCheckbox
key={`${resource.kind}-${getName(resource)}`}
resource={resource}
setVolumesToSave={setVolumesToSave}
volumesToSave={volumesToSave}
/>
))}

{hasSnapshots && (
<StackItem>
<strong>{t('Warning')}: </strong>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { Dispatch, FC, SetStateAction } from 'react';

import { PersistentVolumeClaimModel } from '@kubevirt-ui/kubevirt-api/console';
import DataVolumeModel from '@kubevirt-ui/kubevirt-api/console/models/DataVolumeModel';
import { V1beta1DataVolume } from '@kubevirt-ui/kubevirt-api/containerized-data-importer/models';
import { IoK8sApiCoreV1PersistentVolumeClaim } from '@kubevirt-ui/kubevirt-api/kubernetes';
import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation';
import { getName } from '@kubevirt-utils/resources/shared';
import { Checkbox, StackItem } from '@patternfly/react-core';

import { sameVolume } from '../utils/helpers';

type DeleteVolumeCheckboxProps = {
resource: IoK8sApiCoreV1PersistentVolumeClaim | V1beta1DataVolume;

setVolumesToSave: Dispatch<
SetStateAction<(IoK8sApiCoreV1PersistentVolumeClaim | V1beta1DataVolume)[]>
>;
volumesToSave: (IoK8sApiCoreV1PersistentVolumeClaim | V1beta1DataVolume)[];
};

const DeleteVolumeCheckbox: FC<DeleteVolumeCheckboxProps> = ({
resource,
setVolumesToSave,
volumesToSave,
}) => {
const { t } = useKubevirtTranslation();
const resourceName = getName(resource);

const saveVolume = () => setVolumesToSave((prevVolumes) => [...prevVolumes, resource]);

const deleteVolume = () =>
setVolumesToSave((prevVolumes) =>
prevVolumes.filter((volume) => !sameVolume(volume, resource)),
);

return (
<StackItem>
<Checkbox
label={t('Delete disk {{resourceName}} ({{kindAbbr}})', {
kindAbbr:
resource.kind === PersistentVolumeClaimModel.kind
? PersistentVolumeClaimModel.abbr
: DataVolumeModel.abbr,
resourceName,
})}
id={`${resource.kind}-${resourceName}`}
isChecked={!volumesToSave.find((volume) => sameVolume(volume, resource))}
onChange={(_, checked: boolean) => (checked ? deleteVolume() : saveVolume())}
/>
</StackItem>
);
};

export default DeleteVolumeCheckbox;
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { PersistentVolumeClaimModel } from '@kubevirt-ui/kubevirt-api/console';
import DataVolumeModel from '@kubevirt-ui/kubevirt-api/console/models/DataVolumeModel';
import VirtualMachineModel from '@kubevirt-ui/kubevirt-api/console/models/VirtualMachineModel';
import { V1beta1DataVolume } from '@kubevirt-ui/kubevirt-api/containerized-data-importer/models';
import { IoK8sApiCoreV1PersistentVolumeClaim } from '@kubevirt-ui/kubevirt-api/kubernetes/models';
import { compareOwnerReferences } from '@kubevirt-utils/resources/shared';
import {
K8sModel,
k8sPatch,
K8sResourceCommon,
OwnerReference,
} from '@openshift-console/dynamic-plugin-sdk';
import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt';
import { compareOwnerReferences, getName } from '@kubevirt-utils/resources/shared';
import { getDataVolumeTemplates } from '@kubevirt-utils/resources/vm';
import { isEmpty } from '@kubevirt-utils/utils/utils';
import { k8sPatch, K8sResourceCommon, OwnerReference } from '@openshift-console/dynamic-plugin-sdk';

export const updateVolumeResources = (
resources: IoK8sApiCoreV1PersistentVolumeClaim[] | V1beta1DataVolume[],
resources: (IoK8sApiCoreV1PersistentVolumeClaim | V1beta1DataVolume)[],
vmOwnerRef: OwnerReference,
model: K8sModel,
) => {
return (resources || []).map((resource) => {
const resourceFilteredOwnerReference = resource?.metadata?.ownerReferences?.filter(
Expand All @@ -25,7 +25,10 @@ export const updateVolumeResources = (
value: resourceFilteredOwnerReference,
},
],
model: model,
model:
resource.kind === PersistentVolumeClaimModel.kind
? PersistentVolumeClaimModel
: DataVolumeModel,
resource,
});
});
Expand All @@ -38,3 +41,32 @@ export const findPVCOwner = (
resources.find((resource) =>
pvc?.metadata?.ownerReferences?.find((owner) => owner.uid === resource.metadata.uid),
);

export const removeDataVolumeTemplatesToVM = (
vm: V1VirtualMachine,
dataVolumesToSave: V1beta1DataVolume[],
) => {
const dataVolumeTemplates = getDataVolumeTemplates(vm);

const dvIndexes = dataVolumesToSave
.map((dataVolume) =>
dataVolumeTemplates.findIndex((template) => getName(template) === getName(dataVolume)),
)
.filter((index) => index !== -1);

if (isEmpty(dvIndexes)) return;

return k8sPatch({
data: dvIndexes.map((index) => ({
op: 'remove',
path: `/spec/dataVolumeTemplates/${index}`,
})),
model: VirtualMachineModel,
resource: vm,
});
};

export const sameVolume = (
volumeA: IoK8sApiCoreV1PersistentVolumeClaim | V1beta1DataVolume,
volumeB: IoK8sApiCoreV1PersistentVolumeClaim | V1beta1DataVolume,
) => volumeA.kind === volumeB.kind && getName(volumeA) === volumeB;

0 comments on commit 6b66c15

Please sign in to comment.