compute: VM deployment wizard fixes (#307)

- Fix reactive changes on zone selection
- Template filter on left side of search box
- Allow group name
- Ability to add network while deploying VM
- Show password if VM deployment returns password

Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
Co-authored-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
Hoang Nguyen 2020-05-23 04:20:43 +07:00 committed by Rohit Yadav
parent 7d447e6806
commit b8a22f27ae
11 changed files with 167 additions and 98 deletions

View File

@ -344,6 +344,13 @@
<span v-if="index + 1 < resource.affinitygroup.length">, </span>
</span>
</div>
<div class="resource-detail-item" v-if="resource.templateid">
<div class="resource-detail-item__label">{{ $t('templatename') }}</div>
<div class="resource-detail-item__details">
<a-icon type="picture" />
<router-link :to="{ path: '/template/' + resource.templateid }">{{ resource.templatename || resource.templateid }} </router-link>
</div>
</div>
<div class="resource-detail-item" v-if="resource.serviceofferingname && resource.serviceofferingid">
<div class="resource-detail-item__label">{{ $t('serviceofferingname') }}</div>
<div class="resource-detail-item__details">
@ -352,13 +359,6 @@
<span v-else>{{ resource.serviceofferingname || resource.serviceofferingid }}</span>
</div>
</div>
<div class="resource-detail-item" v-if="resource.templateid">
<div class="resource-detail-item__label">{{ $t('templatename') }}</div>
<div class="resource-detail-item__details">
<a-icon type="picture" />
<router-link :to="{ path: '/template/' + resource.templateid }">{{ resource.templatename || resource.templateid }} </router-link>
</div>
</div>
<div class="resource-detail-item" v-if="resource.diskofferingname && resource.diskofferingid">
<div class="resource-detail-item__label">{{ $t('diskoffering') }}</div>
<div class="resource-detail-item__details">

View File

@ -481,6 +481,7 @@
"label.add.ldap.list.users": "List LDAP users",
"label.add.list.name":"ACL List Name",
"label.add.netScaler.device": "Add Netscaler device",
"label.add.network":"Add Network",
"label.add.network.offering": "Add network offering",
"label.add.new.gateway": "Add new gateway",
"label.add.new.tier": "Add new tier",
@ -1308,5 +1309,7 @@
"writeback": "Write-back disk caching",
"writethrough": "Write-through",
"none": "None",
"maxcpunumber": "Max CPU Cores"
"maxcpunumber": "Max CPU Cores",
"message.template.iso": "Please select a template or ISO to continue",
"label.launch.vm": "Launch Virtual Machine"
}

View File

@ -49,7 +49,7 @@
}"
:loading="zoneLoading"
:placeholder="apiParams.zoneid.description"
@change="val => { this.handleZoneChanged(this.zones[val]) }">
@change="val => { this.handleZoneChange(this.zones[val]) }">
<a-select-option v-for="(opt, optIndex) in this.zones" :key="optIndex">
{{ opt.name || opt.description }}
</a-select-option>

View File

@ -72,11 +72,7 @@
></a-select>
</a-form-item>
<a-form-item :label="this.$t('group')">
<a-select
v-decorator="['group']"
:options="groupsSelectOptions"
:loading="loading.groups"
></a-select>
<a-input v-decorator="['group']" />
</a-form-item>
<a-form-item :label="this.$t('keyboard')">
<a-select
@ -122,8 +118,7 @@
:selected="tabKey"
:loading="loading.isos"
:preFillContent="dataPreFill"
@update-template-iso="updateFieldValue"
></template-iso-selection>
@update-template-iso="updateFieldValue" />
<a-form-item :label="this.$t('hypervisor')">
<a-select
v-decorator="['hypervisor', {
@ -133,9 +128,7 @@
rules: [{ required: true, message: 'Please select option' }]
}]"
:options="hypervisorSelectOptions"
@change="value => this.hypervisor = value"
>
</a-select>
@change="value => this.hypervisor = value" />
</a-form-item>
</p>
</a-card>
@ -318,8 +311,8 @@ import { mixin, mixinDevice } from '@/utils/mixin.js'
import store from '@/store'
import InfoCard from '@/components/view/InfoCard'
import ComputeOfferingSelection from './wizard/ComputeOfferingSelection'
import ComputeSelection from './wizard/ComputeSelection'
import ComputeOfferingSelection from '@views/compute/wizard/ComputeOfferingSelection'
import ComputeSelection from '@views/compute/wizard/ComputeSelection'
import DiskOfferingSelection from '@views/compute/wizard/DiskOfferingSelection'
import DiskSizeSelection from '@views/compute/wizard/DiskSizeSelection'
import TemplateIsoSelection from '@views/compute/wizard/TemplateIsoSelection'
@ -424,6 +417,7 @@ export default {
initDataConfig: {},
defaultNetwork: '',
networkConfig: [],
dataNetworkCreated: [],
tabList: [
{
key: 'templateid',
@ -548,14 +542,6 @@ export default {
type: 'Routing'
},
field: 'hostid'
},
groups: {
list: 'listInstanceGroups',
options: {
listall: false
},
isLoad: true,
field: 'group'
}
}
},
@ -609,14 +595,6 @@ export default {
value: keyboard.id
}
})
},
groupsSelectOptions () {
return this.options.groups.map((group) => {
return {
label: group.name,
value: group.id
}
})
}
},
watch: {
@ -854,10 +832,23 @@ export default {
handleSubmit (e) {
console.log('wizard submit')
e.preventDefault()
this.form.validateFields((err, values) => {
this.form.validateFields(async (err, values) => {
if (err) {
return
}
if (!values.templateid && !values.isoid) {
this.$notification.error({
message: 'Request Failed',
description: this.$t('message.template.iso')
})
return
}
this.loading.deploy = true
let networkIds = []
const deployVmData = {}
// step 1 : select zone
deployVmData.zoneid = values.zoneid
@ -902,15 +893,30 @@ export default {
// step 5: select an affinity group
deployVmData.affinitygroupids = (values.affinitygroupids || []).join(',')
// step 6: select network
if (values.networkids && values.networkids.length > 0) {
for (let i = 0; i < values.networkids.length; i++) {
deployVmData['iptonetworklist[' + i + '].networkid'] = values.networkids[i]
if (this.networkConfig.length > 0) {
const networkConfig = this.networkConfig.filter((item) => item.key === values.networkids[i])
if (networkConfig && networkConfig.length > 0) {
deployVmData['iptonetworklist[' + i + '].ip'] = networkConfig[0].ipAddress ? networkConfig[0].ipAddress : undefined
deployVmData['iptonetworklist[' + i + '].mac'] = networkConfig[0].macAddress ? networkConfig[0].macAddress : undefined
const arrNetwork = []
networkIds = values.networkids
if (networkIds.length > 0) {
for (let i = 0; i < networkIds.length; i++) {
if (networkIds[i] === this.defaultNetwork) {
const ipToNetwork = {
networkid: this.defaultNetwork
}
arrNetwork.unshift(ipToNetwork)
} else {
const ipToNetwork = {
networkid: networkIds[i]
}
arrNetwork.push(ipToNetwork)
}
}
}
for (let j = 0; j < arrNetwork.length; j++) {
deployVmData['iptonetworklist[' + j + '].networkid'] = arrNetwork[j].networkid
if (this.networkConfig.length > 0) {
const networkConfig = this.networkConfig.filter((item) => item.key === arrNetwork[j].networkid)
if (networkConfig && networkConfig.length > 0) {
deployVmData['iptonetworklist[' + j + '].ip'] = networkConfig[0].ipAddress ? networkConfig[0].ipAddress : undefined
deployVmData['iptonetworklist[' + j + '].mac'] = networkConfig[0].macAddress ? networkConfig[0].macAddress : undefined
}
}
}
@ -918,31 +924,34 @@ export default {
deployVmData.keypair = values.keypair
deployVmData.name = values.name
deployVmData.displayname = values.name
const title = this.$t('Launch Virtual Machine')
const description = deployVmData.name ? deployVmData.name : values.zoneid
this.loading.deploy = true
const title = this.$t('label.launch.vm')
const description = values.name || ''
const password = this.$t('password')
api('deployVirtualMachine', deployVmData).then(response => {
const jobId = response.deployvirtualmachineresponse.jobid
if (jobId) {
this.$pollJob({
jobId,
successMethod: result => {
let successDescription = ''
if (result.jobresult.virtualmachine.name) {
successDescription = result.jobresult.virtualmachine.name
} else {
successDescription = result.jobresult.virtualmachine.id
const vm = result.jobresult.virtualmachine
const name = vm.displayname || vm.name || vm.id
if (vm.password) {
this.$notification.success({
message: password + ' for ' + name,
description: vm.password,
duration: 0
})
}
this.$store.dispatch('AddAsyncJob', {
title: title,
jobid: jobId,
description: successDescription,
status: 'progress'
})
},
loadingMessage: `${title} in progress for ${description}`,
loadingMessage: `${title} in progress`,
catchMessage: 'Error encountered while fetching async job result'
})
this.$store.dispatch('AddAsyncJob', {
title: title,
jobid: jobId,
description: description,
status: 'progress'
})
}
this.$router.back()
}).catch(error => {
@ -1036,12 +1045,12 @@ export default {
})
})
},
fetchAllTemplates (filterKey) {
fetchAllTemplates (filterKeys) {
const promises = []
this.options.templates = []
this.loading.templates = true
this.templateFilter.forEach((filter) => {
if (filterKey && filterKey !== filter) {
if (filterKeys && !filterKeys.includes(filter)) {
return true
}
promises.push(this.fetchTemplates(filter))
@ -1095,7 +1104,7 @@ export default {
this.tabKey = 'templateid'
_.each(this.params, (param, name) => {
if (!('isLoad' in param) || param.isLoad) {
this.fetchOptions(param, name, ['zones', 'groups'])
this.fetchOptions(param, name, ['zones'])
}
})
this.fetchAllTemplates()

View File

@ -137,8 +137,10 @@ export default {
this.selectedRowKeys = [this.preFillContent.computeofferingid]
this.$emit('select-compute-item', this.preFillContent.computeofferingid)
} else {
this.selectedRowKeys = []
this.$emit('select-compute-item', null)
if (this.computeItems && this.computeItems.length > 0) {
this.selectedRowKeys = [this.computeItems[0].id]
this.$emit('select-compute-item', this.computeItems[0].id)
}
}
}
}

View File

@ -19,7 +19,7 @@
<a-table
:columns="columns"
:dataSource="dataItems"
:pagination="{showSizeChanger: true}"
:pagination="false"
:rowSelection="rowSelection"
:rowKey="record => record.id"
size="middle"
@ -64,7 +64,7 @@ export default {
{
dataIndex: 'name',
title: this.$t('defaultNetwork'),
width: '40%'
width: '30%'
},
{
dataIndex: 'ip',
@ -88,8 +88,10 @@ export default {
},
created () {
this.dataItems = this.items
this.selectedRowKeys = [this.dataItems[0].id]
this.$emit('select-default-network-item', this.selectedRowKeys)
if (this.dataItems.length > 0) {
this.selectedRowKeys = [this.dataItems[0].id]
this.$emit('select-default-network-item', this.dataItems[0].id)
}
},
computed: {
rowSelection () {
@ -112,6 +114,7 @@ export default {
const keyEx = this.dataItems.filter((item) => this.selectedRowKeys.includes(item.id))
if (!keyEx || keyEx.length === 0) {
this.selectedRowKeys = [this.dataItems[0].id]
this.$emit('select-default-network-item', this.dataItems[0].id)
}
}
}
@ -122,7 +125,8 @@ export default {
this.$emit('select-default-network-item', value[0])
},
updateNetworkData (name, key, value) {
if (this.networks.length === 0) {
const index = this.networks.findIndex(item => item.key === key)
if (index === -1) {
const networkItem = {}
networkItem.key = key
networkItem[name] = value
@ -137,6 +141,15 @@ export default {
}
})
this.$emit('update-network-config', this.networks)
},
removeItem (id) {
this.dataItems = this.dataItems.filter(item => item.id !== id)
if (this.selectedRowKeys.includes(id)) {
if (this.dataItems && this.dataItems.length > 0) {
this.selectedRowKeys = [this.dataItems[0].id]
this.$emit('select-default-network-item', this.dataItems[0].id)
}
}
}
}
}

View File

@ -18,17 +18,13 @@
<template>
<div>
<a-input-search
style="width: 25vw;float: right;margin-bottom: 10px; z-index: 8"
style="width: 25vw; float: right; margin-bottom: 10px; z-index: 8"
placeholder="Search"
v-model="filter"
@search="handleSearch" />
<a-tooltip
arrowPointAtCenter
placement="bottomRight">
<template slot="title">
{{ $t('addNewNetworks') }}
</template>
</a-tooltip>
<a-button type="primary" @click="showCreateForm = true" style="float: right; margin-right: 5px; z-index: 8">
{{ $t('label.add.network') }}
</a-button>
<a-table
:loading="loading"
:columns="columns"
@ -55,6 +51,20 @@
</a-list-item>
</a-list>
</a-table>
<a-modal
:visible="showCreateForm"
:title="$t('label.add.network')"
:closable="true"
:footer="null"
@cancel="showCreateForm = false"
centered
width="auto">
<create-network
:resource="{}"
@refresh-data="handleSearch"
@close-action="showCreateForm = false"
/>
</a-modal>
</div>
</template>
@ -62,9 +72,13 @@
import _ from 'lodash'
import { api } from '@/api'
import store from '@/store'
import CreateNetwork from '@/views/network/CreateNetwork'
export default {
name: 'NetworkSelection',
components: {
CreateNetwork
},
props: {
items: {
type: Array,
@ -96,7 +110,8 @@ export default {
networkOffering: {
loading: false,
opts: []
}
},
showCreateForm: false
}
},
computed: {
@ -173,8 +188,13 @@ export default {
this.selectedRowKeys = this.preFillContent.networkids
this.$emit('select-network-item', this.preFillContent.networkids)
} else {
this.selectedRowKeys = []
this.$emit('select-network-item', null)
if (this.items && this.items.length > 0) {
this.selectedRowKeys = [this.items[0].id]
this.$emit('select-network-item', this.selectedRowKeys)
} else {
this.selectedRowKeys = []
this.$emit('select-network-item', [])
}
}
}
}

View File

@ -37,6 +37,7 @@
<a-form-item :label="$t('filter')">
<a-select
allowClear
mode="multiple"
v-decorator="['filter']">
<a-select-option
v-for="(opt) in filterOpts"
@ -244,21 +245,23 @@ export default {
if (err) {
return
}
if (this.inputDecorator === 'template') {
this.vmFetchTemplates(values.filter)
} else {
this.vmFetchIsos(values.filter)
}
const filtered = values.filter || []
this.filter = ''
filtered.map(item => {
if (this.filter.length === 0) {
this.filter += 'is:' + item
} else {
this.filter += '; is:' + item
}
})
this.filterDataSource(this.filter)
})
},
onClear () {
const field = { filter: undefined }
this.form.setFieldsValue(field)
if (this.inputDecorator === 'template') {
this.vmFetchTemplates()
} else {
this.vmFetchIsos()
}
this.filter = ''
this.filterDataSource('')
},
changeOsName (value) {
this.osType = value
@ -288,8 +291,27 @@ export default {
}
.filter-group {
/deep/.ant-input-affix-wrapper {
float: right;
width: calc(100% - 32px);
.ant-input {
border-radius: 4px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
/deep/.ant-input-group-addon {
padding: 0 5px;
float: left;
width: 32px;
height: 32px;
border-radius: 4px;
border-right: 0;
border-left: 1px solid #d9d9d9;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
padding: 0 0 0 1px;
}
&-button {
@ -301,7 +323,7 @@ export default {
&-button {
position: relative;
display: block;
min-height: 25px;
min-height: 30px;
&-clear {
position: absolute;

View File

@ -54,7 +54,7 @@
}"
:loading="zoneLoading"
:placeholder="this.$t('.zoneid')"
@change="val => { this.handleZoneChanged(this.zones[val]) }">
@change="val => { this.handleZoneChange(this.zones[val]) }">
<a-select-option v-for="(opt, optIndex) in this.zones" :key="optIndex">
{{ opt.name || opt.description }}
</a-select-option>

View File

@ -54,7 +54,7 @@
}"
:loading="zoneLoading"
:placeholder="this.$t('zoneid')"
@change="val => { this.handleZoneChanged(this.zones[val]) }">
@change="val => { this.handleZoneChange(this.zones[val]) }">
<a-select-option v-for="(opt, optIndex) in this.zones" :key="optIndex">
{{ opt.name || opt.description }}
</a-select-option>

View File

@ -54,7 +54,7 @@
}"
:loading="zoneLoading"
:placeholder="this.$t('zoneid')"
@change="val => { this.handleZoneChanged(this.zones[val]) }">
@change="val => { this.handleZoneChange(this.zones[val]) }">
<a-select-option v-for="(opt, optIndex) in this.zones" :key="optIndex">
{{ opt.name || opt.description }}
</a-select-option>
@ -70,7 +70,7 @@
}"
:loading="zoneLoading"
:placeholder="this.$t('physicalnetworkid')"
@change="val => { this.handleZoneChanged(this.formPhysicalNetworks[val]) }">
@change="val => { this.handleZoneChange(this.formPhysicalNetworks[val]) }">
<a-select-option v-for="(opt, optIndex) in this.formPhysicalNetworks" :key="optIndex">
{{ opt.name || opt.description }}
</a-select-option>