mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2025-12-13 21:21:44 +00:00
Update USB devices
- Add columns and filters - Filter other vm devices in pci and usb devices pages when selecting devices Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
parent
6959cee050
commit
7c04b1417a
@ -771,6 +771,14 @@ export function init($plugin, store) {
|
|||||||
hiddenNamespaceGroupButton: true,
|
hiddenNamespaceGroupButton: true,
|
||||||
listGroups: [
|
listGroups: [
|
||||||
{
|
{
|
||||||
|
icon: 'icon-list-grouped',
|
||||||
|
value: 'description',
|
||||||
|
field: 'groupByDevice',
|
||||||
|
hideColumn: 'description',
|
||||||
|
tooltipKey: 'resourceTable.groupBy.device'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'icon-cluster',
|
||||||
value: 'node',
|
value: 'node',
|
||||||
field: 'groupByNode',
|
field: 'groupByNode',
|
||||||
hideColumn: 'node',
|
hideColumn: 'node',
|
||||||
|
|||||||
@ -55,10 +55,13 @@ export default {
|
|||||||
const selectedDevices = [];
|
const selectedDevices = [];
|
||||||
const oldFormatDevices = [];
|
const oldFormatDevices = [];
|
||||||
|
|
||||||
(this.value?.domain?.devices?.hostDevices || []).forEach(({ name, deviceName }) => {
|
const vmDevices = this.value?.domain?.devices?.hostDevices || [];
|
||||||
|
const otherDevices = this.otherDevices(vmDevices).map(({name}) => name);
|
||||||
|
|
||||||
|
vmDevices.forEach(({ name, deviceName }) => {
|
||||||
const checkName = (deviceName || '').split('/')?.[1];
|
const checkName = (deviceName || '').split('/')?.[1];
|
||||||
|
|
||||||
if (checkName && name.includes(checkName)) {
|
if (checkName && name.includes(checkName) && !otherDevices.includes(name)) {
|
||||||
oldFormatDevices.push(name);
|
oldFormatDevices.push(name);
|
||||||
} else if (this.enabledDevices.find(device => device?.metadata?.name === name)) {
|
} else if (this.enabledDevices.find(device => device?.metadata?.name === name)) {
|
||||||
selectedDevices.push(name);
|
selectedDevices.push(name);
|
||||||
@ -96,7 +99,12 @@ export default {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
set(this.value.domain.devices, 'hostDevices', formatted);
|
const devices = [
|
||||||
|
...this.otherDevices(this.value.domain.devices.hostDevices),
|
||||||
|
...formatted,
|
||||||
|
];
|
||||||
|
|
||||||
|
set(this.value.domain.devices, 'hostDevices', devices);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -187,6 +195,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
otherDevices(vmDevices) {
|
||||||
|
return vmDevices.filter((device) => !this.pciDevices.find((pci) => device.name === pci.name));
|
||||||
|
},
|
||||||
|
|
||||||
nodeNameFromUid(uid) {
|
nodeNameFromUid(uid) {
|
||||||
for (const deviceUid in this.uniqueDevices) {
|
for (const deviceUid in this.uniqueDevices) {
|
||||||
const nodes = this.uniqueDevices[deviceUid].nodes;
|
const nodes = this.uniqueDevices[deviceUid].nodes;
|
||||||
|
|||||||
@ -34,6 +34,36 @@ export default {
|
|||||||
const headers = [
|
const headers = [
|
||||||
{ ...STATE },
|
{ ...STATE },
|
||||||
SIMPLE_NAME,
|
SIMPLE_NAME,
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
labelKey: 'tableHeaders.description',
|
||||||
|
value: 'status.description',
|
||||||
|
sort: ['status.description']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'node',
|
||||||
|
labelKey: 'tableHeaders.node',
|
||||||
|
value: 'status.nodeName',
|
||||||
|
sort: ['status.nodeName']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pciAddress',
|
||||||
|
label: 'Address',
|
||||||
|
value: 'status.pciAddress',
|
||||||
|
sort: ['status.pciAddress']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'vendorID',
|
||||||
|
label: 'Vendor ID',
|
||||||
|
value: 'status.vendorID',
|
||||||
|
sort: ['status.vendorID', 'status.productID']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'productID',
|
||||||
|
label: 'Product ID',
|
||||||
|
value: 'status.productID',
|
||||||
|
sort: ['status.productID', 'status.vendorID']
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!isSingleProduct) {
|
if (!isSingleProduct) {
|
||||||
@ -58,12 +88,32 @@ export default {
|
|||||||
handler(v) {
|
handler(v) {
|
||||||
this.rows = v;
|
this.rows = v;
|
||||||
this.filterRows = this.rows;
|
this.filterRows = this.rows;
|
||||||
|
|
||||||
|
console.log(this.filterRows)
|
||||||
},
|
},
|
||||||
immediate: true,
|
immediate: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
enableGroup(rows = []) {
|
||||||
|
const row = rows[0];
|
||||||
|
|
||||||
|
if (row) {
|
||||||
|
row.enablePassthroughBulk(rows);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
disableGroup(rows = []) {
|
||||||
|
rows.forEach((row) => {
|
||||||
|
if (row.passthroughClaim) {
|
||||||
|
row.disablePassthrough();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
groupIsAllEnabled(rows = []) {
|
||||||
|
return !rows.find(device => !device.passthroughClaim);
|
||||||
|
},
|
||||||
|
|
||||||
changeRows(filterRows) {
|
changeRows(filterRows) {
|
||||||
this.$set(this, 'filterRows', filterRows);
|
this.$set(this, 'filterRows', filterRows);
|
||||||
},
|
},
|
||||||
@ -94,6 +144,17 @@ export default {
|
|||||||
:sort-generation-fn="sortGenerationFn"
|
:sort-generation-fn="sortGenerationFn"
|
||||||
:rows-per-page="10"
|
:rows-per-page="10"
|
||||||
>
|
>
|
||||||
|
<template #group-by="{group}">
|
||||||
|
<div :ref="group.key" v-trim-whitespace class="group-tab">
|
||||||
|
<button v-if="groupIsAllEnabled(group.rows)" type="button" class="btn btn-sm role-secondary mr-5" @click="e=>{disableGroup(group.rows); e.target.blur()}">
|
||||||
|
{{ t('harvester.usb.disableGroup') }}
|
||||||
|
</button>
|
||||||
|
<button v-else type="button" class="btn btn-sm role-secondary mr-5" @click="e=>{enableGroup(group.rows); e.target.blur()}">
|
||||||
|
{{ t('harvester.usb.enableGroup') }}
|
||||||
|
</button>
|
||||||
|
<span v-clean-html="group.key" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template #cell:claimed="{row}">
|
<template #cell:claimed="{row}">
|
||||||
<span v-if="row.isEnabled">{{ row.claimedBy }}</span>
|
<span v-if="row.isEnabled">{{ row.claimedBy }}</span>
|
||||||
<span v-else class="text-muted">—</span>
|
<span v-else class="text-muted">—</span>
|
||||||
|
|||||||
@ -8,6 +8,9 @@ import Banner from '@components/Banner/Banner.vue';
|
|||||||
import CompatibilityMatrix from '../CompatibilityMatrix';
|
import CompatibilityMatrix from '../CompatibilityMatrix';
|
||||||
import DeviceList from './DeviceList';
|
import DeviceList from './DeviceList';
|
||||||
|
|
||||||
|
import remove from 'lodash/remove';
|
||||||
|
import { get, set } from '@shell/utils/object';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'VirtualMachineUSBDevices',
|
name: 'VirtualMachineUSBDevices',
|
||||||
components: {
|
components: {
|
||||||
@ -44,6 +47,10 @@ export default {
|
|||||||
for (const key in res) {
|
for (const key in res) {
|
||||||
this[key] = res[key];
|
this[key] = res[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.selectedDevices = (this.value?.domain?.devices?.hostDevices || [])
|
||||||
|
.map(({ name }) => name)
|
||||||
|
.filter((name) => this.enabledDevices.find(device => device?.metadata?.name === name));
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@ -60,13 +67,34 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
selectedDevices(neu) {
|
||||||
|
const formatted = neu.map((selectedDevice) => {
|
||||||
|
const deviceCRD = this.enabledDevices.find(device => device.metadata.name === selectedDevice);
|
||||||
|
const deviceName = deviceCRD?.status?.resourceName;
|
||||||
|
|
||||||
|
return {
|
||||||
|
deviceName,
|
||||||
|
name: deviceCRD?.metadata.name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const devices = [
|
||||||
|
...this.otherDevices(this.value.domain.devices.hostDevices),
|
||||||
|
...formatted,
|
||||||
|
];
|
||||||
|
|
||||||
|
set(this.value.domain.devices, 'hostDevices', devices);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
deviceOpts() {
|
deviceOpts() {
|
||||||
const filteredOptions = this.enabledDevices.filter((deviceCRD) => {
|
const filteredOptions = this.enabledDevices.filter((deviceCRD) => {
|
||||||
if (this.selectedDevices.length > 0) {
|
if (this.selectedDevices.length > 0) {
|
||||||
const selectedDevice = this.enabledDevices.find(device => device.metadata.name === this.selectedDevices[0]);
|
const selectedDevice = this.enabledDevices.find(device => device.metadata.name === this.selectedDevices[0]);
|
||||||
|
|
||||||
return !this.devicesInUse[deviceCRD?.metadata.name] && deviceCRD.status.nodeName === selectedDevice.status.nodeName;
|
return !this.devicesInUse[deviceCRD?.metadata.name] && deviceCRD.status.nodeName === selectedDevice?.status.nodeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !this.devicesInUse[deviceCRD?.metadata.name];
|
return !this.devicesInUse[deviceCRD?.metadata.name];
|
||||||
@ -76,14 +104,14 @@ export default {
|
|||||||
return {
|
return {
|
||||||
value: deviceCRD?.metadata.name,
|
value: deviceCRD?.metadata.name,
|
||||||
label: deviceCRD?.metadata.name,
|
label: deviceCRD?.metadata.name,
|
||||||
displayLabel: deviceCRD?.status?.resourceName,
|
displayLabel: deviceCRD?.status?.description,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
enabledDevices() {
|
enabledDevices() {
|
||||||
return this.devices.filter((device) => {
|
return this.devices.filter((device) => {
|
||||||
return device.isEnabled;
|
return device.status.enabled;
|
||||||
}) || [];
|
}) || [];
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -108,7 +136,7 @@ export default {
|
|||||||
const out = {};
|
const out = {};
|
||||||
|
|
||||||
this.enabledDevices.forEach((deviceCRD) => {
|
this.enabledDevices.forEach((deviceCRD) => {
|
||||||
const nodeName = deviceCRD.spec?.nodeName;
|
const nodeName = deviceCRD.status?.nodeName;
|
||||||
|
|
||||||
if (!out[nodeName]) {
|
if (!out[nodeName]) {
|
||||||
out[nodeName] = [deviceCRD];
|
out[nodeName] = [deviceCRD];
|
||||||
@ -127,13 +155,19 @@ export default {
|
|||||||
remove(out, (nodeName) => {
|
remove(out, (nodeName) => {
|
||||||
const device = this.enabledDevices.find(deviceCRD => deviceCRD.metadata.name === deviceUid);
|
const device = this.enabledDevices.find(deviceCRD => deviceCRD.metadata.name === deviceUid);
|
||||||
|
|
||||||
return device.spec.nodeName !== nodeName;
|
return device?.status.nodeName !== nodeName;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
otherDevices(vmDevices) {
|
||||||
|
return vmDevices.filter((device) => !this.devices.find((usb) => device.name === usb.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1371,9 +1371,11 @@ harvester:
|
|||||||
label: USB Devices
|
label: USB Devices
|
||||||
noPermission: Please contact system admin to add Harvester addons first
|
noPermission: Please contact system admin to add Harvester addons first
|
||||||
goSetting:
|
goSetting:
|
||||||
prefix: The usb addon is not enabled, click
|
prefix: The pcidevices-controller addon is not enabled, click
|
||||||
middle: here
|
middle: here
|
||||||
suffix: to enable it to manage your USB devices.
|
suffix: to enable it to manage your USB devices.
|
||||||
|
enableGroup: Enable Group
|
||||||
|
disableGroup: Disable Group
|
||||||
available: Available USB Devices
|
available: Available USB Devices
|
||||||
compatibleNodes: Compatible Nodes
|
compatibleNodes: Compatible Nodes
|
||||||
impossibleSelection: 'There are no hosts with all of the selected devices.'
|
impossibleSelection: 'There are no hosts with all of the selected devices.'
|
||||||
|
|||||||
@ -6,12 +6,14 @@ import Banner from '@components/Banner/Banner.vue';
|
|||||||
import Loading from '@shell/components/Loading';
|
import Loading from '@shell/components/Loading';
|
||||||
import MessageLink from '@shell/components/MessageLink';
|
import MessageLink from '@shell/components/MessageLink';
|
||||||
import { ADD_ONS } from '../config/harvester-map';
|
import { ADD_ONS } from '../config/harvester-map';
|
||||||
|
import DeviceList from '../edit/kubevirt.io.virtualmachine/VirtualMachineUSBDevices/DeviceList';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ListUsbDevicePage',
|
name: 'ListUsbDevicePage',
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
Banner,
|
Banner,
|
||||||
|
DeviceList,
|
||||||
Loading,
|
Loading,
|
||||||
MessageLink,
|
MessageLink,
|
||||||
},
|
},
|
||||||
@ -29,7 +31,7 @@ export default {
|
|||||||
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
|
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.hasUSBAddon = hash.addons.find(addon => addon.name === ADD_ONS.USB_DEVICE_CONTROLLER)?.spec?.enabled === true;
|
this.hasPCIAddon = hash.addons.find(addon => addon.name === ADD_ONS.PCI_DEVICE_CONTROLLER)?.spec?.enabled === true;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -37,7 +39,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
hasAddonSchema: false,
|
hasAddonSchema: false,
|
||||||
hasUSBAddon: false,
|
hasPCIAddon: false,
|
||||||
schema: null,
|
schema: null,
|
||||||
toUSBAddon: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.USB_DEVICE_CONTROLLER }?mode=edit`,
|
toUSBAddon: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.USB_DEVICE_CONTROLLER }?mode=edit`,
|
||||||
headers: [
|
headers: [
|
||||||
@ -52,7 +54,7 @@ export default {
|
|||||||
return !!this.schema;
|
return !!this.schema;
|
||||||
},
|
},
|
||||||
|
|
||||||
rows() {
|
devices() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
return this.$store.getters[`${ inStore }/all`](HCI.USB_DEVICE) || [];
|
return this.$store.getters[`${ inStore }/all`](HCI.USB_DEVICE) || [];
|
||||||
@ -72,7 +74,7 @@ export default {
|
|||||||
{{ t('harvester.usb.noPermission') }}
|
{{ t('harvester.usb.noPermission') }}
|
||||||
</Banner>
|
</Banner>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!hasUSBAddon">
|
<div v-else-if="!hasPCIAddon">
|
||||||
<Banner color="warning">
|
<Banner color="warning">
|
||||||
<MessageLink
|
<MessageLink
|
||||||
:to="toUSBAddon"
|
:to="toUSBAddon"
|
||||||
@ -82,5 +84,5 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</Banner>
|
</Banner>
|
||||||
</div>
|
</div>
|
||||||
<VGpuDeviceList v-else-if="hasSchema" :devices="rows" :schema="schema" />
|
<DeviceList v-else-if="hasSchema" :devices="devices" :schema="schema" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export default class USBDevice extends SteveModel {
|
|||||||
out.push(
|
out.push(
|
||||||
{
|
{
|
||||||
action: 'enablePassthroughBulk',
|
action: 'enablePassthroughBulk',
|
||||||
enabled: !this.isEnabling,
|
enabled: !this.passthroughClaim && !this.status.enabled,
|
||||||
icon: 'icon icon-fw icon-dot',
|
icon: 'icon icon-fw icon-dot',
|
||||||
label: 'Enable Passthrough',
|
label: 'Enable Passthrough',
|
||||||
bulkable: true,
|
bulkable: true,
|
||||||
@ -40,7 +40,7 @@ export default class USBDevice extends SteveModel {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'disablePassthrough',
|
action: 'disablePassthrough',
|
||||||
enabled: this.isEnabling && this.claimedByMe,
|
enabled: this.status.enabled,
|
||||||
icon: 'icon icon-fw icon-dot-open',
|
icon: 'icon icon-fw icon-dot-open',
|
||||||
label: 'Disable Passthrough',
|
label: 'Disable Passthrough',
|
||||||
bulkable: true
|
bulkable: true
|
||||||
@ -69,7 +69,7 @@ export default class USBDevice extends SteveModel {
|
|||||||
get passthroughClaim() {
|
get passthroughClaim() {
|
||||||
const passthroughClaims = this.$getters['all'](HCI.USB_CLAIM) || [];
|
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);
|
return !!this.status && passthroughClaims.find(req => req?.status?.nodeName === this.status?.nodeName && req?.status?.pciAddress === this.status?.pciAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is an id for each 'type' of device - there may be multiple instances of device CRs
|
// this is an id for each 'type' of device - there may be multiple instances of device CRs
|
||||||
@ -98,23 +98,12 @@ export default class USBDevice extends SteveModel {
|
|||||||
return this.claimedBy === userName;
|
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
|
// map status.passthroughEnabled to disabled/enabled & overwrite default dash colors
|
||||||
get claimStatusDisplay() {
|
get claimStatusDisplay() {
|
||||||
if (!this.passthroughClaim) {
|
if (!this.passthroughClaim) {
|
||||||
return STATUS_DISPLAY.disabled;
|
return STATUS_DISPLAY.disabled;
|
||||||
}
|
}
|
||||||
if (this.isEnabled) {
|
if (this.status.enabled) {
|
||||||
return STATUS_DISPLAY.enabled;
|
return STATUS_DISPLAY.enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,4 +144,16 @@ export default class USBDevice extends SteveModel {
|
|||||||
}, { root: true });
|
}, { root: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// group device list by node
|
||||||
|
get groupByNode() {
|
||||||
|
const name = this.status?.nodeName || this.$rootGetters['i18n/t']('generic.none');
|
||||||
|
|
||||||
|
return this.$rootGetters['i18n/t']('resourceTable.groupLabel.node', { name: escapeHtml(name) });
|
||||||
|
}
|
||||||
|
|
||||||
|
// group device list by unique device (same vendorid and deviceid)
|
||||||
|
get groupByDevice() {
|
||||||
|
return this.status?.description;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user