Compare commits

..
Author SHA1 Message Date
Andy Lee 91232beffc 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>
2026-05-22 11:56:44 +08:00
Andy Lee 09e8946cc3 fix: missing VMIM in host detail VM tab (#876)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-05-18 14:55:02 +08:00
Volker Theile e70c684382 feat: Prevent VM start after restore from backup or snapshot (#865)
* feat: Prevent VM start after restore from backup or snapshot

Related to: https://github.com/harvester/harvester/issues/10422
Related to: https://github.com/harvester/harvester/pull/10480

Signed-off-by: Volker Theile <vtheile@suse.com>

* feat: Prevent VM start after restore from backup or snapshot

Related to: https://github.com/harvester/harvester/issues/10422
Related to: https://github.com/harvester/harvester/pull/10480

Signed-off-by: Volker Theile <vtheile@suse.com>

---------

Signed-off-by: Volker Theile <vtheile@suse.com>
2026-05-18 08:10:36 +02:00
Andy Lee 1446aac168 refactor: enable add volume button regardless of rows length (#869)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-05-18 12:34:32 +08:00
renovate[bot] 8e65274b0c deps: update dependency @types/node to v25.6.2 (#870)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-18 12:04:13 +08:00
renovate[bot] e5432210b9 deps: update dependency semver to v7.8.0 (#871)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-18 12:00:49 +08:00
Andy Lee af52df0ba0 fix: add alternative action for VM restart and soft reboot (#868)
* fix: add alternative action for VM restart and soft reboot

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

* refactor: fix alignment

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-05-15 12:03:20 +08:00
Andy Lee e5a1929ac5 refactor: remove unused pluralization for persistent volume claim in localization file (#867)
Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-05-15 12:02:51 +08:00
Andy Lee 9de065a5c9 feat: allow migrate mutliple VMs functionality and localization (#863)
* feat: enhance VM migration functionality and localization

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

* refactor: add allVmsOnTargetNode method and update migration titles in localization

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

* feat: enhance migration error handling and update localization for migration dialog

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

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-05-13 15:59:13 +08:00
renovate[bot] cd933bdbf8 deps: update dependency eslint-plugin-promise to v7.3.0 (#858)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-12 16:19:04 +08:00
Andy Lee 5fe642a42d feat: add tooltip for route connectivity in network attachment definition (#864) 2026-05-12 16:14:11 +08:00
renovate[bot] 18c66083ab deps: update dependency yaml to v2.8.4 (#857)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-12 15:59:13 +08:00
23 changed files with 276 additions and 161 deletions
+2 -2
View File
@@ -21,7 +21,7 @@
"yaml": "^2.5.1" "yaml": "^2.5.1"
}, },
"resolutions": { "resolutions": {
"@types/node": "25.6.0", "@types/node": "25.6.2",
"cronstrue": "2.59.0", "cronstrue": "2.59.0",
"d3-color": "3.1.0", "d3-color": "3.1.0",
"ejs": "3.1.10", "ejs": "3.1.10",
@@ -35,7 +35,7 @@
"nth-check": "2.1.1", "nth-check": "2.1.1",
"qs": "6.15.1", "qs": "6.15.1",
"roarr": "7.21.4", "roarr": "7.21.4",
"semver": "7.7.4", "semver": "7.8.0",
"@vue/cli-service/html-webpack-plugin": "^5.0.0" "@vue/cli-service/html-webpack-plugin": "^5.0.0"
}, },
"scripts": { "scripts": {
@@ -237,6 +237,7 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimport', labelKey: 'harvester.addons.vmImport.labels.vmimport',
group: 'vmimport', group: 'vmimport',
namespaced: true, namespaced: true,
ifHaveType: HCI.VMIMPORT,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT } params: { resource: HCI.VMIMPORT }
@@ -266,6 +267,7 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceVMWare', labelKey: 'harvester.addons.vmImport.labels.vmimportSourceVMWare',
group: 'vmimport', group: 'vmimport',
namespaced: true, namespaced: true,
ifHaveType: HCI.VMIMPORT_SOURCE_V,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_V } params: { resource: HCI.VMIMPORT_SOURCE_V }
@@ -295,6 +297,7 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOpenStack', labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOpenStack',
group: 'vmimport', group: 'vmimport',
namespaced: true, namespaced: true,
ifHaveType: HCI.VMIMPORT_SOURCE_O,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_O } params: { resource: HCI.VMIMPORT_SOURCE_O }
@@ -323,6 +326,7 @@ export function init($plugin, store) {
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOVA', labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOVA',
group: 'vmimport', group: 'vmimport',
namespaced: true, namespaced: true,
ifHaveType: HCI.VMIMPORT_SOURCE_OVA,
route: { route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`, name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.VMIMPORT_SOURCE_OVA } params: { resource: HCI.VMIMPORT_SOURCE_OVA }
@@ -484,6 +488,7 @@ export function init($plugin, store) {
}); });
virtualType({ virtualType({
ifHaveType: LOGGING.CLUSTER_FLOW,
labelKey: 'harvester.logging.clusterFlow.label', labelKey: 'harvester.logging.clusterFlow.label',
name: HCI.CLUSTER_FLOW, name: HCI.CLUSTER_FLOW,
namespaced: true, namespaced: true,
@@ -507,6 +512,7 @@ export function init($plugin, store) {
}); });
virtualType({ virtualType({
ifHaveType: LOGGING.CLUSTER_OUTPUT,
labelKey: 'harvester.logging.clusterOutput.label', labelKey: 'harvester.logging.clusterOutput.label',
name: HCI.CLUSTER_OUTPUT, name: HCI.CLUSTER_OUTPUT,
namespaced: true, namespaced: true,
@@ -530,6 +536,7 @@ export function init($plugin, store) {
}); });
virtualType({ virtualType({
ifHaveType: LOGGING.FLOW,
labelKey: 'harvester.logging.flow.label', labelKey: 'harvester.logging.flow.label',
name: HCI.FLOW, name: HCI.FLOW,
namespaced: true, namespaced: true,
@@ -553,6 +560,7 @@ export function init($plugin, store) {
}); });
virtualType({ virtualType({
ifHaveType: LOGGING.OUTPUT,
labelKey: 'harvester.logging.output.label', labelKey: 'harvester.logging.output.label',
name: HCI.OUTPUT, name: HCI.OUTPUT,
namespaced: true, namespaced: true,
@@ -27,15 +27,11 @@ export default {
await allHash({ await allHash({
vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }), vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }),
vmis: this.$store.dispatch('harvester/findAll', { type: HCI.VMI }), vmis: this.$store.dispatch('harvester/findAll', { type: HCI.VMI }),
allClusterNetwork: this.$store.dispatch('harvester/findAll', { type: HCI.CLUSTER_NETWORK }), vmims: this.$store.dispatch('harvester/findAll', { type: HCI.VMIM }),
}); });
}, },
computed: { computed: {
allClusterNetwork() {
return this.$store.getters['harvester/all'](HCI.CLUSTER_NETWORK);
},
rows() { rows() {
const vms = this.$store.getters['harvester/all'](HCI.VM); const vms = this.$store.getters['harvester/all'](HCI.VM);
@@ -108,7 +104,6 @@ export default {
<HarvesterVmState <HarvesterVmState
class="vmstate" class="vmstate"
:row="scope.row" :row="scope.row"
:all-cluster-network="allClusterNetwork"
/> />
</div> </div>
</template> </template>
@@ -1,15 +1,12 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { NODE } from '@shell/config/types'; import { NODE } from '@shell/config/types';
import { exceptionToErrorsArray } from '@shell/utils/error'; import { exceptionToErrorsArray } from '@shell/utils/error';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { Card } from '@components/Card'; import { Card } from '@components/Card';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton'; import AsyncButton from '@shell/components/AsyncButton';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import { HCI } from '../types';
export default { export default {
emits: ['close'], emits: ['close'],
@@ -62,28 +59,46 @@ export default {
return this.resources[0]; return this.resources[0];
}, },
vmi() { anyCpuPinning() {
const inStore = this.$store.getters['currentProduct'].inStore; return this.resources.some((r) => r.isCpuPinning);
const vmiResources = this.$store.getters[`${ inStore }/all`](HCI.VMI); },
const resource = vmiResources.find((VMI) => VMI.id === this.actionResource?.id) || null;
return resource; vmsByNode() {
const groups = {};
for (const r of this.resources) {
const node = r.nodeName || '';
const name = r.nameDisplay || r.name || r.id;
if (!groups[node]) {
groups[node] = [];
}
groups[node].push(name);
}
return Object.entries(groups).map(([node, vms]) => ({ node, vms })).sort((a, b) => a.node.localeCompare(b.node));
}, },
cpuPinningAlertMessage() { cpuPinningAlertMessage() {
return this.t('harvester.virtualMachine.cpuPinning.migrationMessage'); return this.t('harvester.virtualMachine.cpuPinning.migrationMessage');
}, },
allVmsOnTargetNode() {
if (!this.nodeName) {
return false;
}
return this.resources.every((r) => r.nodeName === this.nodeName);
},
nodeNameList() { nodeNameList() {
const nodes = this.$store.getters['harvester/all'](NODE); const nodes = this.$store.getters['harvester/all'](NODE);
return nodes.filter((n) => { return nodes.filter((n) => {
const isNotSelfNode = !!this.availableNodes.includes(n.id);
const isNotWitnessNode = n.isEtcd !== 'true'; // do not allow to migrate to self node and witness node const isNotWitnessNode = n.isEtcd !== 'true'; // do not allow to migrate to self node and witness node
const isCpuPinning = this.actionResource?.isCpuPinning; const matchingCpuManagerConfig = !this.anyCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
const matchingCpuManagerConfig = !isCpuPinning || n.isCPUManagerEnabled; // If cpu-pinning is enabled, filter-out non-enabled CPU manager nodes.
return isNotSelfNode && isNotWitnessNode && matchingCpuManagerConfig; return isNotWitnessNode && matchingCpuManagerConfig;
}).map((n) => { }).map((n) => {
let label = n?.metadata?.name; let label = n?.metadata?.name;
const value = n?.metadata?.name; const value = n?.metadata?.name;
@@ -126,7 +141,29 @@ export default {
} }
try { try {
await this.actionResource.doAction('migrate', { nodeName: this.nodeName }, {}, false); // Filter out VMs already running on the selected node
const toMigrate = this.resources.filter((r) => r.nodeName !== this.nodeName);
// await Promise.allSettled(toMigrate.map((r) => r.doAction('migrate', { nodeName: this.nodeName }, {}, false)));
// We want to show all migration errors if there are multiple VMs, so we use allSettled here and handle the results accordingly.
const results = await Promise.allSettled(toMigrate.map((r) => r.doAction('migrate', { nodeName: this.nodeName }, {}, false)));
const failedMigrations = results
.map((result, index) => ({ resource: toMigrate[index], result }))
.filter(({ result }) => result.status === 'rejected');
if (failedMigrations.length) {
this['errors'] = failedMigrations.flatMap(({ resource, result }) => {
const vmName = resource?.nameDisplay || resource?.name || resource?.metadata?.name || this.$store.getters['i18n/t']('generic.unknown');
const error = result.reason?.data || result.reason;
const messages = exceptionToErrorsArray(error);
return messages.map((message) => `${ vmName }: ${ message }`);
});
buttonDone(false);
return;
}
buttonDone(true); buttonDone(true);
this.close(); this.close();
@@ -146,17 +183,35 @@ export default {
<template> <template>
<Card :show-highlight-border="false"> <Card :show-highlight-border="false">
<template #title> <template #title>
{{ t('harvester.modal.migration.title') }} {{ t('harvester.modal.migration.vmMigrationTitle', { count: resources.length }) }}
</template> </template>
<template #body> <template #body>
<Banner <Banner
v-if="actionResource?.isCpuPinning" v-if="anyCpuPinning"
color="warning" color="warning"
:label="cpuPinningAlertMessage" :label="cpuPinningAlertMessage"
/> />
<p>
{{ t('harvester.modal.migration.selectedVMs') }}
</p>
<ul class="vm-list">
<li
v-for="group in vmsByNode"
:key="group.node"
>
{{ group.node || t('harvester.modal.migration.unknownNode') }}: {{ group.vms.join(', ') }}
<span
v-if="nodeName && group.node === nodeName"
class="already-on-target"
>
({{ t('harvester.modal.migration.alreadyOnTarget') }})
</span>
</li>
</ul>
<LabeledSelect <LabeledSelect
v-model:value="nodeName" v-model:value="nodeName"
class="mt-15"
:label="t('harvester.modal.migration.fields.nodeName.label')" :label="t('harvester.modal.migration.fields.nodeName.label')"
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')" :placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
:options="nodeNameList" :options="nodeNameList"
@@ -183,7 +238,7 @@ export default {
<AsyncButton <AsyncButton
mode="apply" mode="apply"
:disabled="!nodeName" :disabled="!nodeName || allVmsOnTargetNode"
@click="apply" @click="apply"
/> />
</div> </div>
@@ -201,4 +256,16 @@ export default {
justify-content: flex-end; justify-content: flex-end;
width: 100%; width: 100%;
} }
.already-on-target {
color: var(--warning);
font-style: italic;
}
.vm-list {
list-style: disc;
padding-left: 1.5em;
margin-bottom: 10px;
margin-top: 10px;
}
</style> </style>
@@ -254,15 +254,24 @@ export default {
v-if="restoreNewVm" v-if="restoreNewVm"
v-model:value="restoreResource.spec.keepMacAddress" v-model:value="restoreResource.spec.keepMacAddress"
type="checkbox" type="checkbox"
class="check mb-20"
:label="t('harvester.backup.restore.keepMacAddress')" :label="t('harvester.backup.restore.keepMacAddress')"
/> />
<LabeledSelect <LabeledSelect
v-if="!restoreNewVm" v-if="!restoreNewVm"
v-model:value="deletionPolicy" v-model:value="deletionPolicy"
class="mb-20"
:label="t('harvester.backup.restore.deletePreviousVolumes')" :label="t('harvester.backup.restore.deletePreviousVolumes')"
:options="deletionPolicyOption" :options="deletionPolicyOption"
/> />
<Checkbox
v-model:value="restoreResource.spec.haltAfterRestore"
type="checkbox"
class="check mb-20"
:label="t('harvester.backup.restore.haltAfterRestore')"
/>
</div> </div>
<Footer <Footer
@@ -2,6 +2,7 @@
import Footer from '@shell/components/form/Footer'; import Footer from '@shell/components/form/Footer';
import { RadioGroup } from '@components/Form/Radio'; import { RadioGroup } from '@components/Form/Radio';
import { LabeledInput } from '@components/Form/LabeledInput'; import { LabeledInput } from '@components/Form/LabeledInput';
import Checkbox from '@components/Form/Checkbox/Checkbox';
import LabeledSelect from '@shell/components/form/LabeledSelect'; import LabeledSelect from '@shell/components/form/LabeledSelect';
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise'; import { allHash } from '@shell/utils/promise';
@@ -32,6 +33,7 @@ const createObject = {
export default { export default {
name: 'CreateRestore', name: 'CreateRestore',
components: { components: {
Checkbox,
Footer, Footer,
RadioGroup, RadioGroup,
LabeledInput, LabeledInput,
@@ -249,9 +251,17 @@ export default {
<LabeledSelect <LabeledSelect
v-if="!restoreNewVm" v-if="!restoreNewVm"
v-model:value="deletionPolicy" v-model:value="deletionPolicy"
class="mb-20"
:label="t('harvester.backup.restore.deletePreviousVolumes')" :label="t('harvester.backup.restore.deletePreviousVolumes')"
:options="deletionPolicyOption" :options="deletionPolicyOption"
/> />
<Checkbox
v-model:value="restoreResource.spec.haltAfterRestore"
type="checkbox"
class="check mb-20"
:label="t('harvester.backup.restore.haltAfterRestore')"
/>
</div> </div>
<Footer <Footer
@@ -152,6 +152,10 @@ export default {
return !rows.find((device) => !device.passthroughClaim); return !rows.find((device) => !device.passthroughClaim);
}, },
canManageGroup(rows = []) {
return rows.length > 0 && rows.every((row) => row.canUpdate === true);
},
changeRows(filterRows, parentSriov) { changeRows(filterRows, parentSriov) {
this['filterRows'] = filterRows; this['filterRows'] = filterRows;
this['parentSriov'] = parentSriov; this['parentSriov'] = parentSriov;
@@ -184,6 +188,10 @@ export default {
:ref="group.key" :ref="group.key"
v-trim-whitespace v-trim-whitespace
class="group-tab" class="group-tab"
>
<div
v-if="canManageGroup(group.rows)"
class="group-actions"
> >
<button <button
v-if="groupIsAllEnabled(group.rows)" v-if="groupIsAllEnabled(group.rows)"
@@ -201,6 +209,7 @@ export default {
> >
{{ t('harvester.pci.enableGroup') }} {{ t('harvester.pci.enableGroup') }}
</button> </button>
</div>
<span v-clean-html="group.key" /> <span v-clean-html="group.key" />
</div> </div>
</template> </template>
@@ -232,3 +241,9 @@ export default {
</template> </template>
</ResourceTable> </ResourceTable>
</template> </template>
<style lang="scss" scoped>
.group-actions {
display: inline;
}
</style>
@@ -113,6 +113,11 @@ export default {
} }
}); });
}, },
canManageGroup(rows = []) {
return rows.every((row) => row.canUpdate === true);
},
groupIsAllEnabled(rows = []) { groupIsAllEnabled(rows = []) {
return !rows.find((device) => !device.passthroughClaim); return !rows.find((device) => !device.passthroughClaim);
}, },
@@ -152,6 +157,10 @@ export default {
:ref="group.key" :ref="group.key"
v-trim-whitespace v-trim-whitespace
class="group-tab" class="group-tab"
>
<div
v-if="canManageGroup(group.rows)"
class="group-actions"
> >
<button <button
v-if="groupIsAllEnabled(group.rows)" v-if="groupIsAllEnabled(group.rows)"
@@ -169,6 +178,7 @@ export default {
> >
{{ t('harvester.usb.enableGroup') }} {{ t('harvester.usb.enableGroup') }}
</button> </button>
</div>
<span v-clean-html="group.key" /> <span v-clean-html="group.key" />
</div> </div>
</template> </template>
@@ -181,3 +191,9 @@ export default {
</template> </template>
</ResourceTable> </ResourceTable>
</template> </template>
<style lang="scss" scoped>
.group-actions {
display: inline;
}
</style>
@@ -475,7 +475,6 @@ export default {
<button <button
type="button" type="button"
class="btn btn-sm bg-primary mr-15 mb-10" class="btn btn-sm bg-primary mr-15 mb-10"
:disabled="rows.length === 0"
@click="addVolume(SOURCE_TYPE.NEW)" @click="addVolume(SOURCE_TYPE.NEW)"
> >
{{ t('harvester.virtualMachine.volume.addVolume') }} {{ t('harvester.virtualMachine.volume.addVolume') }}
@@ -20,12 +20,7 @@ export default {
computed: { computed: {
vmiResource() { vmiResource() {
const vmiList = this.$store.getters['harvester/all'](HCI.VMI) || []; return this.$store.getters['harvester/byId'](HCI.VMI, this.vmResource?.id) || null;
const vmi = vmiList.find( (VMI) => {
return VMI?.metadata?.ownerReferences?.[0]?.uid === this.vmResource?.metadata?.uid;
});
return vmi;
}, },
migrationState() { migrationState() {
return this.vmiResource?.migrationState?.status || ''; return this.vmiResource?.migrationState?.status || '';
@@ -14,20 +14,6 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
allNodeNetwork: {
type: Array,
default: () => {
return [];
}
},
allClusterNetwork: {
type: Array,
default: () => {
return [];
}
}
}, },
data() { data() {
+8 -6
View File
@@ -144,6 +144,10 @@ harvester:
migration: migration:
failedMessage: Latest migration failed! failedMessage: Latest migration failed!
title: Migration title: Migration
vmMigrationTitle: '{count, plural, one {Migrating # VM} other {Migrating # VMs}}'
selectedVMs: "The following virtual machine(s) will be migrated to the target node"
unknownNode: (unknown node)
alreadyOnTarget: Already on Target
fields: fields:
nodeName: nodeName:
label: Target Node label: Target Node
@@ -248,6 +252,7 @@ harvester:
suspendSchedule: Suspend suspendSchedule: Suspend
restoreExistingVM: Replace Existing restoreExistingVM: Replace Existing
migrate: Migrate migrate: Migrate
vmMigrate: Virtual Machine Migration
cpuAndMemoryHotplug: Edit CPU and Memory cpuAndMemoryHotplug: Edit CPU and Memory
abortMigration: Abort Migration abortMigration: Abort Migration
storageMigration: Storage Migration storageMigration: Storage Migration
@@ -298,6 +303,7 @@ harvester:
phase: Phase phase: Phase
attachedVM: Attached Virtual Machine attachedVM: Attached Virtual Machine
cpuManager: CPU Manager cpuManager: CPU Manager
routeConnectivityTooltip: Connectivity between the VM network and the management network, which the Harvester nodes are connected to.
fingerprint: Fingerprint fingerprint: Fingerprint
value: Value value: Value
actions: Actions actions: Actions
@@ -1112,6 +1118,7 @@ harvester:
replaceExisting: Replace existing replaceExisting: Replace existing
virtualMachineName: Virtual Machine Name virtualMachineName: Virtual Machine Name
keepMacAddress: Keep MAC Address keepMacAddress: Keep MAC Address
haltAfterRestore: Keep powered off after restore
matchTarget: The current backup target does not match the existing one. matchTarget: The current backup target does not match the existing one.
progress: progress:
details: Volume details details: Volume details
@@ -1516,6 +1523,7 @@ harvester:
vmSnapshot: vmSnapshot:
label: Virtual Machine Snapshots label: Virtual Machine Snapshots
createText: Restore Snapshot createText: Restore Snapshot
title: Restore Virtual Machine
snapshot: Snapshot snapshot: Snapshot
storage: storage:
@@ -2174,12 +2182,6 @@ typeLabel:
other { PCI Devices } other { PCI Devices }
} }
persistentvolumeclaim: |-
{count, plural,
one { Volume }
other { Volumes }
}
network.harvesterhci.io.clusternetwork: |- network.harvesterhci.io.clusternetwork: |-
{count, plural, {count, plural,
one { Cluster Network } one { Cluster Network }
@@ -112,6 +112,7 @@ export default {
value: 'connectivity', value: 'connectivity',
labelKey: 'tableHeaders.routeConnectivity', labelKey: 'tableHeaders.routeConnectivity',
formatter: 'NetworkRouteConnectivity', formatter: 'NetworkRouteConnectivity',
tooltip: 'harvester.tableHeaders.routeConnectivityTooltip',
formatterOpts: { arbitrary: true }, formatterOpts: { arbitrary: true },
width: 130, width: 130,
}, },
@@ -98,19 +98,9 @@ export default {
this.hasNode = true; this.hasNode = true;
} }
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.NODE_NETWORK)) {
_hash.nodeNetworks = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.NODE_NETWORK });
}
if (this.$store.getters[`${ inStore }/schemaFor`](HCI.CLUSTER_NETWORK)) {
_hash.clusterNetworks = this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK });
}
const hash = await allHash(_hash); const hash = await allHash(_hash);
this.allVMs = hash.vms; this.allVMs = hash.vms;
this.allNodeNetworks = hash.nodeNetworks || [];
this.allClusterNetworks = hash.clusterNetworks || [];
}, },
data() { data() {
@@ -118,8 +108,6 @@ export default {
hasNode: false, hasNode: false,
allVMs: [], allVMs: [],
allVMIs: [], allVMIs: [],
allNodeNetworks: [],
allClusterNetworks: [],
restartNotificationDisplayed: false, restartNotificationDisplayed: false,
HCI HCI
}; };
@@ -246,8 +234,6 @@ export default {
<HarvesterVmState <HarvesterVmState
class="vmstate" class="vmstate"
:row="scope.row" :row="scope.row"
:all-node-network="allNodeNetworks"
:all-cluster-network="allClusterNetworks"
/> />
</div> </div>
</template> </template>
@@ -9,6 +9,7 @@ import HarvesterResource from './harvester';
export default class MIGCONFIGURATION extends HarvesterResource { export default class MIGCONFIGURATION extends HarvesterResource {
get _availableActions() { get _availableActions() {
let out = super._availableActions; let out = super._availableActions;
const canUpdate = !!this.linkFor('update');
out = out.map((action) => { out = out.map((action) => {
if (action.action === 'showConfiguration') { if (action.action === 'showConfiguration') {
@@ -26,13 +27,13 @@ export default class MIGCONFIGURATION extends HarvesterResource {
out.push( out.push(
{ {
action: 'enableConfig', action: 'enableConfig',
enabled: !this.isEnabled, enabled: !this.isEnabled && canUpdate,
icon: 'icon icon-fw icon-dot', icon: 'icon icon-fw icon-dot',
label: 'Enable', label: 'Enable',
}, },
{ {
action: 'disableConfig', action: 'disableConfig',
enabled: this.isEnabled, enabled: this.isEnabled && canUpdate,
icon: 'icon icon-fw icon-dot-open', icon: 'icon icon-fw icon-dot-open',
label: 'Disable', label: 'Disable',
}, },
@@ -30,12 +30,11 @@ const STATUS_DISPLAY = {
export default class PCIDevice extends SteveModel { export default class PCIDevice extends SteveModel {
get _availableActions() { get _availableActions() {
const out = super._availableActions; const out = super._availableActions;
const canUpdate = !!this.linkFor('update');
out.push( out.push(
{ {
action: 'enablePassthroughBulk', action: 'enablePassthroughBulk',
enabled: !this.isEnabling && !this.isvGPUDevice && canUpdate, enabled: !this.isEnabling && !this.isvGPUDevice && this.canUpdate,
icon: 'icon icon-fw icon-dot', icon: 'icon icon-fw icon-dot',
label: 'Enable Passthrough', label: 'Enable Passthrough',
bulkable: true, bulkable: true,
@@ -44,7 +43,7 @@ export default class PCIDevice extends SteveModel {
}, },
{ {
action: 'disablePassthrough', 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', icon: 'icon icon-fw icon-dot-open',
label: 'Disable Passthrough', label: 'Disable Passthrough',
bulkable: true, bulkable: true,
@@ -55,6 +54,10 @@ export default class PCIDevice extends SteveModel {
return out; return out;
} }
get canUpdate() {
return !!this.linkFor('update');
}
get isvGPUDevice() { get isvGPUDevice() {
if (!this.vGPUAsPCIDeviceFeatureEnabled) { if (!this.vGPUAsPCIDeviceFeatureEnabled) {
return false; return false;
@@ -16,13 +16,13 @@ export default class SRIOVDevice extends SteveModel {
out.push( out.push(
{ {
action: 'enableDevice', action: 'enableDevice',
enabled: !this.isEnabled, enabled: !this.isEnabled && this.canUpdate,
icon: 'icon icon-fw icon-dot', icon: 'icon icon-fw icon-dot',
label: 'Enable', label: 'Enable',
}, },
{ {
action: 'disableDevice', action: 'disableDevice',
enabled: this.isEnabled, enabled: this.isEnabled && this.canUpdate,
icon: 'icon icon-fw icon-dot-open', icon: 'icon icon-fw icon-dot-open',
label: 'Disable', label: 'Disable',
}, },
@@ -31,6 +31,10 @@ export default class SRIOVDevice extends SteveModel {
return out; return out;
} }
get canUpdate() {
return !!this.linkFor('update');
}
get canYaml() { get canYaml() {
return false; return false;
} }
@@ -33,7 +33,7 @@ export default class USBDevice extends SteveModel {
out.push( out.push(
{ {
action: 'enablePassthroughBulk', action: 'enablePassthroughBulk',
enabled: !this.passthroughClaim && !this.status.enabled, enabled: !this.passthroughClaim && !this.status.enabled && this.canUpdate,
icon: 'icon icon-fw icon-dot', icon: 'icon icon-fw icon-dot',
label: 'Enable Passthrough', label: 'Enable Passthrough',
bulkable: true, bulkable: true,
@@ -42,7 +42,7 @@ export default class USBDevice extends SteveModel {
}, },
{ {
action: 'disablePassthrough', action: 'disablePassthrough',
enabled: this.status.enabled, enabled: this.status.enabled && this.canUpdate,
icon: 'icon icon-fw icon-dot-open', icon: 'icon icon-fw icon-dot-open',
label: 'Disable Passthrough', label: 'Disable Passthrough',
bulkable: true, bulkable: true,
@@ -53,6 +53,10 @@ export default class USBDevice extends SteveModel {
return out; return out;
} }
get canUpdate() {
return !!this.linkFor('update');
}
get canYaml() { get canYaml() {
return false; return false;
} }
@@ -27,17 +27,18 @@ const STATUS_DISPLAY = {
export default class VGpuDevice extends SteveModel { export default class VGpuDevice extends SteveModel {
get _availableActions() { get _availableActions() {
const out = super._availableActions; const out = super._availableActions;
const canUpdate = !!this.linkFor('update');
out.push( out.push(
{ {
action: 'enableVGpu', action: 'enableVGpu',
enabled: !this.isEnabled, enabled: !this.isEnabled && canUpdate,
icon: 'icon icon-fw icon-dot', icon: 'icon icon-fw icon-dot',
label: 'Enable', label: 'Enable',
}, },
{ {
action: 'disableVGpu', action: 'disableVGpu',
enabled: this.isEnabled, enabled: this.isEnabled && canUpdate,
icon: 'icon icon-fw icon-dot-open', icon: 'icon icon-fw icon-dot-open',
label: 'Disable', label: 'Disable',
bulkable: true, bulkable: true,
@@ -23,17 +23,17 @@ export default class HciVmTemplateVersion extends HarvesterResource {
}); });
const schema = this.$getters['schemaFor'](HCI.VM); const schema = this.$getters['schemaFor'](HCI.VM);
let canCreateVM = true; let canCreateVM = false;
if ( schema && !schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase())) ) { if (schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase())) ) {
canCreateVM = false; canCreateVM = true;
} }
return [ return [
{ {
action: 'launchFromTemplate', action: 'launchFromTemplate',
icon: 'icon icon-spinner', icon: 'icon icon-spinner',
disabled: !canCreateVM || !this.isReady, enabled: canCreateVM && this.isReady,
label: this.t('harvester.action.launchFormTemplate'), label: this.t('harvester.action.launchFormTemplate'),
}, },
{ {
@@ -164,6 +164,7 @@ export default class VirtVm extends HarvesterResource {
}, },
{ {
action: 'restartVM', action: 'restartVM',
altAction: 'altRestartVM',
enabled: !!this.actions?.restart, enabled: !!this.actions?.restart,
icon: 'icon icon-refresh', icon: 'icon icon-refresh',
label: this.t('harvester.action.restart'), label: this.t('harvester.action.restart'),
@@ -172,6 +173,7 @@ export default class VirtVm extends HarvesterResource {
}, },
{ {
action: 'softrebootVM', action: 'softrebootVM',
altAction: 'doSoftReboot',
enabled: !!this.actions?.softreboot, enabled: !!this.actions?.softreboot,
icon: 'icon icon-pipeline', icon: 'icon icon-pipeline',
label: this.t('harvester.action.softreboot') label: this.t('harvester.action.softreboot')
@@ -229,7 +231,9 @@ export default class VirtVm extends HarvesterResource {
action: 'migrateVM', action: 'migrateVM',
enabled: !!this.actions?.migrate, enabled: !!this.actions?.migrate,
icon: 'icon icon-copy', icon: 'icon icon-copy',
label: this.t('harvester.action.migrate') label: this.t('harvester.action.vmMigrate'),
bulkable: true,
bulkAction: 'migrateVM'
}, },
{ {
action: 'abortMigrationVM', action: 'abortMigrationVM',
@@ -369,6 +373,10 @@ export default class VirtVm extends HarvesterResource {
this.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource); this.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
} }
altRestartVM() {
this.doActionGrowl('restart', {});
}
restartVM(resources = this) { restartVM(resources = this) {
this.$dispatch('promptModal', { this.$dispatch('promptModal', {
resources, resources,
+26 -6
View File
@@ -31,22 +31,42 @@ export function registerAddonSideNav(store, productName, {
}, 600); }, 600);
}; };
// Adds or removes the resource IDs from the product visibility whitelist. const hasAccessibleSchema = (t) => {
const setMenuVisibility = (visible) => { try {
if (visible) { return !!store.getters[`${ productName }/schemaFor`]?.(t);
} catch (e) {
return false;
}
};
const showTypes = (visibleTypes) => {
store.commit('type-map/basicType', { store.commit('type-map/basicType', {
product: productName, product: productName,
group: navGroup, 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]; const basicTypes = store.state['type-map'].basicTypes[productName];
if (basicTypes) { if (basicTypes) {
types.forEach((t) => delete basicTypes[t]); 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(); kickSideNav();
}; };
+16 -31
View File
@@ -3308,17 +3308,7 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
"@types/express-serve-static-core@*": "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33":
version "5.1.1"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz#1a77faffee9572d39124933259be2523837d7eaa"
integrity sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==
dependencies:
"@types/node" "*"
"@types/qs" "*"
"@types/range-parser" "*"
"@types/send" "*"
"@types/express-serve-static-core@^4.17.33":
version "4.19.8" version "4.19.8"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz#99b960322a4d576b239a640ab52ef191989b036f" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz#99b960322a4d576b239a640ab52ef191989b036f"
integrity sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA== integrity sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==
@@ -3418,10 +3408,10 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/node@*", "@types/node@25.3.3", "@types/node@25.6.0", "@types/node@^14.14.31": "@types/node@*", "@types/node@25.3.3", "@types/node@25.6.2", "@types/node@^14.14.31":
version "25.6.0" version "25.6.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.6.0.tgz#4e09bad9b469871f2d0f68140198cbd714f4edca" resolved "https://registry.yarnpkg.com/@types/node/-/node-25.6.2.tgz#8c491201373690e4ef2a2ffed0dfb510a5830b92"
integrity sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ== integrity sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==
dependencies: dependencies:
undici-types "~7.19.0" undici-types "~7.19.0"
@@ -4073,16 +4063,11 @@
"@vue/compiler-ssr" "3.5.29" "@vue/compiler-ssr" "3.5.29"
"@vue/shared" "3.5.29" "@vue/shared" "3.5.29"
"@vue/shared@3.5.29": "@vue/shared@3.5.29", "@vue/shared@^3.5.18":
version "3.5.29" version "3.5.29"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.29.tgz#0fe0d7637b05599d56ca58d83a77c637a1774110" resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.29.tgz#0fe0d7637b05599d56ca58d83a77c637a1774110"
integrity sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg== integrity sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==
"@vue/shared@^3.5.18":
version "3.5.32"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.32.tgz#dd8ba0d709bf3f758c324a81c8897bad5e1540cf"
integrity sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==
"@vue/test-utils@2.4.6": "@vue/test-utils@2.4.6":
version "2.4.6" version "2.4.6"
resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.4.6.tgz#7d534e70c4319d2a587d6a3b45a39e9695ade03c" resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.4.6.tgz#7d534e70c4319d2a587d6a3b45a39e9695ade03c"
@@ -7163,9 +7148,9 @@ eslint-plugin-node@^11.1.0:
semver "^6.1.0" semver "^6.1.0"
eslint-plugin-promise@^7.1.0: eslint-plugin-promise@^7.1.0:
version "7.2.1" version "7.3.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-7.2.1.tgz#a0652195700aea40b926dc3c74b38e373377bfb0" resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-7.3.0.tgz#7c61e117f5db8d7a300bd5143c15d1d828e4c124"
integrity sha512-SWKjd+EuvWkYaS+uN2csvj0KoP43YTu7+phKQ5v+xw6+A0gutVX2yqCeCkC3uLCJFiPfR2dD8Es5L7yUsmvEaA== integrity sha512-6uGiOR0INuujr6PEQmeSSP7GbIMJ/ebEXXiEzb/nOj68LknH5Pxzb/AbZivmr6VE6TkTE8rTjRK9zhKpK6HsRA==
dependencies: dependencies:
"@eslint-community/eslint-utils" "^4.4.0" "@eslint-community/eslint-utils" "^4.4.0"
@@ -11981,10 +11966,10 @@ semver-compare@^1.0.0:
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==
"semver@2 || 3 || 4 || 5", semver@7.7.4, semver@^5.5.0, semver@^5.7.1, semver@^6.0.0, semver@^6.1.0, semver@^6.3.0, semver@^6.3.1, semver@^7.0.0, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.6, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.3, semver@~7.0.0: "semver@2 || 3 || 4 || 5", semver@7.8.0, semver@^5.5.0, semver@^5.7.1, semver@^6.0.0, semver@^6.1.0, semver@^6.3.0, semver@^6.3.1, semver@^7.0.0, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.6, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.3, semver@~7.0.0:
version "7.7.4" version "7.8.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.0.tgz#ed0661039fcbcda2ce71f01fa6adbefaa77040df"
integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== integrity sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==
send@0.17.1: send@0.17.1:
version "0.17.1" version "0.17.1"
@@ -13853,9 +13838,9 @@ yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2:
integrity sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA== integrity sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==
yaml@^2.5.1: yaml@^2.5.1:
version "2.8.3" version "2.8.4"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.3.tgz#a0d6bd2efb3dd03c59370223701834e60409bd7d" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.4.tgz#4b5f411dd25f9544914d8673d4da7f29248e5e2e"
integrity sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg== integrity sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==
yargs-parser@^18.1.2: yargs-parser@^18.1.2:
version "18.1.3" version "18.1.3"