You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by ro...@apache.org on 2020/08/07 15:05:37 UTC

[cloudstack-primate] branch master updated: src: assorted bug fixes (#564)

This is an automated email from the ASF dual-hosted git repository.

rohit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cloudstack-primate.git


The following commit(s) were added to refs/heads/master by this push:
     new 51b5ecd  src: assorted bug fixes (#564)
51b5ecd is described below

commit 51b5ecd639fcd306b3b5a19f5bcae86cae2ec782
Author: davidjumani <dj...@gmail.com>
AuthorDate: Fri Aug 7 20:35:31 2020 +0530

    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
---
 src/components/view/ActionButton.vue         |   4 +-
 src/components/view/InfoCard.vue             |   2 +-
 src/components/view/ListResourceTable.vue    |  36 +++-
 src/components/view/ListView.vue             |  58 +++---
 src/config/router.js                         |   2 +-
 src/config/section/account.js                |  24 ++-
 src/config/section/compute.js                |  13 +-
 src/config/section/domain.js                 |  18 +-
 src/config/section/infra/clusters.js         |   6 +-
 src/config/section/infra/routers.js          |   8 +-
 src/config/section/network.js                |  21 ++-
 src/config/section/storage.js                |  28 ++-
 src/config/section/user.js                   |  21 ++-
 src/locales/en.json                          |   1 +
 src/views/AutogenView.vue                    |  10 +-
 src/views/compute/AttachIso.vue              | 167 ++++++++++++++++++
 src/views/compute/InstanceTab.vue            |   7 +
 src/views/dashboard/CapacityDashboard.vue    |   2 +-
 src/views/iam/AddAccount.vue                 |  16 +-
 src/views/iam/CreateRole.vue                 |   2 +-
 src/views/iam/DomainView.vue                 |  18 +-
 src/views/image/RegisterOrUploadTemplate.vue |   9 +-
 src/views/network/CreateVpc.vue              | 253 +++++++++++++++++++++++++++
 src/views/network/VpcTab.vue                 |   2 +-
 src/views/offering/ImportBackupOffering.vue  |   2 +-
 src/views/project/ProjectDetailsTab.vue      |  17 +-
 src/views/storage/TakeSnapshot.vue           |   2 +-
 27 files changed, 644 insertions(+), 105 deletions(-)

diff --git a/src/components/view/ActionButton.vue b/src/components/view/ActionButton.vue
index 66b88ec..ce5c129 100644
--- a/src/components/view/ActionButton.vue
+++ b/src/components/view/ActionButton.vue
@@ -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"
diff --git a/src/components/view/InfoCard.vue b/src/components/view/InfoCard.vue
index 175189e..794f4e6 100644
--- a/src/components/view/InfoCard.vue
+++ b/src/components/view/InfoCard.vue
@@ -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>
diff --git a/src/components/view/ListResourceTable.vue b/src/components/view/ListResourceTable.vue
index 7ac54d8..500a374 100644
--- a/src/components/view/ListResourceTable.vue
+++ b/src/components/view/ListResourceTable.vue
@@ -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
diff --git a/src/components/view/ListView.vue b/src/components/view/ListView.vue
index 5ab0cf6..e324fed 100644
--- a/src/components/view/ListView.vue
+++ b/src/components/view/ListView.vue
@@ -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()
 
-      api(apiString, {
-        id,
-        sortKey: index
-      }).catch(error => {
-        console.error(error)
+      return new Promise((resolve, reject) => {
+        api(apiString, {
+          id,
+          sortKey: index
+        }).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)
diff --git a/src/config/router.js b/src/config/router.js
index 1ead309..43a2da5 100644
--- a/src/config/router.js
+++ b/src/config/router.js
@@ -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),
diff --git a/src/config/section/account.js b/src/config/section/account.js
index 12d1cae..9fdf990 100644
--- a/src/config/section/account.js
+++ b/src/config/section/account.js
@@ -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)
+      }
     }
   ]
 }
diff --git a/src/config/section/compute.js b/src/config/section/compute.js
index 6a9dc58..8981580 100644
--- a/src/config/section/compute.js
+++ b/src/config/section/compute.js
@@ -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',
diff --git a/src/config/section/domain.js b/src/config/section/domain.js
index 64d9e91..a507ab6 100644
--- a/src/config/section/domain.js
+++ b/src/config/section/domain.js
@@ -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']
     }
   ]
diff --git a/src/config/section/infra/clusters.js b/src/config/section/infra/clusters.js
index fcb13b0..8b42184 100644
--- a/src/config/section/infra/clusters.js
+++ b/src/config/section/infra/clusters.js
@@ -42,11 +42,11 @@ export default {
     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: [
     {
diff --git a/src/config/section/infra/routers.js b/src/config/section/infra/routers.js
index 57e12cf..b109914 100644
--- a/src/config/section/infra/routers.js
+++ b/src/config/section/infra/routers.js
@@ -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'
             }
diff --git a/src/config/section/network.js b/src/config/section/network.js
index d22a3c0..300b631 100644
--- a/src/config/section/network.js
+++ b/src/config/section/network.js
@@ -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',
diff --git a/src/config/section/storage.js b/src/config/section/storage.js
index 3ff5b26..2b3d62d 100644
--- a/src/config/section/storage.js
+++ b/src/config/section/storage.js
@@ -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
           }
         }
       ]
diff --git a/src/config/section/user.js b/src/config/section/user.js
index 0b6d504..0ff016e 100644
--- a/src/config/section/user.js
+++ b/src/config/section/user.js
@@ -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)
+      }
     }
   ]
 }
diff --git a/src/locales/en.json b/src/locales/en.json
index d3078e5..c19ac23 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -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.",
diff --git a/src/views/AutogenView.vue b/src/views/AutogenView.vue
index 9277f54..fd6504c 100644
--- a/src/views/AutogenView.vue
+++ b/src/views/AutogenView.vue
@@ -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
               }
-              param.opts = json[obj][res]
+              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()]
               }
-              this.$forceUpdate()
               break
             }
             break
           }
         }
+        this.$forceUpdate()
       }).catch(function (error) {
         console.log(error.stack)
         param.loading = false
diff --git a/src/views/compute/AttachIso.vue b/src/views/compute/AttachIso.vue
new file mode 100644
index 0000000..23bd38c
--- /dev/null
+++ b/src/views/compute/AttachIso.vue
@@ -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>
diff --git a/src/views/compute/InstanceTab.vue b/src/views/compute/InstanceTab.vue
index ff47877..20b344b 100644
--- a/src/views/compute/InstanceTab.vue
+++ b/src/views/compute/InstanceTab.vue
@@ -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>
diff --git a/src/views/dashboard/CapacityDashboard.vue b/src/views/dashboard/CapacityDashboard.vue
index 6c01044..6111475 100644
--- a/src/views/dashboard/CapacityDashboard.vue
+++ b/src/views/dashboard/CapacityDashboard.vue
@@ -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>
diff --git a/src/views/iam/AddAccount.vue b/src/views/iam/AddAccount.vue
index 0809437..b330d17 100644
--- a/src/views/iam/AddAccount.vue
+++ b/src/views/iam/AddAccount.vue
@@ -243,13 +243,17 @@ export default {
     this.form = this.$form.createForm(this)
     this.apiConfig = this.$store.getters.apis.createAccount || {}
     this.apiParams = {}
-    this.apiConfig.params.forEach(param => {
-      this.apiParams[param.name] = param
-    })
+    if (this.apiConfig.params) {
+      this.apiConfig.params.forEach(param => {
+        this.apiParams[param.name] = param
+      })
+    }
     this.apiConfig = this.$store.getters.apis.authorizeSamlSso || {}
-    this.apiConfig.params.forEach(param => {
-      this.apiParams[param.name] = param
-    })
+    if (this.apiConfig.params) {
+      this.apiConfig.params.forEach(param => {
+        this.apiParams[param.name] = param
+      })
+    }
   },
   mounted () {
     this.fetchData()
diff --git a/src/views/iam/CreateRole.vue b/src/views/iam/CreateRole.vue
index 23730b5..5721de2 100644
--- a/src/views/iam/CreateRole.vue
+++ b/src/views/iam/CreateRole.vue
@@ -96,7 +96,7 @@ export default {
   data () {
     return {
       roles: [],
-      defaultRoles: ['Admin', 'DomainAdmin', 'ResourceAdmin', 'User'],
+      defaultRoles: ['Admin', 'DomainAdmin', 'User'],
       createRoleUsing: 'type',
       loading: false
     }
diff --git a/src/views/iam/DomainView.vue b/src/views/iam/DomainView.vue
index b019037..bb4efad 100644
--- a/src/views/iam/DomainView.vue
+++ b/src/views/iam/DomainView.vue
@@ -210,12 +210,18 @@ export default {
         return 0
       })
       this.action.paramFields = []
-      if (action.args && action.args.length > 0) {
-        this.action.paramFields = action.args.map(function (arg) {
-          return paramFields.filter(function (param) {
-            return param.name.toLowerCase() === arg.toLowerCase()
-          })[0]
-        })
+      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) {
diff --git a/src/views/image/RegisterOrUploadTemplate.vue b/src/views/image/RegisterOrUploadTemplate.vue
index f0ad376..9cb3e76 100644
--- a/src/views/image/RegisterOrUploadTemplate.vue
+++ b/src/views/image/RegisterOrUploadTemplate.vue
@@ -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')">
diff --git a/src/views/network/CreateVpc.vue b/src/views/network/CreateVpc.vue
new file mode 100644
index 0000000..571301c
--- /dev/null
+++ b/src/views/network/CreateVpc.vue
@@ -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>
diff --git a/src/views/network/VpcTab.vue b/src/views/network/VpcTab.vue
index d57e287..4739f67 100644
--- a/src/views/network/VpcTab.vue
+++ b/src/views/network/VpcTab.vue
@@ -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>
diff --git a/src/views/offering/ImportBackupOffering.vue b/src/views/offering/ImportBackupOffering.vue
index 8af621a..782df3a 100644
--- a/src/views/offering/ImportBackupOffering.vue
+++ b/src/views/offering/ImportBackupOffering.vue
@@ -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 => {
diff --git a/src/views/project/ProjectDetailsTab.vue b/src/views/project/ProjectDetailsTab.vue
index ebffb59..bdb0842 100644
--- a/src/views/project/ProjectDetailsTab.vue
+++ b/src/views/project/ProjectDetailsTab.vue
@@ -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)
     }
   }
 }
diff --git a/src/views/storage/TakeSnapshot.vue b/src/views/storage/TakeSnapshot.vue
index a3696fe..b53adf9 100644
--- a/src/views/storage/TakeSnapshot.vue
+++ b/src/views/storage/TakeSnapshot.vue
@@ -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>