diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/start-stop-vm-modal.tsx b/frontend/packages/kubevirt-plugin/src/components/modals/start-stop-vm-modal.tsx new file mode 100644 index 00000000000..272fdcfa9c4 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/modals/start-stop-vm-modal.tsx @@ -0,0 +1,71 @@ +import * as React from 'react'; + +import { withHandlePromise } from '@console/internal/components/utils'; + +import { + createModalLauncher, + ModalTitle, + ModalBody, + ModalSubmitFooter, +} from '@console/internal/components/factory'; + +import { k8sPatch } from '@console/internal/module/k8s'; + +import { getPxeBootPatch } from 'kubevirt-web-ui-components'; +import { getName, getNamespace } from '@console/shared/src'; +import { VirtualMachineModel } from '../../models'; +import { VMKind } from '../../types/vm'; + +const StartStopVmModal = withHandlePromise((props: StartStopVmModalProps) => { + const { vm, start, inProgress, errorMessage, handlePromise, close, cancel } = props; + + const submit = (event) => { + event.preventDefault(); + + const patches = []; + + // handle PXE boot + if (start) { + const pxePatch = getPxeBootPatch(vm); + patches.push(...pxePatch); + } + + patches.push({ + op: 'replace', + path: '/spec/running', + value: start, + }); + + const promise = k8sPatch(VirtualMachineModel, vm, patches); + return handlePromise(promise).then(close); + }; + + const action = start ? 'Start' : 'Stop'; + return ( +
+ {action} Virtual Machine + + Are you sure you want to {action.toLowerCase()} {getName(vm)} in namespace{' '} + {getNamespace(vm)}? + + + + ); +}); + +export type StartStopVmModalProps = { + vm: VMKind; + start: boolean; + handlePromise: (promise: Promise) => Promise; + inProgress: boolean; + errorMessage: string; + cancel: () => void; + close: () => void; +}; + +export const startStopVmModal = createModalLauncher(StartStopVmModal); diff --git a/frontend/packages/kubevirt-plugin/src/components/vms/menu-actions.tsx b/frontend/packages/kubevirt-plugin/src/components/vms/menu-actions.tsx new file mode 100644 index 00000000000..6226e705181 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/vms/menu-actions.tsx @@ -0,0 +1,57 @@ +import { + getVmStatus, + VM_STATUS_IMPORTING, + VM_STATUS_V2V_CONVERSION_IN_PROGRESS, +} from 'kubevirt-web-ui-components'; + +import { asAccessReview, Kebab, KebabOption } from '@console/internal/components/utils'; +import { K8sKind, PodKind } from '@console/internal/module/k8s'; +import { isVMRunning } from '../../selectors/vm'; +import { startStopVmModal } from '../modals/start-stop-vm-modal'; +import { VMKind } from '../../types/vm'; + +type ActionArgs = { + pods: PodKind[]; + migrations: any[]; +}; + +const isImporting = (vm: VMKind, { pods, migrations }: ActionArgs): boolean => { + const status = getVmStatus(vm, pods, migrations); + return ( + status && [VM_STATUS_IMPORTING, VM_STATUS_V2V_CONVERSION_IN_PROGRESS].includes(status.status) + ); +}; + +const menuActionStart = (kindObj: K8sKind, vm: VMKind, actionArgs: ActionArgs): KebabOption => { + return { + hidden: isImporting(vm, actionArgs) || isVMRunning(vm), + label: 'Start Virtual Machine', + callback: () => + startStopVmModal({ + vm, + start: true, + }), + accessReview: asAccessReview(kindObj, vm, 'patch'), + }; +}; + +const menuActionStop = (kindObj: K8sKind, vm: VMKind): KebabOption => { + return { + hidden: !isVMRunning(vm), + label: 'Stop Virtual Machine', + callback: () => + startStopVmModal({ + vm, + start: false, + }), + accessReview: asAccessReview(kindObj, vm, 'patch'), + }; +}; + +export const menuActions = [ + menuActionStart, + menuActionStop, + Kebab.factory.ModifyLabels, + Kebab.factory.ModifyAnnotations, + Kebab.factory.Delete, +]; diff --git a/frontend/packages/kubevirt-plugin/src/components/vms/vm.tsx b/frontend/packages/kubevirt-plugin/src/components/vms/vm.tsx index 8e9f421c615..cd0bb34c94b 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vms/vm.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vms/vm.tsx @@ -11,21 +11,23 @@ import { } from 'kubevirt-web-ui-components'; import { getName, getNamespace, getUid } from '@console/shared'; -import { NamespaceModel } from '@console/internal/models'; +import { NamespaceModel, PodModel } from '@console/internal/models'; import { Table, MultiListPage, TableRow, TableData } from '@console/internal/components/factory'; -import { Kebab, ResourceLink } from '@console/internal/components/utils'; +import { FirehoseResult, Kebab, ResourceLink } from '@console/internal/components/utils'; // import { actions } from '../../module/okdk8s'; import { sortable } from '@patternfly/react-table'; +import { PodKind } from '@console/internal/module/k8s'; import { + VirtualMachineInstanceMigrationModel, VirtualMachineModel, // VirtualMachineInstanceModel, - // VirtualMachineInstanceMigrationModel, } from '../../models'; + import { VMKind } from '../../types'; +import { menuActions } from './menu-actions'; // import { openCreateVmWizard } from '../modals/create-vm-modal'; -// import { menuActions } from './menu-actions'; const tableColumnClasses = [ classNames('col-lg-4', 'col-md-4', 'col-sm-6', 'col-xs-6'), @@ -51,18 +53,19 @@ const VMHeader = () => [ // title: 'Status', // props: { className: tableColumnClasses[2] }, // }, - // { - // title: '', - // props: { className: Kebab.columnClass, props: { className: tableColumnClasses[3] } }, - // }, + { + title: '', + props: { className: Kebab.columnClass, props: { className: tableColumnClasses[3] } }, + }, ]; -const VMRow: React.FC = ({ obj: vm, index, key, style }) => { +const VMRow: React.FC = ({ obj: vm, customData, index, key, style }) => { const name = getName(vm); const namespace = getNamespace(vm); + const uid = getUid(vm); return ( - + @@ -70,20 +73,32 @@ const VMRow: React.FC = ({ obj: vm, index, key, style }) => { {/* TODO(mlibra): migrate VM status in a follow-up */} - {/* TODO(mlibra): migrate actions in a follow-up */} + + action(VirtualMachineModel, vm, customData))} + key={`kebab-for-${uid}`} + id={`kebab-for-${uid}`} + /> + ); }; -const VMList: React.FC & VMListProps> = (props) => ( - -); +const VMList: React.FC & VMListProps> = (props) => { + const { resources } = props; + return ( +
+ ); +}; VMList.displayName = 'VMList'; @@ -122,6 +137,12 @@ const getCreateProps = (namespace: string) => ({ export const VirtualMachinesPage: React.FC = (props) => { const { namespace } = props; + const resources = [ + getResource(VirtualMachineModel, { namespace, prop: 'vms' }), + getResource(PodModel, { namespace, prop: 'pods' }), + getResource(VirtualMachineInstanceMigrationModel, { namespace, prop: 'migrations' }), + ]; + const flatten = ({ vms: { data: vmsData, loaded, loadError } }) => loaded && !loadError ? vmsData : []; @@ -133,7 +154,7 @@ export const VirtualMachinesPage: React.FC = (props) = rowFilters={filters} ListComponent={VMList} createProps={getCreateProps(props.namespace)} - resources={[getResource(VirtualMachineModel, { namespace, prop: 'vms' })]} + resources={resources} flatten={flatten} /> ); @@ -144,10 +165,18 @@ type VMRowProps = { index: number; key: string; style: object; + customData: { + pods: PodKind[]; + migrations: any[]; + }; }; type VMListProps = { data: VMKind[]; + resources: { + pods: FirehoseResult; + migrations: FirehoseResult; + }; }; type VirtualMachinesPageProps = { diff --git a/frontend/packages/kubevirt-plugin/src/selectors/vm/selectors.ts b/frontend/packages/kubevirt-plugin/src/selectors/vm/selectors.ts index fd42573c626..c307b55b155 100644 --- a/frontend/packages/kubevirt-plugin/src/selectors/vm/selectors.ts +++ b/frontend/packages/kubevirt-plugin/src/selectors/vm/selectors.ts @@ -7,6 +7,15 @@ export const getDisks = (volume) => _.get(volume, 'spec.template.spec.domain.dev export const getVolumes = (vm: VMKind) => _.get(vm, 'spec.template.spec.volumes', []); export const getDataVolumeTemplates = (vm: VMKind) => _.get(vm, 'spec.dataVolumeTemplates', []); +export const isVMRunning = (value: VMKind) => + _.get(value, 'spec.running', false) as VMKind['spec']['running']; + +export const isVMReady = (value: VMKind) => + _.get(value, 'status.ready', false) as VMKind['status']['ready']; + +export const isVMCreated = (value: VMKind) => + _.get(value, 'status.created', false) as VMKind['status']['created']; + export const getVmPreferableDiskBus = (vm: VMKind) => getDisks(vm) .map((disk) => getDiskBus(disk))