Skip to content

Commit cdd1250

Browse files
committed
vpc & backup offering clone api
1 parent 7fd4567 commit cdd1250

File tree

4 files changed

+1223
-0
lines changed

4 files changed

+1223
-0
lines changed

ui/public/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,10 +559,12 @@
559559
"label.clear.list": "Clear list",
560560
"label.clear.notification": "Clear notification",
561561
"label.clientid": "Provider Client ID",
562+
"label.clone.backup.offering": "Clone Backup Offering",
562563
"label.clone.compute.offering": "Clone Compute Offering",
563564
"label.clone.disk.offering": "Clone Disk Offering",
564565
"label.clone.network.offering": "Clone Network Offering",
565566
"label.clone.system.service.offering": "Clone System Service Offering",
567+
"label.clone.vpc.offering": "Clone VPC Offering",
566568
"label.close": "Close",
567569
"label.cloud.managed": "CloudManaged",
568570
"label.cloudian.admin.password": "Admin Service Password",

ui/src/config/section/offering.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,14 @@ export default {
401401
popup: true,
402402
groupMap: (selection) => { return selection.map(x => { return { id: x } }) },
403403
args: ['name', 'description', 'allowuserdrivenbackups']
404+
}, {
405+
api: 'cloneBackupOffering',
406+
icon: 'copy-outlined',
407+
label: 'label.clone.backup.offering',
408+
docHelp: 'adminguide/virtual_machines.html#importing-backup-offerings',
409+
dataView: true,
410+
popup: true,
411+
component: shallowRef(defineAsyncComponent(() => import('@/views/offering/ImportBackupOffering.vue')))
404412
}, {
405413
api: 'deleteBackupOffering',
406414
icon: 'delete-outlined',
@@ -612,6 +620,14 @@ export default {
612620
dataView: true,
613621
popup: true,
614622
component: shallowRef(defineAsyncComponent(() => import('@/views/offering/UpdateOfferingAccess.vue')))
623+
}, {
624+
api: 'cloneVPCOffering',
625+
icon: 'copy-outlined',
626+
docHelp: 'plugins/nuage-plugin.html?#optional-create-and-enable-vpc-offering',
627+
label: 'label.clone.vpc.offering',
628+
dataView: true,
629+
popup: true,
630+
component: shallowRef(defineAsyncComponent(() => import('@/views/offering/CloneVpcOffering.vue')))
615631
}, {
616632
api: 'deleteVPCOffering',
617633
icon: 'delete-outlined',
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
<template>
19+
<div class="form-layout" v-ctrl-enter="handleSubmit">
20+
<a-spin :spinning="loading">
21+
<a-alert
22+
v-if="resource"
23+
type="info"
24+
style="margin-bottom: 16px">
25+
<template #message>
26+
<div style="display: block; width: 100%;">
27+
<div style="display: block; margin-bottom: 8px;">
28+
<strong>{{ $t('message.clone.offering.from') }}: {{ resource.name }}</strong>
29+
</div>
30+
<div style="display: block; font-size: 12px;">
31+
{{ $t('message.clone.offering.edit.hint') }}
32+
</div>
33+
</div>
34+
</template>
35+
</a-alert>
36+
<a-form
37+
layout="vertical"
38+
:ref="formRef"
39+
:model="form"
40+
:rules="rules"
41+
@finish="handleSubmit"
42+
>
43+
<a-form-item name="name" ref="name">
44+
<template #label>
45+
<tooltip-label :title="$t('label.name')" :tooltip="apiParams.name.description"/>
46+
</template>
47+
<a-input
48+
v-focus="true"
49+
v-model:value="form.name"/>
50+
</a-form-item>
51+
<a-form-item name="description" ref="description">
52+
<template #label>
53+
<tooltip-label :title="$t('label.description')" :tooltip="apiParams.description.description"/>
54+
</template>
55+
<a-input v-model:value="form.description"/>
56+
</a-form-item>
57+
<a-form-item name="zoneid" ref="zoneid">
58+
<template #label>
59+
<tooltip-label :title="$t('label.zoneid')" :tooltip="apiParams.zoneid ? apiParams.zoneid.description : ''"/>
60+
</template>
61+
<a-select
62+
allowClear
63+
v-model:value="form.zoneid"
64+
:loading="zones.loading"
65+
@change="onChangeZone"
66+
showSearch
67+
optionFilterProp="label"
68+
:filterOption="(input, option) => {
69+
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
70+
}" >
71+
<a-select-option v-for="zone in zones.opts" :key="zone.name" :label="zone.name">
72+
<span>
73+
<resource-icon v-if="zone.icon" :image="zone.icon.base64image" size="1x" style="margin-right: 5px"/>
74+
<global-outlined v-else style="margin-right: 5px"/>
75+
{{ zone.name }}
76+
</span>
77+
</a-select-option>
78+
</a-select>
79+
</a-form-item>
80+
<a-form-item name="externalid" ref="externalid">
81+
<template #label>
82+
<tooltip-label :title="$t('label.externalid')" :tooltip="apiParams.externalid ? apiParams.externalid.description : ''"/>
83+
</template>
84+
<a-select
85+
allowClear
86+
v-model:value="form.externalid"
87+
:loading="externals.loading"
88+
showSearch
89+
optionFilterProp="label"
90+
:filterOption="(input, option) => {
91+
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
92+
}" >
93+
<a-select-option v-for="opt in externals.opts" :key="opt.id" :label="opt.name">
94+
{{ opt.name }}
95+
</a-select-option>
96+
</a-select>
97+
</a-form-item>
98+
<a-form-item name="allowuserdrivenbackups" ref="allowuserdrivenbackups">
99+
<template #label>
100+
<tooltip-label :title="$t('label.allowuserdrivenbackups')" :tooltip="apiParams.allowuserdrivenbackups ? apiParams.allowuserdrivenbackups.description : ''"/>
101+
</template>
102+
<a-switch v-model:checked="form.allowuserdrivenbackups"/>
103+
</a-form-item>
104+
<div :span="24" class="action-button">
105+
<a-button :loading="loading" @click="closeAction">{{ $t('label.cancel') }}</a-button>
106+
<a-button :loading="loading" ref="submit" type="primary" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
107+
</div>
108+
</a-form>
109+
</a-spin>
110+
</div>
111+
</template>
112+
113+
<script>
114+
import { ref, reactive, toRaw } from 'vue'
115+
import { getAPI, postAPI } from '@/api'
116+
import ResourceIcon from '@/components/view/ResourceIcon'
117+
import TooltipLabel from '@/components/widgets/TooltipLabel'
118+
import { GlobalOutlined } from '@ant-design/icons-vue'
119+
120+
export default {
121+
name: 'CloneBackupOffering',
122+
components: {
123+
TooltipLabel,
124+
ResourceIcon,
125+
GlobalOutlined
126+
},
127+
props: {
128+
resource: {
129+
type: Object,
130+
required: true
131+
}
132+
},
133+
data () {
134+
return {
135+
loading: false,
136+
zones: {
137+
loading: false,
138+
opts: []
139+
},
140+
externals: {
141+
loading: false,
142+
opts: []
143+
}
144+
}
145+
},
146+
beforeCreate () {
147+
this.apiParams = this.$getApiParams('cloneBackupOffering')
148+
},
149+
created () {
150+
this.initForm()
151+
this.fetchData()
152+
},
153+
methods: {
154+
initForm () {
155+
this.formRef = ref()
156+
this.form = reactive({
157+
allowuserdrivenbackups: true
158+
})
159+
this.rules = reactive({
160+
name: [{ required: true, message: this.$t('message.error.required.input') }],
161+
description: [{ required: true, message: this.$t('message.error.required.input') }]
162+
})
163+
},
164+
fetchData () {
165+
this.fetchZone()
166+
this.$nextTick(() => {
167+
this.populateFormFromResource()
168+
})
169+
},
170+
fetchZone () {
171+
this.zones.loading = true
172+
getAPI('listZones', { available: true, showicon: true }).then(json => {
173+
this.zones.opts = json.listzonesresponse.zone || []
174+
this.$nextTick(() => {
175+
this.populateFormFromResource()
176+
})
177+
}).catch(error => {
178+
this.$notifyError(error)
179+
}).finally(() => {
180+
this.zones.loading = false
181+
})
182+
},
183+
fetchExternal (zoneId) {
184+
if (!zoneId) {
185+
this.externals.opts = []
186+
return
187+
}
188+
this.externals.loading = true
189+
getAPI('listBackupProviderOfferings', { zoneid: zoneId }).then(json => {
190+
this.externals.opts = json.listbackupproviderofferingsresponse.backupoffering || []
191+
}).catch(error => {
192+
this.$notifyError(error)
193+
}).finally(() => {
194+
this.externals.loading = false
195+
})
196+
},
197+
populateFormFromResource () {
198+
if (!this.resource) return
199+
200+
const r = this.resource
201+
202+
this.form.name = r.name + ' - Clone'
203+
this.form.description = r.description || r.name
204+
205+
if (r.allowuserdrivenbackups !== undefined) {
206+
this.form.allowuserdrivenbackups = r.allowuserdrivenbackups
207+
}
208+
209+
if (r.zoneid && this.zones.opts.length > 0) {
210+
const zone = this.zones.opts.find(z => z.id === r.zoneid)
211+
if (zone) {
212+
this.form.zoneid = zone.name
213+
this.fetchExternal(zone.id)
214+
}
215+
}
216+
217+
if (r.externalid) {
218+
this.$nextTick(() => {
219+
this.form.externalid = r.externalid
220+
})
221+
}
222+
},
223+
handleSubmit (e) {
224+
if (e) {
225+
e.preventDefault()
226+
}
227+
if (this.loading) return
228+
229+
this.formRef.value.validate().then(() => {
230+
const values = toRaw(this.form)
231+
const params = {
232+
sourceofferingid: this.resource.id
233+
}
234+
235+
if (values.name) {
236+
params.name = values.name
237+
}
238+
239+
if (values.description) {
240+
params.description = values.description
241+
}
242+
243+
if (values.zoneid) {
244+
const zone = this.zones.opts.find(z => z.name === values.zoneid)
245+
if (zone) {
246+
params.zoneid = zone.id
247+
}
248+
}
249+
250+
if (values.externalid) {
251+
params.externalid = values.externalid
252+
}
253+
254+
if (values.allowuserdrivenbackups !== undefined) {
255+
params.allowuserdrivenbackups = values.allowuserdrivenbackups
256+
}
257+
258+
this.loading = true
259+
const title = this.$t('message.success.clone.backup.offering')
260+
261+
postAPI('cloneBackupOffering', params).then(json => {
262+
const jobId = json.clonebackupofferingresponse?.jobid
263+
if (jobId) {
264+
this.$pollJob({
265+
jobId,
266+
title,
267+
description: values.name,
268+
successMethod: result => {
269+
this.$message.success(`${title}: ${values.name}`)
270+
this.$emit('refresh-data')
271+
this.closeAction()
272+
},
273+
loadingMessage: `${title} ${this.$t('label.in.progress')} ${this.$t('label.for')} ${params.name}`,
274+
catchMessage: this.$t('error.fetching.async.job.result'),
275+
catchMethod: () => {
276+
this.closeAction()
277+
}
278+
})
279+
} else {
280+
this.$message.success(`${title}: ${values.name}`)
281+
this.$emit('refresh-data')
282+
this.closeAction()
283+
}
284+
}).catch(error => {
285+
this.$notifyError(error)
286+
}).finally(() => {
287+
this.loading = false
288+
})
289+
}).catch(error => {
290+
this.formRef.value.scrollToField(error.errorFields[0].name)
291+
})
292+
},
293+
onChangeZone (value) {
294+
if (!value) {
295+
this.externals.opts = []
296+
this.form.externalid = null
297+
return
298+
}
299+
const zone = this.zones.opts.find(z => z.name === value)
300+
if (zone) {
301+
this.fetchExternal(zone.id)
302+
}
303+
},
304+
closeAction () {
305+
this.$emit('close-action')
306+
}
307+
}
308+
}
309+
</script>
310+
311+
<style scoped lang="less">
312+
.form-layout {
313+
width: 30vw;
314+
315+
@media (min-width: 500px) {
316+
width: 450px;
317+
}
318+
}
319+
</style>
320+

0 commit comments

Comments
 (0)