diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/vm-status-modal/index.ts b/frontend/packages/kubevirt-plugin/src/components/modals/vm-status-modal/index.ts new file mode 100644 index 00000000000..8484ed96e7a --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/modals/vm-status-modal/index.ts @@ -0,0 +1 @@ +export * from './vm-status-modal'; diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/vm-status-modal/vm-status-modal.tsx b/frontend/packages/kubevirt-plugin/src/components/modals/vm-status-modal/vm-status-modal.tsx new file mode 100644 index 00000000000..244ac36b280 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/modals/vm-status-modal/vm-status-modal.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; +import { Modal } from '@patternfly/react-core'; +import { HandlePromiseProps, withHandlePromise } from '@console/internal/components/utils'; +import { ModalComponentProps } from '@console/internal/components/factory'; +import { ModalFooter } from '../modal/modal-footer'; +import { PAUSED_VM_MODAL_MESSAGE } from '../../../constants/vm'; +import { VMIKind } from '../../../types'; +import { unpauseVMI } from '../../../k8s/requests/vmi/actions'; + +const modalTitle = 'Edit pause state'; + +const VMStatusModal = withHandlePromise( + ({ vmi, isOpen, setOpen, title = modalTitle, handlePromise, inProgress, errorMessage }) => { + const [showPatchError, setPatchError] = React.useState(false); + + const onSubmit = async (event) => { + event.preventDefault(); + + const promise = unpauseVMI(vmi); + handlePromise(promise) + .then(() => setOpen(false)) + .catch(() => setPatchError(true)); + }; + + const footer = ( + setOpen(false)} + submitButtonText="Unpause" + /> + ); + + return ( + setOpen(false)} + footer={footer} + isFooterLeftAligned + > +
{PAUSED_VM_MODAL_MESSAGE}
+
+ ); + }, +); + +export type VMStatusModalProps = HandlePromiseProps & + ModalComponentProps & { + vmi: VMIKind; + title?: string; + isOpen: boolean; + setOpen: (isOpen: boolean) => void; + }; + +export default VMStatusModal; diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-status/vm-status.tsx b/frontend/packages/kubevirt-plugin/src/components/vm-status/vm-status.tsx index 0f747043e87..84c6ae3bc06 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-status/vm-status.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vm-status/vm-status.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { PodKind, K8sResourceKind } from '@console/internal/module/k8s'; -import { OffIcon, UnknownIcon, SyncAltIcon } from '@patternfly/react-icons'; +import { OffIcon, PausedIcon, SyncAltIcon, UnknownIcon } from '@patternfly/react-icons'; import { PopoverStatus, StatusIconAndText, @@ -10,12 +10,24 @@ import { ProgressStatus, PendingStatus, } from '@console/shared'; -import { Progress, ProgressVariant, ProgressSize } from '@patternfly/react-core'; +import { + Progress, + ProgressVariant, + ProgressSize, + Button, + ButtonVariant, +} from '@patternfly/react-core'; import { Link } from 'react-router-dom'; import { resourcePath } from '@console/internal/components/utils'; import { PodModel } from '@console/internal/models'; +import { unpauseVMI } from '../../k8s/requests/vmi/actions'; import { VirtualMachineModel } from '../../models'; -import { VM_DETAIL_EVENTS_HREF, CDI_KUBEVIRT_IO, STORAGE_IMPORT_PVC_NAME } from '../../constants'; +import { + VM_DETAIL_EVENTS_HREF, + CDI_KUBEVIRT_IO, + STORAGE_IMPORT_PVC_NAME, + PAUSED_VM_MODAL_MESSAGE, +} from '../../constants'; import { getLabels } from '../../selectors/selectors'; import { getVMStatus } from '../../statuses/vm/vm'; import { @@ -33,6 +45,7 @@ import { VM_STATUS_OFF, VM_STATUS_ERROR, VM_STATUS_IMPORT_PENDING, + VM_STATUS_PAUSED, } from '../../statuses/vm/constants'; import { VMKind, VMIKind } from '../../types'; @@ -102,6 +115,11 @@ export const VMStatus: React.FC = ({ )}`; // to default tab const additionalText = verbose ? getAdditionalImportText(statusDetail.pod) : null; + const unpauseVM = async (event: React.MouseEvent) => { + event.preventDefault(); + await unpauseVMI(vmi); + }; + switch (statusDetail.status) { case VM_STATUS_V2V_CONVERSION_PENDING: return ( @@ -248,6 +266,16 @@ export const VMStatus: React.FC = ({ /> ); + case VM_STATUS_PAUSED: + return ( + }> + + + + + ); case VM_STATUS_RUNNING: return } />; case VM_STATUS_OFF: diff --git a/frontend/packages/kubevirt-plugin/src/components/vms/menu-actions.tsx b/frontend/packages/kubevirt-plugin/src/components/vms/menu-actions.tsx index 610932cd040..e36c95efe56 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vms/menu-actions.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vms/menu-actions.tsx @@ -1,7 +1,7 @@ import * as _ from 'lodash'; import * as React from 'react'; import { asAccessReview, Kebab, KebabOption } from '@console/internal/components/utils'; -import { K8sKind, K8sResourceKind, PodKind } from '@console/internal/module/k8s'; +import { K8sKind, K8sResourceCommon, K8sResourceKind, PodKind } from '@console/internal/module/k8s'; import { getName, getNamespace } from '@console/shared'; import { confirmModal } from '@console/internal/components/modals'; import { VMIKind, VMKind } from '../../types/vm'; @@ -15,6 +15,8 @@ import { cancelMigration } from '../../k8s/requests/vmim'; import { cloneVMModal } from '../modals/clone-vm-modal'; import { VMCDRomModal } from '../modals/cdrom-vm-modal'; import { getVMStatus } from '../../statuses/vm/vm'; +import { isVMIPaused } from '../../selectors/vmi'; +import { unpauseVMI, VMIActionType } from '../../k8s/requests/vmi/actions'; type ActionArgs = { migration?: K8sResourceKind; @@ -22,10 +24,10 @@ type ActionArgs = { vmStatus?: VMMultiStatus; }; -const getVMActionMessage = (vm, action: VMActionType) => ( +const getActionMessage = (obj: K8sResourceCommon, action: VMActionType | VMIActionType) => ( <> - Are you sure you want to {action} {getName(vm)} in namespace{' '} - {getNamespace(vm)}? + Are you sure you want to {action} {getName(obj)} in namespace{' '} + {getNamespace(obj)}? ); @@ -41,7 +43,7 @@ export const menuActionStart = ( callback: () => confirmModal({ title, - message: getVMActionMessage(vm, VMActionType.Start), + message: getActionMessage(vm, VMActionType.Start), btnText: _.capitalize(VMActionType.Start), executeFn: () => startVM(vm), }), @@ -57,7 +59,7 @@ const menuActionStop = (kindObj: K8sKind, vm: VMKind): KebabOption => { callback: () => confirmModal({ title, - message: getVMActionMessage(vm, VMActionType.Stop), + message: getActionMessage(vm, VMActionType.Stop), btnText: _.capitalize(VMActionType.Stop), executeFn: () => stopVM(vm), }), @@ -77,7 +79,7 @@ const menuActionRestart = ( callback: () => confirmModal({ title, - message: getVMActionMessage(vm, VMActionType.Restart), + message: getActionMessage(vm, VMActionType.Restart), btnText: _.capitalize(VMActionType.Restart), executeFn: () => restartVM(vm), }), @@ -85,6 +87,21 @@ const menuActionRestart = ( }; }; +const menuActionUnpause = (kindObj: K8sKind, vm: VMKind, { vmi }: ActionArgs): KebabOption => { + const title = 'Unpause Virtual Machine'; + return { + hidden: !isVMIPaused(vmi), + label: title, + callback: () => + confirmModal({ + title, + message: getActionMessage(vmi, VMIActionType.Unpause), + btnText: _.capitalize(VMIActionType.Unpause), + executeFn: () => unpauseVMI(vmi), + }), + }; +}; + const menuActionMigrate = ( kindObj: K8sKind, vm: VMKind, @@ -156,6 +173,7 @@ export const vmMenuActions = [ menuActionStart, menuActionStop, menuActionRestart, + menuActionUnpause, menuActionMigrate, menuActionCancelMigration, menuActionClone, diff --git a/frontend/packages/kubevirt-plugin/src/components/vms/vm-resource.tsx b/frontend/packages/kubevirt-plugin/src/components/vms/vm-resource.tsx index 8c20642afc0..1d2d5d25631 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vms/vm-resource.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vms/vm-resource.tsx @@ -10,6 +10,7 @@ import { vmDescriptionModal, vmFlavorModal } from '../modals'; import { VMCDRomModal } from '../modals/cdrom-vm-modal'; import { DedicatedResourcesModal } from '../modals/dedicated-resources-modal/dedicated-resources-modal'; import { BootOrderModal } from '../modals/boot-order-modal/boot-order-modal'; +import VMStatusModal from '../modals/vm-status-modal/vm-status-modal'; import { getDescription } from '../../selectors/selectors'; import { getCDRoms, isDedicatedCPUPlacement } from '../../selectors/vm/selectors'; import { getVMTemplateNamespacedName } from '../../selectors/vm-template/selectors'; @@ -32,8 +33,8 @@ import { getWorkloadProfile, getDevices, } from '../../selectors/vm'; - import './vm-resource.scss'; +import { isVMIPaused } from '../../selectors/vmi'; export const VMDetailsItem: React.FC = ({ title, @@ -103,6 +104,7 @@ export const VMDetailsList: React.FC = ({ const [isDedicatedResourcesModalOpen, setDedicatedResourcesModalOpen] = React.useState( false, ); + const [isStatusModalOpen, setStatusModalOpen] = React.useState(false); const id = getBasicID(vm); const vmStatus = getVMStatus({ vm, vmi, pods, migrations }); @@ -117,7 +119,14 @@ export const VMDetailsList: React.FC = ({ return (
- + setStatusModalOpen(true)} + idValue={prefixedID(id, 'vm-statuses')} + > + diff --git a/frontend/packages/kubevirt-plugin/src/constants/vm/constants.ts b/frontend/packages/kubevirt-plugin/src/constants/vm/constants.ts index 5b26c42b246..d799a9bb425 100644 --- a/frontend/packages/kubevirt-plugin/src/constants/vm/constants.ts +++ b/frontend/packages/kubevirt-plugin/src/constants/vm/constants.ts @@ -42,3 +42,6 @@ export enum DeviceType { } export const VM_DETAIL_EVENTS_HREF = 'events'; + +export const PAUSED_VM_MODAL_MESSAGE = + 'This VM has been paused. If you wish to unpause it, please click the Unpause button below. For further details, please check with your system administrator.'; diff --git a/frontend/packages/kubevirt-plugin/src/k8s/requests/vmi/actions.ts b/frontend/packages/kubevirt-plugin/src/k8s/requests/vmi/actions.ts new file mode 100644 index 00000000000..1a165f19be4 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/k8s/requests/vmi/actions.ts @@ -0,0 +1,32 @@ +import { coFetch } from '@console/internal/co-fetch'; +import { resourceURL } from '@console/internal/module/k8s'; +import { getName, getNamespace } from '@console/shared'; +import { VirtualMachineInstanceModel } from '../../../models'; +import { VMIKind } from '../../../types/vm'; + +export enum VMIActionType { + Unpause = 'unpause', +} + +const VMIActionRequest = async (vmi: VMIKind, action: VMIActionType) => { + const method = 'PUT'; + let url = resourceURL( + { + ...VirtualMachineInstanceModel, + apiGroup: `subresources.${VirtualMachineInstanceModel.apiGroup}`, + }, + { + ns: getNamespace(vmi), + name: getName(vmi), + }, + ); + + url = `${url}/${action}`; + + const response = await coFetch(url, { method }); + const text = await response.text(); + + return text; +}; + +export const unpauseVMI = async (vmi: VMIKind) => VMIActionRequest(vmi, VMIActionType.Unpause); diff --git a/frontend/packages/kubevirt-plugin/src/selectors/vmi/basic.ts b/frontend/packages/kubevirt-plugin/src/selectors/vmi/basic.ts index a27fcf33b0e..07dbb7ec138 100644 --- a/frontend/packages/kubevirt-plugin/src/selectors/vmi/basic.ts +++ b/frontend/packages/kubevirt-plugin/src/selectors/vmi/basic.ts @@ -26,3 +26,6 @@ export const getVMIInterfaces = (vmi: VMIKind) => (vmi && vmi.status && vmi.status.interfaces) || []; export const getVMINodeName = (vmi: VMIKind) => vmi && vmi.status && vmi.status.nodeName; + +export const isVMIPaused = (vmi: VMIKind): boolean => + getVMIConditionsByType(vmi, 'Paused').length > 0; diff --git a/frontend/packages/kubevirt-plugin/src/statuses/vm/constants.ts b/frontend/packages/kubevirt-plugin/src/statuses/vm/constants.ts index 4b850a41e0a..a9b628647a9 100644 --- a/frontend/packages/kubevirt-plugin/src/statuses/vm/constants.ts +++ b/frontend/packages/kubevirt-plugin/src/statuses/vm/constants.ts @@ -4,6 +4,7 @@ export const VM_STATUS_STARTING = 'VM_STATUS_STARTING'; export const VM_STATUS_VMI_WAITING = 'VM_STATUS_VMI_WAITING'; export const VM_STATUS_IMPORTING = 'VM_STATUS_IMPORTING'; export const VM_STATUS_STOPPING = 'VM_STATUS_STOPPING'; +export const VM_STATUS_PAUSED = 'VM_STATUS_PAUSED'; export const VM_STATUS_V2V_CONVERSION_IN_PROGRESS = 'VM_STATUS_CONVERSION_IN_PROGRESS'; export const VM_STATUS_V2V_CONVERSION_PENDING = 'VM_STATUS_CONVERSION_PENDING'; diff --git a/frontend/packages/kubevirt-plugin/src/statuses/vm/vm.ts b/frontend/packages/kubevirt-plugin/src/statuses/vm/vm.ts index 563b093b918..0c3750eb952 100644 --- a/frontend/packages/kubevirt-plugin/src/statuses/vm/vm.ts +++ b/frontend/packages/kubevirt-plugin/src/statuses/vm/vm.ts @@ -46,8 +46,10 @@ import { VM_STATUS_V2V_CONVERSION_PENDING, CONVERSION_PROGRESS_ANNOTATION, VM_STATUS_IMPORT_PENDING, + VM_STATUS_PAUSED, } from './constants'; import { Status } from '..'; +import { isVMIPaused } from '../../selectors/vmi/basic'; const isBeingMigrated = (vm: VMKind, migrations?: K8sResourceKind[]): VMStatus => { const migration = findVMIMigration(vm, migrations); @@ -96,6 +98,9 @@ const isReady = (vmi: VMIKind, launcherPod: PodKind): VMStatus => { return NOT_HANDLED; }; +const isPaused = (vmi: VMIKind): VMStatus => + isVMIPaused(vmi) ? { status: VM_STATUS_PAUSED } : NOT_HANDLED; + const isVMError = (vm: VMKind): VMStatus => { // is an issue with the VM definition? const condition = getVMStatusConditions(vm)[0]; @@ -237,6 +242,7 @@ export const getVMStatus = ({ }): VMStatus => { const launcherPod = findVMPod(vm, pods); return ( + isPaused(vmi) || isV2VConversion(vm, pods) || // these statuses must precede isRunning() because they do not rely on ready vms isBeingMigrated(vm, migrations) || // -||- isBeingImported(vm, pods) || // -||-