diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineUSBDevices/DeviceList.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineUSBDevices/DeviceList.vue new file mode 100644 index 00000000..921116da --- /dev/null +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineUSBDevices/DeviceList.vue @@ -0,0 +1,102 @@ + + + diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineUSBDevices/index.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineUSBDevices/index.vue index 75a45f6e..2a10c592 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineUSBDevices/index.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineUSBDevices/index.vue @@ -6,12 +6,14 @@ import { STATE, SIMPLE_NAME } from '@shell/config/table-headers'; import LabeledSelect from '@shell/components/form/LabeledSelect'; import Banner from '@components/Banner/Banner.vue'; import CompatibilityMatrix from '../CompatibilityMatrix'; +import DeviceList from './DeviceList'; export default { name: 'VirtualMachineUSBDevices', components: { Banner, CompatibilityMatrix, + DeviceList, LabeledSelect, }, props: { @@ -186,7 +188,7 @@ export default {
- +
diff --git a/pkg/harvester/list/devices.harvesterhci.io.usbdevice.vue b/pkg/harvester/list/devices.harvesterhci.io.usbdevice.vue index 3c1fa3e3..67a9a49e 100644 --- a/pkg/harvester/list/devices.harvesterhci.io.usbdevice.vue +++ b/pkg/harvester/list/devices.harvesterhci.io.usbdevice.vue @@ -5,17 +5,15 @@ import { allHash } from '@shell/utils/promise'; import Banner from '@components/Banner/Banner.vue'; import Loading from '@shell/components/Loading'; import MessageLink from '@shell/components/MessageLink'; -import ResourceTable from '@shell/components/ResourceTable'; import { ADD_ONS } from '../config/harvester-map'; export default { - name: 'ListUSBDevices', + name: 'ListUsbDevicePage', components: { Banner, Loading, MessageLink, - ResourceTable }, async fetch() { @@ -84,14 +82,5 @@ export default { /> - + diff --git a/pkg/harvester/models/devices.harvesterhci.io.usbdevice.js b/pkg/harvester/models/devices.harvesterhci.io.usbdevice.js new file mode 100644 index 00000000..38d18164 --- /dev/null +++ b/pkg/harvester/models/devices.harvesterhci.io.usbdevice.js @@ -0,0 +1,158 @@ +import SteveModel from '@shell/plugins/steve/steve-class'; +import { escapeHtml } from '@shell/utils/string'; +import { HCI } from '../types'; + +const STATUS_DISPLAY = { + enabled: { + displayKey: 'generic.enabled', + color: 'bg-success' + }, + pending: { + displayKey: 'generic.inProgress', + color: 'bg-info' + }, + disabled: { + displayKey: 'generic.disabled', + color: 'bg-warning' + }, + error: { + displayKey: 'generic.disabled', + color: 'bg-warning' + } +}; + +/** + * Class representing USB Device resource. + * @extends SteveModal + */ +export default class USBDevice extends SteveModel { + get _availableActions() { + const out = super._availableActions; + + out.push( + { + action: 'enablePassthroughBulk', + enabled: !this.isEnabling, + icon: 'icon icon-fw icon-dot', + label: 'Enable Passthrough', + bulkable: true, + bulkAction: 'enablePassthroughBulk' + }, + { + action: 'disablePassthrough', + enabled: this.isEnabling && this.claimedByMe, + icon: 'icon icon-fw icon-dot-open', + label: 'Disable Passthrough', + bulkable: true + }, + ); + + return out; + } + + get canYaml() { + return false; + } + + get canDelete() { + return false; + } + + goToDetail() { + return false; + } + + goToEdit() { + return false; + } + + get passthroughClaim() { + const passthroughClaims = this.$getters['all'](HCI.USB_CLAIM) || []; + + return !!this.status && passthroughClaims.find(req => req?.spec?.nodeName === this.status?.nodeName && req?.spec?.address === this.status?.address); + } + + // this is an id for each 'type' of device - there may be multiple instances of device CRs + get uniqueId() { + return `${ this.status?.vendorId }:${ this.status?.deviceId }`; + } + + get claimedBy() { + return this.passthroughClaim?.spec?.userName; + } + + get claimedByMe() { + if (!this.passthroughClaim) { + return false; + } + const isSingleProduct = this.$rootGetters['isSingleProduct']; + let userName = 'admin'; + + // if this is imported Harvester, there may be users other than admin + if (!isSingleProduct) { + const user = this.$rootGetters['auth/v3User']; + + userName = user?.username || user?.id; + } + + return this.claimedBy === userName; + } + + // isEnabled controls visibility in vm create page & ability to delete claim + // isEnabling controls ability to add claim + // there will be a brief period where isEnabling === true && isEnabled === false + get isEnabled() { + return !!this.passthroughClaim?.status?.passthroughEnabled; + } + + get isEnabling() { + return !!this.passthroughClaim; + } + + // map status.passthroughEnabled to disabled/enabled & overwrite default dash colors + get claimStatusDisplay() { + if (!this.passthroughClaim) { + return STATUS_DISPLAY.disabled; + } + if (this.isEnabled) { + return STATUS_DISPLAY.enabled; + } + + return STATUS_DISPLAY.pending; + } + + get stateDisplay() { + const t = this.$rootGetters['i18n/t']; + + return t(this.claimStatusDisplay.displayKey); + } + + get stateBackground() { + return this.claimStatusDisplay.color; + } + + // 'enable' passthrough creates the passthrough claim CRD - + enablePassthroughBulk(resources = this) { + this.$dispatch('promptModal', { + resources, + component: 'EnablePassthrough' + }); + } + + // 'disable' passthrough deletes claim + // backend should return error if device is in use + async disablePassthrough() { + try { + if (!this.claimedByMe) { + throw new Error(this.$rootGetters['i18n/t']('harvester.usb.cantUnclaim', { name: escapeHtml(this.metadata.name) })); + } else { + await this.passthroughClaim.remove(); + } + } catch (err) { + this.$dispatch('growl/fromError', { + title: this.$rootGetters['i18n/t']('harvester.usb.unclaimError', { name: escapeHtml(this.metadata.name) }), + err, + }, { root: true }); + } + } +} diff --git a/pkg/harvester/types.ts b/pkg/harvester/types.ts index 89a85789..27a16ee8 100644 --- a/pkg/harvester/types.ts +++ b/pkg/harvester/types.ts @@ -39,6 +39,7 @@ export const HCI = { VGPU_DEVICE: 'devices.harvesterhci.io.vgpudevice', SR_IOVGPU_DEVICE: 'devices.harvesterhci.io.sriovgpudevice', USB_DEVICE: 'devices.harvesterhci.io.usbdevice', + USB_CLAIM: 'devices.harvesterhci.io.usbdeviceclaim', VLAN_CONFIG: 'network.harvesterhci.io.vlanconfig', VLAN_STATUS: 'network.harvesterhci.io.vlanstatus', ADD_ONS: 'harvesterhci.io.addon',