diff --git a/frontend/public/kubevirt/_style.scss b/frontend/public/kubevirt/_style.scss index 93957b198d8..d8ab00d3c67 100644 --- a/frontend/public/kubevirt/_style.scss +++ b/frontend/public/kubevirt/_style.scss @@ -4,3 +4,4 @@ @import 'components/vm'; @import 'components/vmconsoles'; +@import 'components/disk'; diff --git a/frontend/public/kubevirt/components/_disk.scss b/frontend/public/kubevirt/components/_disk.scss new file mode 100644 index 00000000000..5ab36c4feb3 --- /dev/null +++ b/frontend/public/kubevirt/components/_disk.scss @@ -0,0 +1,4 @@ +.disk-loading { + margin-left: 15px; + left: 0% +} \ No newline at end of file diff --git a/frontend/public/kubevirt/components/disk.jsx b/frontend/public/kubevirt/components/disk.jsx new file mode 100644 index 00000000000..553d7303222 --- /dev/null +++ b/frontend/public/kubevirt/components/disk.jsx @@ -0,0 +1,106 @@ +import React from 'react'; +import * as _ from 'lodash'; +import { List, ColHead, ListHeader, ResourceRow } from './factory/okdfactory'; +import { PersistentVolumeClaimModel } from '../models'; +import { Loading, Firehose, Cog } from './utils/okdutils'; +import { getResourceKind, getFlattenForKind } from './utils/resources'; +import { DASHES, BUS_VIRTIO, DISK } from './utils/constants'; +import { deleteDeviceModal } from './modals/delete-device-modal'; + +const visibleRowStyle = 'col-lg-3 col-md-3 col-sm-3 col-xs-4'; +const hiddenRowStyle = 'col-lg-3 col-md-3 col-sm-3 hidden-xs'; + +const DiskHeader = props => + Disk Name + Size + Interface + Storage Class +; + +const PvcRow = props => { + if (props.loadError) { + return DASHES; + } else if (props.loaded){ + const pvc = props.flatten(props.resources); + return _.get(pvc, props.pvcPath, DASHES); + } + return ; +}; + +const menuActionDelete = (vm, storage) => ({ + label: 'Delete', + callback: () => deleteDeviceModal({ + type: DISK, + device: storage, + vm: vm + }) +}); + +const getActions = (vm, nic) => { + const actions = [menuActionDelete]; + return actions.map(a => a(vm, nic)); +}; + +export const DiskRow = ({obj: storage}) => { + const pvcName = _.get(storage.volume, 'persistentVolumeClaim.claimName'); + let sizeRow = DASHES; + let storageRow = DASHES; + + if (pvcName) { + const pvcs = getResourceKind(PersistentVolumeClaimModel, pvcName, true, storage.vm.metadata.namespace, false); + sizeRow = + + ; + storageRow = + + ; + } else { + const dataVolumeName = _.get(storage.volume, 'dataVolume.name'); + const dataVolume = _.get(storage.vm, 'spec.dataVolumeTemplates', []).find(dv => _.get(dv,'metadata.name') === dataVolumeName); + if (dataVolume) { + sizeRow = _.get(dataVolume,'spec.pvc.resources.requests.storage', DASHES); + storageRow = _.get(dataVolume,'spec.pvc.storageClassName', DASHES); + } + } + + return +
+ {storage.name} +
+
+ {sizeRow} +
+
+ {_.get(storage, 'disk.bus') || BUS_VIRTIO} +
+
+ {storageRow} +
+
+ +
+
; +}; + +export const Disk = ({obj: vm}) => { + const disks = _.get(vm, 'spec.template.spec.domain.devices.disks',[]); + const volumes = _.get(vm,'spec.template.spec.volumes',[]); + const storages = disks.map(disk => { + const volume = volumes.find(v => v.name === disk.volumeName); + return { + ...disk, + vm, + volume + }; + }); + return
+
+ +
+
; +}; diff --git a/frontend/public/kubevirt/components/modals/delete-device-modal.jsx b/frontend/public/kubevirt/components/modals/delete-device-modal.jsx new file mode 100644 index 00000000000..c0c237b1d5a --- /dev/null +++ b/frontend/public/kubevirt/components/modals/delete-device-modal.jsx @@ -0,0 +1,94 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import * as _ from 'lodash'; +import { Form } from 'patternfly-react'; + +import { PromiseComponent } from '../utils/okdutils'; +import { createModalLauncher, ModalTitle, ModalBody, ModalSubmitFooter } from '../factory/okdfactory'; +import { k8sPatch } from '../../module/okdk8s'; +import { VirtualMachineModel } from '../../models'; +import { NETWORK, DISK } from '../utils/constants'; + +class DeleteDeviceModal extends PromiseComponent { + constructor(props) { + super(props); + this._cancel = this.props.cancel.bind(this); + this._submit = this.submit.bind(this); + } + + submit(event) { + event.preventDefault(); + const { vm, type, device } = this.props; + + const deviceType = type === NETWORK ? {type: 'interfaces', spec: 'networks'} : { type: 'disks', spec: 'volumes'}; + const devices = _.get(vm, `spec.template.spec.domain.devices.${deviceType.type}`, []); + + const deviceIndex = devices.findIndex(d => d.name === device.name); + const specIndex = _.get(vm, `spec.template.spec.${deviceType.spec}`,[]).findIndex(spec => spec.name === device.name || spec.name === device.volumeName); + + const patch = []; + + if (deviceIndex !== -1) { + patch.push({ + op: 'remove', + path: `/spec/template/spec/domain/devices/${deviceType.type}/${deviceIndex}`, + }); + } + + if (specIndex !== -1) { + patch.push({ + op: 'remove', + path: `/spec/template/spec/${deviceType.spec}/${specIndex}`, + }); + } + + // disk may have dataVolumeTemplate defined that should be deleted too + if (type === DISK && _.get(device, 'volume.dataVolume') && _.get(vm, 'spec.dataVolumeTemplates')) { + const dataVolumeIndex = vm.spec.dataVolumeTemplates.findIndex(dataVolume => _.get(dataVolume, 'metadata.name') === device.volume.dataVolume.name); + if (dataVolumeIndex !== -1) { + patch.push({ + op: 'remove', + path: `/spec/dataVolumeTemplates/${dataVolumeIndex}`, + }); + } + } + + // if pod network is deleted, we need to set autoAttachPodInterface to false + if (type === NETWORK && _.get(device, 'network.pod')) { + const op = _.has(vm, 'spec.domain.devices.autoAttachPodInterface') ? 'replace' : 'add'; + patch.push({ + op, + path: '/spec/template/spec/domain/devices/autoAttachPodInterface', + value: false + }); + } + + if (patch.length === 0) { + this.props.close(); + } else { + const promise = k8sPatch(VirtualMachineModel, vm, patch); + this.handlePromise(promise).then(this.props.close); + } + } + + render () { + const {vm, device} = this.props; + return
+ Delete {device.name} from {vm.metadata.name} + + Are you sure you want to delete {device.name} + from {vm.metadata.name} ? + + + ; + } +} + +DeleteDeviceModal.propTypes = { + device: PropTypes.object.isRequired, + vm: PropTypes.object.isRequired, + type: PropTypes.string.isRequired, + close: PropTypes.func.isRequired +}; + +export const deleteDeviceModal = createModalLauncher(DeleteDeviceModal); diff --git a/frontend/public/kubevirt/components/nic.jsx b/frontend/public/kubevirt/components/nic.jsx new file mode 100644 index 00000000000..47b9923be34 --- /dev/null +++ b/frontend/public/kubevirt/components/nic.jsx @@ -0,0 +1,83 @@ +import React from 'react'; +import * as _ from 'lodash'; +import { Cog } from './utils/okdutils'; +import { List, ColHead, ListHeader, ResourceRow } from './factory/okdfactory'; +import { DASHES, BUS_VIRTIO, NETWORK_TYPE_MULTUS, NETWORK_TYPE_POD, NETWORK } from './utils/constants'; +import { deleteDeviceModal } from './modals/delete-device-modal'; + +const getNetworkType = network => { + if (network) { + if (network.hasOwnProperty('pod')){ + return NETWORK_TYPE_POD; + } else if (network.hasOwnProperty('multus')){ + return NETWORK_TYPE_MULTUS; + } + } + return DASHES; +}; + +const menuActionDelete = (vm, nic) => ({ + label: 'Delete', + callback: () => deleteDeviceModal({ + type: NETWORK, + device: nic, + vm: vm + }) +}); + +const getActions = (vm, nic) => { + const actions = [menuActionDelete]; + return actions.map(a => a(vm, nic)); +}; + +const visibleRowStyle = 'col-lg-3 col-md-3 col-sm-3 col-xs-4'; +const hiddenRowStyle = 'col-lg-3 col-md-3 col-sm-3 hidden-xs'; + +const NicHeader = props => + Nic Name + Model + Network + Mac Address +; + +export const NicRow = ({obj: nic}) => +
+ {nic.name} +
+
+ {nic.model || BUS_VIRTIO} +
+
+ {getNetworkType(nic.network)} +
+
+ {nic.macAddress || DASHES} +
+
+ +
+
; + +export const Nic = ({obj: vm}) => { + const interfaces = _.get(vm,'spec.template.spec.domain.devices.interfaces',[]); + const networks = _.get(vm,'spec.template.spec.networks',[]); + const nics = interfaces.map(i => { + const network = networks.find(n => n.name === i.name); + return { + ...i, + network, + vm: vm + }; + }); + return
+
+ +
+
; + +}; diff --git a/frontend/public/kubevirt/components/utils/constants.js b/frontend/public/kubevirt/components/utils/constants.js new file mode 100644 index 00000000000..37700976cd9 --- /dev/null +++ b/frontend/public/kubevirt/components/utils/constants.js @@ -0,0 +1,9 @@ +export const DASHES = '---'; + +export const BUS_VIRTIO = 'VirtIO'; + +export const NETWORK_TYPE_MULTUS = 'Multus'; +export const NETWORK_TYPE_POD = 'Pod Networking'; + +export const NETWORK = 'Network'; +export const DISK = 'Disk'; diff --git a/frontend/public/kubevirt/components/vm.jsx b/frontend/public/kubevirt/components/vm.jsx index f7040330a3d..492bcbfbc22 100644 --- a/frontend/public/kubevirt/components/vm.jsx +++ b/frontend/public/kubevirt/components/vm.jsx @@ -23,8 +23,9 @@ import { getResourceKind, getLabelMatcher, findVMI, findPod, getFlattenForKind, import { CreateVmWizard, TEMPLATE_TYPE_LABEL, TEMPLATE_OS_LABEL } from 'kubevirt-web-ui-components'; import VmConsolesConnected from './vmconsoles'; - -const dashes = '---'; +import { Nic } from './nic'; +import { Disk } from './disk'; +import { DASHES } from './utils/constants'; const VMHeader = props => Name @@ -66,16 +67,16 @@ const StateColumn = props => { return getVmStatus(vm); } } - return dashes; + return DASHES; }; const PhaseColumn = props => { if (props.loaded){ const resources = props.flatten(props.resources); const vmi = props.filter(resources); - return _.get(vmi, 'status.phase', dashes); + return _.get(vmi, 'status.phase', DASHES); } - return dashes; + return DASHES; }; const FirehoseResourceLink = props => { @@ -95,7 +96,7 @@ const FirehoseResourceLink = props => { } } } - return dashes; + return DASHES; }; export const VMRow = ({obj: vm}) => { @@ -104,8 +105,7 @@ export const VMRow = ({obj: vm}) => { const podResources = getResourceKind(PodModel, undefined, true, vm.metadata.namespace, true, getLabelMatcher(vm)); return -
- +
@@ -131,6 +131,9 @@ export const VMRow = ({obj: vm}) => { findPod(data, vm.metadata.name)} />
+
+ +
; }; @@ -217,11 +220,11 @@ class VMResourceConfiguration extends Component {

Configuration

Memory:
-
{configuration.memory || dashes}
+
{configuration.memory || DASHES}
CPU:
-
{configuration.cpu || dashes}
+
{configuration.cpu || DASHES}
Operating System:
-
{configuration.os || dashes}
+
{configuration.os || DASHES}
; @@ -269,11 +272,26 @@ export const VirtualMachinesDetailsPage = props => { name: 'Consoles', component: VmConsolesConnected }; + + const nicsPage = { + href: 'nics', + name: 'Networks', + component: Nic + }; + + const disksPage = { + href: 'disks', + name: 'Disks', + component: Disk + }; + const pages = [ navFactory.details(Details), navFactory.editYaml(), consolePage, navFactory.events(VmiEvents), + nicsPage, + disksPage ]; return (