From e86cb950148306db0741eadcc0f32c7a43479b57 Mon Sep 17 00:00:00 2001 From: "andy.lee" Date: Tue, 6 Aug 2024 21:44:06 +0800 Subject: [PATCH 1/7] Add VM schedule pages - add VM schedule list/edit/details pages - add VM schedule column in vmbackup/vmsnapshot list pages - add action menu for VM - show volume backup error message in backup/snapshot detail volume tab Signed-off-by: andy.lee (cherry picked from commit 8ce0f8e897500496144ffe9006c840d0c57a37b3) --- package.json | 4 +- pkg/harvester/components/FilterVMSchedule.vue | 119 +++++++ pkg/harvester/config/harvester.js | 14 + pkg/harvester/config/table-headers.js | 36 ++ .../BackupList.vue | 129 ++++++++ .../SnapshotList.vue | 97 ++++++ .../index.vue | 125 +++++++ .../index.vue | 19 +- .../dialog/HarvesterScheduleModal.vue | 129 ++++++++ .../edit/harvesterhci.io.schedulevmbackup.vue | 312 ++++++++++++++++++ .../VirtualMachineVolume/index.vue | 8 +- .../VirtualMachineVolume/type/container.vue | 28 +- .../VirtualMachineVolume/type/existing.vue | 27 +- .../VirtualMachineVolume/type/vmImage.vue | 25 +- .../VirtualMachineVolume/type/volume.vue | 22 +- .../formatters/BackupCreatedFrom.vue | 41 +++ pkg/harvester/l10n/en-us.yaml | 54 ++- .../list/harvesterhci.io.schedulevmbackup.vue | 124 +++++++ .../harvesterhci.io.virtualmachinebackup.vue | 76 +++-- .../list/harvesterhci.io.vmsnapshot.vue | 51 ++- pkg/harvester/mixins/harvester-vm/index.js | 16 +- .../harvesterhci.io.schedulevmbackup.js | 97 ++++++ .../harvesterhci.io.virtualmachinebackup.js | 14 +- .../models/kubevirt.io.virtualmachine.js | 18 +- pkg/harvester/types.ts | 1 + pkg/rancher-components/package.json | 4 +- .../Form/LabeledInput/LabeledInput.vue | 9 +- shell/package.json | 4 +- shell/utils/validators/cron-schedule.js | 10 + yarn.lock | 13 +- 30 files changed, 1547 insertions(+), 79 deletions(-) create mode 100644 pkg/harvester/components/FilterVMSchedule.vue create mode 100644 pkg/harvester/detail/harvesterhci.io.schedulevmbackup/BackupList.vue create mode 100644 pkg/harvester/detail/harvesterhci.io.schedulevmbackup/SnapshotList.vue create mode 100644 pkg/harvester/detail/harvesterhci.io.schedulevmbackup/index.vue create mode 100644 pkg/harvester/dialog/HarvesterScheduleModal.vue create mode 100644 pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue create mode 100644 pkg/harvester/formatters/BackupCreatedFrom.vue create mode 100644 pkg/harvester/list/harvesterhci.io.schedulevmbackup.vue create mode 100644 pkg/harvester/models/harvesterhci.io.schedulevmbackup.js diff --git a/package.json b/package.json index 0e1a90d69f4..89baadf1633 100644 --- a/package.json +++ b/package.json @@ -69,8 +69,8 @@ "clipboard-polyfill": "4.0.1", "cookie": "0.5.0", "cookie-universal-nuxt": "2.1.4", - "cron-validator": "1.2.0", - "cronstrue": "1.95.0", + "cron-validator": "1.3.1", + "cronstrue": "2.50.0", "cross-env": "6.0.3", "d3": "7.3.0", "d3-selection": "1.4.1", diff --git a/pkg/harvester/components/FilterVMSchedule.vue b/pkg/harvester/components/FilterVMSchedule.vue new file mode 100644 index 00000000000..cb4eccb2445 --- /dev/null +++ b/pkg/harvester/components/FilterVMSchedule.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/pkg/harvester/config/harvester.js b/pkg/harvester/config/harvester.js index 5fad88065b6..712d57506cf 100644 --- a/pkg/harvester/config/harvester.js +++ b/pkg/harvester/config/harvester.js @@ -421,6 +421,7 @@ export function init($plugin, store) { basicType( [ + HCI.SCHEDULE_VM_BACKUP, HCI.BACKUP, HCI.SNAPSHOT, HCI.VM_SNAPSHOT, @@ -468,6 +469,19 @@ export function init($plugin, store) { exact: false }); + configureType(HCI.SCHEDULE_VM_BACKUP, { showListMasthead: false, showConfigView: false }); + virtualType({ + labelKey: 'harvester.schedule.label', + name: HCI.SCHEDULE_VM_BACKUP, + namespaced: true, + weight: 201, + route: { + name: `${ PRODUCT_NAME }-c-cluster-resource`, + params: { resource: HCI.SCHEDULE_VM_BACKUP } + }, + exact: false + }); + configureType(HCI.BACKUP, { showListMasthead: false, showConfigView: false }); virtualType({ labelKey: 'harvester.backup.label', diff --git a/pkg/harvester/config/table-headers.js b/pkg/harvester/config/table-headers.js index b580f4f0026..26bf2e047d8 100644 --- a/pkg/harvester/config/table-headers.js +++ b/pkg/harvester/config/table-headers.js @@ -40,3 +40,39 @@ export const SNAPSHOT_TARGET_VOLUME = { sort: 'spec.source.persistentVolumeClaimName', formatter: 'SnapshotTargetVolume', }; + +// The column of cron expression volume on VM schedules list page +export const VM_SCHEDULE_CRON = { + name: 'CronExpression', + labelKey: 'harvester.tableHeaders.cronExpression', + value: 'spec.cron', + align: 'center', + sort: 'spec.cron', +}; + +// The column of retain on VM schedules list page +export const VM_SCHEDULE_RETAIN = { + name: 'Retain', + labelKey: 'harvester.tableHeaders.retain', + value: 'spec.retain', + sort: 'spec.retain', + align: 'center', +}; + +// The column of maxFailure on VM schedules list page +export const VM_SCHEDULE_MAX_FAILURE = { + name: 'MaxFailure', + labelKey: 'harvester.tableHeaders.maxFailure', + value: 'spec.maxFailure', + sort: 'spec.maxFailure', + align: 'center', +}; + +// The column of type on VM schedules list page +export const VM_SCHEDULE_TYPE = { + name: 'Type', + labelKey: 'harvester.tableHeaders.scheduleType', + value: 'spec.vmbackup.type', + sort: 'spec.vmbackup.type', + align: 'center', +}; diff --git a/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/BackupList.vue b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/BackupList.vue new file mode 100644 index 00000000000..d8ff2abfc83 --- /dev/null +++ b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/BackupList.vue @@ -0,0 +1,129 @@ + + + diff --git a/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/SnapshotList.vue b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/SnapshotList.vue new file mode 100644 index 00000000000..99d68f9aa38 --- /dev/null +++ b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/SnapshotList.vue @@ -0,0 +1,97 @@ + + + diff --git a/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/index.vue b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/index.vue new file mode 100644 index 00000000000..274bcf5eefd --- /dev/null +++ b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/index.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/pkg/harvester/detail/harvesterhci.io.virtualmachinebackup/index.vue b/pkg/harvester/detail/harvesterhci.io.virtualmachinebackup/index.vue index f97206f2b42..affa26ceaf8 100644 --- a/pkg/harvester/detail/harvesterhci.io.virtualmachinebackup/index.vue +++ b/pkg/harvester/detail/harvesterhci.io.virtualmachinebackup/index.vue @@ -127,29 +127,29 @@ export default {
-
+
-
+
-
+
-
+
- - - +
+ +
-
+
-
+
diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/existing.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/existing.vue index 935697d6546..936c46af598 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/existing.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/existing.vue @@ -1,9 +1,10 @@ + + diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml index b58a2569f20..9d52c152d9b 100644 --- a/pkg/harvester/l10n/en-us.yaml +++ b/pkg/harvester/l10n/en-us.yaml @@ -57,6 +57,12 @@ harvester: tip: Please enter a template name! success: 'Template { templateName } created successfully.' failed: 'Failed generated template!' + schedule: + title: Create Schedule + message: + tip: Please enter a schedule name! + success: 'Schedule { name } created successfully.' + failed: 'Failed create schedule!' cloneVM: title: Clone Virtual Machine name: New Virtual Machine Name @@ -144,8 +150,11 @@ harvester: setDefaultVersion: Set default version addTemplateVersion: Add template version backup: Take Backup + createSchedule: Create VM Schedule restore: Restore restoreNewVM: Restore New + resumeSchedule: Resume + suspendSchedule: Suspend restoreExistingVM: Replace Existing migrate: Migrate abortMigration: Abort Migration @@ -182,6 +191,12 @@ harvester: readyToUse: Ready To Use backupTarget: Backup Target targetVm: Target Virtual Machine + cronExpression: Cron Expression + retain: Retain + scheduleType: Type + maxFailure: Max Failure + sourceVm: Source VM + vmSchedule: VM Schedule hostIp: Host IP vm: ipAddress: IP Address @@ -228,6 +243,7 @@ harvester: promiscuous: Promiscuous ipv4Address: IPv4 address filterLabels: Filter labels + filterSchedule: Filter schedule storageClass: Storage class dockerImage: Docker image pci: @@ -550,6 +566,7 @@ harvester: size: Size edit: Edit bus: Bus + readyToUse: Ready To Use bootOrder: Boot Order volume: Volume dockerImage: Docker Image @@ -808,6 +825,37 @@ harvester: doc: Read the documentation before starting the upgrade process. Ensure that you complete procedures that are relevant to your environment and the version you are upgrading to. tip: Unmet system requirements and incorrectly performed procedures may cause complete upgrade failure and other issues that require manual workarounds. moreNotes: For more details about the release notes, please visit - + + schedule: + label: VM Schedules + createTitle: Create VM Schedule + createButtonText: Create VM Schedule + scheduleType: VM Schedule Type + cron: Cron Schedule + detail: + namespace: Namespace + sourceVM: Source Virtual Machine + tabs: + basic: Basic + backups: Backups + snapshots: Snapshots + message: + noSetting: + suffix: before creating a backup schedule + retain: + label: Retain + count: Count + tooltip: Number of up-to-date VM backups to retain. Maximum to 250, minimum to 2. + maxFailure: + label: Max Failure + count: Count + tooltip: Max number of consecutive failed backups that could be tolerated. If reach this threshold, Harvester controller will suspend the schedule job. This value should less than retain count + virtualMachine: + title: Virtual Machine Name + placeholder: Select a virtual machine + type: + snapshot: Snapshot + backup: Backup backup: label: Virtual Machine Backups @@ -846,7 +894,6 @@ harvester: starting: Backup initiating progress: Backup in progress complete: Backup completed - restore: progress: details: Volume details @@ -1492,6 +1539,11 @@ typeLabel: one { Template } other { Templates } } + harvesterhci.io.schedulevmbackup: |- + {count, plural, + one { VM Schedule } + other { VM Schedules } + } harvesterhci.io.virtualmachinebackup: |- {count, plural, one { Virtual Machines Backup } diff --git a/pkg/harvester/list/harvesterhci.io.schedulevmbackup.vue b/pkg/harvester/list/harvesterhci.io.schedulevmbackup.vue new file mode 100644 index 00000000000..29b8e30bcd4 --- /dev/null +++ b/pkg/harvester/list/harvesterhci.io.schedulevmbackup.vue @@ -0,0 +1,124 @@ + + + diff --git a/pkg/harvester/list/harvesterhci.io.virtualmachinebackup.vue b/pkg/harvester/list/harvesterhci.io.virtualmachinebackup.vue index d563a3252f0..a3d0c2fc06a 100644 --- a/pkg/harvester/list/harvesterhci.io.virtualmachinebackup.vue +++ b/pkg/harvester/list/harvesterhci.io.virtualmachinebackup.vue @@ -4,36 +4,38 @@ import Loading from '@shell/components/Loading'; import MessageLink from '@shell/components/MessageLink'; import Masthead from '@shell/components/ResourceList/Masthead'; import ResourceTable from '@shell/components/ResourceTable'; - +import FilterVMSchedule from '../components/FilterVMSchedule'; import { HCI } from '../types'; import { allSettled } from '../utils/promise'; import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers'; import { BACKUP_TYPE } from '../config/types'; +import { defaultTableSortGenerationFn } from '@shell/components/ResourceTable.vue'; export default { name: 'HarvesterListBackup', components: { - ResourceTable, Banner, Loading, Masthead, MessageLink + ResourceTable, Banner, Loading, Masthead, MessageLink, FilterVMSchedule }, props: { schema: { type: Object, required: true, - } + }, }, async fetch() { const inStore = this.$store.getters['currentProduct'].inStore; const hash = await allSettled({ - vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }), - settings: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SETTING }), - rows: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BACKUP }), + vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }), + settings: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SETTING }), + backups: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BACKUP }), + scheduleList: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SCHEDULE_VM_BACKUP }), }); - this.rows = hash.rows; + this.backups = hash.backups; + this.rows = hash.backups; this.settings = hash.settings; - if (this.$store.getters[`${ inStore }/schemaFor`](HCI.SETTING)) { const backupTargetResource = hash.settings.find( O => O.id === 'backup-target'); const isEmpty = this.getBackupTargetValueIsEmpty(backupTargetResource); @@ -50,10 +52,12 @@ export default { const resource = params.resource; return { - rows: [], - settings: [], + rows: [], + backups: [], + settings: [], resource, - to: `${ HCI.SETTING }/backup-target?mode=edit`, + to: `${ HCI.SETTING }/backup-target?mode=edit`, + searchSchedule: '' }; }, @@ -85,7 +89,25 @@ export default { } return out; - } + }, + + getRow(row) { + return row.status && row.status.source; + }, + + changeRows(filteredRows, searchSchedule) { + this.$set(this, 'searchSchedule', searchSchedule); + this.$set(this, 'backups', filteredRows); + }, + + sortGenerationFn() { + let base = defaultTableSortGenerationFn(this.schema, this.$store); + + base += this.searchSchedule; + + return base; + }, + }, computed: { @@ -101,6 +123,12 @@ export default { align: 'left', formatter: 'AttachVMWithName' }, + { + name: 'backupCreatedFrom', + labelKey: 'harvester.tableHeaders.vmSchedule', + value: 'sourceSchedule', + formatter: 'BackupCreatedFrom', + }, { name: 'backupTarget', labelKey: 'tableHeaders.backupTarget', @@ -112,7 +140,7 @@ export default { name: 'readyToUse', labelKey: 'tableHeaders.readyToUse', value: 'status.readyToUse', - align: 'left', + align: 'center', formatter: 'Checked', }, ]; @@ -124,25 +152,24 @@ export default { value: 'backupProgress', align: 'left', formatter: 'HarvesterBackupProgressBar', - width: 200, }); } - cols.push(AGE); return cols; }, hasBackupProgresses() { - return !!this.rows.find(R => R.status?.progress !== undefined); + return !!this.backups.find(r => r.status?.progress !== undefined); }, - filteredRows() { - return this.rows.filter(R => R.spec?.type !== BACKUP_TYPE.SNAPSHOT); + return this.backups.filter(r => r.spec?.type !== BACKUP_TYPE.SNAPSHOT); + }, + getRawRows() { + return this.rows.filter(r => r.spec?.type === BACKUP_TYPE.BACKUP); }, - backupTargetResource() { - return this.settings.find( O => O.id === 'backup-target'); + return this.settings.find(O => O.id === 'backup-target'); }, isEmptyValue() { @@ -211,16 +238,23 @@ export default { :headers="headers" :groupable="true" :rows="filteredRows" + :sort-generation-fn="sortGenerationFn" :schema="schema" key-field="_key" default-sort-by="age" v-on="$listeners" > + diff --git a/pkg/harvester/mixins/harvester-vm/index.js b/pkg/harvester/mixins/harvester-vm/index.js index cf5a4b9bf39..330af948d3f 100644 --- a/pkg/harvester/mixins/harvester-vm/index.js +++ b/pkg/harvester/mixins/harvester-vm/index.js @@ -25,6 +25,7 @@ import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations import impl, { QGA_JSON, USB_TABLET } from './impl'; import { uniq } from '@shell/utils/array'; import { parseVolumeClaimTemplates } from '../../utils/vm'; +import aardvark_blue from '../../../../../iTerm2-Color-Schemes/screenshots/aardvark_blue.png'; export const MANAGEMENT_NETWORK = 'management Network'; @@ -303,7 +304,7 @@ export default { } = config; const vm = this.resource === HCI.VM ? value : this.resource === HCI.BACKUP ? this.value.status?.source : value.spec.vm; - + const volumeBackups = this.resource === HCI.BACKUP ? this.value.status?.volumeBackups : null; const spec = vm?.spec; if (!spec) { @@ -337,7 +338,8 @@ export default { const sshKey = this.getSSHFromAnnotation(spec) || []; const imageId = this.getRootImageId(vm) || ''; - const diskRows = this.getDiskRows(vm); + const diskRows = this.getDiskRows(vm, volumeBackups); + const networkRows = this.getNetworkRows(vm, { fromTemplate, init }); const hasCreateVolumes = this.getHasCreatedVolumes(spec) || []; @@ -406,7 +408,7 @@ export default { this.refreshYamlEditor(); }, - getDiskRows(vm) { + getDiskRows(vm, volBackups) { const namespace = vm.metadata.namespace; const _volumes = vm.spec.template.spec.volumes || []; const _disks = vm.spec.template.spec.domain.devices.disks || []; @@ -424,6 +426,7 @@ export default { const isIsoImage = /iso$/i.test(imageResource?.imageSuffix); const imageSize = Math.max(imageResource?.status?.size, imageResource?.status?.virtualSize); const isEncrypted = imageResource?.isEncrypted || false; + const volumeBackups = volBackups?.find(vBackup => vBackup.volumeName === 'disk-0') || null ; if (isIsoImage) { bus = 'sata'; @@ -451,7 +454,8 @@ export default { storageClassName: '', image: this.imageId, volumeMode: 'Block', - isEncrypted + isEncrypted, + volumeBackups, }); } else { out = _disks.map( (DISK, index) => { @@ -533,6 +537,7 @@ export default { const volumeStatus = pvc?.relatedPV?.metadata?.annotations?.[HCI_ANNOTATIONS.VOLUME_ERROR]; const isEncrypted = pvc?.isEncrypted || false; + const volumeBackups = volBackups?.find(vBackup => vBackup.volumeName === DISK.name) || null; return { id: randomStr(5), @@ -553,7 +558,8 @@ export default { volumeStatus, dataSource, namespace, - isEncrypted + isEncrypted, + volumeBackups, }; }); } diff --git a/pkg/harvester/models/harvesterhci.io.schedulevmbackup.js b/pkg/harvester/models/harvesterhci.io.schedulevmbackup.js new file mode 100644 index 00000000000..13a2f566100 --- /dev/null +++ b/pkg/harvester/models/harvesterhci.io.schedulevmbackup.js @@ -0,0 +1,97 @@ +import HarvesterResource from './harvester'; +import { get } from '@shell/utils/object'; +import { findBy } from '@shell/utils/array'; +import { colorForState, stateDisplay, STATES } from '@shell/plugins/dashboard-store/resource-class'; +import { _CREATE } from '@shell/config/query-params'; +import { ucFirst, escapeHtml } from '@shell/utils/string'; + +export default class ScheduleVmBackup extends HarvesterResource { + detailPageHeaderActionOverride(realMode) { + if (realMode === _CREATE) { + return this.t('harvester.schedule.createTitle'); + } + } + + get _availableActions() { + const toFilter = ['goToClone']; + + const out = super._availableActions.filter((action) => { + if (!toFilter.includes(action.action)) { + return action; + } + }); + + return [ + { + action: 'resumeSchedule', + enabled: ucFirst(this.state) === STATES.suspended.label, + icon: 'icons icon-play', + label: this.t('harvester.action.resumeSchedule'), + }, + { + action: 'suspendSchedule', + enabled: ucFirst(this.state) === STATES.active.label, + icon: 'icons icon-pause', + label: this.t('harvester.action.suspendSchedule'), + }, + ...out + ]; + } + + async suspendSchedule() { + try { + this.spec.suspend = true; // suspend schedule + await this.save(); + } catch (err) { + this.spec.suspend = false; + + this.$dispatch('growl/fromError', { + title: this.t('generic.notification.title.error', { name: escapeHtml(this.metadata.name) }), + err, + }, { root: true }); + } + } + + async resumeSchedule() { + try { + this.spec.suspend = false; // resume schedule + await this.save(); + } catch (err) { + this.spec.suspend = true; + + this.$dispatch('growl/fromError', { + title: this.t('generic.notification.title.error', { name: escapeHtml(this.metadata.name) }), + err, + }, { root: true }); + } + } + + get state() { + const conditions = get(this, 'status.conditions'); + const isSuspended = findBy(conditions, 'type', 'BackupSuspend')?.status === 'True'; + + if (isSuspended) { + return STATES.suspended.label; + } + + return this.metadata.state.name; + } + + get stateDescription() { + const suspendedCondition = (this.status?.conditions || []).find(c => c.type === 'BackupSuspend'); + + return ucFirst(suspendedCondition?.message) || super.stateDescription; + } + + get stateBackground() { + return colorForState(this.stateDisplay).replace('text-', 'bg-'); + } + + get stateColor() { + return colorForState(this.state); + } + + get stateDisplay() { + return stateDisplay(this.state); + } +} diff --git a/pkg/harvester/models/harvesterhci.io.virtualmachinebackup.js b/pkg/harvester/models/harvesterhci.io.virtualmachinebackup.js index 8857f1352e9..fa9a2220c28 100644 --- a/pkg/harvester/models/harvesterhci.io.virtualmachinebackup.js +++ b/pkg/harvester/models/harvesterhci.io.virtualmachinebackup.js @@ -18,9 +18,8 @@ export default class HciVmBackup extends HarvesterResource { get detailLocation() { const detailLocation = clone(this._detailLocation); - const route = this.currentRoute(); - detailLocation.params.resource = route.params.resource; + detailLocation.params.resource = HCI.BACKUP; return detailLocation; } @@ -81,23 +80,22 @@ export default class HciVmBackup extends HarvesterResource { } restoreExistingVM(resource = this) { - const route = this.currentRoute(); const router = this.currentRouter(); router.push({ name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`, - params: { resource: route.params.resource }, + params: { resource: HCI.BACKUP }, query: { restoreMode: 'existing', resourceName: resource.name } }); } restoreNewVM(resource = this) { - const route = this.currentRoute(); + // const route = this.currentRoute(); const router = this.currentRouter(); router.push({ name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`, - params: { resource: route.params.resource }, + params: { resource: HCI.BACKUP }, query: { restoreMode: 'new', resourceName: resource.name } }); } @@ -125,6 +123,10 @@ export default class HciVmBackup extends HarvesterResource { return colorForState(state); } + get sourceSchedule() { + return this.metadata?.annotations['harvesterhci.io/svmbackupId']; + } + get attachVM() { return this.spec.source.name; } diff --git a/pkg/harvester/models/kubevirt.io.virtualmachine.js b/pkg/harvester/models/kubevirt.io.virtualmachine.js index 6ad7ee9861a..2ca459ec835 100644 --- a/pkg/harvester/models/kubevirt.io.virtualmachine.js +++ b/pkg/harvester/models/kubevirt.io.virtualmachine.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import { load } from 'js-yaml'; import { omitBy, pickBy } from 'lodash'; - +import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester'; import { colorForState } from '@shell/plugins/dashboard-store/resource-class'; import { POD, NODE, PVC } from '@shell/config/types'; import { HCI } from '../types'; @@ -162,6 +162,12 @@ export default class VirtVm extends HarvesterResource { icon: 'icon icon-storage', label: this.t('harvester.action.editVMQuota') }, + { + action: 'createSchedule', + enabled: true, + icon: 'icon icon-history', + label: this.t('harvester.action.createSchedule') + }, { action: 'restoreVM', enabled: !!this.actions?.restore, @@ -324,6 +330,16 @@ export default class VirtVm extends HarvesterResource { ); } + createSchedule(resources = this) { + const router = this.currentRouter(); + + router.push({ + name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`, + params: { resource: HCI.SCHEDULE_VM_BACKUP }, + query: { vmNamespace: this.metadata.namespace, vmName: this.metadata.name } + }); + } + backupVM(resources = this) { this.$dispatch('promptModal', { resources, diff --git a/pkg/harvester/types.ts b/pkg/harvester/types.ts index 27a16ee83c8..3e889a0a294 100644 --- a/pkg/harvester/types.ts +++ b/pkg/harvester/types.ts @@ -11,6 +11,7 @@ export const HCI = { SETTING: 'harvesterhci.io.setting', UPGRADE: 'harvesterhci.io.upgrade', UPGRADE_LOG: 'harvesterhci.io.upgradelog', + SCHEDULE_VM_BACKUP: 'harvesterhci.io.schedulevmbackup', BACKUP: 'harvesterhci.io.virtualmachinebackup', RESTORE: 'harvesterhci.io.virtualmachinerestore', NODE_NETWORK: 'network.harvesterhci.io.nodenetwork', diff --git a/pkg/rancher-components/package.json b/pkg/rancher-components/package.json index d31e73d2955..ed7767b5615 100644 --- a/pkg/rancher-components/package.json +++ b/pkg/rancher-components/package.json @@ -32,8 +32,8 @@ "@vue/test-utils": "1.2.1", "babel-eslint": "10.1.0", "core-js": "3.25.3", - "cron-validator": "1.2.0", - "cronstrue": "1.95.0", + "cron-validator": "1.3.1", + "cronstrue": "2.50.0", "eslint": "7.32.0", "eslint-plugin-import": "2.23.4", "eslint-plugin-node": "11.1.0", diff --git a/pkg/rancher-components/src/components/Form/LabeledInput/LabeledInput.vue b/pkg/rancher-components/src/components/Form/LabeledInput/LabeledInput.vue index c4bd59177ac..109d19bb6fd 100644 --- a/pkg/rancher-components/src/components/Form/LabeledInput/LabeledInput.vue +++ b/pkg/rancher-components/src/components/Form/LabeledInput/LabeledInput.vue @@ -144,11 +144,16 @@ export default ( if (this.type !== 'cron' || !this.value) { return; } - if (!isValidCron(this.value)) { + // refer https://github.com/GuillaumeRochat/cron-validator#readme + if (!isValidCron(this.value, { + alias: true, + allowBlankDay: true, + allowSevenAsSunday: true, + })) { return this.t('generic.invalidCron'); } try { - const hint = cronstrue.toString(this.value); + const hint = cronstrue.toString(this.value, { verbose: true }); return hint; } catch (e) { diff --git a/shell/package.json b/shell/package.json index d2b28d60de1..0aca2c6dab0 100644 --- a/shell/package.json +++ b/shell/package.json @@ -59,8 +59,8 @@ "cookie": "0.5.0", "cookie-universal-nuxt": "2.1.4", "core-js": "3.21.1", - "cron-validator": "1.2.0", - "cronstrue": "1.95.0", + "cron-validator": "1.3.1", + "cronstrue": "2.50.0", "cross-env": "6.0.3", "css-loader": "6.7.3", "csv-loader": "3.0.3", diff --git a/shell/utils/validators/cron-schedule.js b/shell/utils/validators/cron-schedule.js index d726f6644c3..ebea8885c5f 100644 --- a/shell/utils/validators/cron-schedule.js +++ b/shell/utils/validators/cron-schedule.js @@ -7,3 +7,13 @@ export function cronSchedule(schedule = '', getters, errors) { errors.push(getters['i18n/t']('validation.invalidCron')); } } + +export function isCronValid(schedule = '') { + try { + const hint = cronstrue.toString(schedule); + + return !!hint; + } catch (e) { + return false; + } +} diff --git a/yarn.lock b/yarn.lock index e48ede151c5..cb5e8e1333c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5486,10 +5486,15 @@ cron-validator@1.2.0: resolved "https://registry.yarnpkg.com/cron-validator/-/cron-validator-1.2.0.tgz#952d2c926b85724dfe9c0d0ca781fe956124de93" integrity sha512-fX9eq71ToAt4bJeJzFNe8OCljKNQdc2Otw4kZDfB3vyplrAyEO9Q20YgmCJ4pr+jI/QQ2yizM87Eh+b2Ty7GuQ== -cronstrue@1.95.0: - version "1.95.0" - resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-1.95.0.tgz#171df1fad8b0f0cb636354dd1d7842161c15478f" - integrity sha512-CdbQ17Z8Na2IdrK1SiD3zmXfE66KerQZ8/iApkGsxjmUVGJPS9M9oK4FZC3LM6ohUjjq3UeaSk+90Cf3QbXDfw== +cron-validator@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/cron-validator/-/cron-validator-1.3.1.tgz#8f2fe430f92140df77f91178ae31fc1e3a48a20e" + integrity sha512-C1HsxuPCY/5opR55G5/WNzyEGDWFVG+6GLrA+fW/sCTcP6A6NTjUP2AK7B8n2PyFs90kDG2qzwm8LMheADku6A== + +cronstrue@2.50.0: + version "2.50.0" + resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-2.50.0.tgz#eabba0f915f186765258b707b7a3950c663b5573" + integrity sha512-ULYhWIonJzlScCCQrPUG5uMXzXxSixty4djud9SS37DoNxDdkeRocxzHuAo4ImRBUK+mAuU5X9TSwEDccnnuPg== cross-env@6.0.3: version "6.0.3" From c946a94c308c448de2fb2c58e4349d97e4e5848f Mon Sep 17 00:00:00 2001 From: "andy.lee" Date: Wed, 14 Aug 2024 18:09:22 +0800 Subject: [PATCH 2/7] fix unit test failed Signed-off-by: andy.lee (cherry picked from commit 6f165167fb7ae275826e465bf7744b1da42d8117) --- pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue | 2 -- .../VirtualMachineVolume/type/container.vue | 2 +- .../VirtualMachineVolume/type/existing.vue | 2 +- .../VirtualMachineVolume/type/vmImage.vue | 2 +- .../VirtualMachineVolume/type/volume.vue | 2 +- pkg/harvester/list/harvesterhci.io.vmsnapshot.vue | 3 +++ 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue b/pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue index b204444a9c2..d4fcc3fca42 100644 --- a/pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue +++ b/pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue @@ -6,7 +6,6 @@ import CruResource from '@shell/components/CruResource'; import Tabbed from '@shell/components/Tabbed'; import Tab from '@shell/components/Tabbed/Tab'; import MessageLink from '@shell/components/MessageLink'; -import Checkbox from '@components/Form/Checkbox/Checkbox'; import LabeledSelect from '@shell/components/form/LabeledSelect'; import CreateEditView from '@shell/mixins/create-edit-view'; import { isCronValid } from '@shell/utils/validators/cron-schedule'; @@ -20,7 +19,6 @@ import { _EDIT, _CREATE } from '@shell/config/query-params'; export default { name: 'CreateVMSchedule', components: { - Checkbox, CruResource, Tabbed, Tab, diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/container.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/container.vue index 91b4103bc81..4e8c3786b42 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/container.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/container.vue @@ -145,7 +145,7 @@ export default {
Date: Wed, 21 Aug 2024 10:07:09 +0800 Subject: [PATCH 3/7] add HarvesterCronExpression formatter Signed-off-by: andy.lee (cherry picked from commit 5a9a6620725f3c0ed32800a680a967e8cba4b845) --- pkg/harvester/config/table-headers.js | 11 ++++--- .../edit/harvesterhci.io.schedulevmbackup.vue | 15 ++------- .../formatters/HarvesterCronExpression.vue | 32 +++++++++++++++++++ 3 files changed, 40 insertions(+), 18 deletions(-) create mode 100644 pkg/harvester/formatters/HarvesterCronExpression.vue diff --git a/pkg/harvester/config/table-headers.js b/pkg/harvester/config/table-headers.js index 26bf2e047d8..05f07425827 100644 --- a/pkg/harvester/config/table-headers.js +++ b/pkg/harvester/config/table-headers.js @@ -43,11 +43,12 @@ export const SNAPSHOT_TARGET_VOLUME = { // The column of cron expression volume on VM schedules list page export const VM_SCHEDULE_CRON = { - name: 'CronExpression', - labelKey: 'harvester.tableHeaders.cronExpression', - value: 'spec.cron', - align: 'center', - sort: 'spec.cron', + name: 'CronExpression', + labelKey: 'harvester.tableHeaders.cronExpression', + value: 'spec.cron', + align: 'center', + sort: 'spec.cron', + formatter: 'HarvesterCronExpression', }; // The column of retain on VM schedules list page diff --git a/pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue b/pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue index d4fcc3fca42..820315e3ecf 100644 --- a/pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue +++ b/pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue @@ -180,18 +180,8 @@ export default { }, validateFailure(count) { - if (count > this.value.spec.retain) { - this.$set(this, 'maxFailure', this.value.spec.retain); - } else if (count < 2) { - this.$set(this, 'maxFailure', 2); - } - }, - - validateRetain(count) { - if (count > 250) { - this.$set(this, 'retain', 250); - } else if (count < 2) { - this.$set(this, 'retain', 2); + if (this.value.spec.retain && count > this.value.spec.retain) { + this.$set(this.value.spec, 'maxFailure', this.value.spec.retain); } }, }, @@ -290,7 +280,6 @@ export default { required :tooltip="t('harvester.schedule.retain.tooltip')" :disabled="isBackupTargetUnAvailable || isView" - @input="validateRetain" /> +import cronstrue from 'cronstrue'; + +export default { + props: { + value: { + type: String, + default: '' + }, + }, + computed: { + cronTooltipHint() { + let cronHint = ''; + + try { + cronHint = cronstrue.toString(this.value, { verbose: true }); + } catch (e) { + cronHint = this.t('generic.invalidCron'); + } + + return cronHint || this.value.spec.cron; + } + + } +}; + + + From 9e3db5d218ccee9a48d71a292108e9fbb3641a15 Mon Sep 17 00:00:00 2001 From: "andy.lee" Date: Thu, 19 Sep 2024 12:57:12 +0800 Subject: [PATCH 4/7] move isCronValid in harvester code base (cherry picked from commit a915640949dfab2b02b6ebb5c1d130c55b850c99) --- .../edit/harvesterhci.io.schedulevmbackup.vue | 2 +- pkg/harvester/l10n/en-us.yaml | 16 ++++++++-------- .../list/harvesterhci.io.schedulevmbackup.vue | 2 +- pkg/harvester/utils/cron.js | 11 +++++++++++ shell/utils/validators/cron-schedule.js | 10 ---------- 5 files changed, 21 insertions(+), 20 deletions(-) create mode 100644 pkg/harvester/utils/cron.js diff --git a/pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue b/pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue index 820315e3ecf..c6c174d4e7e 100644 --- a/pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue +++ b/pkg/harvester/edit/harvesterhci.io.schedulevmbackup.vue @@ -8,7 +8,7 @@ import Tab from '@shell/components/Tabbed/Tab'; import MessageLink from '@shell/components/MessageLink'; import LabeledSelect from '@shell/components/form/LabeledSelect'; import CreateEditView from '@shell/mixins/create-edit-view'; -import { isCronValid } from '@shell/utils/validators/cron-schedule'; +import { isCronValid } from '@pkg/harvester/utils/cron'; import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester'; import { allHash } from '@shell/utils/promise'; import { HCI } from '../types'; diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml index 9d52c152d9b..5e1fe8a1b59 100644 --- a/pkg/harvester/l10n/en-us.yaml +++ b/pkg/harvester/l10n/en-us.yaml @@ -150,7 +150,7 @@ harvester: setDefaultVersion: Set default version addTemplateVersion: Add template version backup: Take Backup - createSchedule: Create VM Schedule + createSchedule: Create Schedule restore: Restore restoreNewVM: Restore New resumeSchedule: Resume @@ -196,7 +196,7 @@ harvester: scheduleType: Type maxFailure: Max Failure sourceVm: Source VM - vmSchedule: VM Schedule + vmSchedule: Virtual Machine Schedule hostIp: Host IP vm: ipAddress: IP Address @@ -827,10 +827,10 @@ harvester: moreNotes: For more details about the release notes, please visit - schedule: - label: VM Schedules - createTitle: Create VM Schedule - createButtonText: Create VM Schedule - scheduleType: VM Schedule Type + label: Virtual Machine Schedules + createTitle: Create Schedule + createButtonText: Create Schedule + scheduleType: Virtual Machine Schedule Type cron: Cron Schedule detail: namespace: Namespace @@ -1541,8 +1541,8 @@ typeLabel: } harvesterhci.io.schedulevmbackup: |- {count, plural, - one { VM Schedule } - other { VM Schedules } + one { Virtual Machine Schedule } + other { Virtual Machine Schedules } } harvesterhci.io.virtualmachinebackup: |- {count, plural, diff --git a/pkg/harvester/list/harvesterhci.io.schedulevmbackup.vue b/pkg/harvester/list/harvesterhci.io.schedulevmbackup.vue index 29b8e30bcd4..a875b317099 100644 --- a/pkg/harvester/list/harvesterhci.io.schedulevmbackup.vue +++ b/pkg/harvester/list/harvesterhci.io.schedulevmbackup.vue @@ -91,7 +91,7 @@ export default { :schema="schema" :resource="resource" :type-display="typeDisplay" - :parentNameOverride="'VM schedule'" + :parentNameOverride="'Virtual Machine schedule'" :create-button-label="t('harvester.schedule.createButtonText')" /> Date: Mon, 23 Sep 2024 15:31:43 +0800 Subject: [PATCH 5/7] Add ifHaveType for schedule page Signed-off-by: andy.lee (cherry picked from commit c02f2c2b541ccd22740d9836170afc1bd3a52cbc) --- pkg/harvester/config/harvester.js | 3 ++- .../VirtualMachineVolume/type/vmImage.vue | 24 ++++++++++--------- .../VirtualMachineVolume/type/volume.vue | 24 +++++++++++-------- pkg/harvester/mixins/harvester-vm/index.js | 1 - 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/pkg/harvester/config/harvester.js b/pkg/harvester/config/harvester.js index 712d57506cf..bcd921bf28f 100644 --- a/pkg/harvester/config/harvester.js +++ b/pkg/harvester/config/harvester.js @@ -479,7 +479,8 @@ export function init($plugin, store) { name: `${ PRODUCT_NAME }-c-cluster-resource`, params: { resource: HCI.SCHEDULE_VM_BACKUP } }, - exact: false + exact: false, + ifHaveType: HCI.SCHEDULE_VM_BACKUP, }); configureType(HCI.BACKUP, { showListMasthead: false, showConfigView: false }); diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/vmImage.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/vmImage.vue index b517fc81f0e..4cb20b568c1 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/vmImage.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/vmImage.vue @@ -11,7 +11,6 @@ import { HCI } from '../../../../types'; import { formatSi, parseSi } from '@shell/utils/units'; import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map'; import { _VIEW } from '@shell/config/query-params'; - import { ucFirst } from '@shell/utils/string'; export default { @@ -101,6 +100,12 @@ export default { return image ? image.label : '-'; }, + readyToUse() { + const val = String(this.value.volumeBackups?.readyToUse || false); + + return ucFirst(val); + }, + pvcsResource() { const allPVCs = this.$store.getters['harvester/all'](PVC) || []; @@ -308,16 +313,13 @@ export default { :value="encryptionValue" />
-
- - - +
+
+
+
-
- - - +
+
+
+
Date: Thu, 26 Sep 2024 22:25:26 +0800 Subject: [PATCH 6/7] fix Restore New / Restore Existing action route Signed-off-by: andy.lee (cherry picked from commit eabd94355468750110ef747a1cacf2613f30c8c7) # Conflicts: # pkg/harvester/config/labels-annotations.js --- pkg/harvester/components/FilterVMSchedule.vue | 2 +- pkg/harvester/config/labels-annotations.js | 6 + .../BackupList.vue | 3 +- .../SnapshotList.vue | 3 +- .../dialog/HarvesterScheduleModal.vue | 129 ------------------ .../harvesterhci.io.virtualmachinebackup.vue | 6 +- .../edit/harvesterhci.io.vmsnapshot.vue | 5 +- .../harvesterhci.io.virtualmachinebackup.js | 20 ++- 8 files changed, 34 insertions(+), 140 deletions(-) delete mode 100644 pkg/harvester/dialog/HarvesterScheduleModal.vue diff --git a/pkg/harvester/components/FilterVMSchedule.vue b/pkg/harvester/components/FilterVMSchedule.vue index cb4eccb2445..0cf6002a67b 100644 --- a/pkg/harvester/components/FilterVMSchedule.vue +++ b/pkg/harvester/components/FilterVMSchedule.vue @@ -69,7 +69,7 @@ export default { diff --git a/pkg/harvester/config/labels-annotations.js b/pkg/harvester/config/labels-annotations.js index 1a01034507f..c6c74954e11 100644 --- a/pkg/harvester/config/labels-annotations.js +++ b/pkg/harvester/config/labels-annotations.js @@ -51,5 +51,11 @@ export const HCI = { PARENT_SRIOV_GPU: 'harvesterhci.io/parentSRIOVGPUDevice', VM_MAINTENANCE_MODE_STRATEGY: 'harvesterhci.io/maintain-mode-strategy', NODE_CPU_MANAGER_UPDATE_STATUS: 'harvesterhci.io/cpu-manager-update-status', +<<<<<<< HEAD CPU_MANAGER: 'cpumanager' +======= + CPU_MANAGER: 'cpumanager', + VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails', + SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId', +>>>>>>> eabd94355 (fix Restore New / Restore Existing action route) }; diff --git a/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/BackupList.vue b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/BackupList.vue index d8ff2abfc83..1ae77e55bf0 100644 --- a/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/BackupList.vue +++ b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/BackupList.vue @@ -4,6 +4,7 @@ import { STATE, NAME, AGE } from '@shell/config/table-headers'; import { allSettled } from '../../utils/promise'; import { BACKUP_TYPE } from '../../config/types'; import { HCI } from '../../types'; +import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; export default { name: 'BackupList', @@ -90,7 +91,7 @@ export default { let r = this.rows.filter(row => row.spec?.type === BACKUP_TYPE.BACKUP); if (this.id) { - r = r.filter(backup => backup.metadata.annotations?.['harvesterhci.io/svmbackupId'] === this.id); + r = r.filter(backup => backup.metadata.annotations?.[HCI_ANNOTATIONS.SVM_BACKUP_ID] === this.id); } return r; diff --git a/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/SnapshotList.vue b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/SnapshotList.vue index 99d68f9aa38..9fa3111dcc0 100644 --- a/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/SnapshotList.vue +++ b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/SnapshotList.vue @@ -3,6 +3,7 @@ import ResourceTable from '@shell/components/ResourceTable'; import { STATE, NAME, AGE } from '@shell/config/table-headers'; import { allSettled } from '../../utils/promise'; import { BACKUP_TYPE } from '../../config/types'; +import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { HCI } from '../../types'; import { schema } from '../../list/harvesterhci.io.vmsnapshot'; @@ -58,7 +59,7 @@ export default { let r = this.rows.filter(row => row.spec?.type === BACKUP_TYPE.SNAPSHOT); if (this.id) { - r = r.filter(row => row.metadata.annotations?.['harvesterhci.io/svmbackupId'] === this.id); + r = r.filter(row => row.metadata.annotations?.[HCI_ANNOTATIONS.SVM_BACKUP_ID] === this.id); } return r; diff --git a/pkg/harvester/dialog/HarvesterScheduleModal.vue b/pkg/harvester/dialog/HarvesterScheduleModal.vue deleted file mode 100644 index a623ab742b7..00000000000 --- a/pkg/harvester/dialog/HarvesterScheduleModal.vue +++ /dev/null @@ -1,129 +0,0 @@ - - - - - diff --git a/pkg/harvester/edit/harvesterhci.io.virtualmachinebackup.vue b/pkg/harvester/edit/harvesterhci.io.virtualmachinebackup.vue index 05b4c1b3139..b42c29e4a81 100644 --- a/pkg/harvester/edit/harvesterhci.io.virtualmachinebackup.vue +++ b/pkg/harvester/edit/harvesterhci.io.virtualmachinebackup.vue @@ -159,6 +159,10 @@ export default { }, methods: { + cancelAction() { + this.$router.go(-1); + }, + async saveRestore(buttonCb) { this.update(); @@ -261,7 +265,7 @@ export default { />
-