compute: Fix VM Deployment Wizard Issues (#273)

Fixes #254

 Pod, cluster, host should be hidden for normal and domain admin user
 Listing of all items are not done by passing account/domainid, or zoneid (for example listing of networks, template, isos for zone; keypair, affiinity groups etc for account/domain)
 Add a way to filter templates by featured, community, shared, mine?
 Multiple listing of templates for the same zone? (multiple radios button)
 Add button to add network that can open the add network popup or add a router-link?
 User data not properly base64 encoded (check encoding?)
 Add support for processing min/max cpu/ram based on type of compute offering selected and custom disk offering

Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
Co-authored-by: Hoang Nguyen <hoangnm@unitech.vn>
Co-authored-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
Co-authored-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
Hoang Nguyen 2020-04-22 16:13:12 +07:00 committed by Rohit Yadav
parent 51b3e033ca
commit 1312ec2b8b
14 changed files with 902 additions and 176 deletions

View File

@ -67,9 +67,9 @@ export default {
if (json && json.listprojectsresponse && json.listprojectsresponse.project) {
this.projects.push(...json.listprojectsresponse.project)
}
const currentProject = Vue.ls.get(CURRENT_PROJECT)
const currentProject = Vue.ls.get(CURRENT_PROJECT) || {}
for (var project of this.projects) {
if (project.id === currentProject.id) {
if (project && currentProject && project.id === currentProject.id) {
this.setSelectedProject(project)
break
}

View File

@ -1151,6 +1151,8 @@
"message.required.traffic.type": "Error in configuration! All required traffic types should be added and with multiple physical networks each network should have a label.",
"message.desc.primary.storage": "Each cluster must contain one or more primary storage servers, and we will add the first one now. Primary storage contains the disk volumes for all the VMs running on hosts in the cluster. Use any standards-compliant protocol that is supported by the underlying hypervisor.",
"message.desc.secondary.storage": "Each zone must have at least one NFS or secondary storage server, and we will add the first one now. Secondary storage stores VM templates, ISO images, and VM disk volume snapshots. This server must be available to all hosts in the zone.<br/><br/>Provide the IP address and exported path.",
"message.error.required.input": "Please enter input",
"message.error.invalid.range": "Please enter values from {min} to {max}",
"label.name": "Name",
"label.ipv4.dns1": "IPv4 DNS1",
"label.ipv4.dns2": "IPv4 DNS2",
@ -1266,5 +1268,10 @@
"label.launch.zone": "Launch Zone",
"label.done": "Done",
"label.fix.errors": "Fix errors",
"error.something.went.wrong.please.correct.the.following": "Something went wrong; please correct the following"
"error.something.went.wrong.please.correct.the.following": "Something went wrong; please correct the following",
"filter": "Filter",
"featured": "Featured",
"community": "Community",
"selfexecutable": "Self",
"sharedexecutable": "Shared"
}

View File

@ -396,6 +396,8 @@ export default {
if (this.searchQuery !== '') {
if (this.apiName === 'listRoles') {
params.name = this.searchQuery
} else if (this.apiName === 'quotaEmailTemplateList') {
params.templatetype = this.searchQuery
} else {
params.keyword = this.searchQuery
}

View File

@ -44,21 +44,27 @@
:loading="loading.zones"
></a-select>
</a-form-item>
<a-form-item :label="this.$t('podId')">
<a-form-item
v-if="!isNormalAndDomainUser"
:label="this.$t('podId')">
<a-select
v-decorator="['podid']"
:options="podSelectOptions"
:loading="loading.pods"
></a-select>
</a-form-item>
<a-form-item :label="this.$t('clusterid')">
<a-form-item
v-if="!isNormalAndDomainUser"
:label="this.$t('clusterid')">
<a-select
v-decorator="['clusterid']"
:options="clusterSelectOptions"
:loading="loading.clusters"
></a-select>
</a-form-item>
<a-form-item :label="this.$t('hostId')">
<a-form-item
v-if="!isNormalAndDomainUser"
:label="this.$t('hostId')">
<a-select
v-decorator="['hostid']"
:options="hostSelectOptions"
@ -101,10 +107,12 @@
:items="options.templates"
:selected="tabKey"
:loading="loading.templates"
:preFillContent="dataPreFill"
@update-template-iso="updateFieldValue"
></template-iso-selection>
<disk-size-selection
input-decorator="rootdisksize"
:preFillContent="dataPreFill"
@update-disk-size="updateFieldValue"/>
</p>
<p v-else>
@ -113,8 +121,22 @@
:items="options.isos"
:selected="tabKey"
:loading="loading.isos"
:preFillContent="dataPreFill"
@update-template-iso="updateFieldValue"
></template-iso-selection>
<a-form-item :label="this.$t('hypervisor')">
<a-select
v-decorator="['hypervisor', {
initialValue: hypervisorSelectOptions && hypervisorSelectOptions.length > 0
? hypervisorSelectOptions[0].value
: null,
rules: [{ required: true, message: 'Please select option' }]
}]"
:options="hypervisorSelectOptions"
@change="value => this.hypervisor = value"
>
</a-select>
</a-form-item>
</p>
</a-card>
<a-form-item class="form-item-hidden">
@ -134,13 +156,42 @@
:status="zoneSelected ? 'process' : 'wait'">
<template slot="description">
<div v-if="zoneSelected">
<compute-selection
<compute-offering-selection
:compute-items="options.serviceOfferings"
:value="serviceOffering ? serviceOffering.id : ''"
:loading="loading.serviceOfferings"
:preFillContent="dataPreFill"
@select-compute-item="($event) => updateComputeOffering($event)"
@handle-search-filter="($event) => handleSearchFilter('serviceOfferings', $event)"
></compute-selection>
></compute-offering-selection>
<compute-selection
v-if="serviceOffering && serviceOffering.iscustomized"
cpunumber-input-decorator="cpunumber"
cpuspeed-input-decorator="cpuspeed"
memory-input-decorator="memory"
:preFillContent="dataPreFill"
:computeOfferingId="instanceConfig.computeofferingid"
:isConstrained="'serviceofferingdetails' in serviceOffering"
:minCpu="'serviceofferingdetails' in serviceOffering ? serviceOffering.serviceofferingdetails.mincpunumber*1 : 1"
:maxCpu="'serviceofferingdetails' in serviceOffering ? serviceOffering.serviceofferingdetails.maxcpunumber*1 : Number.MAX_SAFE_INTEGER"
:minMemory="'serviceofferingdetails' in serviceOffering ? serviceOffering.serviceofferingdetails.minmemory*1 : 1"
:maxMemory="'serviceofferingdetails' in serviceOffering ? serviceOffering.serviceofferingdetails.maxmemory*1 : Number.MAX_SAFE_INTEGER"
@update-compute-cpunumber="updateFieldValue"
@update-compute-cpuspeed="updateFieldValue"
@update-compute-memory="updateFieldValue" />
<span v-if="serviceOffering && serviceOffering.iscustomized">
<a-form-item class="form-item-hidden" >
<a-input v-decorator="['cpunumber']"/>
</a-form-item>
<a-form-item
class="form-item-hidden"
v-if="serviceOffering && !(serviceOffering.cpuspeed > 0)">
<a-input v-decorator="['cpuspeed']"/>
</a-form-item>
<a-form-item class="form-item-hidden">
<a-input v-decorator="['memory']"/>
</a-form-item>
</span>
</div>
</template>
</a-step>
@ -153,12 +204,14 @@
:items="options.diskOfferings"
:value="diskOffering ? diskOffering.id : ''"
:loading="loading.diskOfferings"
:preFillContent="dataPreFill"
@select-disk-offering-item="($event) => updateDiskOffering($event)"
@handle-search-filter="($event) => handleSearchFilter('diskOfferings', $event)"
></disk-offering-selection>
<disk-size-selection
v-if="diskOffering && diskOffering.iscustomized"
input-decorator="size"
:preFillContent="dataPreFill"
@update-disk-size="updateFieldValue" />
<a-form-item class="form-item-hidden">
<a-input v-decorator="['size']"/>
@ -175,6 +228,7 @@
:items="options.affinityGroups"
:value="affinityGroupIds"
:loading="loading.affinityGroups"
:preFillContent="dataPreFill"
@select-affinity-group-item="($event) => updateAffinityGroups($event)"
@handle-search-filter="($event) => handleSearchFilter('affinityGroups', $event)"
></affinity-group-selection>
@ -191,12 +245,14 @@
:value="networkOfferingIds"
:loading="loading.networks"
:zoneId="zoneId"
:preFillContent="dataPreFill"
@select-network-item="($event) => updateNetworks($event)"
@handle-search-filter="($event) => handleSearchFilter('networks', $event)"
></network-selection>
<network-configuration
v-if="networks.length > 0"
:items="networks"
:preFillContent="dataPreFill"
@update-network-config="($event) => updateNetworkConfig($event)"
@select-default-network-item="($event) => updateDefaultNetworks($event)"
></network-configuration>
@ -212,6 +268,7 @@
:items="options.sshKeyPairs"
:value="sshKeyPair ? sshKeyPair.name : ''"
:loading="loading.sshKeyPairs"
:preFillContent="dataPreFill"
@select-ssh-key-pair-item="($event) => updateSshKeyPairs($event)"
@handle-search-filter="($event) => handleSearchFilter('sshKeyPairs', $event)"
/>
@ -261,6 +318,7 @@ 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 DiskOfferingSelection from '@views/compute/wizard/DiskOfferingSelection'
import DiskSizeSelection from '@views/compute/wizard/DiskSizeSelection'
@ -281,11 +339,16 @@ export default {
DiskSizeSelection,
DiskOfferingSelection,
InfoCard,
ComputeOfferingSelection,
ComputeSelection
},
props: {
visible: {
type: Boolean
},
preFillContent: {
type: Object,
default: () => {}
}
},
mixins: [mixin, mixinDevice],
@ -297,6 +360,7 @@ export default {
options: {
templates: [],
isos: [],
hypervisors: [],
serviceOfferings: [],
diskOfferings: [],
zones: [],
@ -313,6 +377,7 @@ export default {
deploy: false,
templates: false,
isos: false,
hypervisors: false,
serviceOfferings: false,
diskOfferings: false,
affinityGroups: false,
@ -324,9 +389,10 @@ export default {
hosts: false,
groups: false
},
instanceConfig: [],
instanceConfig: {},
template: {},
iso: {},
hypervisor: '',
serviceOffering: {},
diskOffering: {},
affinityGroups: [],
@ -368,10 +434,14 @@ export default {
tab: this.$t('ISOs')
}
],
tabKey: 'templateid'
tabKey: 'templateid',
dataPreFill: {}
}
},
computed: {
isNormalAndDomainUser () {
return ['DomainAdmin', 'User'].includes(this.$store.getters.userInfo.roletype)
},
diskSize () {
const rootDiskSize = _.get(this.instanceConfig, 'rootdisksize', 0)
const customDiskSize = _.get(this.instanceConfig, 'size', 0)
@ -411,23 +481,33 @@ export default {
}
},
zones: {
list: 'listZones'
list: 'listZones',
isLoad: true,
field: 'zoneid'
},
hypervisors: {
list: 'listHypervisors',
options: {
zoneid: _.get(this.zone, 'id')
},
field: 'hypervisor'
},
affinityGroups: {
list: 'listAffinityGroups',
options: {
page: 1,
pageSize: 10,
keyword: undefined
keyword: undefined,
listall: false
}
},
sshKeyPairs: {
list: 'listSSHKeyPairs',
options: {
zoneid: _.get(this.zone, 'id'),
page: 1,
pageSize: 10,
keyword: undefined
keyword: undefined,
listall: false
}
},
networks: {
@ -436,6 +516,8 @@ export default {
zoneid: _.get(this.zone, 'id'),
canusefordeploy: true,
projectid: store.getters.project.id,
domainid: store.getters.project.id ? null : store.getters.userInfo.domainid,
account: store.getters.project.id ? null : store.getters.userInfo.account,
page: 1,
pageSize: 10,
keyword: undefined
@ -443,26 +525,37 @@ export default {
},
pods: {
list: 'listPods',
isLoad: !this.isNormalAndDomainUser,
options: {
zoneid: _.get(this.zone, 'id')
}
},
field: 'podid'
},
clusters: {
list: 'listClusters',
isLoad: !this.isNormalAndDomainUser,
options: {
zoneid: _.get(this.zone, 'id')
}
},
field: 'clusterid'
},
hosts: {
list: 'listHosts',
isLoad: !this.isNormalAndDomainUser,
options: {
zoneid: _.get(this.zone, 'id'),
state: 'Up',
type: 'Routing'
}
},
field: 'hostid'
},
groups: {
list: 'listInstanceGroups'
list: 'listInstanceGroups',
options: {
listall: false
},
isLoad: true,
field: 'group'
}
}
},
@ -477,6 +570,14 @@ export default {
}
})
},
hypervisorSelectOptions () {
return this.options.hypervisors.map((hypervisor) => {
return {
label: hypervisor.name,
value: hypervisor.name
}
})
},
podSelectOptions () {
return this.options.pods.map((pod) => {
return {
@ -527,6 +628,8 @@ export default {
instanceConfig (instanceConfig) {
this.template = _.find(this.options.templates, (option) => option.id === instanceConfig.templateid)
this.iso = _.find(this.options.isos, (option) => option.id === instanceConfig.isoid)
var hypervisorItem = _.find(this.options.hypervisors, (option) => option.name === instanceConfig.hypervisor)
this.hypervisor = hypervisorItem ? hypervisorItem.name : null
this.serviceOffering = _.find(this.options.serviceOfferings, (option) => option.id === instanceConfig.computeofferingid)
this.diskOffering = _.find(this.options.diskOfferings, (option) => option.id === instanceConfig.diskofferingid)
this.zone = _.find(this.options.zones, (option) => option.id === instanceConfig.zoneid)
@ -551,6 +654,9 @@ export default {
this.vm.templatename = this.iso.displaytext
this.vm.ostypeid = this.iso.ostypeid
this.vm.ostypename = this.iso.ostypename
if (this.hypervisor) {
this.vm.hypervisor = this.hypervisor
}
}
if (this.serviceOffering) {
@ -572,7 +678,7 @@ export default {
}
}
},
beforeCreate () {
created () {
this.form = this.$form.createForm(this, {
onValuesChange: (props, fields) => {
if (fields.isoid) {
@ -584,36 +690,63 @@ export default {
this.form.setFieldsValue({ isoid: null })
}
this.instanceConfig = { ...this.form.getFieldsValue(), ...fields }
this.vm = this.instanceConfig
this.vm = Object.assign({}, this.instanceConfig)
}
})
this.form.getFieldDecorator('computeofferingid', { initialValue: undefined, preserve: true })
this.form.getFieldDecorator('diskofferingid', { initialValue: undefined, preserve: true })
this.form.getFieldDecorator('affinitygroupids', { initialValue: [], preserve: true })
this.form.getFieldDecorator('isoid', { initialValue: undefined, preserve: true })
this.form.getFieldDecorator('networkids', { initialValue: [], preserve: true })
this.form.getFieldDecorator('keypair', { initialValue: undefined, preserve: true })
this.apiParams = {}
this.apiDeployVirtualMachine = this.$store.getters.apis.deployVirtualMachine || {}
this.apiDeployVirtualMachine.params.forEach(param => {
this.apiParams[param.name] = param
})
this.form.getFieldDecorator('cpunumber', { initialValue: undefined, preserve: true })
this.form.getFieldDecorator('cpuSpeed', { initialValue: undefined, preserve: true })
this.form.getFieldDecorator('memory', { initialValue: undefined, preserve: true })
},
created () {
mounted () {
this.dataPreFill = this.preFillContent && Object.keys(this.preFillContent).length > 0 ? this.preFillContent : {}
this.fetchData()
},
provide () {
return {
vmFetchTemplates: this.fetchAllTemplates,
vmFetchIsos: this.fetchAllIsos,
vmFetchNetworks: this.fetchNetwork
}
},
methods: {
fillValue (field) {
this.form.getFieldDecorator([field], { initialValue: this.dataPreFill[field] })
},
fetchData () {
this.fetchOptions(this.params.zones, 'zones')
this.fetchOptions(this.params.pods, 'pods')
this.fetchOptions(this.params.clusters, 'clusters')
this.fetchOptions(this.params.hosts, 'hosts')
this.fetchOptions(this.params.groups, 'groups')
if (this.dataPreFill.zoneid) {
this.fetchDataByZone(this.dataPreFill.zoneid)
} else {
_.each(this.params, (param, name) => {
if (param.isLoad) {
this.fetchOptions(param, name)
}
})
}
this.fetchKeyboard()
Vue.nextTick().then(() => {
['name', 'keyboard', 'userdata'].forEach(this.fillValue)
this.instanceConfig = this.form.getFieldsValue() // ToDo: maybe initialize with some other defaults
})
},
async fetchDataByZone (zoneId) {
this.fillValue('zoneid')
this.options.zones = await this.fetchZones()
this.zoneId = zoneId
this.zoneSelected = true
this.tabKey = 'templateid'
await _.each(this.params, (param, name) => {
if (!('isLoad' in param) || param.isLoad) {
this.fetchOptions(param, name, ['zones'])
}
})
await this.fetchAllTemplates()
},
fetchKeyboard () {
const keyboardType = []
keyboardType.push({
@ -643,6 +776,10 @@ export default {
this.$set(this.options, 'keyboards', keyboardType)
},
fetchNetwork () {
const param = this.params.networks
this.fetchOptions(param, 'networks')
},
resetData () {
this.vm = {}
this.zoneSelected = false
@ -654,13 +791,13 @@ export default {
this.tabKey = 'templateid'
this.form.setFieldsValue({
templateid: value,
isoid: undefined
isoid: null
})
} else if (name === 'isoid') {
this.tabKey = 'isoid'
this.form.setFieldsValue({
isoid: value,
templateid: undefined
templateid: null
})
} else {
this.form.setFieldsValue({
@ -729,8 +866,8 @@ export default {
deployVmData.hostid = values.hostid
deployVmData.group = values.group
deployVmData.keyboard = values.keyboard
if (values.keyboard && values.keyboard.length > 0) {
deployVmData.userdata = encodeURIComponent(btoa(this.sanitizeReverse(values.keyboard)))
if (values.userdata && values.userdata.length > 0) {
deployVmData.userdata = encodeURIComponent(btoa(this.sanitizeReverse(values.userdata)))
}
// step 2: select template/iso
if (this.tabKey === 'templateid') {
@ -741,15 +878,29 @@ export default {
if (values.rootdisksize && values.rootdisksize > 0) {
deployVmData.rootdisksize = values.rootdisksize
}
if (values.hypervisor && values.hypervisor.length > 0) {
deployVmData.hypervisor = values.hypervisor
}
// step 3: select service offering
deployVmData.serviceofferingid = values.computeofferingid
if (values.cpunumber || values.cpuspeed || values.memory) {
if (values.cpunumber) {
deployVmData['details[0].cpuNumber'] = values.cpunumber
}
if (values.cpuspeed) {
deployVmData['details[0].cpuSpeed'] = values.cpuspeed
}
if (values.memory) {
deployVmData['details[0].memory'] = values.memory
}
}
// step 4: select disk offering
deployVmData.diskofferingid = values.diskofferingid
if (values.size) {
deployVmData.size = values.size
}
// step 5: select an affinity group
deployVmData.affinitygroupids = values.affinitygroupids.join(',')
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++) {
@ -804,6 +955,20 @@ export default {
})
})
},
fetchZones () {
return new Promise((resolve) => {
this.loading.zones = true
const param = this.params.zones
api(param.list, { listall: true }).then(json => {
const zones = json.listzonesresponse.zone || []
resolve(zones)
}).catch(function (error) {
console.log(error.stack)
}).finally(() => {
this.loading.zones = false
})
})
},
fetchOptions (param, name, exclude) {
if (exclude && exclude.length > 0) {
if (exclude.includes(name)) {
@ -814,7 +979,9 @@ export default {
param.loading = true
param.opts = []
const options = param.options || {}
options.listall = true
if (!('listall' in options)) {
options.listall = true
}
api(param.list, options).then((response) => {
param.loading = false
_.map(response, (responseItem, responseKey) => {
@ -833,6 +1000,9 @@ export default {
param.opts = response
this.options[name] = response
this.$forceUpdate()
if (param.field) {
this.fillValue(param.field)
}
})
})
}).catch(function (error) {
@ -869,11 +1039,14 @@ export default {
})
})
},
fetchAllTemplates () {
fetchAllTemplates (filterKey) {
const promises = []
this.options.templates = []
this.loading.templates = true
this.templateFilter.forEach((filter) => {
if (filterKey && filterKey !== filter) {
return true
}
promises.push(this.fetchTemplates(filter))
})
Promise.all(promises).then(response => {
@ -888,11 +1061,14 @@ export default {
this.loading.templates = false
})
},
fetchAllIsos () {
fetchAllIsos (filterKey) {
const promises = []
this.options.isos = []
this.loading.isos = true
this.isoFilter.forEach((filter) => {
if (filterKey && filterKey !== filter) {
return true
}
promises.push(this.fetchIsos(filter))
})
Promise.all(promises).then(response => {
@ -908,7 +1084,9 @@ export default {
})
},
onSelectZoneId (value) {
this.dataPreFill = {}
this.zoneId = value
this.zone = _.find(this.options.zones, (option) => option.id === value)
this.zoneSelected = true
this.form.setFieldsValue({
clusterid: undefined,
@ -919,7 +1097,9 @@ export default {
})
this.tabKey = 'templateid'
_.each(this.params, (param, name) => {
this.fetchOptions(param, name, ['zones', 'groups'])
if (!('isLoad' in param) || param.isLoad) {
this.fetchOptions(param, name, ['zones', 'groups'])
}
})
this.fetchAllTemplates()
},

View File

@ -53,6 +53,10 @@ export default {
loading: {
type: Boolean,
default: false
},
preFillContent: {
type: Object,
default: () => {}
}
},
data () {
@ -96,6 +100,17 @@ export default {
if (newValue && !_.isEqual(newValue, oldValue)) {
this.selectedRowKeys = newValue
}
},
loading () {
if (!this.loading) {
if (this.preFillContent.affinitygroupids) {
this.selectedRowKeys = this.preFillContent.affinitygroupids
this.$emit('select-affinity-group-item', this.preFillContent.affinitygroupids)
} else {
this.selectedRowKeys = []
this.$emit('select-affinity-group-item', null)
}
}
}
},
methods: {

View File

@ -0,0 +1,169 @@
// 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-input-search
style="width: 25vw;float: right;margin-bottom: 10px; z-index: 8"
placeholder="Search"
v-model="filter"
@search="handleSearch" />
<a-table
:columns="columns"
:dataSource="tableSource"
:pagination="{showSizeChanger: true}"
:rowSelection="rowSelection"
:loading="loading"
size="middle"
@change="handleTableChange"
:scroll="{ y: 225 }"
>
<span slot="cpuTitle"><a-icon type="appstore" /> {{ $t('cpu') }}</span>
<span slot="ramTitle"><a-icon type="bulb" /> {{ $t('memory') }}</span>
</a-table>
</div>
</template>
<script>
export default {
name: 'ComputeOfferingSelection',
props: {
computeItems: {
type: Array,
default: () => []
},
value: {
type: String,
default: ''
},
loading: {
type: Boolean,
default: false
},
preFillContent: {
type: Object,
default: () => {}
}
},
data () {
return {
filter: '',
columns: [
{
dataIndex: 'name',
title: this.$t('serviceOfferingId'),
width: '40%'
},
{
dataIndex: 'cpu',
slots: { title: 'cpuTitle' },
width: '30%'
},
{
dataIndex: 'ram',
slots: { title: 'ramTitle' },
width: '30%'
}
],
selectedRowKeys: []
}
},
computed: {
options () {
return {
page: 1,
pageSize: 10,
keyword: ''
}
},
tableSource () {
return this.computeItems.map((item) => {
var cpuNumberValue = item.cpunumber + ''
var cpuSpeedValue = (item.cpuspeed !== null && item.cpuspeed !== undefined && item.cpuspeed > 0) ? parseFloat(item.cpuspeed / 1000.0).toFixed(2) + '' : ''
var ramValue = item.memory + ''
if (item.iscustomized === true) {
cpuNumberValue = ''
ramValue = ''
if ('serviceofferingdetails' in item &&
'mincpunumber' in item.serviceofferingdetails &&
'maxcpunumber' in item.serviceofferingdetails) {
cpuNumberValue = item.serviceofferingdetails.mincpunumber + '-' + item.serviceofferingdetails.maxcpunumber
}
if ('serviceofferingdetails' in item &&
'minmemory' in item.serviceofferingdetails &&
'maxmemory' in item.serviceofferingdetails) {
ramValue = item.serviceofferingdetails.minmemory + '-' + item.serviceofferingdetails.maxmemory
}
}
return {
key: item.id,
name: item.name,
cpu: cpuNumberValue.length > 0 ? `${cpuNumberValue} CPU x ${cpuSpeedValue} Ghz` : '',
ram: ramValue.length > 0 ? `${ramValue} MB` : ''
}
})
},
rowSelection () {
return {
type: 'radio',
selectedRowKeys: this.selectedRowKeys,
onChange: this.onSelectRow
}
}
},
watch: {
value (newValue, oldValue) {
if (newValue && newValue !== oldValue) {
this.selectedRowKeys = [newValue]
}
},
loading () {
if (!this.loading) {
if (this.preFillContent.computeofferingid) {
this.selectedRowKeys = [this.preFillContent.computeofferingid]
this.$emit('select-compute-item', this.preFillContent.computeofferingid)
} else {
this.selectedRowKeys = []
this.$emit('select-compute-item', null)
}
}
}
},
methods: {
onSelectRow (value) {
this.selectedRowKeys = value
this.$emit('select-compute-item', value[0])
},
handleSearch (value) {
this.filter = value
this.options.keyword = this.filter
this.$emit('handle-search-filter', this.options)
},
handleTableChange (pagination) {
this.options.page = pagination.current
this.options.pageSize = pagination.pageSize
this.$emit('handle-search-filter', this.options)
}
}
}
</script>
<style lang="less" scoped>
.ant-table-wrapper {
margin: 2rem 0;
}
</style>

View File

@ -16,122 +16,228 @@
// under the License.
<template>
<div>
<a-input-search
style="width: 25vw;float: right;margin-bottom: 10px; z-index: 8"
placeholder="Search"
v-model="filter"
@search="handleSearch" />
<a-table
:columns="columns"
:dataSource="tableSource"
:pagination="{showSizeChanger: true}"
:rowSelection="rowSelection"
:loading="loading"
size="middle"
@change="handleTableChange"
:scroll="{ y: 225 }"
>
<span slot="cpuTitle"><a-icon type="appstore" /> {{ $t('cpu') }}</span>
<span slot="ramTitle"><a-icon type="bulb" /> {{ $t('memory') }}</span>
</a-table>
</div>
<a-card>
<a-col>
<a-row>
<a-col :md="colContraned" :lg="colContraned">
<a-form-item
:label="this.$t('cpunumber')"
:validate-status="errors.cpu.status"
:help="errors.cpu.message">
<a-row :gutter="12">
<a-col :md="10" :lg="10" v-show="isConstrained">
<a-slider
:min="minCpu"
:max="maxCpu"
v-model="cpuNumberInputValue"
@change="($event) => updateComputeCpuNumber($event)"
/>
</a-col>
<a-col :md="4" :lg="4">
<a-input-number
v-model="cpuNumberInputValue"
:formatter="value => `${value}`"
@change="($event) => updateComputeCpuNumber($event)"
/>
</a-col>
</a-row>
</a-form-item>
</a-col>
<a-col :md="8" :lg="8" v-show="!isConstrained">
<a-form-item
:label="this.$t('cpuspeed')"
:validate-status="errors.cpuspeed.status"
:help="errors.cpuspeed.message">
<a-input-number
v-model="cpuSpeedInputValue"
@change="($event) => updateComputeCpuSpeed($event)"
/>
</a-form-item>
</a-col>
<a-col :md="colContraned" :lg="colContraned">
<a-form-item
:label="this.$t('memory')"
:validate-status="errors.memory.status"
:help="errors.memory.message">
<a-row :gutter="12">
<a-col :md="10" :lg="10" v-show="isConstrained">
<a-slider
:min="minMemory"
:max="maxMemory"
v-model="memoryInputValue"
@change="($event) => updateComputeMemory($event)"
/>
</a-col>
<a-col :md="4" :lg="4">
<a-input-number
v-model="memoryInputValue"
:formatter="value => `${value} MB`"
:parser="value => value.replace(' MB', '')"
@change="($event) => updateComputeMemory($event)"
/>
</a-col>
</a-row>
</a-form-item>
</a-col>
</a-row>
</a-col>
</a-card>
</template>
<script>
export default {
name: 'ComputeSelection',
props: {
computeItems: {
type: Array,
default: () => []
computeOfferingId: {
type: String,
default: () => ''
},
value: {
isConstrained: {
type: Boolean,
default: true
},
minCpu: {
type: Number,
default: 1
},
maxCpu: {
type: Number,
default: 2
},
minMemory: {
type: Number,
default: 1
},
maxMemory: {
type: Number,
default: 256
},
cpunumberInputDecorator: {
type: String,
default: ''
},
loading: {
type: Boolean,
default: false
cpuspeedInputDecorator: {
type: String,
default: ''
},
memoryInputDecorator: {
type: String,
default: ''
},
preFillContent: {
type: Object,
default: () => {}
}
},
data () {
return {
filter: '',
columns: [
{
dataIndex: 'name',
title: this.$t('serviceOfferingId'),
width: '40%'
cpuNumberInputValue: 1,
cpuSpeedInputValue: 1,
memoryInputValue: 1,
errors: {
cpu: {
status: '',
message: ''
},
{
dataIndex: 'cpu',
slots: { title: 'cpuTitle' },
width: '30%'
cpuspeed: {
status: '',
message: ''
},
{
dataIndex: 'ram',
slots: { title: 'ramTitle' },
width: '30%'
memory: {
status: '',
message: ''
}
],
selectedRowKeys: []
}
}
},
computed: {
options () {
return {
page: 1,
pageSize: 10,
keyword: ''
}
},
tableSource () {
return this.computeItems.map((item) => {
return {
key: item.id,
name: item.name,
cpu: `${item.cpunumber} CPU x ${parseFloat(item.cpuspeed / 1000.0).toFixed(2)} Ghz`,
ram: `${item.memory} MB`
}
})
},
rowSelection () {
return {
type: 'radio',
selectedRowKeys: this.selectedRowKeys,
onChange: this.onSelectRow
}
colContraned () {
return this.isConstrained ? 12 : 8
}
},
watch: {
value (newValue, oldValue) {
if (newValue && newValue !== oldValue) {
this.selectedRowKeys = [newValue]
computeOfferingId (newValue, oldValue) {
if (newValue !== oldValue) {
this.cpuNumberInputValue = this.minCpu
this.memoryInputValue = this.minMemory
}
}
},
mounted () {
this.cpuNumberInputValue = this.minCpu
this.memoryInputValue = this.minMemory
this.fillValue()
},
methods: {
onSelectRow (value) {
this.selectedRowKeys = value
this.$emit('select-compute-item', value[0])
fillValue () {
if (this.preFillContent.cpunumber) {
this.cpuNumberInputValue = this.preFillContent.cpunumber
}
if (this.preFillContent.cpuspeed) {
this.cpuSpeedInputValue = this.preFillContent.cpuspeed
}
if (this.preFillContent.memory) {
this.memoryInputValue = this.preFillContent.memory
}
},
handleSearch (value) {
this.filter = value
this.options.keyword = this.filter
this.$emit('handle-search-filter', this.options)
updateComputeCpuNumber (value) {
if (!this.validateInput('cpu', value)) {
return
}
this.$emit('update-compute-cpunumber', this.cpunumberInputDecorator, value)
},
handleTableChange (pagination) {
this.options.page = pagination.current
this.options.pageSize = pagination.pageSize
this.$emit('handle-search-filter', this.options)
updateComputeCpuSpeed (value) {
this.$emit('update-compute-cpuspeed', this.cpuspeedInputDecorator, value)
},
updateComputeMemory (value) {
if (!this.validateInput('memory', value)) {
return
}
this.$emit('update-compute-memory', this.memoryInputDecorator, value)
},
validateInput (input, value) {
this.errors[input].status = ''
this.errors[input].message = ''
if (value === null || value === undefined || value.length === 0) {
this.errors[input].status = 'error'
this.errors[input].message = this.$t('message.error.required.input')
return false
}
if (!this.isConstrained) {
return true
}
let min
let max
switch (input) {
case 'cpu':
min = this.minCpu
max = this.maxCpu
break
case 'memory':
min = this.minMemory
max = this.maxMemory
break
}
if (!this.checkValidRange(value, min, max)) {
this.errors[input].status = 'error'
this.errors[input].message = this.$t('message.error.invalid.range', { min: min, max: max })
return false
}
return true
},
checkValidRange (value, min, max) {
if (value < min || value > max) {
return false
}
return true
}
}
}
</script>
<style lang="less" scoped>
.ant-table-wrapper {
margin: 2rem 0;
}
</style>

View File

@ -64,6 +64,10 @@ export default {
loading: {
type: Boolean,
default: false
},
preFillContent: {
type: Object,
default: () => {}
}
},
data () {
@ -93,15 +97,7 @@ export default {
}
},
created () {
this.dataItems = []
this.dataItems.push({
id: '0',
name: this.$t('noselect'),
diskSize: undefined,
miniops: undefined,
maxiops: undefined,
isCustomized: undefined
})
this.initDataItem()
},
computed: {
options () {
@ -139,11 +135,34 @@ export default {
},
items (newData, oldData) {
if (newData && newData.length > 0) {
this.initDataItem()
this.dataItems = this.dataItems.concat(newData)
}
},
loading () {
if (!this.loading) {
if (this.preFillContent.diskofferingid) {
this.selectedRowKeys = [this.preFillContent.diskofferingid]
this.$emit('select-disk-offering-item', this.preFillContent.diskofferingid)
} else {
this.selectedRowKeys = ['0']
this.$emit('select-disk-offering-item', '0')
}
}
}
},
methods: {
initDataItem () {
this.dataItems = []
this.dataItems.push({
id: '0',
name: this.$t('noselect'),
diskSize: undefined,
miniops: undefined,
maxiops: undefined,
isCustomized: undefined
})
},
onSelectRow (value) {
this.selectedRowKeys = value
this.$emit('select-disk-offering-item', value[0])

View File

@ -16,22 +16,26 @@
// under the License.
<template>
<a-form-item :label="this.$t('diskSize')">
<a-row>
<a-col :span="10">
<a-form-item
:label="this.$t('diskSize')"
class="form-item">
<a-row :gutter="12">
<a-col :md="10" :lg="10">
<a-slider
:min="0"
:max="1024"
v-model="inputValue"
@change="($event) => updateDickSize($event)"
@change="($event) => updateDiskSize($event)"
/>
</a-col>
<a-col :span="4">
<a-input-number
v-model="inputValue"
:formatter="value => `${value} GB`"
:parser="value => value.replace(' GB', '')"
/>
<a-col :md="4" :lg="4">
<span style="display: inline-flex">
<a-input-number
v-model="inputValue"
@change="($event) => updateDiskSize($event)"
/>
<span style="padding-top: 6px">GB</span>
</span>
</a-col>
</a-row>
</a-form-item>
@ -44,6 +48,10 @@ export default {
inputDecorator: {
type: String,
default: ''
},
preFillContent: {
type: Object,
default: () => {}
}
},
data () {
@ -51,14 +59,27 @@ export default {
inputValue: 0
}
},
mounted () {
this.fillValue()
},
methods: {
updateDickSize (value) {
fillValue () {
if (this.inputDecorator === 'rootdisksize') {
this.inputValue = this.preFillContent.rootdisksize ? this.preFillContent.rootdisksize : 0
} else if (this.inputDecorator === 'size') {
this.inputValue = this.preFillContent.size ? this.preFillContent.size : 0
}
this.$emit('update-disk-size', this.inputDecorator, this.inputValue)
},
updateDiskSize (value) {
this.$emit('update-disk-size', this.inputDecorator, value)
}
}
}
</script>
<style scoped>
<style scoped lang="less">
.form-item {
margin: 0 5px;
}
</style>

View File

@ -28,13 +28,13 @@
<template slot="ipAddress" slot-scope="text, record">
<a-input
style="width: 150px;"
:placeholder="$t('ipAddress')"
:placeholder="$t('ipaddress')"
@change="($event) => updateNetworkData('ipAddress', record.id, $event.target.value)" />
</template>
<template slot="macAddress" slot-scope="text, record">
<a-input
style="width: 150px;"
:placeholder="$t('macAddress')"
:placeholder="$t('macaddress')"
@change="($event) => updateNetworkData('macAddress', record.id, $event.target.value)" />
</template>
</a-table>
@ -51,6 +51,10 @@ export default {
value: {
type: String,
default: ''
},
preFillContent: {
type: Object,
default: () => {}
}
},
data () {

View File

@ -22,6 +22,13 @@
placeholder="Search"
v-model="filter"
@search="handleSearch" />
<a-tooltip
arrowPointAtCenter
placement="bottomRight">
<template slot="title">
{{ $t('addNewNetworks') }}
</template>
</a-tooltip>
<a-table
:loading="loading"
:columns="columns"
@ -70,6 +77,14 @@ export default {
loading: {
type: Boolean,
default: false
},
zoneId: {
type: String,
default: () => ''
},
preFillContent: {
type: Object,
default: () => {}
}
},
data () {
@ -77,7 +92,11 @@ export default {
filter: '',
selectedRowKeys: [],
vpcs: [],
filteredInfo: null
filteredInfo: null,
networkOffering: {
loading: false,
opts: []
}
}
},
computed: {
@ -147,8 +166,22 @@ export default {
if (newValue && !_.isEqual(newValue, oldValue)) {
this.selectedRowKeys = newValue
}
},
loading () {
if (!this.loading) {
if (this.preFillContent.networkids) {
this.selectedRowKeys = this.preFillContent.networkids
this.$emit('select-network-item', this.preFillContent.networkids)
} else {
this.selectedRowKeys = []
this.$emit('select-network-item', null)
}
}
}
},
beforeCreate () {
this.form = this.$form.createForm(this)
},
created () {
api('listVPCs', {
projectid: store.getters.project.id
@ -156,6 +189,7 @@ export default {
this.vpcs = _.get(response, 'listvpcsresponse.vpc')
})
},
inject: ['vmFetchNetworks'],
methods: {
getDetails (network) {
return [
@ -178,6 +212,24 @@ export default {
this.options.page = pagination.current
this.options.pageSize = pagination.pageSize
this.$emit('handle-search-filter', this.options)
},
listNetworkOfferings () {
return new Promise((resolve, reject) => {
const args = {}
args.forvpc = false
args.zoneid = this.zoneId
args.guestiptype = 'Isolated'
args.supportedServices = 'SourceNat'
args.specifyvlan = false
args.state = 'Enabled'
api('listNetworkOfferings', args).then(json => {
const listNetworkOfferings = json.listnetworkofferingsresponse.networkoffering || []
resolve(listNetworkOfferings)
}).catch(error => {
resolve(error)
})
})
}
}
}

View File

@ -53,6 +53,10 @@ export default {
loading: {
type: Boolean,
default: false
},
preFillContent: {
type: Object,
default: () => {}
}
},
data () {
@ -80,12 +84,7 @@ export default {
}
},
created () {
this.dataItems = []
this.dataItems.push({
name: this.$t('noselect'),
account: '-',
domain: '-'
})
this.initDataItem()
},
computed: {
options () {
@ -121,11 +120,31 @@ export default {
},
items (newData, oldData) {
if (newData && newData.length > 0) {
this.initDataItem()
this.dataItems = this.dataItems.concat(newData)
}
},
loading () {
if (!this.loading) {
if (this.preFillContent.keypair) {
this.selectedRowKeys = [this.preFillContent.keypair]
this.$emit('select-ssh-key-pair-item', this.preFillContent.keypair)
} else {
this.selectedRowKeys = [this.$t('noselect')]
this.$emit('select-ssh-key-pair-item', this.$t('noselect'))
}
}
}
},
methods: {
initDataItem () {
this.dataItems = []
this.dataItems.push({
name: this.$t('noselect'),
account: '-',
domain: '-'
})
},
onSelectRow (value) {
this.selectedRowKeys = value
this.$emit('select-ssh-key-pair-item', value[0])

View File

@ -81,6 +81,14 @@ export default {
itemCount: {
type: Number,
default: 0
},
osType: {
type: String,
default: ''
},
preFillContent: {
type: Object,
default: () => {}
}
},
data () {
@ -90,16 +98,14 @@ export default {
pageSize: 10
}
},
created () {
this.value = this.selected
this.$emit('emit-update-template-iso', this.inputDecorator, this.value)
},
watch: {
inputDecorator (value) {
if (value === 'templateid') {
this.value = this.selected
}
mounted () {
if (this.inputDecorator === 'templateid') {
this.value = !this.preFillContent.templateid ? this.selected : this.preFillContent.templateid
} else {
this.value = !this.preFillContent.isoid ? this.selected : this.preFillContent.isoid
}
this.$emit('emit-update-template-iso', this.inputDecorator, this.value)
},
computed: {
pagination () {

View File

@ -17,26 +17,74 @@
<template>
<div>
<a-input-search
class="search-input"
placeholder="Search"
v-model="filter"
@search="filterDataSource"/>
<span class="filter-group">
<a-input-search
class="search-input"
placeholder="Search"
v-model="filter"
@search="filterDataSource">
<a-popover
placement="bottomRight"
slot="addonAfter"
trigger="click"
v-model="visibleFilter">
<template slot="content">
<a-form
style="width: 170px"
:form="form"
layout="vertical"
@submit="handleSubmit">
<a-form-item :label="$t('filter')">
<a-select
allowClear
v-decorator="['filter']">
<a-select-option
v-for="(opt) in filterOpts"
:key="opt.id">{{ $t(opt.name) }}</a-select-option>
</a-select>
</a-form-item>
<div class="filter-group-button">
<a-button
class="filter-group-button-clear"
type="default"
size="small"
icon="stop"
@click="onClear">Clear</a-button>
<a-button
class="filter-group-button-search"
type="primary"
size="small"
icon="search"
@click="handleSubmit">Search</a-button>
</div>
</a-form>
</template>
<a-button
class="filter-group-button"
icon="filter"
size="small"/>
</a-popover>
</a-input-search>
</span>
<a-spin :spinning="loading">
<a-tabs
tabPosition="top"
:animated="false"
:defaultActiveKey="Object.keys(dataSource)[0]">
:defaultActiveKey="Object.keys(dataSource)[0]"
tabPosition="top"
v-model="osType"
@change="changeOsName">
<a-tab-pane v-for="(osList, osName) in dataSource" :key="osName">
<span slot="tab">
<os-logo :os-name="osName"></os-logo>
</span>
<TemplateIsoRadioGroup
:osType="osName"
v-if="osType===osName"
:osType="osType"
:osList="dataSource[osName]"
:input-decorator="inputDecorator"
:selected="checkedValue"
:itemCount="itemCount[osName]"
:preFillContent="preFillContent"
@handle-filter-tag="filterDataSource"
@emit-update-template-iso="updateTemplateIso"
></TemplateIsoRadioGroup>
@ -72,6 +120,10 @@ export default {
loading: {
type: Boolean,
default: false
},
preFillContent: {
type: Object,
default: () => {}
}
},
data () {
@ -80,7 +132,22 @@ export default {
filteredItems: this.items,
checkedValue: '',
dataSource: {},
itemCount: {}
itemCount: {},
visibleFilter: false,
filterOpts: [{
id: 'featured',
name: 'featured'
}, {
id: 'community',
name: 'community'
}, {
id: 'selfexecutable',
name: 'selfexecutable'
}, {
id: 'sharedexecutable',
name: 'sharedexecutable'
}],
osType: ''
}
},
watch: {
@ -92,6 +159,7 @@ export default {
this.checkedValue = items[0].id
}
this.dataSource = this.mappingDataSource()
this.osType = Object.keys(this.dataSource)[0]
},
inputDecorator (newValue, oldValue) {
if (newValue !== oldValue) {
@ -99,6 +167,10 @@ export default {
}
}
},
beforeCreate () {
this.form = this.$form.createForm(this)
},
inject: ['vmFetchTemplates', 'vmFetchIsos'],
methods: {
mappingDataSource () {
let mappedItems = {}
@ -124,6 +196,7 @@ export default {
return mappedItems
},
updateTemplateIso (name, id) {
this.checkedValue = id
this.$emit('update-template-iso', name, id)
},
filterDataSource (strQuery) {
@ -164,6 +237,31 @@ export default {
}
return arrResult
},
handleSubmit (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (err) {
return
}
if (this.inputDecorator === 'template') {
this.vmFetchTemplates(values.filter)
} else {
this.vmFetchIsos(values.filter)
}
})
},
onClear () {
const field = { filter: undefined }
this.form.setFieldsValue(field)
if (this.inputDecorator === 'template') {
this.vmFetchTemplates()
} else {
this.vmFetchIsos()
}
},
changeOsName (value) {
this.osType = value
}
}
}
@ -188,4 +286,32 @@ export default {
/deep/.ant-tabs-nav-scroll {
min-height: 45px;
}
.filter-group {
/deep/.ant-input-group-addon {
padding: 0 5px;
}
&-button {
background: inherit;
border: 0;
padding: 0;
}
&-button {
position: relative;
display: block;
min-height: 25px;
&-clear {
position: absolute;
left: 0;
}
&-search {
position: absolute;
right: 0;
}
}
}
</style>