mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
Allow populating generic templates during Zone Deployment (#10947)
This commit is contained in:
parent
53eb2c5b9b
commit
65d359228d
110
ui/public/cloud-image-templates.json
vendored
Normal file
110
ui/public/cloud-image-templates.json
vendored
Normal file
@ -0,0 +1,110 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Ubuntu 24.04",
|
||||
"arch": "x86_64",
|
||||
"url": "https://download.cloudstack.org/templates/cloud-images/ubuntu/ubuntu-24.04-server-cloudimg-amd64.img"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Ubuntu 22.04",
|
||||
"arch": "x86_64",
|
||||
"url": "https://download.cloudstack.org/templates/cloud-images/ubuntu/ubuntu-22.04-server-cloudimg-amd64.img"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Ubuntu 20.04",
|
||||
"arch": "x86_64",
|
||||
"url": "https://download.cloudstack.org/templates/cloud-images/ubuntu/ubuntu-20.04-server-cloudimg-amd64.img"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Debian GNU/Linux 12 (64-bit)",
|
||||
"arch": "x86_64",
|
||||
"url": "https://download.cloudstack.org/templates/cloud-images/debian/debian-12-genericcloud-amd64.qcow2"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Rocky Linux 8",
|
||||
"arch": "x86_64",
|
||||
"url": "https://download.cloudstack.org/templates/cloud-images/rockylinux/Rocky-8-GenericCloud.latest.x86_64.qcow2"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "Rocky Linux 9",
|
||||
"arch": "x86_64",
|
||||
"url": "https://download.cloudstack.org/templates/cloud-images/rockylinux/Rocky-9-GenericCloud.latest.x86_64.qcow2"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "OpenSUSE 15.5",
|
||||
"arch": "x86_64",
|
||||
"url": "https://download.cloudstack.org/templates/cloud-images/opensuse/openSUSE-Leap-15.5-Minimal-VM.x86_64-Cloud.qcow2"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "Ubuntu 24.04",
|
||||
"arch": "aarch64",
|
||||
"url": "https://download.cloudstack.org/templates/cloud-images/ubuntu/ubuntu-24.04-server-cloudimg-arm64.img"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "Ubuntu 22.04",
|
||||
"arch": "aarch64",
|
||||
"url": "https://download.cloudstack.org/templates/cloud-images/ubuntu/ubuntu-22.04-server-cloudimg-arm64.img"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "Ubuntu 20.04",
|
||||
"arch": "aarch64",
|
||||
"url": "https://download.cloudstack.org/templates/cloud-images/ubuntu/ubuntu-20.04-server-cloudimg-arm64.img"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "Debian GNU/Linux 12 (64-bit)",
|
||||
"arch": "aarch64",
|
||||
"url": "https://download.cloudstack.org/templates/cloud-images/debian/debian-12-genericcloud-arm64.qcow2"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "Rocky Linux 8",
|
||||
"arch": "aarch64",
|
||||
"url": "https://download.cloudstack.org/templates/cloud-images/rockylinux/Rocky-8-GenericCloud.latest.aarch64.qcow2"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "Rocky Linux 9",
|
||||
"arch": "aarch64",
|
||||
"url": "https://download.cloudstack.org/templates/cloud-images/rockylinux/Rocky-9-GenericCloud.latest.aarch64.qcow2"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "OpenSUSE 15.5",
|
||||
"arch": "aarch64",
|
||||
"url": "https://download.cloudstack.org/templates/cloud-images/opensuse/openSUSE-Leap-15.5-Minimal-VM.aarch64-Cloud.qcow2"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "Oracle Linux 8",
|
||||
"arch": "aarch64",
|
||||
"url": "https://download.cloudstack.org/templates/cloud-images/oraclelinux/OL8U10_aarch64-kvm-b122.qcow2"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "Oracle Linux 8",
|
||||
"arch": "x86_64",
|
||||
"url": "https://download.cloudstack.org/templates/cloud-images/oraclelinux/OL8U10_x86_64-kvm-b258.qcow2"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": "Oracle Linux 9",
|
||||
"arch": "aarch64",
|
||||
"url": "https://download.cloudstack.org/templates/cloud-images/oraclelinux/OL9U5_aarch64-kvm-b126.qcow2"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "Oracle Linux 9",
|
||||
"arch": "x86_64",
|
||||
"url": "https://download.cloudstack.org/templates/cloud-images/oraclelinux/OL9U5_x86_64-kvm-b259.qcow2"
|
||||
}
|
||||
]
|
||||
@ -1023,6 +1023,7 @@
|
||||
"label.filter.annotations.self": "Created by me",
|
||||
"label.filterby": "Filter by",
|
||||
"label.fingerprint": "FingerPrint",
|
||||
"label.skip": "Skip",
|
||||
"label.finish": "Finish",
|
||||
"label.firewall": "Firewall",
|
||||
"label.firewall.policy": "Firewall Policy",
|
||||
@ -3100,6 +3101,7 @@
|
||||
"message.desc.importexportinstancewizard": "By choosing to manage an Instance, CloudStack takes over the orchestration of that Instance. Unmanaging an Instance removes CloudStack ability to manage it. In both cases, the Instance is left running and no changes are done to the VM on the hypervisor.<br><br>For KVM, managing a VM is an experimental feature.",
|
||||
"message.desc.importmigratefromvmwarewizard": "By selecting an existing or external VMware Datacenter and an instance to import, CloudStack migrates the selected instance from VMware to KVM on a conversion host using virt-v2v and imports it into a KVM cluster",
|
||||
"message.desc.primary.storage": "Each cluster must contain one or more primary storage servers. We will add the first one now. Primary storage contains the disk volumes for all the Instances running on hosts in the cluster. Use any standards-compliant protocol that is supported by the underlying hypervisor.",
|
||||
"message.desc.register.template": "Hosted on download.cloudstack.org, these templates can be easily registered directly within CloudStack. Simply click <strong>Register Template</strong> for the templates you wish to use.",
|
||||
"message.desc.reset.ssh.key.pair": "Please specify a ssh key pair that you would like to add to this Instance.",
|
||||
"message.desc.secondary.storage": "Each zone must have at least one NFS or secondary storage server. We will add the first one now. Secondary storage stores Instance Templates, ISO images, and Instance disk volume Snapshots. This server must be available to all hosts in the zone.<br/><br/>Provide the IP address and exported path.",
|
||||
"message.desc.register.user.data": "Please fill in the following data to register a User data.",
|
||||
@ -3810,6 +3812,7 @@
|
||||
"message.warn.change.primary.storage.scope": "This feature is tested and supported for the following configurations:<br>KVM - NFS/Ceph - DefaultPrimary<br>VMware - NFS - DefaultPrimary<br>*There might be extra steps involved to make it work for other configurations.",
|
||||
"message.warn.filetype": "jpg, jpeg, png, bmp and svg are the only supported image formats.",
|
||||
"message.warn.importing.instance.without.nic": "WARNING: This Instance is being imported without NICs and many Network resources will not be available. Consider creating a NIC via vCenter before importing or as soon as the Instance is imported.",
|
||||
"message.warn.select.template": "Please select a Template for Registration.",
|
||||
"message.warn.zone.mtu.update": "Please note that this limit won't affect pre-existing Network’s MTU settings",
|
||||
"message.webhook.deliveries.time.filter": "Webhook deliveries list can be filtered based on date-time. Select 'Custom' for specifying start and end date range.",
|
||||
"message.zone.creation.complete": "Zone creation complete.",
|
||||
|
||||
@ -74,8 +74,10 @@
|
||||
:prefillContent="zoneConfig"
|
||||
/>
|
||||
<zone-wizard-launch-zone
|
||||
v-else
|
||||
v-else-if="zoneSteps[currentStep].name === 'launch'"
|
||||
@backPressed="backPressed"
|
||||
@nextPressed="nextPressed"
|
||||
@fieldsChanged="onFieldsChanged"
|
||||
@closeAction="onCloseAction"
|
||||
@refresh-data="onRefreshData"
|
||||
@stepError="onStepError"
|
||||
@ -85,6 +87,15 @@
|
||||
:isFixError="stepFixError"
|
||||
:prefillContent="zoneConfig"
|
||||
/>
|
||||
|
||||
<zone-wizard-register-template
|
||||
v-else-if="zoneSteps[currentStep].name === 'registerTemplate'"
|
||||
:zoneid="returnedZoneId"
|
||||
:arch="selectedArch"
|
||||
:zoneSuperType="selectedZoneSuperType"
|
||||
@closeAction="onCloseAction"
|
||||
@refresh-data="onRefreshData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -95,6 +106,7 @@ import ZoneWizardCoreZoneTypeStep from '@views/infra/zone/ZoneWizardCoreZoneType
|
||||
import ZoneWizardZoneDetailsStep from '@views/infra/zone/ZoneWizardZoneDetailsStep'
|
||||
import ZoneWizardNetworkSetupStep from '@views/infra/zone/ZoneWizardNetworkSetupStep'
|
||||
import ZoneWizardAddResources from '@views/infra/zone/ZoneWizardAddResources'
|
||||
import ZoneWizardRegisterTemplate from '@views/infra/zone/ZoneWizardRegisterTemplate'
|
||||
import ZoneWizardLaunchZone from '@views/infra/zone/ZoneWizardLaunchZone'
|
||||
|
||||
export default {
|
||||
@ -103,6 +115,7 @@ export default {
|
||||
ZoneWizardCoreZoneTypeStep,
|
||||
ZoneWizardZoneDetailsStep,
|
||||
ZoneWizardNetworkSetupStep,
|
||||
ZoneWizardRegisterTemplate,
|
||||
ZoneWizardAddResources,
|
||||
ZoneWizardLaunchZone
|
||||
},
|
||||
@ -114,6 +127,13 @@ export default {
|
||||
launchZone: false,
|
||||
launchData: {},
|
||||
stepChild: '',
|
||||
registerTemplateStep: {
|
||||
name: 'registerTemplate',
|
||||
title: 'label.register.template',
|
||||
step: ['registerTemplateAction'],
|
||||
description: this.$t('message.desc.register.template'),
|
||||
hint: this.$t('message.hint.register.template')
|
||||
},
|
||||
coreZoneTypeStep: {
|
||||
name: 'coreType',
|
||||
title: 'label.core.zone.type',
|
||||
@ -158,7 +178,10 @@ export default {
|
||||
hint: this.$t('message.launch.zone.hint')
|
||||
}
|
||||
],
|
||||
zoneConfig: {}
|
||||
zoneConfig: {},
|
||||
returnedZoneId: null,
|
||||
selectedArch: null,
|
||||
selectedZoneSuperType: 'Core'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -167,6 +190,9 @@ export default {
|
||||
if (this.zoneConfig.zoneSuperType !== 'Edge') {
|
||||
steps.splice(1, 0, this.coreZoneTypeStep)
|
||||
}
|
||||
if (this.zoneConfig.hypervisor === 'KVM') {
|
||||
steps.splice(steps.length, 0, this.registerTemplateStep)
|
||||
}
|
||||
return steps
|
||||
}
|
||||
},
|
||||
@ -200,7 +226,15 @@ export default {
|
||||
data.zoneType !== this.zoneConfig.zoneType) {
|
||||
this.zoneConfig.physicalNetworks = null
|
||||
}
|
||||
|
||||
if (data.zoneSuperType && data.zoneSuperType !== this.selectedZoneSuperType) {
|
||||
this.selectedZoneSuperType = data.zoneSuperType
|
||||
}
|
||||
if (data.arch && data.arch !== this.selectedArch) {
|
||||
this.selectedArch = data.arch
|
||||
}
|
||||
if (data.zoneReturned?.id) {
|
||||
this.returnedZoneId = data.zoneReturned.id
|
||||
}
|
||||
this.zoneConfig = { ...this.zoneConfig, ...data }
|
||||
},
|
||||
onCloseAction () {
|
||||
|
||||
@ -1723,8 +1723,13 @@ export default {
|
||||
await this.$message.success('Success')
|
||||
this.loading = false
|
||||
this.steps = []
|
||||
this.$emit('closeAction')
|
||||
this.$emit('refresh-data')
|
||||
if (this.prefillContent.hypervisor === 'KVM') {
|
||||
this.$emit('fieldsChanged', { zoneReturned: { id: this.stepData.zoneReturned.id } })
|
||||
this.$emit('nextPressed')
|
||||
} else {
|
||||
this.$emit('closeAction')
|
||||
this.$emit('refresh-data')
|
||||
}
|
||||
} catch (e) {
|
||||
this.loading = false
|
||||
await this.$notification.error({
|
||||
|
||||
298
ui/src/views/infra/zone/ZoneWizardRegisterTemplate.vue
Normal file
298
ui/src/views/infra/zone/ZoneWizardRegisterTemplate.vue
Normal file
@ -0,0 +1,298 @@
|
||||
// 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>
|
||||
<a-card class="ant-form-text" style="text-align: justify; margin: 10px 0; padding: 15px;" v-html="$t('message.desc.register.template')" />
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:dataSource="predefinedTemplates"
|
||||
:rowSelection="rowSelection"
|
||||
:loading="loading"
|
||||
:scroll="{ y: 450 }"
|
||||
size="middle"
|
||||
:rowKey="record => record.id"
|
||||
:pagination="false"
|
||||
class="form-content"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'name'">
|
||||
<os-logo :osName="record.name" size="xl" />
|
||||
{{ record.name }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<div class="form-action">
|
||||
<a-button class="button-back" @click="handleDone">{{ selectedRowKeys.length > 0 ? $t('label.done') : $t('label.skip') }}</a-button>
|
||||
<a-button class="button-next" type="primary" @click="handleSubmit" ref="submit">{{ $t('label.register.template') }}</a-button>
|
||||
</div>
|
||||
|
||||
<a-modal
|
||||
:visible="showAlert"
|
||||
:footer="null"
|
||||
style="top: 20px;"
|
||||
centered
|
||||
width="auto"
|
||||
@cancel="showAlert = false"
|
||||
>
|
||||
<template #title>
|
||||
{{ $t('label.warning') }}
|
||||
</template>
|
||||
<a-alert type="warning">
|
||||
<template #message>
|
||||
<span v-html="$t('message.warn.select.template')" />
|
||||
</template>
|
||||
</a-alert>
|
||||
<a-divider style="margin-top: 0;"></a-divider>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from '@/api'
|
||||
import { genericCompare } from '@/utils/sort.js'
|
||||
import OsLogo from '@/components/widgets/OsLogo'
|
||||
|
||||
export default {
|
||||
name: 'ZoneWizardRegisterTemplate',
|
||||
components: {
|
||||
OsLogo
|
||||
},
|
||||
props: {
|
||||
zoneid: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
arch: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
zoneSuperType: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
columns: null,
|
||||
loading: false,
|
||||
predefinedTemplates: [],
|
||||
rowKey: 0,
|
||||
selectedRowKeys: [],
|
||||
defaultOsTypeId: null,
|
||||
deployedTemplates: {},
|
||||
showAlert: false
|
||||
}),
|
||||
created () {
|
||||
this.initForm()
|
||||
},
|
||||
mounted () {
|
||||
this.fetchPredefinedTemplates()
|
||||
},
|
||||
computed: {
|
||||
rowSelection () {
|
||||
return {
|
||||
selectedRowKeys: this.selectedRowKeys || [],
|
||||
onChange: this.onSelectRow,
|
||||
getCheckboxProps: (record) => {
|
||||
return {
|
||||
disabled: record.disabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async initForm () {
|
||||
this.columns = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 240,
|
||||
sorter: (a, b) => { return genericCompare(a.name || '', b.name || '') }
|
||||
},
|
||||
{
|
||||
title: 'Arch',
|
||||
dataIndex: 'arch',
|
||||
key: 'arch',
|
||||
width: 80,
|
||||
sorter: (a, b) => { return genericCompare(a.arch || '', b.arch || '') }
|
||||
},
|
||||
{
|
||||
title: 'URL',
|
||||
dataIndex: 'url',
|
||||
key: 'url'
|
||||
}
|
||||
]
|
||||
this.defaultOsTypeId = await this.fetchOsTypeId('Other Linux (64-bit)')
|
||||
},
|
||||
handleDone () {
|
||||
this.$emit('refresh-data')
|
||||
},
|
||||
async handleSubmit () {
|
||||
await this.stepRegisterTemplates()
|
||||
},
|
||||
onSelectRow (value) {
|
||||
this.selectedRowKeys = value
|
||||
},
|
||||
async registerTemplate (templateData) {
|
||||
const params = {
|
||||
displaytext: templateData.name + ' ' + templateData.arch,
|
||||
format: this.getImageFormat(templateData.url),
|
||||
hypervisor: 'KVM',
|
||||
name: templateData.name,
|
||||
arch: templateData.arch,
|
||||
url: templateData.url,
|
||||
ostypeid: await this.fetchOsTypeId(templateData.name),
|
||||
zoneid: this.zoneid
|
||||
}
|
||||
if (this.zoneSuperType === 'Edge') {
|
||||
params.directdownload = true
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
api('registerTemplate', params).then(json => {
|
||||
const result = json.registertemplateresponse.template[0]
|
||||
resolve(result)
|
||||
}).catch(error => {
|
||||
const message = error.response.headers['x-description']
|
||||
reject(message)
|
||||
})
|
||||
})
|
||||
},
|
||||
async stepRegisterTemplates () {
|
||||
const templatesToRegister = this.predefinedTemplates.filter(template => this.selectedRowKeys.includes(template.id) && this.deployedTemplates[template.id] !== true)
|
||||
if (templatesToRegister.length === 0) {
|
||||
this.showAlert = true
|
||||
return
|
||||
}
|
||||
const registrationResults = []
|
||||
for (const templateData of templatesToRegister) {
|
||||
const promise = this.registerTemplate(templateData)
|
||||
.then(() => ({
|
||||
id: templateData.id,
|
||||
status: 'success',
|
||||
name: templateData.name
|
||||
}))
|
||||
.catch(() => ({
|
||||
id: templateData.id,
|
||||
status: 'error',
|
||||
name: templateData.name
|
||||
}))
|
||||
registrationResults.push(promise)
|
||||
}
|
||||
const results = await Promise.all(registrationResults)
|
||||
const successful = results.filter(r => r.status === 'success')
|
||||
const failed = results.filter(r => r.status === 'error')
|
||||
|
||||
if (successful.length > 0) {
|
||||
this.$notification.success({
|
||||
message: this.$t('label.register.template'),
|
||||
description: 'Succesfully registered templates: ' + successful.map(r => r.name).join(', ')
|
||||
})
|
||||
|
||||
successful.forEach(r => {
|
||||
this.deployedTemplates[r.id] = true
|
||||
this.predefinedTemplates.find(t => t.id === r.id).disabled = true
|
||||
})
|
||||
}
|
||||
if (failed.length > 0) {
|
||||
this.$notification.error({
|
||||
message: this.$t('label.register.template'),
|
||||
description: 'Failed registering templates: ' + failed.map(r => r.name).join(', ')
|
||||
})
|
||||
|
||||
failed.forEach(r => {
|
||||
this.predefinedTemplates.find(t => t.id === r.id).disabled = true
|
||||
this.selectedRowKeys = this.selectedRowKeys.filter(id => id !== r.id)
|
||||
})
|
||||
}
|
||||
},
|
||||
async fetchOsTypeId (osName) {
|
||||
let osTypeId = this.defaultOsTypeId
|
||||
this.loading = true
|
||||
try {
|
||||
const json = await api('listOsTypes', { keyword: osName, filter: 'name,id' })
|
||||
if (json && json.listostypesresponse && json.listostypesresponse.ostype && json.listostypesresponse.ostype.length > 0) {
|
||||
osTypeId = json.listostypesresponse.ostype[0].id
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching OS types:', error)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
return osTypeId
|
||||
},
|
||||
getImageFormat (url) {
|
||||
const fileExtension = url.split('.').pop()
|
||||
var format = fileExtension
|
||||
switch (fileExtension) {
|
||||
case 'img':
|
||||
format = 'RAW'
|
||||
break
|
||||
case 'qcow2':
|
||||
format = 'qcow2'
|
||||
break
|
||||
default:
|
||||
format = 'RAW'
|
||||
}
|
||||
return format
|
||||
},
|
||||
async fetchPredefinedTemplates () {
|
||||
this.loading = true
|
||||
try {
|
||||
const response = await fetch('./cloud-image-templates.json')
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error fetching predefined templates, status_code: ${response.status}`)
|
||||
}
|
||||
const templates = await response.json()
|
||||
this.predefinedTemplates = this.arch
|
||||
? templates.filter(template => template.arch === this.arch)
|
||||
: templates
|
||||
|
||||
// Replace 'https' with 'http' in all URLs for EdgeZone
|
||||
if (this.zoneSuperType === 'Edge') {
|
||||
this.predefinedTemplates.forEach(template => {
|
||||
if (template.url.startsWith('https://')) {
|
||||
template.url = template.url.replace('https://', 'http://')
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching predefined templates:', error)
|
||||
this.predefinedTemplates = []
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.form-content {
|
||||
border: 1px dashed #e9e9e9;
|
||||
border-radius: 6px;
|
||||
background-color: #fafafa;
|
||||
min-height: 440px;
|
||||
text-align: center;
|
||||
vertical-align: center;
|
||||
padding: 8px;
|
||||
padding-top: 16px;
|
||||
margin-top: 8px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
Loading…
x
Reference in New Issue
Block a user