fix: vGPU and USB enable/disable actions needs to be hidden for read only users (#877)

* fix: vGPU / USB enable/disable actions needs to be hidden for read only users

Signed-off-by: Andy Lee <andy.lee@suse.com>

* style: add scoped styles for group actions in DeviceList component

Signed-off-by: Andy Lee <andy.lee@suse.com>

* style: remove width property from group actions in DeviceList components

Signed-off-by: Andy Lee <andy.lee@suse.com>

* fix: update logging type in init function and improve SideNav visibility handling

Signed-off-by: Andy Lee <andy.lee@suse.com>

* fix: ensure canManageGroup only returns true for non-empty rows with updatable permissions

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
This commit is contained in:
Andy Lee 2026-05-22 11:56:44 +08:00 committed by GitHub
parent 09e8946cc3
commit 91232beffc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 129 additions and 57 deletions

View File

@ -237,6 +237,7 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimport',
group: 'vmimport',
namespaced: true,
ifHaveType: HCI.VMIMPORT,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT }
@ -266,6 +267,7 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceVMWare',
group: 'vmimport',
namespaced: true,
ifHaveType: HCI.VMIMPORT_SOURCE_V,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_V }
@ -295,6 +297,7 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOpenStack',
group: 'vmimport',
namespaced: true,
ifHaveType: HCI.VMIMPORT_SOURCE_O,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_O }
@ -323,6 +326,7 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOVA',
group: 'vmimport',
namespaced: true,
ifHaveType: HCI.VMIMPORT_SOURCE_OVA,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_OVA }
@ -484,6 +488,7 @@ export function init($plugin, store) {
});
virtualType({
ifHaveType: LOGGING.CLUSTER_FLOW,
labelKey: 'harvester.logging.clusterFlow.label',
name: HCI.CLUSTER_FLOW,
namespaced: true,
@ -507,6 +512,7 @@ export function init($plugin, store) {
});
virtualType({
ifHaveType: LOGGING.CLUSTER_OUTPUT,
labelKey: 'harvester.logging.clusterOutput.label',
name: HCI.CLUSTER_OUTPUT,
namespaced: true,
@ -530,6 +536,7 @@ export function init($plugin, store) {
});
virtualType({
ifHaveType: LOGGING.FLOW,
labelKey: 'harvester.logging.flow.label',
name: HCI.FLOW,
namespaced: true,
@ -553,6 +560,7 @@ export function init($plugin, store) {
});
virtualType({
ifHaveType: LOGGING.OUTPUT,
labelKey: 'harvester.logging.output.label',
name: HCI.OUTPUT,
namespaced: true,

View File

@ -152,6 +152,10 @@ export default {
return !rows.find((device) => !device.passthroughClaim);
},
canManageGroup(rows = []) {
return rows.length > 0 && rows.every((row) => row.canUpdate === true);
},
changeRows(filterRows, parentSriov) {
this['filterRows'] = filterRows;
this['parentSriov'] = parentSriov;
@ -184,6 +188,10 @@ export default {
:ref="group.key"
v-trim-whitespace
class="group-tab"
>
<div
v-if="canManageGroup(group.rows)"
class="group-actions"
>
<button
v-if="groupIsAllEnabled(group.rows)"
@ -201,6 +209,7 @@ export default {
>
{{ t('harvester.pci.enableGroup') }}
</button>
</div>
<span v-clean-html="group.key" />
</div>
</template>
@ -232,3 +241,9 @@ export default {
</template>
</ResourceTable>
</template>
<style lang="scss" scoped>
.group-actions {
display: inline;
}
</style>

View File

@ -113,6 +113,11 @@ export default {
}
});
},
canManageGroup(rows = []) {
return rows.every((row) => row.canUpdate === true);
},
groupIsAllEnabled(rows = []) {
return !rows.find((device) => !device.passthroughClaim);
},
@ -152,6 +157,10 @@ export default {
:ref="group.key"
v-trim-whitespace
class="group-tab"
>
<div
v-if="canManageGroup(group.rows)"
class="group-actions"
>
<button
v-if="groupIsAllEnabled(group.rows)"
@ -169,6 +178,7 @@ export default {
>
{{ t('harvester.usb.enableGroup') }}
</button>
</div>
<span v-clean-html="group.key" />
</div>
</template>
@ -181,3 +191,9 @@ export default {
</template>
</ResourceTable>
</template>
<style lang="scss" scoped>
.group-actions {
display: inline;
}
</style>

View File

@ -9,6 +9,7 @@ import HarvesterResource from './harvester';
export default class MIGCONFIGURATION extends HarvesterResource {
get _availableActions() {
let out = super._availableActions;
const canUpdate = !!this.linkFor('update');
out = out.map((action) => {
if (action.action === 'showConfiguration') {
@ -26,13 +27,13 @@ export default class MIGCONFIGURATION extends HarvesterResource {
out.push(
{
action: 'enableConfig',
enabled: !this.isEnabled,
enabled: !this.isEnabled && canUpdate,
icon: 'icon icon-fw icon-dot',
label: 'Enable',
},
{
action: 'disableConfig',
enabled: this.isEnabled,
enabled: this.isEnabled && canUpdate,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable',
},

View File

@ -30,12 +30,11 @@ const STATUS_DISPLAY = {
export default class PCIDevice extends SteveModel {
get _availableActions() {
const out = super._availableActions;
const canUpdate = !!this.linkFor('update');
out.push(
{
action: 'enablePassthroughBulk',
enabled: !this.isEnabling && !this.isvGPUDevice && canUpdate,
enabled: !this.isEnabling && !this.isvGPUDevice && this.canUpdate,
icon: 'icon icon-fw icon-dot',
label: 'Enable Passthrough',
bulkable: true,
@ -44,7 +43,7 @@ export default class PCIDevice extends SteveModel {
},
{
action: 'disablePassthrough',
enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice && canUpdate,
enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice && this.canUpdate,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable Passthrough',
bulkable: true,
@ -55,6 +54,10 @@ export default class PCIDevice extends SteveModel {
return out;
}
get canUpdate() {
return !!this.linkFor('update');
}
get isvGPUDevice() {
if (!this.vGPUAsPCIDeviceFeatureEnabled) {
return false;

View File

@ -16,13 +16,13 @@ export default class SRIOVDevice extends SteveModel {
out.push(
{
action: 'enableDevice',
enabled: !this.isEnabled,
enabled: !this.isEnabled && this.canUpdate,
icon: 'icon icon-fw icon-dot',
label: 'Enable',
},
{
action: 'disableDevice',
enabled: this.isEnabled,
enabled: this.isEnabled && this.canUpdate,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable',
},
@ -31,6 +31,10 @@ export default class SRIOVDevice extends SteveModel {
return out;
}
get canUpdate() {
return !!this.linkFor('update');
}
get canYaml() {
return false;
}

View File

@ -33,7 +33,7 @@ export default class USBDevice extends SteveModel {
out.push(
{
action: 'enablePassthroughBulk',
enabled: !this.passthroughClaim && !this.status.enabled,
enabled: !this.passthroughClaim && !this.status.enabled && this.canUpdate,
icon: 'icon icon-fw icon-dot',
label: 'Enable Passthrough',
bulkable: true,
@ -42,7 +42,7 @@ export default class USBDevice extends SteveModel {
},
{
action: 'disablePassthrough',
enabled: this.status.enabled,
enabled: this.status.enabled && this.canUpdate,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable Passthrough',
bulkable: true,
@ -53,6 +53,10 @@ export default class USBDevice extends SteveModel {
return out;
}
get canUpdate() {
return !!this.linkFor('update');
}
get canYaml() {
return false;
}

View File

@ -27,17 +27,18 @@ const STATUS_DISPLAY = {
export default class VGpuDevice extends SteveModel {
get _availableActions() {
const out = super._availableActions;
const canUpdate = !!this.linkFor('update');
out.push(
{
action: 'enableVGpu',
enabled: !this.isEnabled,
enabled: !this.isEnabled && canUpdate,
icon: 'icon icon-fw icon-dot',
label: 'Enable',
},
{
action: 'disableVGpu',
enabled: this.isEnabled,
enabled: this.isEnabled && canUpdate,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable',
bulkable: true,

View File

@ -23,17 +23,17 @@ export default class HciVmTemplateVersion extends HarvesterResource {
});
const schema = this.$getters['schemaFor'](HCI.VM);
let canCreateVM = true;
let canCreateVM = false;
if ( schema && !schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase())) ) {
canCreateVM = false;
if (schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase())) ) {
canCreateVM = true;
}
return [
{
action: 'launchFromTemplate',
icon: 'icon icon-spinner',
disabled: !canCreateVM || !this.isReady,
enabled: canCreateVM && this.isReady,
label: this.t('harvester.action.launchFormTemplate'),
},
{

View File

@ -31,22 +31,42 @@ export function registerAddonSideNav(store, productName, {
}, 600);
};
// Adds or removes the resource IDs from the product visibility whitelist.
const setMenuVisibility = (visible) => {
if (visible) {
const hasAccessibleSchema = (t) => {
try {
return !!store.getters[`${ productName }/schemaFor`]?.(t);
} catch (e) {
return false;
}
};
const showTypes = (visibleTypes) => {
store.commit('type-map/basicType', {
product: productName,
group: navGroup,
types
types: visibleTypes
});
} else {
// Manually delete the keys from the state object to hide them.
};
const hideTypes = () => {
const basicTypes = store.state['type-map'].basicTypes[productName];
if (basicTypes) {
types.forEach((t) => delete basicTypes[t]);
}
};
// Adds or removes the resource IDs from the product visibility whitelist.
const setMenuVisibility = (visible) => {
const accessibleTypes = visible ? types.filter(hasAccessibleSchema) : [];
// Always clear first to remove any previously-registered types that are
// no longer accessible (e.g. partial permission changes like types=[A,B] where B is dropped).
hideTypes();
if (accessibleTypes.length > 0) {
showTypes(accessibleTypes);
}
kickSideNav();
};