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 @@
+
+
+
+
+
+ {{ row.claimedBy }}
+ —
+
+
+
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',