compute: work-in-progress VM deployment wizard (#7)

This implements a work-in-progress VM deployment wizard.

Co-authored-by: Rohit Yadav <rohit@apache.org>
Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
Florian Symanowski 2020-01-31 10:14:34 +01:00 committed by Rohit Yadav
parent 2e50c068c7
commit ef189cea3f
16 changed files with 1331 additions and 254 deletions

8
ui/package-lock.json generated
View File

@ -15839,7 +15839,8 @@
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
"dev": true
},
"lodash.identity": {
"version": "3.0.0",
@ -15871,11 +15872,6 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
"lodash.pick": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz",
"integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM="
},
"lodash.pickby": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz",

View File

@ -43,8 +43,7 @@
"core-js": "^3.6.1",
"enquire.js": "^2.1.6",
"js-cookie": "^2.2.1",
"lodash.get": "^4.4.2",
"lodash.pick": "^4.4.0",
"lodash": "^4.17.15",
"md5": "^2.2.1",
"moment": "^2.24.0",
"node-emoji": "^1.10.0",

View File

@ -425,6 +425,16 @@
<a-icon type="calendar" />{{ resource.created }}
</div>
</div>
<div class="resource-detail-item" v-if="resource.affinitygroup && resource.affinitygroup.length > 0">
<a-icon type="swap" />
<span
v-for="(group, index) in resource.affinitygroup"
:key="group.id"
>
<router-link :to="{ path: '/affinitygroup/' + group.id }">{{ group.name }}</router-link>
<span v-if="index + 1 < resource.affinitygroup.length">, </span>
</span>
</div>
</div>
<div class="account-center-tags" v-if="$route.meta.related">

View File

@ -1055,7 +1055,17 @@
"instance": "Instance",
"yourInstance": "Your instance",
"newInstance": "New instance",
"defaultNetwork": "Default network",
"cpu": "CPU",
"ram": "RAM",
"minMaxIops": "Min IOPS / Max IOPS",
"isSelf": "Self",
"isShared": "Shared",
"networks": "Networks",
"BasicSetup": "Basic setup",
"templateIso": "Template/ISO",
"addAnotherNetwork": "Add another network",
"addNewNetworks": "Add new networks",
"existingNetworks": "Existing networks",
"sshKeyPairs": "SSH keypairs",
"wednesday": "Wednesday"
}

View File

@ -25,88 +25,120 @@
@submit="handleSubmit"
layout="vertical"
>
<a-form-item :label="this.$t('name')">
<a-input
v-decorator="['name']"
:placeholder="this.$t('vm.name.description')"
/>
</a-form-item>
<a-form-item :label="this.$t('zoneid')">
<a-select
v-decorator="['zoneid', {
rules: [{ required: zoneId.required, message: 'Please select option' }]
}]"
:placeholder="this.$t('vm.zone.description')"
>
<a-select-option
v-for="(opt, optIndex) in zoneId.opts"
:key="optIndex"
:value="opt.id"
>
{{ opt.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-collapse
:accordion="true"
defaultActiveKey="templates"
:accordion="false"
:bordered="false"
defaultActiveKey="basic"
>
<a-collapse-panel :header="this.$t('Templates')" key="templates">
<template-selection
:templates="templateId.opts"
></template-selection>
<a-form-item :label="this.$t('diskSize')">
<a-row>
<a-col :span="10">
<a-slider
:min="0"
:max="1024"
v-decorator="['rootdisksize']"
/>
</a-col>
<a-col :span="4">
<a-input-number
v-decorator="['rootdisksize', {
rules: [{ required: false, message: 'Please enter a number' }]
}]"
:placeholder="this.$t('vm.rootdisksize')"
:formatter="value => `${value} GB`"
:parser="value => value.replace(' GB', '')"
/>
</a-col>
</a-row>
<a-collapse-panel :header="this.$t('BasicSetup')" key="basic">
<a-form-item :label="this.$t('name')">
<a-input
v-decorator="['name']"
:placeholder="this.$t('vm.name.description')"
/>
</a-form-item>
<a-form-item :label="this.$t('zoneid')">
<a-select
v-decorator="['zoneid', {
rules: [{ required: true, message: 'Please select option' }]
}]"
:placeholder="this.$t('vm.zone.description')"
:options="zoneSelectOptions"
@change="onSelectZoneId"
></a-select>
</a-form-item>
</a-collapse-panel>
<a-collapse-panel :header="this.$t('ISOs')" key="isos">
<!-- ToDo: Add iso selection -->
<a-collapse-panel :header="this.$t('templateIso')" key="templates-isos">
<a-collapse
:accordion="true"
defaultActiveKey="templates"
@change="onTemplatesIsosCollapseChange"
>
<a-collapse-panel :header="this.$t('Templates')" key="templates">
<template-iso-selection
input-decorator="templateid"
:items="options.templates"
></template-iso-selection>
<disk-size-selection
input-decorator="rootdisksize"
></disk-size-selection>
</a-collapse-panel>
<a-collapse-panel :header="this.$t('ISOs')" key="isos">
<template-iso-selection
input-decorator="isoid"
:items="options.isos"
></template-iso-selection>
</a-collapse-panel>
</a-collapse>
</a-collapse-panel>
<a-collapse-panel :header="this.$t('serviceOfferingId')" key="compute">
<compute-selection
:compute-items="options.serviceOfferings"
:value="serviceOffering ? serviceOffering.id : ''"
@select-compute-item="($event) => updateComputeOffering($event)"
></compute-selection>
</a-collapse-panel>
<a-collapse-panel :header="this.$t('diskOfferingId')" key="disk">
<disk-offering-selection
:items="options.diskOfferings"
:value="diskOffering ? diskOffering.id : ''"
@select-disk-offering-item="($event) => updateDiskOffering($event)"
></disk-offering-selection>
<disk-size-selection
v-if="diskOffering && diskOffering.iscustomized"
input-decorator="size"
></disk-size-selection>
</a-collapse-panel>
<a-collapse-panel :header="this.$t('Affinity Groups')" key="affinity">
<affinity-group-selection
:items="options.affinityGroups"
:value="affinityGroupIds"
@select-affinity-group-item="($event) => updateAffinityGroups($event)"
></affinity-group-selection>
</a-collapse-panel>
<a-collapse-panel :header="this.$t('networks')" key="networks">
<a-collapse
:accordion="false"
>
<a-collapse-panel
:header="$t('existingNetworks')"
>
<network-selection
:items="options.networks"
:value="networkOfferingIds"
@select-network-item="($event) => updateNetworks($event)"
></network-selection>
</a-collapse-panel>
<a-collapse-panel
:header="$t('addNewNetworks')"
>
<network-creation></network-creation>
</a-collapse-panel>
</a-collapse>
<network-configuration
v-if="networks.length > 0"
:items="networks"
></network-configuration>
</a-collapse-panel>
<a-collapse-panel :header="this.$t('sshKeyPairs')" key="sshKeyPairs">
<ssh-key-pair-selection
:items="options.sshKeyPairs"
:value="sshKeyPair ? sshKeyPair.name : ''"
@select-ssh-key-pair-item="($event) => updateSshKeyPairs($event)"
></ssh-key-pair-selection>
</a-collapse-panel>
</a-collapse>
<compute-selection
:compute-items="serviceOfferingId.opts"
:value="serviceOffering ? serviceOffering.id : ''"
@select-compute-item="($event) => updateComputeOffering($event)"
></compute-selection>
<a-form-item :label="this.$t('diskOfferingId')">
<a-select
v-decorator="['diskofferingid', {
rules: [{ required: diskOfferingId.required, message: 'Please select option' }]
}]"
:placeholder="this.$t('vm.diskoffering.description')"
>
<a-select-option
v-for="(opt, optIndex) in diskOfferingId.opts"
:key="optIndex"
:value="opt.id"
>
{{ opt.name }}
</a-select-option>
</a-select>
</a-form-item>
<div class="card-footer">
<!-- ToDo extract as component -->
<a-button @click="() => this.$router.back()">{{ this.$t('cancel') }}</a-button>
@ -115,17 +147,22 @@
</a-form>
</a-card>
</a-col>
<a-col :md="24" :lg="7">
<info-card :resource="vm" :title="this.$t('yourInstance')" >
<div slot="details" v-if="vm.diskofferingid || instanceConfig.rootdisksize">
<a-icon type="hdd"></a-icon>
<span style="margin-left: 10px">
<span v-if="instanceConfig.rootdisksize">{{ instanceConfig.rootdisksize }} GB (Root)</span>
<span v-if="instanceConfig.rootdisksize && instanceConfig.diskofferingid"> | </span>
<span v-if="instanceConfig.diskofferingid">{{ diskOffering.disksize }} GB (Data)</span>
</span>
</div>
</info-card>
<a-col :md="24" :lg="7" v-if="!isMobile()">
<a-affix :offsetTop="75">
<info-card :resource="vm" :title="this.$t('yourInstance')">
<!-- ToDo: Refactor this, maybe move everything to the info-card component -->
<div slot="details" v-if="diskSize" style="margin-bottom: 12px;">
<a-icon type="hdd"></a-icon>
<span style="margin-left: 10px">{{ diskSize }}</span>
</div>
<div slot="details" v-if="networks">
<div v-for="network in networks" :key="network.id" style="margin-bottom: 12px;">
<a-icon type="api"></a-icon>
<span style="margin-left: 10px">{{ network.name }}</span>
</div>
</div>
</info-card>
</a-affix>
</a-col>
</a-row>
</div>
@ -134,69 +171,144 @@
<script>
import Vue from 'vue'
import { api } from '@/api'
import store from '@/store'
import _ from 'lodash'
import { mixin, mixinDevice } from '@/utils/mixin.js'
import store from '@/store'
import InfoCard from '@/components/view/InfoCard'
import ComputeSelection from './wizard/ComputeSelection'
import TemplateSelection from './wizard/TemplateSelection'
import DiskOfferingSelection from '@views/compute/wizard/DiskOfferingSelection'
import DiskSizeSelection from '@views/compute/wizard/DiskSizeSelection'
import TemplateIsoSelection from '@views/compute/wizard/TemplateIsoSelection'
import AffinityGroupSelection from '@views/compute/wizard/AffinityGroupSelection'
import NetworkSelection from '@views/compute/wizard/NetworkSelection'
import NetworkConfiguration from '@views/compute/wizard/NetworkConfiguration'
import NetworkCreation from '@views/compute/wizard/NetworksCreation'
import SshKeyPairSelection from '@views/compute/wizard/SshKeyPairSelection'
export default {
name: 'Wizard',
components: {
SshKeyPairSelection,
NetworkCreation,
NetworkConfiguration,
NetworkSelection,
AffinityGroupSelection,
TemplateIsoSelection,
DiskSizeSelection,
DiskOfferingSelection,
InfoCard,
ComputeSelection,
TemplateSelection
ComputeSelection
},
props: {
visible: {
type: Boolean
}
},
mixins: [mixin, mixinDevice],
data () {
return {
vm: {},
params: [],
visibleParams: [
'name',
'templateid',
'serviceofferingid',
'diskofferingid',
'zoneid',
'rootdisksize'
],
options: {
templates: [],
isos: [],
serviceOfferings: [],
diskOfferings: [],
zones: [],
affinityGroups: [],
networks: [],
sshKeyPairs: []
},
instanceConfig: [],
template: {},
iso: {},
serviceOffering: {},
diskOffering: {},
zone: {}
affinityGroups: [],
networks: [],
zone: {},
sshKeyPair: {},
isoFilter: [
'executable',
'selfexecutable',
'sharedexecutable'
]
}
},
computed: {
filteredParams () {
return this.visibleParams.map((fieldName) => {
return this.params.find((param) => fieldName === param.name)
diskSize () {
const rootDiskSize = _.get(this.instanceConfig, 'rootdisksize', 0)
const customDiskSize = _.get(this.instanceConfig, 'size', 0)
const diskOfferingDiskSize = _.get(this.diskOffering, 'disksize', 0)
const dataDiskSize = diskOfferingDiskSize > 0 ? diskOfferingDiskSize : customDiskSize
const size = []
if (rootDiskSize > 0) {
size.push(`${rootDiskSize} GB (Root)`)
}
if (dataDiskSize > 0) {
size.push(`${dataDiskSize} GB (Data)`)
}
return size.join(' | ')
},
affinityGroupIds () {
return _.map(this.affinityGroups, 'id')
},
params () {
return {
templates: {
list: 'listTemplates',
options: {
templatefilter: 'executable',
zoneid: _.get(this.zone, 'id')
}
},
serviceOfferings: {
list: 'listServiceOfferings'
},
diskOfferings: {
list: 'listDiskOfferings'
},
zones: {
list: 'listZones'
},
affinityGroups: {
list: 'listAffinityGroups'
},
sshKeyPairs: {
list: 'listSSHKeyPairs'
},
networks: {
list: 'listNetworks',
options: {
zoneid: _.get(this.zone, 'id'),
canusefordeploy: true,
projectid: store.getters.project.id
}
}
}
},
networkOfferingIds () {
return _.map(this.networks, 'id')
},
zoneSelectOptions () {
return this.options.zones.map((zone) => {
return {
label: zone.name,
value: zone.id
}
})
},
templateId () {
return this.getParam('templateid')
},
serviceOfferingId () {
return this.getParam('serviceofferingid')
},
diskOfferingId () {
return this.getParam('diskofferingid')
},
zoneId () {
return this.getParam('zoneid')
}
},
watch: {
instanceConfig (instanceConfig) {
this.template = this.templateId.opts.find((option) => option.id === instanceConfig.templateid)
this.serviceOffering = this.serviceOfferingId.opts.find((option) => option.id === instanceConfig.computeofferingid)
this.diskOffering = this.diskOfferingId.opts.find((option) => option.id === instanceConfig.diskofferingid)
this.zone = this.zoneId.opts.find((option) => option.id === instanceConfig.zoneid)
this.template = _.find(this.options.templates, (option) => option.id === instanceConfig.templateid)
this.iso = _.find(this.options.isos, (option) => option.id === instanceConfig.isoid)
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)
this.affinityGroups = _.filter(this.options.affinityGroups, (option) => _.includes(instanceConfig.affinitygroupids, option.id))
this.networks = _.filter(this.options.networks, (option) => _.includes(instanceConfig.networkids, option.id))
this.sshKeyPair = _.find(this.options.sshKeyPairs, (option) => option.name === instanceConfig.keypair)
if (this.zone) {
this.vm.zoneid = this.zone.id
@ -210,6 +322,13 @@ export default {
this.vm.ostypename = this.template.ostypename
}
if (this.iso) {
this.vm.templateid = this.iso.id
this.vm.templatename = this.iso.displaytext
this.vm.ostypeid = this.iso.ostypeid
this.vm.ostypename = this.iso.ostypename
}
if (this.serviceOffering) {
this.vm.serviceofferingid = this.serviceOffering.id
this.vm.serviceofferingname = this.serviceOffering.displaytext
@ -223,22 +342,36 @@ export default {
this.vm.diskofferingname = this.diskOffering.displaytext
this.vm.diskofferingsize = this.diskOffering.disksize
}
if (this.affinityGroups) {
this.vm.affinitygroup = this.affinityGroups
}
}
},
beforeCreate () {
this.form = this.$form.createForm(this, {
onValuesChange: (props, fields) => {
if (fields.isoid) {
this.form.setFieldsValue({
templateid: null,
rootdisksize: 0
})
} else if (fields.templateid) {
this.form.setFieldsValue({ isoid: null })
}
this.instanceConfig = { ...this.form.getFieldsValue(), ...fields }
this.vm = this.instanceConfig
}
})
this.form.getFieldDecorator('computeofferingid', { initialValue: [], preserve: true })
this.form.getFieldDecorator('diskofferingid', { initialValue: [], preserve: true })
this.form.getFieldDecorator('affinitygroupids', { initialValue: [], preserve: true })
this.form.getFieldDecorator('isoid', { initialValue: [], preserve: true })
this.form.getFieldDecorator('networkids', { initialValue: [], preserve: true })
this.form.getFieldDecorator('keypair', { initialValue: [], preserve: true })
},
created () {
this.params = store.getters.apis[this.$route.name].params
this.filteredParams.forEach((param) => {
this.fetchOptions(param)
})
_.each(this.params, this.fetchOptions)
Vue.nextTick().then(() => {
this.instanceConfig = this.form.getFieldsValue() // ToDo: maybe initialize with some other defaults
})
@ -249,8 +382,25 @@ export default {
computeofferingid: id
})
},
getParam (paramName) {
return this.params.find((param) => param.name === paramName)
updateDiskOffering (id) {
this.form.setFieldsValue({
diskofferingid: id
})
},
updateAffinityGroups (ids) {
this.form.setFieldsValue({
affinitygroupids: ids
})
},
updateNetworks (ids) {
this.form.setFieldsValue({
networkids: ids
})
},
updateSshKeyPairs (name) {
this.form.setFieldsValue({
keypair: name
})
},
getText (option) {
return _.get(option, 'displaytext', _.get(option, 'name'))
@ -258,30 +408,12 @@ export default {
handleSubmit () {
console.log('wizard submit')
},
fetchOptions (param) {
const paramName = param.name
const possibleName = `list${paramName.replace('id', '').toLowerCase()}s`
let possibleApi
if (paramName === 'id') {
possibleApi = this.apiName
} else {
possibleApi = _.filter(Object.keys(store.getters.apis), (api) => {
return api.toLowerCase().startsWith(possibleName)
})[0]
}
if (!possibleApi) {
return
}
fetchOptions (param, name) {
param.loading = true
param.opts = []
const params = {}
params.listall = true
if (possibleApi === 'listTemplates') {
params.templatefilter = 'executable'
}
api(possibleApi, params).then((response) => {
const options = param.options || {}
options.listall = true
api(param.list, options).then((response) => {
param.loading = false
_.map(response, (responseItem, responseKey) => {
if (!responseKey.includes('response')) {
@ -292,6 +424,7 @@ export default {
return
}
param.opts = response
this.options[name] = response
this.$forceUpdate()
})
})
@ -299,6 +432,40 @@ export default {
console.log(error.stack)
param.loading = false
})
},
fetchIsos (isoFilter) {
api('listIsos', {
zoneid: _.get(this.zone, 'id'),
isofilter: isoFilter,
bootable: true
}).then((response) => {
const concatedIsos = _.concat(this.options.isos, _.get(response, 'listisosresponse.iso', []))
this.options.isos = _.uniqWith(concatedIsos, _.isEqual)
this.$forceUpdate()
}).catch((reason) => {
// ToDo: Handle errors
console.log(reason)
})
},
fetchAllIsos () {
this.options.isos = []
this.isoFilter.forEach((filter) => {
this.fetchIsos(filter)
})
},
onTemplatesIsosCollapseChange (key) {
if (key === 'isos' && this.options.isos.length === 0) {
this.fetchAllIsos()
}
},
onSelectZoneId () {
this.$nextTick(() => {
if (this.options.isos.length !== 0) {
this.fetchAllIsos()
}
this.fetchOptions(this.params.templates, 'templates')
this.fetchOptions(this.params.networks, 'networks')
})
}
}
}
@ -307,6 +474,7 @@ export default {
<style lang="less" scoped>
.card-footer {
text-align: right;
margin-top: 2rem;
button + button {
margin-left: 8px;
@ -321,3 +489,20 @@ export default {
margin: 2rem 0;
}
</style>
<style lang="less">
@import url('../../style/index');
.ant-table-selection-column {
// Fix for the table header if the row selection use radio buttons instead of checkboxes
> div:empty {
width: 16px;
}
}
.ant-collapse-borderless > .ant-collapse-item {
border: 1px solid @border-color-split;
border-radius: @border-radius-base !important;
margin: 0 0 1.2rem;
}
</style>

View File

@ -0,0 +1,87 @@
// 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>
<a-table
:columns="columns"
:dataSource="items"
:rowKey="record => record.id"
:pagination="{showSizeChanger: true}"
:rowSelection="rowSelection"
size="middle"
>
</a-table>
</template>
<script>
import _ from 'lodash'
export default {
name: 'AffinityGroupSelection',
props: {
items: {
type: Array,
default: () => []
},
value: {
type: Array,
default: () => []
}
},
data () {
return {
columns: [
{
dataIndex: 'name',
title: this.$t('Affinity Groups'),
width: '40%'
},
{
dataIndex: 'description',
title: this.$t('description'),
width: '60%'
}
],
selectedRowKeys: []
}
},
computed: {
rowSelection () {
return {
type: 'checkbox',
selectedRowKeys: this.selectedRowKeys,
onChange: (rows) => {
this.$emit('select-affinity-group-item', rows)
}
}
}
},
watch: {
value (newValue, oldValue) {
if (newValue && !_.isEqual(newValue, oldValue)) {
this.selectedRowKeys = newValue
}
}
}
}
</script>
<style lang="less" scoped>
.ant-table-wrapper {
margin: 2rem 0;
}
</style>

View File

@ -19,13 +19,12 @@
<a-table
:columns="columns"
:dataSource="tableSource"
:pagination="false"
:scroll="{x: 0, y: 320}"
:pagination="{showSizeChanger: true}"
:rowSelection="rowSelection"
size="middle"
>
<span slot="cpuTitle"><a-icon type="appstore" /> {{ $t('cpu') }}</span>
<span slot="ramTitle"><a-icon type="bulb" /> {{ $t('ram') }}</span>
<span slot="ramTitle"><a-icon type="bulb" /> {{ $t('memory') }}</span>
</a-table>
</template>
@ -47,6 +46,7 @@ export default {
columns: [
{
dataIndex: 'name',
title: this.$t('serviceOfferingId'),
width: '40%'
},
{
@ -99,12 +99,3 @@ export default {
margin: 2rem 0;
}
</style>
<style lang="less">
.ant-table-selection-column {
// Fix for the table header if the row selection use radio buttons instead of checkboxes
> div:empty {
width: 16px;
}
}
</style>

View File

@ -0,0 +1,107 @@
// 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>
<a-table
:columns="columns"
:dataSource="tableSource"
:pagination="{showSizeChanger: true}"
:rowSelection="rowSelection"
size="middle"
>
<span slot="diskSizeTitle"><a-icon type="hdd" /> {{ $t('disksize') }}</span>
<span slot="iopsTitle"><a-icon type="rocket" /> {{ $t('minMaxIops') }}</span>
<template slot="diskSize" slot-scope="text, record">
<div v-if="record.isCustomized">{{ $t('isCustomized') }}</div>
<div v-else>{{ record.diskSize }} GB</div>
</template>
</a-table>
</template>
<script>
export default {
name: 'DiskOfferingSelection',
props: {
items: {
type: Array,
default: () => []
},
value: {
type: String,
default: ''
}
},
data () {
return {
columns: [
{
dataIndex: 'name',
title: this.$t('diskOffering'),
width: '40%'
},
{
dataIndex: 'diskSize',
slots: { title: 'diskSizeTitle' },
width: '30%',
scopedSlots: { customRender: 'diskSize' }
},
{
dataIndex: 'iops',
slots: { title: 'iopsTitle' },
width: '30%'
}
],
selectedRowKeys: []
}
},
computed: {
tableSource () {
return this.items.map((item) => {
return {
key: item.id,
name: item.name,
diskSize: item.disksize,
iops: `${item.miniops} ${item.maxiops}`,
isCustomized: item.iscustomized
}
})
},
rowSelection () {
return {
type: 'radio',
selectedRowKeys: this.selectedRowKeys,
onSelect: (row) => {
this.$emit('select-disk-offering-item', row.key)
}
}
}
},
watch: {
value (newValue, oldValue) {
if (newValue && newValue !== oldValue) {
this.selectedRowKeys = [newValue]
}
}
}
}
</script>
<style lang="less" scoped>
.ant-table-wrapper {
margin: 2rem 0;
}
</style>

View File

@ -0,0 +1,55 @@
// 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>
<a-form-item :label="this.$t('diskSize')">
<a-row>
<a-col :span="10">
<a-slider
:min="0"
:max="1024"
v-decorator="[inputDecorator]"
/>
</a-col>
<a-col :span="4">
<a-input-number
v-decorator="[inputDecorator, {
rules: [{ required: false, message: 'Please enter a number' }]
}]"
:formatter="value => `${value} GB`"
:parser="value => value.replace(' GB', '')"
/>
</a-col>
</a-row>
</a-form-item>
</template>
<script>
export default {
name: 'DiskSizeSelection',
props: {
inputDecorator: {
type: String,
default: ''
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,102 @@
// 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>
<a-table
:columns="columns"
:dataSource="items"
:pagination="{showSizeChanger: true}"
:rowSelection="rowSelection"
:rowKey="record => record.id"
size="middle"
>
<template v-slot:ipAddress="text">
<a-input
:value="text"
></a-input>
</template>
<template v-slot:macAddress="text">
<a-input
:value="text"
></a-input>
</template>
</a-table>
</template>
<script>
export default {
name: 'NetworkConfiguration',
props: {
items: {
type: Array,
default: () => []
},
value: {
type: String,
default: ''
}
},
data () {
return {
columns: [
{
dataIndex: 'name',
title: this.$t('defaultNetwork'),
width: '40%'
},
{
dataIndex: 'ip',
title: this.$t('ip'),
width: '30%',
scopedSlots: { customRender: 'ipAddress' }
},
{
dataIndex: 'mac',
title: this.$t('macaddress'),
width: '30%',
scopedSlots: { customRender: 'macAddress' }
}
],
selectedRowKeys: []
}
},
computed: {
rowSelection () {
return {
type: 'radio',
selectedRowKeys: this.selectedRowKeys,
onSelect: (row) => {
this.$emit('select-default-network-item', row.key)
}
}
}
},
watch: {
value (newValue, oldValue) {
if (newValue && newValue !== oldValue) {
this.selectedRowKeys = [newValue]
}
}
}
}
</script>
<style lang="less" scoped>
.ant-table-wrapper {
margin: 2rem 0;
}
</style>

View File

@ -0,0 +1,158 @@
// 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>
<a-table
:columns="columns"
:dataSource="networkItems"
:rowKey="record => record.id"
:pagination="{showSizeChanger: true}"
:rowSelection="rowSelection"
>
<a-list
slot="expandedRowRender"
slot-scope="record"
:key="record.id"
:dataSource="getDetails(record)"
size="small"
>
<a-list-item slot="renderItem" slot-scope="item" :key="item.id">
<a-list-item-meta
:description="item.description"
>
<template v-slot:title>{{ item.title }}</template>
</a-list-item-meta>
</a-list-item>
</a-list>
</a-table>
</template>
<script>
import _ from 'lodash'
import { api } from '@/api'
import store from '@/store'
export default {
name: 'NetworkSelection',
props: {
items: {
type: Array,
default: () => []
},
value: {
type: Array,
default: () => []
}
},
data () {
return {
selectedRowKeys: [],
vpcs: [],
filteredInfo: null
}
},
computed: {
columns () {
let vpcFilter = []
if (this.vpcs) {
vpcFilter = this.vpcs.map((vpc) => {
return {
text: vpc.displaytext,
value: vpc.id
}
})
}
return [
{
dataIndex: 'name',
title: this.$t('networks'),
width: '40%'
},
{
dataIndex: 'type',
title: this.$t('guestIpType'),
width: '30%'
},
{
dataIndex: 'vpcName',
title: this.$t('VPC'),
width: '30%',
filters: vpcFilter,
filteredValue: _.get(this.filteredInfo, 'id'),
onFilter: (value, record) => {
return record.vpcid === value
}
}
]
},
rowSelection () {
return {
type: 'checkbox',
selectedRowKeys: this.selectedRowKeys,
onChange: (rows) => {
this.$emit('select-network-item', rows)
}
}
},
networkItems () {
return this.items.map((network) => {
const vpc = _.find(this.vpcs, { id: network.vpcid })
return {
...network,
...{
vpcName: _.get(vpc, 'displaytext')
}
}
})
}
},
watch: {
value (newValue, oldValue) {
if (newValue && !_.isEqual(newValue, oldValue)) {
this.selectedRowKeys = newValue
}
}
},
created () {
api('listVPCs', {
projectid: store.getters.project.id
}).then((response) => {
this.vpcs = _.get(response, 'listvpcsresponse.vpc')
})
},
methods: {
getDetails (network) {
return [
{
title: this.$t('description'),
description: network.displaytext
},
{
title: this.$t('networkOfferingId'),
description: network.networkofferingdisplaytext
}
]
}
}
}
</script>
<style lang="less" scoped>
.ant-table-wrapper {
margin: 2rem 0;
}
</style>

View File

@ -0,0 +1,150 @@
// 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-table
v-if="networkItems.length > 0"
:columns="columns"
:dataSource="networkItems"
:pagination="false"
>
<template v-slot:name="text">
<a-input
:value="text"
></a-input>
</template>
<template v-slot:operation>
<a-popconfirm
v-if="networkItems.length"
title="Sure to delete?"
@confirm="removeItem()"
>
<a-button type="link">Delete</a-button>
</a-popconfirm>
</template>
<template v-slot:networkOffering>
<a-select
:placeholder="$t('networkOfferingId')"
:options="networkOfferingOptions"
></a-select>
</template>
<template v-slot:vpc>
<a-select
:placeholder="$t('vpc')"
:options="vpcOptions"
></a-select>
</template>
</a-table>
<div style="text-align: right; margin-top: 1rem;">
<a-button
type="primary"
@click="addNewItem"
>{{ $t('addAnotherNetwork') }}
</a-button>
</div>
</div>
</template>
<script>
import { api } from '@/api'
import store from '@/store'
import _ from 'lodash'
/*
* ToDo: Implement real functionality
*/
export default {
name: 'NetworkCreation',
data () {
return {
networkItems: [{}],
columns: [
{
dataIndex: 'name',
title: this.$t('networks'),
scopedSlots: { customRender: 'name' },
width: '30%'
},
{
dataIndex: 'offering',
title: this.$t('networkOfferingId'),
scopedSlots: { customRender: 'networkOffering' },
width: '30%'
},
{
dataIndex: 'vpcName',
title: this.$t('VPC'),
scopedSlots: { customRender: 'vpc' },
width: '30%'
},
{
dataIndex: 'action',
scopedSlots: { customRender: 'operation' },
width: '10%'
}
],
networkOfferings: [],
vpcs: []
}
},
computed: {
networkOfferingOptions () {
return this.networkOfferings.map((offering) => {
return {
label: offering.name,
value: offering.id
}
})
},
vpcOptions () {
return this.vpcs.map((vpc) => {
return {
label: vpc.name,
value: vpc.id
}
})
}
},
created () {
api('listNetworkOfferings', {
// ToDo: Add the zoneId
}).then((response) => {
this.networkOfferings = _.get(response, 'listnetworkofferingsresponse.networkoffering')
})
// ToDo: Remove this redundant api call see the NetworkSelection component
api('listVPCs', {
projectid: store.getters.project.id
}).then((response) => {
this.vpcs = _.get(response, 'listvpcsresponse.vpc')
})
},
methods: {
addNewItem () {
this.networkItems.push({})
},
removeItem () {
this.networkItems.pop()
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,101 @@
// 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>
<a-table
:columns="columns"
:dataSource="tableSource"
:pagination="{showSizeChanger: true}"
:rowSelection="rowSelection"
size="middle"
>
<template v-slot:account><a-icon type="user" /> {{ $t('account') }}</template>
<template v-slot:domain><a-icon type="block" /> {{ $t('domain') }}</template>
</a-table>
</template>
<script>
export default {
name: 'SshKeyPairSelection',
props: {
items: {
type: Array,
default: () => []
},
value: {
type: String,
default: ''
}
},
data () {
return {
columns: [
{
dataIndex: 'name',
title: this.$t('sshKeyPairs'),
width: '40%'
},
{
dataIndex: 'account',
slots: { title: 'account' },
width: '30%'
},
{
dataIndex: 'domain',
slots: { title: 'domain' },
width: '30%'
}
],
selectedRowKeys: []
}
},
computed: {
tableSource () {
return this.items.map((item) => {
return {
key: item.name,
name: item.name,
account: item.account,
domain: item.domain
}
})
},
rowSelection () {
return {
type: 'radio',
selectedRowKeys: this.selectedRowKeys,
onSelect: (row) => {
this.$emit('select-ssh-key-pair-item', row.key)
}
}
}
},
watch: {
value (newValue, oldValue) {
if (newValue && newValue !== oldValue) {
this.selectedRowKeys = [newValue]
}
}
}
}
</script>
<style lang="less" scoped>
.ant-table-wrapper {
margin: 2rem 0;
}
</style>

View File

@ -0,0 +1,92 @@
// 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>
<a-form-item>
<a-radio-group
v-for="(os, osIndex) in osList"
:key="osIndex"
class="radio-group"
v-decorator="[inputDecorator, {
rules: [{ required: true, message: 'Please select option' }]
}]"
>
<a-radio
class="radio-group__radio"
:value="os.id"
>
{{ os.displaytext }}&nbsp;
<a-tag
:visible="os.ispublic && !os.isfeatured"
color="blue"
>{{ $t('isPublic') }}</a-tag>
<a-tag
:visible="os.isfeatured"
color="green"
>{{ $t('isFeatured') }}</a-tag>
<a-tag
:visible="isSelf(os)"
color="orange"
>{{ $t('isSelf') }}</a-tag>
<a-tag
:visible="isShared(os)"
color="cyan"
>{{ $t('isShared') }}</a-tag>
</a-radio>
</a-radio-group>
</a-form-item>
</template>
<script>
import store from '@/store'
export default {
name: 'TemplateIsoRadioGroup',
props: {
osList: {
type: Array,
default: () => []
},
inputDecorator: {
type: String,
default: ''
}
},
methods: {
isShared (item) {
return !item.ispublic && (item.account !== store.getters.userInfo.account)
},
isSelf (item) {
return !item.ispublic && (item.account === store.getters.userInfo.account)
}
}
}
</script>
<style lang="less" scoped>
.radio-group {
display: block;
&__radio {
margin: 0.5rem 0;
}
}
.ant-tag {
margin-left: 0.4rem;
}
</style>

View File

@ -0,0 +1,120 @@
// 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>
<a-tabs :defaultActiveKey="Object.keys(osTypes)[0]" v-if="view === TAB_VIEW">
<a-button icon="search" slot="tabBarExtraContent" @click="() => toggleView(FILTER_VIEW)"/>
<a-tab-pane v-for="(osList, osName) in osTypes" :key="osName">
<span slot="tab">
<os-logo :os-name="osName"></os-logo>
</span>
<TemplateIsoRadioGroup
:osList="osList"
:input-decorator="inputDecorator"
></TemplateIsoRadioGroup>
</a-tab-pane>
</a-tabs>
<div v-else>
<a-input class="search-input" v-model="filter">
<a-icon slot="prefix" type="search"/>
<a-icon slot="addonAfter" type="close" @click="toggleView(TAB_VIEW)"/>
</a-input>
<TemplateIsoRadioGroup
:osList="filteredItems"
:input-decorator="inputDecorator"
></TemplateIsoRadioGroup>
</div>
</template>
<script>
import OsLogo from '@/components/widgets/OsLogo'
import { getNormalizedOsName } from '@/utils/icons'
import _ from 'lodash'
import TemplateIsoRadioGroup from '@views/compute/wizard/TemplateIsoRadioGroup'
export const TAB_VIEW = 1
export const FILTER_VIEW = 2
export default {
name: 'TemplateIsoSelection',
components: { TemplateIsoRadioGroup, OsLogo },
props: {
items: {
type: Array,
default: () => []
},
inputDecorator: {
type: String,
default: ''
}
},
data () {
return {
TAB_VIEW: TAB_VIEW,
FILTER_VIEW: FILTER_VIEW,
visible: false,
filter: '',
filteredItems: this.items,
view: TAB_VIEW
}
},
computed: {
osTypes () {
let mappedItems = {}
this.items.forEach((os) => {
const osName = getNormalizedOsName(os.ostypename)
if (Array.isArray(mappedItems[osName])) {
mappedItems[osName].push(os)
} else {
mappedItems[osName] = [os]
}
})
mappedItems = _.mapValues(mappedItems, (list) => {
let featuredItems = list.filter((item) => item.isfeatured)
let nonFeaturedItems = list.filter((item) => !item.isfeatured)
featuredItems = _.sortBy(featuredItems, (item) => item.displaytext.toLowerCase())
nonFeaturedItems = _.sortBy(nonFeaturedItems, (item) => item.displaytext.toLowerCase())
return featuredItems.concat(nonFeaturedItems) // pin featured isos/templates at the top
})
return mappedItems
}
},
watch: {
items (items) {
this.filteredItems = items
},
filter (filterString) {
if (filterString !== '') {
this.filteredItems = this.filteredItems.filter((item) => item.displaytext.toLowerCase().includes(filterString))
} else {
this.filteredItems = this.items
}
}
},
methods: {
toggleView (view) {
this.view = view
}
}
}
</script>
<style lang="less" scoped>
.search-input {
margin: 0.5rem 0 1rem;
}
</style>

View File

@ -1,86 +0,0 @@
// 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>
<a-tabs :defaultActiveKey="Object.keys(osTypes)[0]">
<a-tab-pane v-for="(osList, osName) in osTypes" :key="osName">
<span slot="tab">
<os-logo :os-name="osName"></os-logo>
</span>
<a-form-item>
<a-radio-group
v-for="(os, osIndex) in osList"
:key="osIndex"
class="radio-group"
v-decorator="['templateid', {
rules: [{ required: true, message: 'Please select option' }]
}]"
>
<a-radio
class="radio-group__radio"
:value="os.id"
>{{ os.displaytext }}
</a-radio>
</a-radio-group>
</a-form-item>
</a-tab-pane>
</a-tabs>
</template>
<script>
import OsLogo from '@/components/widgets/OsLogo'
import { getNormalizedOsName } from '@/utils/icons'
export default {
name: 'TemplateSelection',
components: { OsLogo },
props: {
templates: {
type: Array,
default: () => []
}
},
data () {
return {}
},
computed: {
osTypes () {
const mappedTemplates = {}
this.templates.forEach((os) => {
const osName = getNormalizedOsName(os.ostypename)
if (Array.isArray(mappedTemplates[osName])) {
mappedTemplates[osName].push(os)
} else {
mappedTemplates[osName] = [os]
}
})
return mappedTemplates
}
}
}
</script>
<style lang="less" scoped>
.radio-group {
display: flex;
flex-direction: column;
&__radio {
margin: 0.5rem 0;
}
}
</style>