compute: VM deployment wizard (#146)

A mostly functional vm deployment wizard.

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
Hoang Nguyen 2020-03-26 04:18:39 +07:00 committed by Rohit Yadav
parent ba6dec532b
commit 373fc23c45
19 changed files with 1244 additions and 493 deletions

View File

@ -1103,7 +1103,7 @@
"sshKeyPairs": "SSH keypairs",
"wednesday": "Wednesday",
"noselect": "No thanks",
"groupname": "Add to group",
"group": "Group",
"keyboard": "Keyboard language",
"userdata": "Userdata",
"label.back": "Back",

View File

@ -42,9 +42,8 @@ export const deviceEnquire = function (callback) {
}
}
// screen and (max-width: 1087.99px)
enquireJs
.register('screen and (max-width: 576px)', matchMobile)
.register('screen and (min-width: 576px) and (max-width: 1280px)', matchTablet)
.register('screen and (min-width: 1281px)', matchDesktop)
.register('screen and (max-width: 800px)', matchMobile)
.register('screen and (min-width: 800px) and (max-width: 1366px)', matchTablet)
.register('screen and (min-width: 1367px)', matchDesktop)
}

View File

@ -235,7 +235,7 @@
:pageSize="pageSize"
:total="itemCount"
:showTotal="total => `Total ${total} items`"
:pageSizeOptions="['10', '20', '40', '80', '100']"
:pageSizeOptions="['10', '20', '40', '80', '100', '500']"
@change="changePage"
@showSizeChange="changePageSize"
showSizeChanger

View File

@ -25,131 +25,216 @@
@submit="handleSubmit"
layout="vertical"
>
<a-collapse
:accordion="false"
:bordered="false"
defaultActiveKey="basic"
>
<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('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>
<a-steps direction="vertical" size="small">
<a-step :title="this.$t('details')" status="process">
<template slot="description">
<div style="margin-top: 15px">
<a-form-item :label="this.$t('name')">
<a-input
v-decorator="['name']"
/>
</a-form-item>
<a-form-item :label="this.$t('zoneid')">
<a-select
v-decorator="['zoneid', {
rules: [{ required: true, message: 'Please select option' }]
}]"
:options="zoneSelectOptions"
@change="onSelectZoneId"
:loading="loading.zones"
></a-select>
</a-form-item>
<a-form-item :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-select
v-decorator="['clusterid']"
:options="clusterSelectOptions"
:loading="loading.clusters"
></a-select>
</a-form-item>
<a-form-item :label="this.$t('hostId')">
<a-select
v-decorator="['hostid']"
:options="hostSelectOptions"
:loading="loading.hosts"
></a-select>
</a-form-item>
<a-form-item :label="this.$t('group')">
<a-select
v-decorator="['group']"
:options="groupsSelectOptions"
:loading="loading.groups"
></a-select>
</a-form-item>
<a-form-item :label="this.$t('keyboard')">
<a-select
v-decorator="['keyboard']"
:options="keyboardSelectOptions"
></a-select>
</a-form-item>
<a-form-item :label="this.$t('userdata')">
<a-textarea
v-decorator="['userdata']">
</a-textarea>
</a-form-item>
</div>
</template>
</a-step>
<a-step
:title="this.$t('templateIso')"
:status="zoneSelected ? 'process' : 'wait'">
<template slot="description">
<div v-if="zoneSelected" style="margin-top: 15px">
<a-card
:tabList="tabList"
:activeTabKey="tabKey"
@tabChange="key => onTabChange(key, 'tabKey')">
<p v-if="tabKey === 'templateid'">
<template-iso-selection
input-decorator="templateid"
:items="options.templates"
:selected="tabKey"
:loading="loading.templates"
@update-template-iso="updateFieldValue"
></template-iso-selection>
<disk-size-selection
input-decorator="rootdisksize"
@update-disk-size="updateFieldValue"/>
</p>
<p v-else>
<template-iso-selection
input-decorator="isoid"
:items="options.isos"
:selected="tabKey"
:loading="loading.isos"
@update-template-iso="updateFieldValue"
></template-iso-selection>
</p>
</a-card>
<a-form-item class="form-item-hidden">
<a-input v-decorator="['templateid']"/>
</a-form-item>
<a-form-item class="form-item-hidden">
<a-input v-decorator="['isoid']"/>
</a-form-item>
<a-form-item class="form-item-hidden">
<a-input v-decorator="['rootdisksize']"/>
</a-form-item>
</div>
</template>
</a-step>
<a-step
:title="this.$t('serviceOfferingId')"
:status="zoneSelected ? 'process' : 'wait'">
<template slot="description">
<div v-if="zoneSelected">
<compute-selection
:compute-items="options.serviceOfferings"
:value="serviceOffering ? serviceOffering.id : ''"
:loading="loading.serviceOfferings"
@select-compute-item="($event) => updateComputeOffering($event)"
@handle-search-filter="($event) => handleSearchFilter('serviceOfferings', $event)"
></compute-selection>
</div>
</template>
</a-step>
<a-step
:title="this.$t('diskofferingid')"
:status="zoneSelected ? 'process' : 'wait'">
<template slot="description">
<div v-if="zoneSelected">
<disk-offering-selection
:items="options.diskOfferings"
:value="diskOffering ? diskOffering.id : ''"
:loading="loading.diskOfferings"
@select-disk-offering-item="($event) => updateDiskOffering($event)"
@handle-search-filter="($event) => handleSearchFilter('diskOfferings', $event)"
></disk-offering-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')"
>
v-if="diskOffering && diskOffering.iscustomized"
input-decorator="size"
@update-disk-size="updateFieldValue" />
<a-form-item class="form-item-hidden">
<a-input v-decorator="['size']"/>
</a-form-item>
</div>
</template>
</a-step>
<a-step
:title="this.$t('Affinity Groups')"
:status="zoneSelected ? 'process' : 'wait'">
<template slot="description">
<div v-if="zoneSelected">
<affinity-group-selection
:items="options.affinityGroups"
:value="affinityGroupIds"
:loading="loading.affinityGroups"
@select-affinity-group-item="($event) => updateAffinityGroups($event)"
@handle-search-filter="($event) => handleSearchFilter('affinityGroups', $event)"
></affinity-group-selection>
</div>
</template>
</a-step>
<a-step
:title="this.$t('networks')"
:status="zoneSelected ? 'process' : 'wait'">
<template slot="description">
<div v-if="zoneSelected">
<network-selection
:items="options.networks"
:value="networkOfferingIds"
:loading="loading.networks"
:zoneId="zoneId"
@select-network-item="($event) => updateNetworks($event)"
@handle-search-filter="($event) => handleSearchFilter('networks', $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>
<network-configuration
v-if="networks.length > 0"
:items="networks"
@update-network-config="($event) => updateNetworkConfig($event)"
@select-default-network-item="($event) => updateDefaultNetworks($event)"
></network-configuration>
</div>
</template>
</a-step>
<a-step
:title="this.$t('sshKeyPairs')"
:status="zoneSelected ? 'process' : 'wait'">
<template slot="description">
<div v-if="zoneSelected">
<ssh-key-pair-selection
:items="options.sshKeyPairs"
:value="sshKeyPair ? sshKeyPair.name : ''"
:loading="loading.sshKeyPairs"
@select-ssh-key-pair-item="($event) => updateSshKeyPairs($event)"
@handle-search-filter="($event) => handleSearchFilter('sshKeyPairs', $event)"
/>
</div>
</template>
</a-step>
</a-steps>
<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>
<a-button @click="() => this.$router.back()" :loading="loading.deploy">
{{ this.$t('cancel') }}
</a-button>
<a-button type="primary" @click="handleSubmit" :loading="loading.deploy">
<a-icon type="rocket" />
{{ this.$t('Launch VM') }}
</a-button>
</div>
</a-form>
</a-card>
</a-col>
<a-col :md="24" :lg="7" v-if="!isMobile()">
<a-affix :offsetTop="75">
<info-card :resource="vm" :title="this.$t('yourInstance')">
<info-card class="vm-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>
@ -183,14 +268,12 @@ 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,
@ -208,6 +291,8 @@ export default {
mixins: [mixin, mixinDevice],
data () {
return {
zoneId: '',
zoneSelected: false,
vm: {},
options: {
templates: [],
@ -217,7 +302,27 @@ export default {
zones: [],
affinityGroups: [],
networks: [],
sshKeyPairs: []
sshKeyPairs: [],
pods: [],
clusters: [],
hosts: [],
groups: [],
keyboards: []
},
loading: {
deploy: false,
templates: false,
isos: false,
serviceOfferings: false,
diskOfferings: false,
affinityGroups: false,
networks: false,
sshKeyPairs: false,
zones: false,
pods: false,
clusters: false,
hosts: false,
groups: false
},
instanceConfig: [],
template: {},
@ -226,13 +331,44 @@ export default {
diskOffering: {},
affinityGroups: [],
networks: [],
networksAdd: [],
zone: {},
sshKeyPair: {},
isoFilter: [
'executable',
templateFilter: [
'featured',
'community',
'selfexecutable',
'sharedexecutable'
]
],
isoFilter: [
'featured',
'community',
'selfexecutable',
'sharedexecutable'
],
steps: {
BASIC: 0,
TEMPLATE_ISO: 1,
COMPUTE: 2,
DISK_OFFERING: 3,
AFFINITY_GROUP: 4,
NETWORK: 5,
SSH_KEY_PAIR: 6
},
initDataConfig: {},
defaultNetwork: '',
networkConfig: [],
tabList: [
{
key: 'templateid',
tab: this.$t('Templates')
},
{
key: 'isoid',
tab: this.$t('ISOs')
}
],
tabKey: 'templateid'
}
},
computed: {
@ -255,35 +391,78 @@ export default {
},
params () {
return {
templates: {
list: 'listTemplates',
serviceOfferings: {
list: 'listServiceOfferings',
options: {
templatefilter: 'executable',
zoneid: _.get(this.zone, 'id')
zoneid: _.get(this.zone, 'id'),
issystem: false,
page: 1,
pageSize: 10,
keyword: undefined
}
},
serviceOfferings: {
list: 'listServiceOfferings'
},
diskOfferings: {
list: 'listDiskOfferings'
list: 'listDiskOfferings',
options: {
zoneid: _.get(this.zone, 'id'),
page: 1,
pageSize: 10,
keyword: undefined
}
},
zones: {
list: 'listZones'
},
affinityGroups: {
list: 'listAffinityGroups'
list: 'listAffinityGroups',
options: {
page: 1,
pageSize: 10,
keyword: undefined
}
},
sshKeyPairs: {
list: 'listSSHKeyPairs'
list: 'listSSHKeyPairs',
options: {
zoneid: _.get(this.zone, 'id'),
page: 1,
pageSize: 10,
keyword: undefined
}
},
networks: {
list: 'listNetworks',
options: {
zoneid: _.get(this.zone, 'id'),
canusefordeploy: true,
projectid: store.getters.project.id
projectid: store.getters.project.id,
page: 1,
pageSize: 10,
keyword: undefined
}
},
pods: {
list: 'listPods',
options: {
zoneid: _.get(this.zone, 'id')
}
},
clusters: {
list: 'listClusters',
options: {
zoneid: _.get(this.zone, 'id')
}
},
hosts: {
list: 'listHosts',
options: {
zoneid: _.get(this.zone, 'id'),
state: 'Up',
type: 'Routing'
}
},
groups: {
list: 'listInstanceGroups'
}
}
},
@ -297,9 +476,54 @@ export default {
value: zone.id
}
})
},
podSelectOptions () {
return this.options.pods.map((pod) => {
return {
label: pod.name,
value: pod.id
}
})
},
clusterSelectOptions () {
return this.options.clusters.map((cluster) => {
return {
label: cluster.name,
value: cluster.id
}
})
},
hostSelectOptions () {
return this.options.hosts.map((host) => {
return {
label: host.name,
value: host.id
}
})
},
keyboardSelectOptions () {
return this.options.keyboards.map((keyboard) => {
return {
label: this.$t(keyboard.description),
value: keyboard.id
}
})
},
groupsSelectOptions () {
return this.options.groups.map((group) => {
return {
label: group.name,
value: group.id
}
})
}
},
watch: {
'$route' (to, from) {
if (to.name === 'deployVirtualMachine') {
this.resetData()
}
},
instanceConfig (instanceConfig) {
this.template = _.find(this.options.templates, (option) => option.id === instanceConfig.templateid)
this.iso = _.find(this.options.isos, (option) => option.id === instanceConfig.isoid)
@ -363,26 +587,99 @@ export default {
this.vm = this.instanceConfig
}
})
this.form.getFieldDecorator('computeofferingid', { initialValue: [], preserve: true })
this.form.getFieldDecorator('diskofferingid', { initialValue: [], preserve: true })
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: [], preserve: true })
this.form.getFieldDecorator('isoid', { initialValue: undefined, preserve: true })
this.form.getFieldDecorator('networkids', { initialValue: [], preserve: true })
this.form.getFieldDecorator('keypair', { initialValue: [], preserve: true })
},
created () {
_.each(this.params, this.fetchOptions)
Vue.nextTick().then(() => {
this.instanceConfig = this.form.getFieldsValue() // ToDo: maybe initialize with some other defaults
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
})
},
created () {
this.fetchData()
},
methods: {
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')
this.fetchKeyboard()
Vue.nextTick().then(() => {
this.instanceConfig = this.form.getFieldsValue() // ToDo: maybe initialize with some other defaults
})
},
fetchKeyboard () {
const keyboardType = []
keyboardType.push({
id: '',
description: ''
})
keyboardType.push({
id: 'us',
description: 'label.standard.us.keyboard'
})
keyboardType.push({
id: 'uk',
description: 'label.uk.keyboard'
})
keyboardType.push({
id: 'fr',
description: 'label.french.azerty.keyboard'
})
keyboardType.push({
id: 'jp',
description: 'label.japanese.keyboard'
})
keyboardType.push({
id: 'sc',
description: 'label.simplified.chinese.keyboard'
})
this.$set(this.options, 'keyboards', keyboardType)
},
resetData () {
this.vm = {}
this.zoneSelected = false
this.form.resetFields()
this.fetchData()
},
updateFieldValue (name, value) {
if (name === 'templateid') {
this.tabKey = 'templateid'
this.form.setFieldsValue({
templateid: value,
isoid: undefined
})
} else if (name === 'isoid') {
this.tabKey = 'isoid'
this.form.setFieldsValue({
isoid: value,
templateid: undefined
})
} else {
this.form.setFieldsValue({
[name]: value
})
}
},
updateComputeOffering (id) {
this.form.setFieldsValue({
computeofferingid: id
})
},
updateDiskOffering (id) {
if (id === '0') {
this.form.setFieldsValue({
diskofferingid: undefined
})
return
}
this.form.setFieldsValue({
diskofferingid: id
})
@ -397,7 +694,19 @@ export default {
networkids: ids
})
},
updateDefaultNetworks (id) {
this.defaultNetwork = id
},
updateNetworkConfig (networks) {
this.networkConfig = networks
},
updateSshKeyPairs (name) {
if (name === this.$t('noselect')) {
this.form.setFieldsValue({
keypair: undefined
})
return
}
this.form.setFieldsValue({
keypair: name
})
@ -405,10 +714,103 @@ export default {
getText (option) {
return _.get(option, 'displaytext', _.get(option, 'name'))
},
handleSubmit () {
handleSubmit (e) {
console.log('wizard submit')
e.preventDefault()
this.form.validateFields((err, values) => {
if (err) {
return
}
const deployVmData = {}
// step 1 : select zone
deployVmData.zoneid = values.zoneid
deployVmData.podid = values.podid
deployVmData.clusterid = values.clusterid
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)))
}
// step 2: select template/iso
if (this.tabKey === 'templateid') {
deployVmData.templateid = values.templateid
} else {
deployVmData.templateid = values.isoid
}
if (values.rootdisksize && values.rootdisksize > 0) {
deployVmData.rootdisksize = values.rootdisksize
}
// step 3: select service offering
deployVmData.serviceofferingid = values.computeofferingid
// 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(',')
// step 6: select network
if (values.networkids && values.networkids.length > 0) {
for (let i = 0; i < values.networkids.length; i++) {
deployVmData['iptonetworklist[' + i + '].networkid'] = values.networkids[i]
if (this.networkConfig.length > 0) {
const networkConfig = this.networkConfig.filter((item) => item.key === values.networkids[i])
if (networkConfig && networkConfig.length > 0) {
deployVmData['iptonetworklist[' + i + '].ip'] = networkConfig[0].ipAddress ? networkConfig[0].ipAddress : undefined
deployVmData['iptonetworklist[' + i + '].mac'] = networkConfig[0].macAddress ? networkConfig[0].macAddress : undefined
}
}
}
}
// step 7: select ssh key pair
deployVmData.keypair = values.keypair
deployVmData.name = values.name
deployVmData.displayname = values.name
const title = this.$t('Launch Virtual Machine')
const description = deployVmData.name ? deployVmData.name : values.zoneid
this.loading.deploy = true
api('deployVirtualMachine', deployVmData).then(response => {
const jobId = response.deployvirtualmachineresponse.jobid
if (jobId) {
this.$pollJob({
jobId,
successMethod: result => {
let successDescription = ''
if (result.jobresult.virtualmachine.name) {
successDescription = result.jobresult.virtualmachine.name
} else {
successDescription = result.jobresult.virtualmachine.id
}
this.$store.dispatch('AddAsyncJob', {
title: title,
jobid: jobId,
description: successDescription,
status: 'progress'
})
},
loadingMessage: `${title} in progress for ${description}`,
catchMessage: 'Error encountered while fetching async job result'
})
}
this.$router.back()
}).catch(error => {
this.$notification.error({
message: 'Request Failed',
description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message
})
}).finally(() => {
this.loading.deploy = false
})
})
},
fetchOptions (param, name) {
fetchOptions (param, name, exclude) {
if (exclude && exclude.length > 0) {
if (exclude.includes(name)) {
return
}
}
this.loading[name] = true
param.loading = true
param.opts = []
const options = param.options || {}
@ -416,6 +818,11 @@ export default {
api(param.list, options).then((response) => {
param.loading = false
_.map(response, (responseItem, responseKey) => {
if (Object.keys(responseItem).length === 0) {
this.options[name] = []
this.$forceUpdate()
return
}
if (!responseKey.includes('response')) {
return
}
@ -431,41 +838,108 @@ export default {
}).catch(function (error) {
console.log(error.stack)
param.loading = false
}).finally(() => {
this.loading[name] = false
})
},
fetchTemplates (templateFilter) {
return new Promise((resolve, reject) => {
api('listTemplates', {
zoneid: _.get(this.zone, 'id'),
templatefilter: templateFilter
}).then((response) => {
resolve(response)
}).catch((reason) => {
// ToDo: Handle errors
reject(reason)
})
})
},
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()
return new Promise((resolve, reject) => {
api('listIsos', {
zoneid: _.get(this.zone, 'id'),
isofilter: isoFilter,
bootable: true
}).then((response) => {
resolve(response)
}).catch((reason) => {
// ToDo: Handle errors
reject(reason)
})
})
},
fetchAllTemplates () {
const promises = []
this.options.templates = []
this.loading.templates = true
this.templateFilter.forEach((filter) => {
promises.push(this.fetchTemplates(filter))
})
Promise.all(promises).then(response => {
response.forEach((resItem) => {
const concatTemplates = _.concat(this.options.templates, _.get(resItem, 'listtemplatesresponse.template', []))
this.options.templates = _.uniqWith(concatTemplates, _.isEqual)
this.$forceUpdate()
})
}).catch((reason) => {
// ToDo: Handle errors
console.log(reason)
}).finally(() => {
this.loading.templates = false
})
},
fetchAllIsos () {
const promises = []
this.options.isos = []
this.loading.isos = true
this.isoFilter.forEach((filter) => {
this.fetchIsos(filter)
promises.push(this.fetchIsos(filter))
})
Promise.all(promises).then(response => {
response.forEach((resItem) => {
const concatedIsos = _.concat(this.options.isos, _.get(resItem, 'listisosresponse.iso', []))
this.options.isos = _.uniqWith(concatedIsos, _.isEqual)
this.$forceUpdate()
})
}).catch((reason) => {
console.log(reason)
}).finally(() => {
this.loading.isos = false
})
},
onTemplatesIsosCollapseChange (key) {
if (key === 'isos' && this.options.isos.length === 0) {
onSelectZoneId (value) {
this.zoneId = value
this.zoneSelected = true
this.form.setFieldsValue({
clusterid: undefined,
podid: undefined,
hostid: undefined,
templateid: undefined,
isoid: undefined
})
this.tabKey = 'templateid'
_.each(this.params, (param, name) => {
this.fetchOptions(param, name, ['zones', 'groups'])
})
this.fetchAllTemplates()
},
handleSearchFilter (name, options) {
this.params[name].options = { ...this.params[name].options, ...options }
this.fetchOptions(this.params[name], name)
},
onTabChange (key, type) {
this[type] = key
if (key === 'isoid') {
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')
})
sanitizeReverse (value) {
const reversedValue = value
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
return reversedValue
}
}
}
@ -505,4 +979,22 @@ export default {
border-radius: @border-radius-base !important;
margin: 0 0 1.2rem;
}
.vm-info-card {
.resource-detail-item__label {
font-weight: normal;
}
.resource-detail-item__details, .resource-detail-item {
a {
color: rgba(0, 0, 0, 0.65);
cursor: default;
pointer-events: none;
}
}
}
.form-item-hidden {
display: none;
}
</style>

View File

@ -19,7 +19,7 @@
<a-spin :spinning="networkLoading">
<a-tabs
:activeKey="currentTab"
:tabPosition="device === 'tablet' || device === 'mobile' ? 'top' : 'left'"
:tabPosition="device === 'mobile' ? 'top' : 'left'"
:animated="false"
@change="handleChangeTab">
<a-tab-pane :tab="$t('details')" key="details">

View File

@ -16,15 +16,24 @@
// under the License.
<template>
<a-table
:columns="columns"
:dataSource="items"
:rowKey="record => record.id"
:pagination="{showSizeChanger: true}"
:rowSelection="rowSelection"
size="middle"
>
</a-table>
<div>
<a-input-search
style="width: 25vw;float: right;margin-bottom: 10px; z-index: 8"
placeholder="Search"
v-model="filter"
@search="handleSearch" />
<a-table
:loading="loading"
:columns="columns"
:dataSource="items"
:rowKey="record => record.id"
:pagination="{showSizeChanger: true}"
:rowSelection="rowSelection"
size="middle"
:scroll="{ y: 225 }"
>
</a-table>
</div>
</template>
<script>
@ -40,10 +49,15 @@ export default {
value: {
type: Array,
default: () => []
},
loading: {
type: Boolean,
default: false
}
},
data () {
return {
filter: '',
columns: [
{
dataIndex: 'name',
@ -60,6 +74,13 @@ export default {
}
},
computed: {
options () {
return {
page: 1,
pageSize: 10,
keyword: ''
}
},
rowSelection () {
return {
type: 'checkbox',
@ -76,6 +97,18 @@ export default {
this.selectedRowKeys = newValue
}
}
},
methods: {
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>

View File

@ -16,16 +16,26 @@
// under the License.
<template>
<a-table
:columns="columns"
:dataSource="tableSource"
: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('memory') }}</span>
</a-table>
<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>
@ -39,10 +49,15 @@ export default {
value: {
type: String,
default: ''
},
loading: {
type: Boolean,
default: false
}
},
data () {
return {
filter: '',
columns: [
{
dataIndex: 'name',
@ -64,6 +79,13 @@ export default {
}
},
computed: {
options () {
return {
page: 1,
pageSize: 10,
keyword: ''
}
},
tableSource () {
return this.computeItems.map((item) => {
return {
@ -78,9 +100,7 @@ export default {
return {
type: 'radio',
selectedRowKeys: this.selectedRowKeys,
onSelect: (row) => {
this.$emit('select-compute-item', row.key)
}
onChange: this.onSelectRow
}
}
},
@ -90,6 +110,22 @@ export default {
this.selectedRowKeys = [newValue]
}
}
},
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>

View File

@ -16,20 +16,37 @@
// 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>
<div>
<a-input-search
style="width: 25vw;float: right;margin-bottom: 10px; z-index: 8"
placeholder="Search"
v-model="filter"
@search="handleSearch" />
<a-table
:loading="loading"
:columns="columns"
:dataSource="tableSource"
:pagination="{showSizeChanger: true}"
:rowSelection="rowSelection"
size="middle"
@change="handleTableChange"
:scroll="{ y: 225 }"
>
<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-if="record.diskSize">{{ record.diskSize }} GB</div>
<div v-else>-</div>
</template>
<template slot="iops" slot-scope="text, record">
<span v-if="record.miniops && record.maxiops">{{ record.miniops }} - {{ record.maxiops }}</span>
<span v-else-if="record.miniops && !record.maxiops">{{ record.miniops }}</span>
<span v-else-if="!record.miniops && record.maxiops">{{ record.maxiops }}</span>
<span v-else>-</span>
</template>
</a-table>
</div>
</template>
<script>
@ -43,18 +60,23 @@ export default {
value: {
type: String,
default: ''
},
loading: {
type: Boolean,
default: false
}
},
data () {
return {
filter: '',
columns: [
{
dataIndex: 'name',
title: this.$t('diskOffering'),
title: this.$t('diskoffering'),
width: '40%'
},
{
dataIndex: 'diskSize',
dataIndex: 'disksize',
slots: { title: 'diskSizeTitle' },
width: '30%',
scopedSlots: { customRender: 'diskSize' }
@ -62,20 +84,41 @@ export default {
{
dataIndex: 'iops',
slots: { title: 'iopsTitle' },
width: '30%'
width: '30%',
scopedSlots: { customRender: 'iops' }
}
],
selectedRowKeys: []
selectedRowKeys: ['0'],
dataItems: []
}
},
created () {
this.dataItems = []
this.dataItems.push({
id: '0',
name: this.$t('noselect'),
diskSize: undefined,
miniops: undefined,
maxiops: undefined,
isCustomized: undefined
})
},
computed: {
options () {
return {
page: 1,
pageSize: 10,
keyword: ''
}
},
tableSource () {
return this.items.map((item) => {
return this.dataItems.map((item) => {
return {
key: item.id,
name: item.name,
diskSize: item.disksize,
iops: `${item.miniops} ${item.maxiops}`,
miniops: item.miniops,
maxiops: item.maxiops,
isCustomized: item.iscustomized
}
})
@ -84,9 +127,7 @@ export default {
return {
type: 'radio',
selectedRowKeys: this.selectedRowKeys,
onSelect: (row) => {
this.$emit('select-disk-offering-item', row.key)
}
onChange: this.onSelectRow
}
}
},
@ -95,6 +136,27 @@ export default {
if (newValue && newValue !== oldValue) {
this.selectedRowKeys = [newValue]
}
},
items (newData, oldData) {
if (newData && newData.length > 0) {
this.dataItems = this.dataItems.concat(newData)
}
}
},
methods: {
onSelectRow (value) {
this.selectedRowKeys = value
this.$emit('select-disk-offering-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)
}
}
}

View File

@ -22,14 +22,13 @@
<a-slider
:min="0"
:max="1024"
v-decorator="[inputDecorator]"
v-model="inputValue"
@change="($event) => updateDickSize($event)"
/>
</a-col>
<a-col :span="4">
<a-input-number
v-decorator="[inputDecorator, {
rules: [{ required: false, message: 'Please enter a number' }]
}]"
v-model="inputValue"
:formatter="value => `${value} GB`"
:parser="value => value.replace(' GB', '')"
/>
@ -46,6 +45,16 @@ export default {
type: String,
default: ''
}
},
data () {
return {
inputValue: 0
}
},
methods: {
updateDickSize (value) {
this.$emit('update-disk-size', this.inputDecorator, value)
}
}
}
</script>

View File

@ -18,21 +18,24 @@
<template>
<a-table
:columns="columns"
:dataSource="items"
:dataSource="dataItems"
:pagination="{showSizeChanger: true}"
:rowSelection="rowSelection"
:rowKey="record => record.id"
size="middle"
:scroll="{ y: 225 }"
>
<template v-slot:ipAddress="text">
<template slot="ipAddress" slot-scope="text, record">
<a-input
:value="text"
></a-input>
style="width: 150px;"
:placeholder="$t('ipAddress')"
@change="($event) => updateNetworkData('ipAddress', record.id, $event.target.value)" />
</template>
<template v-slot:macAddress="text">
<template slot="macAddress" slot-scope="text, record">
<a-input
:value="text"
></a-input>
style="width: 150px;"
:placeholder="$t('macAddress')"
@change="($event) => updateNetworkData('macAddress', record.id, $event.target.value)" />
</template>
</a-table>
</template>
@ -52,6 +55,7 @@ export default {
},
data () {
return {
networks: [],
columns: [
{
dataIndex: 'name',
@ -71,17 +75,24 @@ export default {
scopedSlots: { customRender: 'macAddress' }
}
],
selectedRowKeys: []
selectedRowKeys: [],
dataItems: []
}
},
beforeCreate () {
this.dataItems = []
},
created () {
this.dataItems = this.items
this.selectedRowKeys = [this.dataItems[0].id]
this.$emit('select-default-network-item', this.selectedRowKeys)
},
computed: {
rowSelection () {
return {
type: 'radio',
selectedRowKeys: this.selectedRowKeys,
onSelect: (row) => {
this.$emit('select-default-network-item', row.key)
}
onChange: this.onSelectRow
}
}
},
@ -90,6 +101,38 @@ export default {
if (newValue && newValue !== oldValue) {
this.selectedRowKeys = [newValue]
}
},
items (newData, oldData) {
if (newData && newData.length > 0) {
this.dataItems = newData
const keyEx = this.dataItems.filter((item) => this.selectedRowKeys.includes(item.id))
if (!keyEx || keyEx.length === 0) {
this.selectedRowKeys = [this.dataItems[0].id]
}
}
}
},
methods: {
onSelectRow (value) {
this.selectedRowKeys = value
this.$emit('select-default-network-item', value[0])
},
updateNetworkData (name, key, value) {
if (this.networks.length === 0) {
const networkItem = {}
networkItem.key = key
networkItem[name] = value
this.networks.push(networkItem)
this.$emit('update-network-config', this.networks)
return
}
this.networks.filter((item, index) => {
if (item.key === key) {
this.$set(this.networks[index], name, value)
}
})
this.$emit('update-network-config', this.networks)
}
}
}

View File

@ -16,29 +16,39 @@
// 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"
<div>
<a-input-search
style="width: 25vw;float: right;margin-bottom: 10px; z-index: 8"
placeholder="Search"
v-model="filter"
@search="handleSearch" />
<a-table
:loading="loading"
:columns="columns"
:dataSource="networkItems"
:rowKey="record => record.id"
:pagination="{showSizeChanger: true, size: 'small'}"
:rowSelection="rowSelection"
@change="handleTableChange"
:scroll="{ y: 225 }"
>
<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>
<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>
</div>
</template>
<script>
@ -56,16 +66,28 @@ export default {
value: {
type: Array,
default: () => []
},
loading: {
type: Boolean,
default: false
}
},
data () {
return {
filter: '',
selectedRowKeys: [],
vpcs: [],
filteredInfo: null
}
},
computed: {
options () {
return {
page: 1,
pageSize: 10,
keyword: ''
}
},
columns () {
let vpcFilter = []
if (this.vpcs) {
@ -146,6 +168,16 @@ export default {
description: network.networkofferingdisplaytext
}
]
},
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)
}
}
}

View File

@ -1,150 +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>
<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

@ -16,16 +16,26 @@
// 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>
<div>
<a-input-search
style="width: 25vw;float: right;margin-bottom: 10px; z-index: 8"
placeholder="Search"
v-model="filter"
@search="handleSearch" />
<a-table
:loading="loading"
:columns="columns"
:dataSource="tableSource"
:pagination="{showSizeChanger: true}"
:rowSelection="rowSelection"
size="middle"
@change="handleTableChange"
:scroll="{ y: 225 }"
>
<template v-slot:account><a-icon type="user" /> {{ $t('account') }}</template>
<template v-slot:domain><a-icon type="block" /> {{ $t('domain') }}</template>
</a-table>
</div>
</template>
<script>
@ -39,10 +49,15 @@ export default {
value: {
type: String,
default: ''
},
loading: {
type: Boolean,
default: false
}
},
data () {
return {
filter: '',
columns: [
{
dataIndex: 'name',
@ -60,12 +75,28 @@ export default {
width: '30%'
}
],
selectedRowKeys: []
selectedRowKeys: [this.$t('noselect')],
dataItems: []
}
},
created () {
this.dataItems = []
this.dataItems.push({
name: this.$t('noselect'),
account: '-',
domain: '-'
})
},
computed: {
options () {
return {
page: 1,
pageSize: 10,
keyword: ''
}
},
tableSource () {
return this.items.map((item) => {
return this.dataItems.map((item) => {
return {
key: item.name,
name: item.name,
@ -78,9 +109,7 @@ export default {
return {
type: 'radio',
selectedRowKeys: this.selectedRowKeys,
onSelect: (row) => {
this.$emit('select-ssh-key-pair-item', row.key)
}
onChange: this.onSelectRow
}
}
},
@ -89,6 +118,27 @@ export default {
if (newValue && newValue !== oldValue) {
this.selectedRowKeys = [newValue]
}
},
items (newData, oldData) {
if (newData && newData.length > 0) {
this.dataItems = this.dataItems.concat(newData)
}
}
},
methods: {
onSelectRow (value) {
this.selectedRowKeys = value
this.$emit('select-ssh-key-pair-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)
}
}
}

View File

@ -17,37 +17,46 @@
<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-list
class="form-item-scroll"
itemLayout="vertical"
size="small"
:dataSource="osList"
:pagination="pagination">
<a-list-item slot="renderItem" slot-scope="os, osIndex" key="os.id">
<a-radio-group
class="radio-group"
:key="osIndex"
v-model="value"
@change="($event) => updateSelectionTemplateIso($event.target.value)">
<a-radio
class="radio-group__radio"
:value="os.id">
{{ os.displaytext }}&nbsp;
<a-tag
:visible="os.ispublic && !os.isfeatured"
color="blue"
@click="onFilterTag('is: public')"
>{{ $t('isPublic') }}</a-tag>
<a-tag
:visible="os.isfeatured"
color="green"
@click="onFilterTag('is: featured')"
>{{ $t('isFeatured') }}</a-tag>
<a-tag
:visible="isSelf(os)"
color="orange"
@click="onFilterTag('is: self')"
>{{ $t('isSelf') }}</a-tag>
<a-tag
:visible="isShared(os)"
color="cyan"
@click="onFilterTag('is: shared')"
>{{ $t('isShared') }}</a-tag>
</a-radio>
</a-radio-group>
</a-list-item>
</a-list>
</a-form-item>
</template>
@ -64,6 +73,45 @@ export default {
inputDecorator: {
type: String,
default: ''
},
selected: {
type: String,
default: ''
},
itemCount: {
type: Number,
default: 0
}
},
data () {
return {
value: '',
page: 1,
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
}
}
},
computed: {
pagination () {
return {
size: 'small',
page: 1,
pageSize: 10,
total: this.itemCount,
showSizeChanger: true,
onChange: this.onChangePage,
onShowSizeChange: this.onChangePageSize
}
}
},
methods: {
@ -72,6 +120,22 @@ export default {
},
isSelf (item) {
return !item.ispublic && (item.account === store.getters.userInfo.account)
},
updateSelectionTemplateIso (id) {
this.$emit('emit-update-template-iso', this.inputDecorator, id)
},
onChangePage (page, pageSize) {
this.pagination.page = page
this.pagination.pageSize = pageSize
this.$forceUpdate()
},
onChangePageSize (page, pageSize) {
this.pagination.page = page
this.pagination.pageSize = pageSize
this.$forceUpdate()
},
onFilterTag (tag) {
this.$emit('handle-filter-tag', tag)
}
}
}
@ -89,4 +153,14 @@ export default {
.ant-tag {
margin-left: 0.4rem;
}
/deep/.ant-spin-container {
max-height: 200px;
overflow-y: auto;
}
.pagination {
margin-top: 20px;
float: right;
}
</style>

View File

@ -16,27 +16,33 @@
// 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>
<a-input-search
class="search-input"
placeholder="Search"
v-model="filter"
@search="filterDataSource"/>
<a-spin :spinning="loading">
<a-tabs
tabPosition="top"
:animated="false"
:defaultActiveKey="Object.keys(dataSource)[0]">
<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"
:osList="dataSource[osName]"
:input-decorator="inputDecorator"
:selected="checkedValue"
:itemCount="itemCount[osName]"
@handle-filter-tag="filterDataSource"
@emit-update-template-iso="updateTemplateIso"
></TemplateIsoRadioGroup>
</a-tab-pane>
</a-tabs>
</a-spin>
</div>
</template>
@ -45,9 +51,7 @@ 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
import store from '@/store'
export default {
name: 'TemplateIsoSelection',
@ -60,27 +64,53 @@ export default {
inputDecorator: {
type: String,
default: ''
},
selected: {
type: String,
default: ''
},
loading: {
type: Boolean,
default: false
}
},
data () {
return {
TAB_VIEW: TAB_VIEW,
FILTER_VIEW: FILTER_VIEW,
visible: false,
filter: '',
filteredItems: this.items,
view: TAB_VIEW
checkedValue: '',
dataSource: {},
itemCount: {}
}
},
computed: {
osTypes () {
watch: {
items (items) {
this.filteredItems = []
this.checkedValue = ''
if (items && items.length > 0) {
this.filteredItems = items
this.checkedValue = items[0].id
}
this.dataSource = this.mappingDataSource()
},
inputDecorator (newValue, oldValue) {
if (newValue !== oldValue) {
this.filter = ''
}
}
},
methods: {
mappingDataSource () {
let mappedItems = {}
this.items.forEach((os) => {
const itemCount = {}
this.filteredItems.forEach((os) => {
const osName = getNormalizedOsName(os.ostypename)
if (Array.isArray(mappedItems[osName])) {
mappedItems[osName].push(os)
itemCount[osName] = itemCount[osName] + 1
} else {
mappedItems[osName] = [os]
itemCount[osName] = 1
}
})
mappedItems = _.mapValues(mappedItems, (list) => {
@ -90,24 +120,50 @@ export default {
nonFeaturedItems = _.sortBy(nonFeaturedItems, (item) => item.displaytext.toLowerCase())
return featuredItems.concat(nonFeaturedItems) // pin featured isos/templates at the top
})
this.itemCount = itemCount
return mappedItems
}
},
watch: {
items (items) {
this.filteredItems = items
},
filter (filterString) {
if (filterString !== '') {
this.filteredItems = this.filteredItems.filter((item) => item.displaytext.toLowerCase().includes(filterString))
updateTemplateIso (name, id) {
this.$emit('update-template-iso', name, id)
},
filterDataSource (strQuery) {
if (strQuery !== '' && strQuery.includes('is:')) {
this.filteredItems = []
this.filter = strQuery
const filters = strQuery.split(';')
filters.forEach((filter) => {
const query = filter.replace(/ /g, '')
const data = this.filterDataSourceByTag(query)
this.filteredItems = this.filteredItems.concat(data)
})
} else if (strQuery !== '') {
this.filteredItems = this.items.filter((item) => item.displaytext.toLowerCase().includes(strQuery.toLowerCase()))
} else {
this.filteredItems = this.items
}
}
},
methods: {
toggleView (view) {
this.view = view
this.dataSource = this.mappingDataSource()
},
filterDataSourceByTag (tag) {
let arrResult = []
if (tag.includes('public')) {
arrResult = this.items.filter((item) => {
return item.ispublic && item.isfeatured
})
} else if (tag.includes('featured')) {
arrResult = this.items.filter((item) => {
return item.isfeatured
})
} else if (tag.includes('self')) {
arrResult = this.items.filter((item) => {
return !item.ispublic && (item.account === store.getters.userInfo.account)
})
} else if (tag.includes('shared')) {
arrResult = this.items.filter((item) => {
return !item.ispublic && (item.account !== store.getters.userInfo.account)
})
}
return arrResult
}
}
}
@ -115,6 +171,21 @@ export default {
<style lang="less" scoped>
.search-input {
margin: 0.5rem 0 1rem;
width: 25vw;
z-index: 8;
position: absolute;
top: 11px;
right: 10px;
@media (max-width: 600px) {
position: relative;
width: 100%;
top: 0;
right: 0;
}
}
/deep/.ant-tabs-nav-scroll {
min-height: 45px;
}
</style>

View File

@ -81,7 +81,7 @@ export default {
detailColumn: [],
detail: [],
page: 1,
pageSize: 20,
pageSize: 10,
quickview: false,
loading: false
}

View File

@ -70,7 +70,7 @@ export default {
detailColumn: [],
detail: [],
page: 1,
pageSize: 20,
pageSize: 10,
itemCount: 0,
fetchLoading: false
}

View File

@ -68,7 +68,7 @@ export default {
columns: [],
dataSource: [],
page: 1,
pageSize: 20,
pageSize: 10,
itemCount: 0,
fetchLoading: false
}

View File

@ -19,7 +19,7 @@
<a-spin :spinning="fetchLoading">
<a-tabs
:activeKey="currentTab"
:tabPosition="device === 'tablet' || device === 'mobile' ? 'top' : 'left'"
:tabPosition="device === 'mobile' ? 'top' : 'left'"
:animated="false"
@change="handleChangeTab">
<a-tab-pane :tab="$t('details')" key="details">