src: assorted bug fixes (#564)

Fixes :
 - Fixing scale router
 - Fixing account actions
 - Fixing user actions
 - Adding message for create vm backup
 - Fix default allowuserdrivenbackups in ImportBackupOfferings
 - Fix typo in TakeSnapshot
 - Ensuring zone mandatory in upload template
 - Adding securitygroup to instacetab
 - Adding related vms to routers
 - Adding makeredundant to restart network
 - Fixing no key in listview
 - Link to ipaddress only if router path is publicip
 - Show vpc routers only to admin
 - Fix restartVPC args
 - Fix storage action visibility
 - Reorder routes to match legacy
 - Reorder cluster tabs
 - Fix number input width
 - Fix create vpc
 - List events also on fetchlatest
 - Fix show domain actions
 - Removing resource admin from default roles
 - Fix missing store
 - Adding createVPC view
 - Adding attachiso view
Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
davidjumani 2020-08-07 20:35:31 +05:30 committed by Rohit Yadav
parent fa934769d6
commit 3979f1f5d5
27 changed files with 644 additions and 105 deletions

View File

@ -32,7 +32,7 @@
:count="actionBadge[action.api] ? actionBadge[action.api].badgeNum : 0"
v-if="action.api in $store.getters.apis &&
action.showBadge && (
(!dataView && (action.listView || (action.groupAction && selectedRowKeys.length > 0 && ('groupShow' in action ? action.show(resource, $store.getters) : true)))) ||
(!dataView && ((action.listView && ('show' in action ? action.show(resource, $store.getters) : true)) || (action.groupAction && selectedRowKeys.length > 0 && ('groupShow' in action ? action.show(resource, $store.getters) : true)))) ||
(dataView && action.dataView && ('show' in action ? action.show(resource, $store.getters) : true))
)" >
<a-button
@ -50,7 +50,7 @@
<a-button
v-if="action.api in $store.getters.apis &&
!action.showBadge && (
(!dataView && (action.listView || (action.groupAction && selectedRowKeys.length > 0 && ('groupShow' in action ? action.show(resource, $store.getters) : true)))) ||
(!dataView && ((action.listView && ('show' in action ? action.show(resource, $store.getters) : true)) || (action.groupAction && selectedRowKeys.length > 0 && ('groupShow' in action ? action.show(resource, $store.getters) : true)))) ||
(dataView && action.dataView && ('show' in action ? action.show(resource, $store.getters) : true))
)"
:icon="action.icon"

View File

@ -519,7 +519,7 @@
<div v-for="item in $route.meta.related" :key="item.path">
<router-link
v-if="$router.resolve('/' + item.name).route.name !== '404'"
:to="{ path: '/' + item.name + '?' + item.param + '=' + (item.param === 'account' ? resource.name + '&domainid=' + resource.domainid : resource.id) }">
:to="{ path: '/' + item.name + '?' + item.param + '=' + (item.value ? resource[item.value] : item.param === 'account' ? resource.name + '&domainid=' + resource.domainid : resource.id) }">
<a-button style="margin-right: 10px" :icon="$router.resolve('/' + item.name).route.meta.icon" >
{{ $t('label.view') + ' ' + $t(item.title) }}
</a-button>

View File

@ -27,10 +27,10 @@
<a-table
size="small"
:columns="fetchColumns()"
:dataSource="items"
:dataSource="dataSource"
:rowKey="item => item.id"
:loading="loading"
:pagination="false"
:pagination="defaultPagination"
@change="handleTableChange"
@handle-search-filter="handleTableChange" >
@ -50,7 +50,7 @@
</a-table>
<div style="display: block; text-align: right; margin-top: 10px;">
<div v-if="!defaultPagination" style="display: block; text-align: right; margin-top: 10px;">
<a-pagination
size="small"
:current="options.page"
@ -88,7 +88,7 @@ export default {
},
apiName: {
type: String,
required: true
default: ''
},
routerlinks: {
type: Function,
@ -96,7 +96,7 @@ export default {
},
params: {
type: Object,
required: true
default: () => {}
},
columns: {
type: Array,
@ -105,14 +105,19 @@ export default {
showSearch: {
type: Boolean,
default: true
},
items: {
type: Array,
default: () => []
}
},
data () {
return {
loading: false,
items: [],
dataSource: [],
total: 0,
filter: '',
defaultPagination: false,
options: {
page: 1,
pageSize: 10,
@ -126,6 +131,11 @@ export default {
this.fetchData()
}
},
items (newItem, oldItem) {
if (newItem) {
this.dataSource = newItem
}
},
'$i18n.locale' (to, from) {
if (to !== from) {
this.fetchData()
@ -137,6 +147,14 @@ export default {
},
methods: {
fetchData () {
if (this.items && this.items.length > 0) {
this.dataSource = this.items
this.defaultPagination = {
showSizeChanger: true,
pageSizeOptions: this.mixinDevice === 'desktop' ? ['20', '50', '100', '500'] : ['10', '20', '50', '100', '500']
}
return
}
this.loading = true
var params = { ...this.params, ...this.options }
params.listall = true
@ -159,9 +177,9 @@ export default {
objectName = key
break
}
this.items = json[responseName][objectName]
if (!this.items || this.items.length === 0) {
this.items = []
this.dataSource = json[responseName][objectName]
if (!this.dataSource || this.dataSource.length === 0) {
this.dataSource = []
}
}).finally(() => {
this.loading = false

View File

@ -21,7 +21,7 @@
:loading="loading"
:columns="isOrderUpdatable() ? columns : columns.filter(x => x.dataIndex !== 'order')"
:dataSource="items"
:rowKey="record => record.id || record.name || record.usageType"
:rowKey="(record, idx) => record.id || record.name || record.usageType || idx + '-' + Math.random()"
:pagination="false"
:rowSelection="['vm', 'event', 'alert'].includes($route.name) ? {selectedRowKeys: selectedRowKeys, onChange: onSelectChange} : null"
:rowClassName="getRowClassName"
@ -99,13 +99,14 @@
<router-link :to="{ path: '/accountuser', query: { username: record.username, domainid: record.domainid } }" v-else-if="$store.getters.userInfo.roletype !== 'User'">{{ text }}</router-link>
<span v-else>{{ text }}</span>
</span>
<a slot="ipaddress" slot-scope="text, record" href="javascript:;">
<router-link :to="{ path: $route.path + '/' + record.id }">{{ text }}</router-link>
<span slot="ipaddress" slot-scope="text, record" href="javascript:;">
<router-link v-if="$route.path === '/publicip'" :to="{ path: $route.path + '/' + record.id }">{{ text }}</router-link>
<span v-else>{{ text }}</span>
<span v-if="record.issourcenat">
&nbsp;
<a-tag>source-nat</a-tag>
</span>
</a>
</span>
<a slot="publicip" slot-scope="text, record" href="javascript:;">
<router-link :to="{ path: $route.path + '/' + record.id }">{{ text }}</router-link>
</a>
@ -438,59 +439,56 @@ export default {
this.parentToggleLoading()
const apiString = this.getUpdateApi()
return new Promise((resolve, reject) => {
api(apiString, {
id,
sortKey: index
}).catch(error => {
console.error(error)
}).then((response) => {
resolve(response)
}).catch((reason) => {
reject(reason)
})
})
},
updateOrder (data) {
const promises = []
data.forEach((item, index) => {
promises.push(this.handleUpdateOrder(item.id, index + 1))
})
Promise.all(promises).catch((reason) => {
console.log(reason)
}).finally(() => {
this.parentFetchData()
this.parentToggleLoading()
this.parentFetchData()
})
},
moveItemUp (record) {
const data = this.items
const index = data.findIndex(item => item.id === record.id)
if (index === 0) return
data.splice(index - 1, 0, data.splice(index, 1)[0])
data.forEach((item, index) => {
this.handleUpdateOrder(item.id, index + 1)
})
this.updateOrder(data)
},
moveItemDown (record) {
const data = this.items
const index = data.findIndex(item => item.id === record.id)
if (index === data.length - 1) return
data.splice(index + 1, 0, data.splice(index, 1)[0])
data.forEach((item, index) => {
this.handleUpdateOrder(item.id, index + 1)
})
this.updateOrder(data)
},
moveItemTop (record) {
const data = this.items
const index = data.findIndex(item => item.id === record.id)
if (index === 0) return
data.unshift(data.splice(index, 1)[0])
data.forEach((item, index) => {
this.handleUpdateOrder(item.id, index + 1)
})
this.updateOrder(data)
},
moveItemBottom (record) {
const data = this.items
const index = data.findIndex(item => item.id === record.id)
if (index === data.length - 1) return
data.push(data.splice(index, 1)[0])
data.forEach((item, index) => {
this.handleUpdateOrder(item.id, index + 1)
})
this.updateOrder(data)
},
editTariffValue (record) {
this.parentEditTariffAction(true, record)

View File

@ -218,9 +218,9 @@ export function asyncRouterMap () {
generateRouterMap(event),
generateRouterMap(project),
generateRouterMap(user),
generateRouterMap(role),
generateRouterMap(account),
generateRouterMap(domain),
generateRouterMap(role),
generateRouterMap(infra),
generateRouterMap(offering),
generateRouterMap(config),

View File

@ -94,6 +94,7 @@ export default {
label: 'label.action.update.resource.count',
message: 'message.update.resource.count',
dataView: true,
show: (record, store) => { return ['Admin', 'DomainAdmin'].includes(store.userInfo.roletype) },
args: ['account', 'domainid'],
mapping: {
account: {
@ -110,7 +111,11 @@ export default {
label: 'label.action.enable.account',
message: 'message.enable.account',
dataView: true,
show: (record) => { return record.state === 'disabled' || record.state === 'locked' },
show: (record, store) => {
return ['Admin', 'DomainAdmin'].includes(store.userInfo.roletype) && !record.isdefault &&
!(record.domain === 'ROOT' && record.name === 'admin' && record.accounttype === 1) &&
(record.state === 'disabled' || record.state === 'locked')
},
params: { lock: 'false' }
},
{
@ -119,7 +124,11 @@ export default {
label: 'label.action.disable.account',
message: 'message.disable.account',
dataView: true,
show: (record) => { return record.state === 'enabled' },
show: (record, store) => {
return ['Admin', 'DomainAdmin'].includes(store.userInfo.roletype) && !record.isdefault &&
!(record.domain === 'ROOT' && record.name === 'admin' && record.accounttype === 1) &&
record.state === 'enabled'
},
args: ['lock'],
mapping: {
lock: {
@ -133,7 +142,11 @@ export default {
label: 'label.action.lock.account',
message: 'message.lock.account',
dataView: true,
show: (record) => { return record.state === 'enabled' },
show: (record, store) => {
return ['Admin', 'DomainAdmin'].includes(store.userInfo.roletype) && !record.isdefault &&
!(record.domain === 'ROOT' && record.name === 'admin' && record.accounttype === 1) &&
record.state === 'enabled'
},
args: ['lock'],
mapping: {
lock: {
@ -163,7 +176,10 @@ export default {
label: 'label.action.delete.account',
message: 'message.delete.account',
dataView: true,
hidden: (record) => { return record.name === 'admin' }
show: (record, store) => {
return ['Admin', 'DomainAdmin'].includes(store.userInfo.roletype) && !record.isdefault &&
!(record.domain === 'ROOT' && record.name === 'admin' && record.accounttype === 1)
}
}
]
}

View File

@ -188,6 +188,7 @@ export default {
api: 'createBackup',
icon: 'cloud-upload',
label: 'label.create.backup',
message: 'message.backup.create',
docHelp: 'adminguide/virtual_machines.html#creating-vm-backups',
dataView: true,
args: ['virtualmachineid'],
@ -237,17 +238,9 @@ export default {
label: 'label.action.attach.iso',
docHelp: 'adminguide/templates.html#attaching-an-iso-to-a-vm',
dataView: true,
args: ['id', 'virtualmachineid'],
popup: true,
show: (record) => { return ['Running', 'Stopped'].includes(record.state) && !record.isoid },
mapping: {
id: {
api: 'listIsos',
params: (record) => { return { zoneid: record.zoneid } }
},
virtualmachineid: {
value: (record, params) => { return record.id }
}
}
component: () => import('@/views/compute/AttachIso.vue')
},
{
api: 'detachIso',

View File

@ -77,7 +77,17 @@ export default {
label: 'label.action.edit.domain',
listView: true,
dataView: true,
args: ['name', 'networkdomain']
args: (record) => {
var fields = ['networkdomain']
if (record.name !== 'ROOT') {
fields.unshift('name')
}
return fields
},
show: (record, store) => {
return ['Admin'].includes(store.userInfo.roletype) ||
['DomainAdmin'].includes(store.userInfo.roletype) && record.domainid !== store.userInfo.domainid
}
},
{
api: 'updateResourceCount',
@ -119,7 +129,11 @@ export default {
label: 'label.action.delete.domain',
listView: true,
dataView: true,
show: (record) => { return record.level !== 0 },
show: (record, store) => {
console.log(record)
return ['Admin'].includes(store.userInfo.roletype) && record.level !== 0 ||
['DomainAdmin'].includes(store.userInfo.roletype) && record.domainid !== store.userInfo.domainid
},
args: ['cleanup']
}
]

View File

@ -41,12 +41,12 @@ export default {
tabs: [{
name: 'details',
component: () => import('@/components/view/DetailsTab.vue')
}, {
name: 'settings',
component: () => import('@/components/view/SettingsTab.vue')
}, {
name: 'resources',
component: () => import('@/views/infra/Resources.vue')
}, {
name: 'settings',
component: () => import('@/components/view/SettingsTab.vue')
}],
actions: [
{

View File

@ -36,6 +36,12 @@ export default {
show: (record, route, user) => { return ['Running'].includes(record.state) && ['Admin'].includes(user.roletype) },
component: () => import('@views/infra/routers/RouterHealthCheck.vue')
}],
related: [{
name: 'vm',
title: 'label.instances',
param: 'networkid',
value: 'guestnetworkid'
}],
actions: [
{
api: 'startRouter',
@ -75,7 +81,7 @@ export default {
api: 'listServiceOfferings',
params: (record) => {
return {
virtualmachineid: record.virtualmachineid,
virtualmachineid: record.id,
issystem: true,
systemvmtype: 'domainrouter'
}

View File

@ -68,14 +68,20 @@ export default {
icon: 'edit',
label: 'label.edit',
dataView: true,
args: ['name', 'displaytext', 'guestvmcidr']
args: (record) => {
var fields = ['name', 'displaytext', 'guestvmcidr']
if (record.type === 'Isolated') {
fields.push(...['networkofferingid', 'networkdomain'])
}
return fields
}
},
{
api: 'restartNetwork',
icon: 'sync',
label: 'label.restart.network',
dataView: true,
args: ['cleanup']
args: ['cleanup', 'makeredundant']
},
{
api: 'replaceNetworkACLList',
@ -139,7 +145,8 @@ export default {
label: 'label.add.vpc',
docHelp: 'adminguide/networking_and_traffic.html#adding-a-virtual-private-cloud',
listView: true,
args: ['name', 'displaytext', 'zoneid', 'cidr', 'networkdomain', 'vpcofferingid', 'start']
popup: true,
component: () => import('@/views/network/CreateVpc.vue')
},
{
api: 'updateVPC',
@ -154,7 +161,13 @@ export default {
label: 'label.restart.vpc',
message: 'message.restart.vpc',
dataView: true,
args: ['makeredundant', 'cleanup']
args: (record) => {
var fields = ['cleanup']
if (!record.redundantvpcrouter) {
fields.push('makeredundant')
}
return fields
}
},
{
api: 'deleteVPC',

View File

@ -111,7 +111,7 @@ export default {
message: 'message.detach.disk',
dataView: true,
show: (record) => {
return record.type !== 'ROOT' && 'virtualmachineid' in record && record.virtualmachineid &&
return record.type !== 'ROOT' && record.virtualmachineid &&
['Running', 'Stopped', 'Destroyed'].includes(record.vmstate)
}
},
@ -121,7 +121,11 @@ export default {
docHelp: 'adminguide/storage.html#working-with-volume-snapshots',
label: 'label.action.take.snapshot',
dataView: true,
show: (record) => { return record.state === 'Ready' },
show: (record, store) => {
return record.state === 'Ready' && (record.hypervisor !== 'KVM' ||
record.hypervisor === 'KVM' && record.vmstate === 'Running' && store.features.kvmsnapshotenabled ||
record.hypervisor === 'KVM' && record.vmstate !== 'Running')
},
popup: true,
component: () => import('@/views/storage/TakeSnapshot.vue')
},
@ -131,7 +135,11 @@ export default {
docHelp: 'adminguide/storage.html#working-with-volume-snapshots',
label: 'label.action.recurring.snapshot',
dataView: true,
show: (record) => { return record.state === 'Ready' },
show: (record, store) => {
return record.state === 'Ready' && (record.hypervisor !== 'KVM' ||
record.hypervisor === 'KVM' && record.vmstate === 'Running' && store.features.kvmsnapshotenabled ||
record.hypervisor === 'KVM' && record.vmstate !== 'Running')
},
popup: true,
component: () => import('@/views/storage/RecurringSnapshotVolume.vue'),
mapping: {
@ -160,7 +168,7 @@ export default {
label: 'label.migrate.volume',
args: ['volumeid', 'storageid', 'livemigrate'],
dataView: true,
show: (record, store) => { return record && record.state === 'Ready' && ['Admin', 'DomainAdmin'].includes(store.userInfo.roletype) },
show: (record, store) => { return record.state === 'Ready' && ['Admin'].includes(store.userInfo.roletype) && record.virtualmachineid },
popup: true,
component: () => import('@/views/storage/MigrateVolume.vue')
},
@ -170,7 +178,7 @@ export default {
label: 'label.action.download.volume',
message: 'message.download.volume.confirm',
dataView: true,
show: (record) => { return record && record.state === 'Ready' && (record.vmstate === 'Stopped' || record.virtualmachineid == null) },
show: (record) => { return record.state === 'Ready' && (record.vmstate === 'Stopped' || !record.virtualmachineid) },
args: ['zoneid', 'mode'],
mapping: {
zoneid: {
@ -187,7 +195,11 @@ export default {
icon: 'picture',
label: 'label.action.create.template.from.volume',
dataView: true,
show: (record) => { return (record.type === 'ROOT' && record.vmstate === 'Stopped') || (record.type !== 'ROOT' && !('virtualmachineid' in record) && !['Allocated', 'Uploaded', 'Destroy'].includes(record.state)) },
show: (record) => {
return !['Destroy', 'Destroyed', 'Expunging', 'Expunged', 'Migrating', 'Uploading', 'UploadError', 'Creating'].includes(record.state) &&
((record.type === 'ROOT' && record.vmstate === 'Stopped') ||
(record.type !== 'ROOT' && !record.virtualmachineid && !['Allocated', 'Uploaded'].includes(record.state)))
},
args: ['volumeid', 'name', 'displaytext', 'ostypeid', 'ispublic', 'isfeatured', 'isdynamicallyscalable', 'requireshvm', 'passwordenabled', 'sshkeyenabled'],
mapping: {
volumeid: {
@ -214,6 +226,7 @@ export default {
groupAction: true,
show: (record, store) => {
return ['Expunging', 'Expunged', 'UploadError'].includes(record.state) ||
['Allocated', 'Uploaded'].includes(record.state) && record.type !== 'ROOT' && !record.virtualmachineid ||
((['Admin', 'DomainAdmin'].includes(store.userInfo.roletype) || store.features.allowuserexpungerecovervolume) && record.state === 'Destroy')
}
},
@ -227,7 +240,8 @@ export default {
return (!['Admin', 'DomainAdmin'].includes(store.userInfo.roletype) && !store.features.allowuserexpungerecovervolumestore) ? [] : ['expunge']
},
show: (record, store) => {
return (!['Creating'].includes(record.state) && record.type !== 'ROOT' && !('virtualmachineid' in record) && record.state !== 'Destroy')
return !['Destroy', 'Destroyed', 'Expunging', 'Expunged', 'Migrating', 'Uploading', 'UploadError', 'Creating', 'Allocated', 'Uploaded'].includes(record.state) &&
record.type !== 'ROOT' && !record.virtualmachineid
}
}
]

View File

@ -62,7 +62,11 @@ export default {
label: 'label.action.enable.user',
message: 'message.enable.user',
dataView: true,
show: (record) => { return record.state === 'disabled' }
show: (record, store) => {
return ['Admin', 'DomainAdmin'].includes(store.userInfo.roletype) && !record.isdefault &&
!(record.domain === 'ROOT' && record.account === 'admin' && record.accounttype === 1) &&
record.state === 'disabled'
}
},
{
api: 'disableUser',
@ -70,7 +74,11 @@ export default {
label: 'label.action.disable.user',
message: 'message.disable.user',
dataView: true,
show: (record) => { return record.state === 'enabled' }
show: (record, store) => {
return ['Admin', 'DomainAdmin'].includes(store.userInfo.roletype) && !record.isdefault &&
!(record.domain === 'ROOT' && record.account === 'admin' && record.accounttype === 1) &&
record.state === 'enabled'
}
},
{
api: 'authorizeSamlSso',
@ -78,6 +86,9 @@ export default {
label: 'Configure SAML SSO Authorization',
dataView: true,
popup: true,
show: (record, store) => {
return ['Admin', 'DomainAdmin'].includes(store.userInfo.roletype)
},
component: () => import('@/views/iam/ConfigureSamlSsoAuth.vue')
},
{
@ -85,7 +96,11 @@ export default {
icon: 'delete',
label: 'label.action.delete.user',
message: 'message.delete.user',
dataView: true
dataView: true,
show: (record, store) => {
return ['Admin', 'DomainAdmin'].includes(store.userInfo.roletype) && !record.isdefault &&
!(record.domain === 'ROOT' && record.account === 'admin' && record.accounttype === 1)
}
}
]
}

View File

@ -2490,6 +2490,7 @@
"message.attach.volume": "Please fill in the following data to attach a new volume. If you are attaching a disk volume to a Windows based virtual machine, you will need to reboot the instance to see the attached disk.",
"message.authorization.failed": "Session expired, authorization verification failed",
"message.backup.attach.restore": "Please confirm that you want to restore and attach the volume from the backup?",
"message.backup.create": "Are you sure you want create a VM backup?",
"message.backup.offering.remove": "Are you sure you want to remove VM from backup offering and delete the backup chain?",
"message.backup.restore": "Please confirm that you want to restore the vm backup?",
"message.basic.mode.desc": "Choose this network model if you do <b>*<u>not</u>*</b> want to enable any VLAN support. All virtual instances created under this network model will be assigned an IP directly from the network and security groups are used to provide security and segregation.",

View File

@ -238,6 +238,7 @@
</span>
<span v-else-if="field.type==='long'">
<a-input-number
style="width: 100%;"
v-decorator="[field.name, {
rules: [{ required: field.required, message: `${$t('message.validate.number')}` }]
}]"
@ -747,16 +748,21 @@ export default {
if (res === 'count') {
continue
}
const filter = this.currentAction.mapping[param.name].filter
if (filter) {
param.opts = json[obj][res].filter(filter)
} else {
param.opts = json[obj][res]
}
if (['listTemplates', 'listIsos'].includes(possibleApi)) {
param.opts = [...new Map(param.opts.map(x => [x.id, x])).values()]
}
break
}
break
}
}
this.$forceUpdate()
break
}
break
}
}
}).catch(function (error) {
console.log(error.stack)
param.loading = false

View File

@ -0,0 +1,167 @@
// 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 class="form-layout">
<a-spin :spinning="loading">
<a-form
:form="form"
layout="vertical">
<a-form-item :label="$t('label.iso.name')">
<a-select
:loading="loading"
v-decorator="['id', {
initialValue: this.selectedIso,
rules: [{ required: true, message: `${this.$t('label.required')}`}]
}]" >
<a-select-option v-for="iso in isos" :key="iso.id">
{{ iso.displaytext || iso.name }}
</a-select-option>
</a-select>
</a-form-item>
</a-form>
<div :span="24" class="action-button">
<a-button @click="closeAction">{{ this.$t('label.cancel') }}</a-button>
<a-button :loading="loading" type="primary" @click="handleSubmit">{{ this.$t('label.ok') }}</a-button>
</div>
</a-spin>
</div>
</template>
<script>
import { api } from '@/api'
import _ from 'lodash'
export default {
name: 'AttachIso',
props: {
resource: {
type: Object,
required: true
}
},
inject: ['parentFetchData'],
data () {
return {
loading: false,
selectedIso: '',
isos: []
}
},
beforeCreate () {
this.form = this.$form.createForm(this)
},
mounted () {
this.fetchData()
},
methods: {
fetchData () {
const isoFiters = ['featured', 'community', 'selfexecutable']
this.loading = true
const promises = []
isoFiters.forEach((filter) => {
promises.push(this.fetchIsos(filter))
})
Promise.all(promises).then(() => {
this.isos = _.uniqBy(this.isos, 'id')
if (this.isos.length > 0) {
this.selectedIso = this.isos[0].id
}
}).catch((error) => {
console.log(error)
}).finally(() => {
this.loading = false
})
},
fetchIsos (isoFilter) {
const params = {
listall: true,
zoneid: this.resource.zoneid,
isready: true,
isofilter: isoFilter
}
return new Promise((resolve, reject) => {
api('listIsos', params).then((response) => {
const isos = response.listisosresponse.iso || []
this.isos.push(...isos)
resolve(response)
}).catch((error) => {
reject(error)
})
})
},
closeAction () {
this.$emit('close-action')
},
handleSubmit (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (err) {
return
}
const params = {
id: values.id,
virtualmachineid: this.resource.id
}
this.loading = true
const title = this.$t('label.action.attach.iso')
api('attachIso', params).then(json => {
const jobId = json.attachisoresponse.jobid
if (jobId) {
this.$pollJob({
jobId,
successMethod: result => {
this.$store.dispatch('AddAsyncJob', {
title: title,
jobid: jobId,
status: this.$t('progress')
})
this.parentFetchData()
},
successMessage: `${this.$t('label.action.attach.iso')} ${this.$t('label.success')}`,
loadingMessage: `${title} ${this.$t('label.in.progress')}`,
catchMessage: this.$t('error.fetching.async.job.result')
})
}
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading = false
this.closeAction()
})
})
}
}
}
</script>
<style lang="scss" scoped>
.form-layout {
width: 80vw;
@media (min-width: 700px) {
width: 600px;
}
}
.form {
margin: 10px 0;
}
.action-button {
text-align: right;
button {
margin-right: 5px;
}
}
</style>

View File

@ -132,6 +132,13 @@
:routerlinks="(record) => { return { id: '/backup/' + record.id } }"
:showSearch="false"/>
</a-tab-pane>
<a-tab-pane :tab="$t('label.securitygroups')" key="securitygroups" v-if="this.resource.securitygroup && this.resource.securitygroup.length > 0">
<ListResourceTable
:items="this.resource.securitygroup"
:columns="['name', 'description']"
:routerlinks="(record) => { return { name: '/securitygroups/' + record.id } }"
:showSearch="false"/>
</a-tab-pane>
<a-tab-pane :tab="$t('label.settings')" key="settings">
<DetailSettings :resource="resource" :loading="loading" />
</a-tab-pane>

View File

@ -35,7 +35,7 @@
<div class="capacity-dashboard-button">
<a-button
shape="round"
@click="listCapacity(zoneSelected, true)">
@click="() => { listCapacity(zoneSelected, true); listEvents() }">
{{ $t('label.fetch.latest') }}
</a-button>
</div>

View File

@ -243,13 +243,17 @@ export default {
this.form = this.$form.createForm(this)
this.apiConfig = this.$store.getters.apis.createAccount || {}
this.apiParams = {}
if (this.apiConfig.params) {
this.apiConfig.params.forEach(param => {
this.apiParams[param.name] = param
})
}
this.apiConfig = this.$store.getters.apis.authorizeSamlSso || {}
if (this.apiConfig.params) {
this.apiConfig.params.forEach(param => {
this.apiParams[param.name] = param
})
}
},
mounted () {
this.fetchData()

View File

@ -96,7 +96,7 @@ export default {
data () {
return {
roles: [],
defaultRoles: ['Admin', 'DomainAdmin', 'ResourceAdmin', 'User'],
defaultRoles: ['Admin', 'DomainAdmin', 'User'],
createRoleUsing: 'type',
loading: false
}

View File

@ -210,13 +210,19 @@ export default {
return 0
})
this.action.paramFields = []
if (action.args && action.args.length > 0) {
this.action.paramFields = action.args.map(function (arg) {
if (action.args) {
var args = action.args
if (typeof action.args === 'function') {
args = action.args(action.resource, this.$store.getters)
}
if (args.length > 0) {
this.action.paramFields = args.map(function (arg) {
return paramFields.filter(function (param) {
return param.name.toLowerCase() === arg.toLowerCase()
})[0]
})
}
}
this.showAction = true
for (const param of this.action.paramFields) {
if (param.type === 'list' && ['tags', 'hosttags'].includes(param.name)) {

View File

@ -113,9 +113,16 @@
:help="zoneErrorMessage">
<a-select
v-decorator="['zoneid', {
initialValue: this.zoneSelected
initialValue: this.zoneSelected,
rules: [
{
required: true,
message: `${this.$t('message.error.select')}`
}
]
}]"
@change="handlerSelectZone"
:placeholder="apiParams.zoneid.description"
:loading="zones.loading">
<a-select-option :value="zone.id" v-for="zone in zones.opts" :key="zone.id">
<div v-if="zone.name !== $t('label.all.zone')">

View File

@ -0,0 +1,253 @@
// 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 class="form-layout">
<a-spin :spinning="loading">
<a-form
:form="form"
layout="vertical">
<a-form-item>
<span slot="label">
{{ $t('label.name') }}
<a-tooltip :title="apiParams.name.description">
<a-icon type="info-circle" style="color: rgba(0,0,0,.45)" />
</a-tooltip>
</span>
<a-input
v-decorator="['name', {
rules: [{ required: true, message: $t('message.error.required.input') }]
}]"
:placeholder="apiParams.name.description"/>
</a-form-item>
<a-form-item>
<span slot="label">
{{ $t('label.displaytext') }}
<a-tooltip :title="apiParams.displaytext.description">
<a-icon type="info-circle" style="color: rgba(0,0,0,.45)" />
</a-tooltip>
</span>
<a-input
v-decorator="['displaytext', {
rules: [{ required: true, message: $t('message.error.required.input') }]
}]"
:placeholder="apiParams.displaytext.description"/>
</a-form-item>
<a-form-item>
<span slot="label">
{{ $t('label.zoneid') }}
<a-tooltip :title="apiParams.zoneid.description">
<a-icon type="info-circle" style="color: rgba(0,0,0,.45)" />
</a-tooltip>
</span>
<a-select
:loading="loadingZone"
v-decorator="['zoneid', {
initialValue: this.selectedZone,
rules: [{ required: true, message: `${this.$t('label.required')}`}]
}]"
@change="val => changeZone(val)">
<a-select-option v-for="zone in zones" :key="zone.id">
{{ zone.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<span slot="label">
{{ $t('label.cidr') }}
<a-tooltip :title="apiParams.cidr.description">
<a-icon type="info-circle" style="color: rgba(0,0,0,.45)" />
</a-tooltip>
</span>
<a-input
v-decorator="['cidr', {
rules: [{ required: true, message: $t('message.error.required.input') }]
}]"
:placeholder="apiParams.cidr.description"/>
</a-form-item>
<a-form-item>
<span slot="label">
{{ $t('label.networkdomain') }}
<a-tooltip :title="apiParams.networkdomain.description">
<a-icon type="info-circle" style="color: rgba(0,0,0,.45)" />
</a-tooltip>
</span>
<a-input
v-decorator="['networkdomain']"
:placeholder="apiParams.networkdomain.description"/>
</a-form-item>
<a-form-item>
<span slot="label">
{{ $t('label.vpcofferingid') }}
<a-tooltip :title="apiParams.vpcofferingid.description">
<a-icon type="info-circle" style="color: rgba(0,0,0,.45)" />
</a-tooltip>
</span>
<a-select
:loading="loadingOffering"
v-decorator="['vpcofferingid', {
initialValue: this.selectedOffering,
rules: [{ required: true, message: `${this.$t('label.required')}`}]}]">
<a-select-option :value="offering.id" v-for="offering in vpcOfferings" :key="offering.id">
{{ offering.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<span slot="label">
{{ $t('label.start') }}
<a-tooltip :title="apiParams.start.description">
<a-icon type="info-circle" style="color: rgba(0,0,0,.45)" />
</a-tooltip>
</span>
<a-switch v-decorator="['start']" />
</a-form-item>
</a-form>
<div :span="24" class="action-button">
<a-button @click="closeAction">{{ this.$t('label.cancel') }}</a-button>
<a-button :loading="loading" type="primary" @click="handleSubmit">{{ this.$t('label.ok') }}</a-button>
</div>
</a-spin>
</div>
</template>
<script>
import { api } from '@/api'
export default {
name: 'CreateVpc',
data () {
return {
loading: false,
loadingZone: false,
loadingOffering: false,
selectedZone: '',
zones: [],
vpcOfferings: [],
selectedOffering: ''
}
},
beforeCreate () {
this.form = this.$form.createForm(this)
this.apiParams = {}
var apiConfig = this.$store.getters.apis.createVPC || []
apiConfig.params.forEach(param => {
this.apiParams[param.name] = param
})
},
mounted () {
this.fetchData()
},
methods: {
fetchData () {
this.fetchZones()
},
fetchZones () {
this.loadingZone = true
api('listZones', { listAll: true }).then((response) => {
const listZones = response.listzonesresponse.zone || []
this.zones = listZones.filter(zone => !zone.securitygroupsenabled)
this.selectedZone = ''
if (this.zones.length > 0) {
this.selectedZone = this.zones[0].id
this.changeZone(this.selectedZone)
}
}).finally(() => {
this.loadingZone = false
})
},
changeZone (value) {
this.selectedZone = value
if (this.selectedZone === '') {
this.selectedOffering = ''
return
}
this.fetchOfferings()
},
fetchOfferings () {
this.loadingOffering = true
api('listVPCOfferings', { zoneid: this.selectedZone, state: 'Enabled' }).then((reponse) => {
this.vpcOfferings = reponse.listvpcofferingsresponse.vpcoffering
this.selectedOffering = this.vpcOfferings[0].id || ''
}).finally(() => {
this.loadingOffering = false
})
},
closeAction () {
this.$emit('close-action')
},
handleSubmit (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (err) {
return
}
const params = {}
for (const key in values) {
const input = values[key]
if (input === '' || input === null || input === undefined) {
continue
}
params[key] = input
}
this.loading = true
const title = this.$t('label.add.vpc')
const description = this.$t('message.success.add.vpc.network')
api('createVPC', params).then(json => {
const jobId = json.createvpcresponse.jobid
if (jobId) {
this.$pollJob({
jobId,
successMethod: result => {
this.$store.dispatch('AddAsyncJob', {
title: title,
jobid: jobId,
description: description,
status: this.$t('progress')
})
},
loadingMessage: `${title} ${this.$t('label.in.progress')}`,
catchMessage: this.$t('error.fetching.async.job.result')
})
}
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading = false
this.closeAction()
})
})
}
}
}
</script>
<style lang="scss" scoped>
.form-layout {
width: 80vw;
@media (min-width: 700px) {
width: 600px;
}
}
.form {
margin: 10px 0;
}
.action-button {
text-align: right;
button {
margin-right: 5px;
}
}
</style>

View File

@ -259,7 +259,7 @@
</a-spin>
</a-modal>
</a-tab-pane>
<a-tab-pane :tab="$t('label.virtual.routers')" key="vr" v-if="'listRouters' in $store.getters.apis">
<a-tab-pane :tab="$t('label.virtual.routers')" key="vr" v-if="$store.getters.userInfo.roletype === 'Admin'">
<RoutersTab :resource="resource" :loading="loading" />
</a-tab-pane>
</a-tabs>

View File

@ -177,7 +177,7 @@ export default {
params[key] = input
}
}
params.allowuserdrivenbackups = values.allowuserdrivenbackups ? values.allowuserdrivenbackups : false
params.allowuserdrivenbackups = values.allowuserdrivenbackups ? values.allowuserdrivenbackups : true
this.loading = true
const title = this.$t('label.import.offering')
api('importBackupOffering', params).then(json => {

View File

@ -36,21 +36,22 @@ export default {
return
}
this.resource = newItem
this.fetchProjectAccounts()
this.determineOwner()
}
},
methods: {
fetchProjectAccounts () {
var owner = this.resource.owner
determineOwner () {
var owner = this.resource.owner || []
// If current backend does not support multiple project admins
if (owner.length === 0) {
this.$set(this.resource, 'isCurrentUserProjectAdmin', this.resource.account === this.$store.getters.userInfo.account)
return
}
owner = owner.filter(projectaccount => {
return (projectaccount.userid && projectaccount.userid === this.$store.getters.userInfo.id) ||
projectaccount.account === this.$store.getters.userInfo.account
})
var isCurrentUserProjectAdmin = false
if (owner.length > 0) {
isCurrentUserProjectAdmin = true
}
this.$set(this.resource, 'isCurrentUserProjectAdmin', isCurrentUserProjectAdmin)
this.$set(this.resource, 'isCurrentUserProjectAdmin', owner.length > 0)
}
}
}

View File

@ -62,7 +62,7 @@
compact>
<a-input ref="input" :value="inputKey" @change="handleKeyChange" style="width: 100px; text-align: center" :placeholder="$t('label.key')" />
<a-input style=" width: 30px; border-left: 0; pointer-events: none; backgroundColor: #fff" placeholder="=" disabled />
<a-input :value="inputValue" @change="handleValueChange" style="width: 100px; text-align: center; border-left: 0" :placeholder="$('label.value')" />
<a-input :value="inputValue" @change="handleValueChange" style="width: 100px; text-align: center; border-left: 0" :placeholder="$t('label.value')" />
<a-button shape="circle" size="small" @click="handleInputConfirm">
<a-icon type="check"/>
</a-button>