compute: VM deployment wizard

This adds a work in progress VM deployment wizard page.

Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
Florian Symanowski 2019-10-21 18:46:23 +02:00 committed by Rohit Yadav
parent bb9ab291df
commit cb9c85706f
7 changed files with 543 additions and 17 deletions

View File

@ -16,7 +16,7 @@
// under the License.
<template>
<a-card :bordered="true">
<a-card :bordered="true" :title="title">
<a-skeleton active v-if="loading" />
<div v-else>
<div class="resource-details">
@ -30,7 +30,7 @@
<slot name="name">
<h4>
{{ resource.displayname || resource.name }}
<console :resource="resource" size="default" />
<console :resource="resource" size="default" v-if="resource.id" />
</h4>
<a-tag v-if="resource.instancename">
{{ resource.instancename }}
@ -73,11 +73,6 @@
<span style="margin-left: 8px">{{ resource.ostypename }}</span>
</div>
<div class="resource-detail-item">
<slot name="details">
</slot>
</div>
<div class="resource-detail-item" v-if="resource.keypair">
<a-icon type="key" />
<router-link :to="{ path: '/ssh/' + resource.keypair }">{{ resource.keypair }}</router-link>
@ -170,6 +165,12 @@
<span v-if="resource.nic && resource.nic.length > 0">{{ resource.nic.filter(e => { return e.ipaddress }).map(e => { return e.ipaddress }).join(', ') }}</span>
<span v-else>{{ resource.ipaddress }}</span>
</div>
<div class="resource-detail-item">
<slot name="details">
</slot>
</div>
<div class="resource-detail-item" v-if="resource.virtualmachineid">
<a-icon type="desktop" class="resource-detail-item"/>
<router-link :to="{ path: '/vm/' + resource.virtualmachineid }">{{ resource.vmname || resource.vm || resource.virtualmachinename || resource.virtualmachineid }} </router-link>
@ -187,6 +188,10 @@
<a-icon type="picture" class="resource-detail-item"/>
<router-link :to="{ path: '/template/' + resource.templateid }">{{ resource.templatename || resource.templateid }} </router-link>
</div>
<div class="resource-detail-item" v-if="resource.diskofferingname && resource.diskofferingid">
<a-icon type="hdd" class="resource-detail-item"/>
<router-link :to="{ path: '/diskoffering/' + resource.diskofferingid }">{{ resource.diskofferingname || resource.diskofferingid }} </router-link>
</div>
<div class="resource-detail-item" v-if="resource.networkofferingid">
<a-icon type="wifi" class="resource-detail-item"/>
<router-link :to="{ path: '/networkoffering/' + resource.networkofferingid }">{{ resource.networkofferingname || resource.networkofferingid }} </router-link>
@ -203,6 +208,7 @@
<a-icon type="gateway" class="resource-detail-item"/>
<router-link :to="{ path: '/guestnetwork/' + resource.guestnetworkid }">{{ resource.guestnetworkname || resource.guestnetworkid }} </router-link>
</div>
<div class="resource-detail-item" v-if="resource.storageid">
<a-icon type="database" class="resource-detail-item"/>
<router-link :to="{ path: '/storagepool/' + resource.storageid }">{{ resource.storage || resource.storageid }} </router-link>
@ -401,6 +407,10 @@ export default {
loading: {
type: Boolean,
default: false
},
title: {
type: String,
default: ''
}
},
data () {

View File

@ -938,5 +938,10 @@
"zone": "Zone",
"zoneId": "Zone",
"zoneid": "Zone",
"zonename": "Zone"
"zonename": "Zone",
"instance": "Instance",
"yourInstance": "Your instance",
"newInstance": "New instance",
"cpu": "CPU",
"ram": "RAM"
}

37
ui/src/utils/icons.js Normal file
View File

@ -0,0 +1,37 @@
// 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.
import _ from 'lodash'
const osMapping = {
centos: 'centos',
ubuntu: 'ubuntu',
suse: 'suse',
redhat: 'redhat',
fedora: 'fedora',
linux: 'linux',
bsd: 'freebsd',
apple: 'apple',
dos: 'windows',
windows: 'windows',
oracle: 'java'
}
export const getNormalizedOsName = (osName) => {
osName = osName.toLowerCase()
return _.find(osMapping, (value, key) => osName.includes(key)) || 'linux'
}

View File

@ -118,7 +118,7 @@
:footer="null"
centered
>
<component :is="currentAction.component"/></component>
<component :is="currentAction.component" :resource="resource" :loading="loading" v-bind="{currentAction}" />
</a-modal>
</keep-alive>
<a-modal

View File

@ -17,29 +17,307 @@
<template>
<div>
Finish this component
<a-steps direction="vertical" :current="1">
<a-step title="Finished" description="This is a description." />
<a-step title="In Progress" description="This is a description." />
<a-step title="Waiting" description="This is a description." />
</a-steps>
<a-row :gutter="12">
<a-col :md="24" :lg="17">
<a-card :bordered="true" :title="this.$t('newInstance')">
<a-form
:form="form"
@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"
>
<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-form-item>
</a-collapse-panel>
<a-collapse-panel :header="this.$t('ISOs')" key="isos">
<!-- ToDo: Add iso 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>
<a-button type="primary" @click="handleSubmit">{{ this.$t('submit') }}</a-button>
</div>
</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>
</a-row>
</div>
</template>
<script>
import Vue from 'vue'
import { api } from '@/api'
import store from '@/store'
import _ from 'lodash'
import InfoCard from '@/components/view/InfoCard'
import ComputeSelection from './wizard/ComputeSelection'
import TemplateSelection from './wizard/TemplateSelection'
export default {
name: 'DeployVM',
name: 'Wizard',
components: {
InfoCard,
ComputeSelection,
TemplateSelection
},
props: {
visible: {
type: Boolean
}
},
data () {
return {
vm: {},
params: [],
visibleParams: [
'name',
'templateid',
'serviceofferingid',
'diskofferingid',
'zoneid',
'rootdisksize'
],
instanceConfig: [],
template: {},
serviceOffering: {},
diskOffering: {},
zone: {}
}
},
computed: {
filteredParams () {
return this.visibleParams.map((fieldName) => {
return this.params.find((param) => fieldName === param.name)
})
},
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)
if (this.zone) {
this.vm['zoneid'] = this.zone.id
this.vm['zonename'] = this.zone.name
}
if (this.template) {
this.vm['templateid'] = this.template.id
this.vm['templatename'] = this.template.displaytext
this.vm['ostypeid'] = this.template.ostypeid
this.vm['ostypename'] = this.template.ostypename
}
if (this.serviceOffering) {
this.vm['serviceofferingid'] = this.serviceOffering.id
this.vm['serviceofferingname'] = this.serviceOffering.displaytext
this.vm['cpunumber'] = this.serviceOffering.cpunumber
this.vm['cpuspeed'] = this.serviceOffering.cpuspeed
this.vm['memory'] = this.serviceOffering.memory
}
if (this.diskOffering) {
this.vm['diskofferingid'] = this.diskOffering.id
this.vm['diskofferingname'] = this.diskOffering.displaytext
this.vm['diskofferingsize'] = this.diskOffering.disksize
}
}
},
beforeCreate () {
this.form = this.$form.createForm(this, {
onValuesChange: (props, fields) => {
this.instanceConfig = { ...this.form.getFieldsValue(), ...fields }
this.vm = this.instanceConfig
}
})
this.form.getFieldDecorator('computeofferingid', { initialValue: [], preserve: true })
},
created () {
this.params = store.getters.apis[this.$route.name]['params']
this.filteredParams.forEach((param) => {
this.fetchOptions(param)
})
Vue.nextTick().then(() => {
this.instanceConfig = this.form.getFieldsValue() // ToDo: maybe initialize with some other defaults
})
},
methods: {
updateComputeOffering (id) {
this.form.setFieldsValue({
computeofferingid: id
})
},
getParam (paramName) {
return this.params.find((param) => param.name === paramName)
},
getText (option) {
return _.get(option, 'displaytext', _.get(option, 'name'))
},
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
}
param.loading = true
param.opts = []
const params = {}
params.listall = true
if (possibleApi === 'listTemplates') {
params.templatefilter = 'executable'
}
api(possibleApi, params).then((response) => {
param.loading = false
_.map(response, (responseItem, responseKey) => {
if (!responseKey.includes('response')) {
return
}
_.map(responseItem, (response, key) => {
if (key === 'count') {
return
}
param.opts = response
this.$forceUpdate()
})
})
}).catch(function (error) {
console.log(error.stack)
param.loading = false
})
}
}
}
</script>
<style scoped>
<style lang="less" scoped>
.card-footer {
text-align: right;
button + button {
margin-left: 8px;
}
}
.ant-list-item-meta-avatar {
font-size: 1rem;
}
.ant-collapse {
margin: 2rem 0;
}
</style>

View File

@ -0,0 +1,110 @@
// 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="false"
:scroll="{x: 0, y: 320}"
:rowSelection="rowSelection"
size="middle"
>
<span slot="cpuTitle"><a-icon type="appstore" /> {{ $t('cpu') }}</span>
<span slot="ramTitle"><a-icon type="bulb" /> {{ $t('ram') }}</span>
</a-table>
</template>
<script>
export default {
name: 'ComputeSelection',
props: {
computeItems: {
type: Array,
default: () => []
},
value: {
type: String,
default: ''
}
},
data () {
return {
columns: [
{
dataIndex: 'name',
width: '40%'
},
{
dataIndex: 'cpu',
slots: { title: 'cpuTitle' },
width: '30%'
},
{
dataIndex: 'ram',
slots: { title: 'ramTitle' },
width: '30%'
}
],
selectedRowKeys: []
}
},
computed: {
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,
onSelect: (row) => {
this.$emit('select-compute-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>
<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,86 @@
// 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>