ui: allow provisioning backups during instance deploy (#11612)

* ui: allow assigning backup offring during instance deploy

Add backup offering selection to Deploy VM wizard and assign selected backup offering to the VM after successful deployment. This enables users to choose a backup offering during VM creation, and the VM is automatically associated with the selected offering post-deployment.

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>

* changes for schedules

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>

* fix

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>

* fix

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>

* Update ui/public/locales/pt_BR.json

* Update ui/src/views/compute/wizard/DeployInstanceBackupSelection.vue

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* address review

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>

* fix

* allow only one schdeule per interval type

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>

* show message same internaltype schedule

* show backup step only when zone has offering

---------

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Abhishek Kumar 2025-09-23 14:51:42 +05:30 committed by GitHub
parent d9abc078cf
commit 4884f52c90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 384 additions and 19 deletions

View File

@ -259,6 +259,7 @@
"label.available": "Verfügbar",
"label.back": "Zurück",
"label.backup": "Backup",
"label.backups": "Backup",
"label.backup.attach.restore": "Backup-Volume wiederherstellen und anhängen",
"label.backup.offering.assign": "VM zum Backup-Angebot zuordnen",
"label.backup.offering.remove": "VM vom Backup-Angebot entfernen",

View File

@ -317,6 +317,7 @@
"label.availableprocessors": "Διαθέσιμοι πυρήνες επεξεργαστή",
"label.back": "Πίσω",
"label.backup": "Αντίγραφα ασφαλείας",
"label.backups": "Αντίγραφα ασφαλείας",
"label.backup.attach.restore": "Επαναφορά και επισύναψη τόμου αντιγράφου ασφαλείας",
"label.backup.offering.assign": "Αντιστοίχιση εικονικής μηχανής με προσφορά δημιουργίας αντιγράφων ασφαλείας",
"label.backup.offering.remove": "Κατάργηση εικονικής μηχανής από την προσφορά δημιουργίας αντιγράφων ασφαλείας",

View File

@ -252,6 +252,7 @@
"label.add.acl.rule": "Add ACL rule",
"label.add.acl": "Add ACL",
"label.add.affinity.group": "Add new Affinity Group",
"label.add.backup.schedule": "Add Backup Schedule",
"label.add.baremetal.dhcp.device": "Add bare metal DHCP device",
"label.add.bgp.peer": "Add BGP Peer",
"label.add.bigswitchbcf.device": "Add BigSwitch BCF Controller",
@ -442,14 +443,17 @@
"label.availablevirtualmachinecount": "Available Instances",
"label.back": "Back",
"label.back.login": "Back to login",
"label.backup": "Backups",
"label.backup": "Backup",
"label.backups": "Backups",
"label.backup.attach.restore": "Restore and attach backup volume",
"label.backup.configure.schedule": "Configure Backup Schedule",
"label.backup.offering.assign": "Assign Instance to backup offering",
"label.backup.offering.remove": "Remove Instance from backup offering",
"label.backup.offerings": "Backup Offerings",
"label.backup.offering.assign.failed": "Failed to assign Backup Offering",
"label.backup.repository": "Backup Repository",
"label.backup.restore": "Restore Instance backup",
"label.backup.schedule.create.failed": "Failed to create Backup Schedule",
"label.backuplimit": "Backup Limits",
"label.backup.storage": "Backup Storage",
"label.backupstoragelimit": "Backup Storage Limits (GiB)",
@ -2194,6 +2198,7 @@
"label.select.all": "Select all",
"label.select.columns": "Select columns",
"label.select.a.zone": "Select a Zone",
"label.select.backup.offering": "Select Backup Offering",
"label.select.deployment.infrastructure": "Select deployment infrastructure",
"label.select.guest.os.type": "Please select the guest OS type",
"label.select.network": "Select Network",
@ -3075,7 +3080,9 @@
"message.backup.attach.restore": "Please confirm that you want to restore and attach the volume from the backup?",
"message.backup.create": "Are you sure you want to create an Instance backup?",
"message.backup.offering.remove": "Are you sure you want to remove Instance from backup offering and delete the backup chain?",
"message.backup.provision.instance": "Select a backup offering to assign to the Instance. You can also add one or more backup schedules for different interval types to automate backups for this Instance. Assigning a backup offering and schedules helps protect your data by enabling automated and scheduled backups.",
"message.backup.restore": "Please confirm that you want to restore the Instance backup?",
"message.backup.update.existing.schedule": "Updating existing backup schedule for the same interval type",
"message.cancel.shutdown": "Please confirm that you would like to cancel the shutdown on this Management Server. It will resume accepting any new Async Jobs.",
"message.cancel.maintenance": "Please confirm that you would like to cancel the maintenance on this Management Server. It will resume accepting any new Async Jobs.",
"message.certificate.upload.processing": "Certificate upload in progress",

View File

@ -222,6 +222,7 @@
"label.available": "Disponible",
"label.back": "Volver",
"label.backup": "Respaldos",
"label.backups": "Respaldos",
"label.backup.attach.restore": "Restaurar y conectar un Volumen de Respaldo",
"label.backup.offering.assign": "Asignar instancia a una oferta de respaldo",
"label.backup.offering.remove": "remover instancia de una oferta de respaldo",

View File

@ -485,6 +485,7 @@
"label.available.public.ips": "使用できるパブリックIPアドレス",
"label.back": "戻る",
"label.backup": "バックアップ",
"label.backups": "バックアップ",
"label.backup.attach.restore": "復元とバックアップボリュームをアタッチ",
"label.backup.offering.assign": "VMをバックアップオファリングに割り当て",
"label.backup.offering.remove": "VMバックアップオファリングから削除",

View File

@ -269,6 +269,7 @@
"label.available": "\uc0ac\uc6a9 \uac00\ub2a5",
"label.back": "\ub4a4\ub85c",
"label.backup": "\ubc31\uc5c5",
"label.backups": "\ubc31\uc5c5",
"label.backup.attach.restore": "\ubc31\uc5c5 \ubcfc\ub968 \ubcf5\uc6d0 \ubc0f \uc5f0\uacb0",
"label.backup.offering.assign": "\uac00\uc0c1\uba38\uc2e0\uc5d0 \ubc31\uc5c5 \uc624\ud37c\ub9c1 \ud560\ub2f9",
"label.backup.offering.remove": "\uac00\uc0c1\uba38\uc2e0\uc5d0 \ubc31\uc5c5 \uc624\ud37c\ub9c1 \uc81c\uac70",

View File

@ -292,7 +292,8 @@
"label.availability": "Disponibilidade",
"label.available": "Dispon\u00edvel",
"label.back": "Voltar",
"label.backup": "Backups",
"label.backup": "Backup",
"label.backups": "Backups",
"label.backup.attach.restore": "Restaurar e anexar volume de backup",
"label.backup.offering.assign": "Atribuir VM a oferta de backup",
"label.backup.offering.remove": "Remover VM de oferta de backup",

View File

@ -419,6 +419,7 @@
"label.back": "వెనుకకు",
"label.back.login": "తిరిగి లాగిన్‌కి",
"label.backup": "బ్యాకప్‌లు",
"label.backups": "బ్యాకప్‌లు",
"label.backup.attach.restore": "బ్యాకప్ వాల్యూమ్‌ను పునరుద్ధరించండి మరియు అటాచ్ చేయండి",
"label.backup.configure.schedule": "బ్యాకప్ షెడ్యూల్‌ను కాన్ఫిగర్ చేయండి",
"label.backup.offering.assign": "బ్యాకప్ సమర్పణకు ఉదాహరణను కేటాయించండి",

View File

@ -561,6 +561,7 @@
"label.back": "\u540E\u9000",
"label.backup": "\u5907\u4EFD",
"label.backups": "\u5907\u4EFD",
"label.backup.attach.restore": "\u6062\u590D\u5E76\u8FDE\u63A5\u5907\u4EFD\u5377",
"label.backup.offering.assign": "\u5C06\u865A\u62DF\u673A\u5206\u914D\u7ED9\u5907\u4EFD\u4EA7\u54C1",
"label.backup.offering.remove": "\u4ECE\u5907\u4EFD\u4EA7\u54C1\u4E2D\u5220\u9664\u865A\u62DF\u673A",

View File

@ -77,7 +77,7 @@
</template>
<a-select-option v-for="option in options" :key="option.id" :value="option[optionValueKey]">
<span>
<span v-if="showIcon">
<span v-if="showIcon && option.showicon !== false">
<resource-icon v-if="option.icon && option.icon.base64image" :image="option.icon.base64image" size="1x" style="margin-right: 5px"/>
<render-icon v-else :icon="defaultIcon" style="margin-right: 5px" />
</span>

View File

@ -415,7 +415,7 @@ export default {
},
{
name: 'backup',
title: 'label.backup',
title: 'label.backups',
icon: 'cloud-upload-outlined',
permission: ['listBackups'],
params: { listvmdetails: 'true' },

View File

@ -40,7 +40,8 @@ import {
dialogUtilPlugin,
cpuArchitectureUtilPlugin,
imagesUtilPlugin,
extensionsUtilPlugin
extensionsUtilPlugin,
backupUtilPlugin
} from './utils/plugins'
import { VueAxios } from './utils/request'
import directives from './utils/directives'
@ -63,6 +64,7 @@ vueApp.use(dialogUtilPlugin)
vueApp.use(cpuArchitectureUtilPlugin)
vueApp.use(imagesUtilPlugin)
vueApp.use(extensionsUtilPlugin)
vueApp.use(backupUtilPlugin)
vueApp.use(extensions)
vueApp.use(directives)

View File

@ -597,3 +597,14 @@ export const extensionsUtilPlugin = {
}
}
}
export const backupUtilPlugin = {
install (app) {
app.config.globalProperties.$isBackupProviderSupportsQuiesceVm = function (provider) {
if (!provider && typeof provider !== 'string') {
return false
}
return ['nas'].includes(provider.toLowerCase())
}
}
}

View File

@ -21,13 +21,11 @@
<a-tab-pane :tab="$t('label.schedule')" key="1">
<FormSchedule
:loading="loading"
:resource="resource"
:dataSource="dataSource"/>
:resource="resource"/>
</a-tab-pane>
<a-tab-pane :tab="$t('label.scheduled.backups')" key="2">
<BackupSchedule
:loading="loading"
:resource="resource"
:dataSource="dataSource" />
</a-tab-pane>
</a-tabs>

View File

@ -540,6 +540,25 @@
</div>
</template>
</a-step>
<a-step
v-if="zoneAllowsBackupOperations"
:title="$t('label.backup')"
:status="zoneSelected ? 'process' : 'wait'">
<template #description>
<div v-if="zoneSelected" style="margin-top: 15px">
<deploy-instance-backup-selection
:zoneId="zoneId"
v-model:backupOfferingId="form.backupofferingid"
:backupSchedules="backupSchedules"
@change-backup-offering="onChangeBackupOffering"
@add-backup-schedule="onAddBackupSchedule"
@delete-backup-schedule="backupSchedules = backupSchedules.filter(schedule => schedule.id !== $event.id)" />
<a-form-item class="form-item-hidden">
<a-input v-model:value="form.backupofferingid" />
</a-form-item>
</div>
</template>
</a-step>
<a-step
:title="$t('label.advanced.mode')"
:status="zoneSelected ? 'process' : 'wait'">
@ -901,7 +920,7 @@
<script>
import { ref, reactive, toRaw, nextTick, h } from 'vue'
import { Button } from 'ant-design-vue'
import { Button, message } from 'ant-design-vue'
import { getAPI, postAPI } from '@/api'
import _ from 'lodash'
import { mixin, mixinDevice } from '@/utils/mixin.js'
@ -930,6 +949,7 @@ import SecurityGroupSelection from '@views/compute/wizard/SecurityGroupSelection
import TooltipLabel from '@/components/widgets/TooltipLabel'
import InstanceNicsNetworkSelectListView from '@/components/view/InstanceNicsNetworkSelectListView'
import DetailsInput from '@/components/widgets/DetailsInput'
import DeployInstanceBackupSelection from '@views/compute/wizard/DeployInstanceBackupSelection'
export default {
name: 'Wizard',
@ -955,7 +975,8 @@ export default {
SecurityGroupSelection,
TooltipLabel,
InstanceNicsNetworkSelectListView,
DetailsInput
DetailsInput,
DeployInstanceBackupSelection
},
props: {
visible: {
@ -1135,7 +1156,10 @@ export default {
opts: []
},
externalDetailsEnabled: false,
selectedExtensionId: null
selectedExtensionId: null,
zoneAllowsBackupOperations: false,
selectedBackupOffering: null,
backupSchedules: []
}
},
computed: {
@ -1672,6 +1696,13 @@ export default {
if (this.leaseduration < 1) {
this.vm.leaseduration = undefined
}
delete this.vm.backupofferingid
delete this.vm.backupofferingname
if (this.form.backupofferingid && this.selectedBackupOffering) {
this.vm.backupofferingid = this.selectedBackupOffering.id
this.vm.backupofferingname = this.selectedBackupOffering.name
}
}
}
},
@ -2507,6 +2538,7 @@ export default {
duration: 0
})
}
this.performPostDeployBackupActions(vm)
eventBus.emit('vm-refresh-data')
},
loadingMessage: `${title} ${this.$t('label.in.progress')}`,
@ -2979,6 +3011,31 @@ export default {
this.updateTemplateKey()
this.formModel = toRaw(this.form)
},
async updateZoneAllowsBackupOperations () {
this.zoneAllowsBackupOperations = false
if (!this.zoneId) {
return
}
if (!('listBackupOfferings' in this.$store.getters.apis) ||
!('assignVirtualMachineToBackupOffering' in this.$store.getters.apis)) {
return
}
const params = {
zoneid: this.zoneId,
issystem: false,
listall: true,
page: 1,
pageSize: 1
}
try {
const response = await getAPI('listBackupOfferings', params)
const backupOfferings = response.listbackupofferingsresponse.backupoffering || []
this.zoneAllowsBackupOperations = backupOfferings.length > 0
} catch (error) {
console.error('Error fetching backup offerings:', error)
this.zoneAllowsBackupOperations = false
}
},
onSelectZoneId (value) {
if (this.dataPreFill.zoneid !== value) {
this.dataPreFill = {}
@ -3004,7 +3061,10 @@ export default {
this.resetTemplatesList()
this.resetIsosList()
this.imageType = this.queryIsoId ? 'isoid' : 'templateid'
this.form.backupofferingid = undefined
this.selectedBackupOffering = null
this.fetchZoneOptions()
this.updateZoneAllowsBackupOperations()
},
onSelectPodId (value) {
this.podId = value
@ -3395,6 +3455,113 @@ export default {
return
}
this.form.externaldetails = undefined
},
onChangeBackupOffering (val) {
if (!val || !val.id) {
this.selectedBackupOffering = null
this.backupSchedules = []
return
}
this.selectedBackupOffering = val
if (this.backupSchedules && this.backupSchedules.length > 0 && !this.$isBackupProviderSupportsQuiesceVm(val.provider)) {
this.backupSchedules = this.backupSchedules.filter(item => !item.quiescevm)
}
},
onAddBackupSchedule (schedule) {
if (!schedule) {
return
}
// This is in accordance with the API behavior that only one schedule per intervaltype is allowed
const existingIndex = this.backupSchedules.findIndex(item => item.intervaltype === schedule.intervaltype)
if (existingIndex !== -1) {
message.warning({
content: this.$t('message.backup.update.existing.schedule') + ' ' + schedule.intervaltype,
duration: 2
})
this.backupSchedules.splice(existingIndex, 1, schedule)
return
}
this.backupSchedules.push(schedule)
},
async performPostDeployBackupActions (vm) {
if (!this.zoneAllowsBackupOperations) {
return
}
const assigned = await this.assignVirtualMachineToBackupOfferingIfNeeded(vm)
if (assigned) {
await this.createVirtualMachineBackupSchedulesIfNeeded(vm)
}
},
assignVirtualMachineToBackupOfferingIfNeeded (vm) {
if (!this.form.backupofferingid || !vm || !vm.id) {
return Promise.resolve(false)
}
const params = {
virtualmachineid: vm.id,
backupofferingid: this.form.backupofferingid
}
return new Promise((resolve, reject) => {
postAPI('assignVirtualMachineToBackupOffering', params).then(json => {
const jobId = json.assignvirtualmachinetobackupofferingresponse?.jobid
if (!jobId) {
resolve(false)
return
}
this.$pollJob({
jobId,
loadingMessage: `${this.$t('label.backup.offering.assign')} ${this.$t('label.in.progress')}`,
successMethod: () => {
resolve(true)
},
errorMethod: (result) => {
this.$notification.error({
message: this.$t('label.backup.offering.assign.failed'),
description: result?.jobresult?.errortext || this.$t('error.fetching.async.job.result')
})
resolve(false)
},
catchMessage: this.$t('error.fetching.async.job.result')
})
}).catch(error => {
this.$notification.error({
message: this.$t('label.backup.offering.assign.failed'),
description: error.message || error
})
resolve(false)
})
})
},
createVirtualMachineBackupSchedulesIfNeeded (vm) {
if (!vm || !vm.id || !this.backupSchedules) {
return Promise.resolve()
}
const promises = (this.backupSchedules || []).map(item =>
this.createVirtualMachineBackupSchedule(vm, item)
)
return Promise.all(promises)
},
createVirtualMachineBackupSchedule (vm, item) {
const params = {
virtualmachineid: vm.id,
intervaltype: item.intervaltype,
maxbackups: item.maxbackups,
timezone: item.timezone,
schedule: item.schedule
}
if (item.quiescevm) {
params.quiescevm = item.quiescevm
}
return new Promise((resolve, reject) => {
postAPI('createBackupSchedule', params).then(response => {
resolve(response)
}).catch(error => {
this.$notification.error({
message: this.$t('label.backup.schedule.create.failed'),
description: error.message || error
})
reject(error)
})
})
}
}
}

View File

@ -102,9 +102,9 @@ export default {
type: Object,
required: true
},
resource: {
type: Object,
required: true
deleteFn: {
type: Function,
default: null
}
},
data () {
@ -183,6 +183,10 @@ export default {
},
methods: {
handleClickDelete (record) {
if (this.deleteFn) {
this.deleteFn(record)
return
}
const params = {}
params.id = record.id
this.actionLoading = true

View File

@ -133,7 +133,7 @@
</a-form-item>
</a-col>
<a-col :md="24" :lg="12">
<a-form-item v-if="backupProvider === 'nas'" name="quiescevm" ref="quiescevm">
<a-form-item v-if="isQuiesceVmSupported" name="quiescevm" ref="quiescevm">
<a-switch v-model:checked="form.quiescevm"/>
<template #label>
<tooltip-label :title="$t('label.quiescevm')" :tooltip="apiParams.quiescevm.description"/>
@ -180,13 +180,13 @@ export default {
type: Boolean,
default: false
},
dataSource: {
type: Object,
required: true
},
resource: {
type: Object,
required: true
},
submitFn: {
type: Function,
default: null
}
},
data () {
@ -211,6 +211,11 @@ export default {
this.fetchBackupOffering()
},
inject: ['refreshSchedule', 'closeSchedule'],
computed: {
isQuiesceVmSupported () {
return this.$isBackupProviderSupportsQuiesceVm(this.backupProvider)
}
},
methods: {
initForm () {
this.formRef = ref()
@ -226,6 +231,10 @@ export default {
})
},
fetchBackupOffering () {
if ('backupoffering' in this.resource) {
this.backupProvider = this.resource.backupoffering.provider
return
}
getAPI('listBackupOfferings', { id: this.resource.backupofferingid }).then(json => {
if (json.listbackupofferingsresponse && json.listbackupofferingsresponse.backupoffering) {
const backupoffering = json.listbackupofferingsresponse.backupoffering[0]
@ -305,6 +314,11 @@ export default {
params.schedule = [values.timeSelect.format('mm:HH'), values['day-of-month']].join(':')
break
}
if (this.submitFn) {
this.submitFn(params)
this.resetForm()
return
}
this.actionLoading = true
postAPI('createBackupSchedule', params).then(json => {
this.$notification.success({

View File

@ -0,0 +1,154 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<div>
<div>{{ $t('message.backup.provision.instance') }}</div>
<infinite-scroll-select
style="margin-top: 10px; width: 100%;"
v-model:value="localBackupOfferingId"
placeholder="Select backup offering"
api="listBackupOfferings"
:apiParams="listBackupOfferingApiParams"
resourceType="backupoffering"
defaultIcon="cloud-upload-outlined"
:defaultOption="backupOfferingDefaultOption"
@change-option="handleChangeBackupOffering" />
<div v-if="backupOfferingId && 'createBackupSchedule' in $store.getters.apis" style="margin-top: 15px">
<a-form-item :label="$t('label.schedule')">
<a-button
type="dashed"
style="width: 100%"
@click="onShowAddBackupSchedule">
<template #icon><plus-outlined /></template>
{{ $t('label.add.backup.schedule') }}
</a-button>
</a-form-item>
<backup-schedule
style="margin-top: 10px;"
:dataSource="backupSchedules"
:deleteFn="handleDeleteBackupSchedule" />
</div>
<a-modal
style="min-width: 400px;"
:visible="showAddBackupSchedule"
:title="$t('label.add.backup.schedule')"
:maskClosable="false"
:closable="true"
:footer="null"
@cancel="closeModals">
<form-schedule
:resource="addFormResource"
:submitFn="handleAddBackupSchedule" />
</a-modal>
</div>
</template>
<script>
import InfiniteScrollSelect from '@/components/widgets/InfiniteScrollSelect'
import BackupSchedule from '@views/compute/backup/BackupSchedule'
import FormSchedule from '@views/compute/backup/FormSchedule'
export default {
name: 'DeployInstanceBackupSelection',
components: {
InfiniteScrollSelect,
BackupSchedule,
FormSchedule
},
props: {
zoneId: {
type: String,
default: null
},
backupOfferingId: {
type: String,
default: null
},
backupSchedules: {
type: Array,
default: () => []
}
},
data () {
return {
backupOffering: null,
showAddBackupSchedule: false,
localBackupOfferingId: this.backupOfferingId
}
},
provide () {
return {
refreshSchedule: null,
closeSchedule: this.closeModals
}
},
emits: ['change-backup-offering', 'add-backup-schedule', 'delete-backup-schedule', 'update:backupOfferingId'],
computed: {
listBackupOfferingApiParams () {
return {
zoneid: this.zoneId
}
},
backupOfferingDefaultOption () {
return { id: null, name: this.$t('label.noselect'), showicon: false }
},
addFormResource () {
return {
id: 'NEW',
backupofferingid: this.backupOfferingId,
backupoffering: this.backupOffering
}
}
},
watch: {
localBackupOfferingId (val) {
if (val !== this.backupOfferingId) {
this.$emit('update:backupOfferingId', val)
}
},
backupOfferingId (val) {
if (val !== this.localBackupOfferingId) {
this.localBackupOfferingId = val
}
}
},
methods: {
handleChangeBackupOffering (offering) {
this.$emit('change-backup-offering', offering)
this.backupOffering = offering
},
onShowAddBackupSchedule () {
this.showAddBackupSchedule = true
},
handleAddBackupSchedule (schedule) {
schedule.id = 'SCH_' + new Date().getTime()
schedule.intervaltype = schedule.intervaltype?.toUpperCase()
this.$emit('add-backup-schedule', schedule)
this.closeModals()
},
handleDeleteBackupSchedule (schedule) {
this.$emit('delete-backup-schedule', schedule)
},
closeModals () {
this.showAddBackupSchedule = false
}
}
}
</script>