Merge pull request #744 from utchoang/feature/fix-public-vpc-network

diff --git a/src/components/menu/SideMenu.vue b/src/components/menu/SideMenu.vue
index 20c76da..c670c9f 100644
--- a/src/components/menu/SideMenu.vue
+++ b/src/components/menu/SideMenu.vue
@@ -110,7 +110,6 @@
   }
 
   &.light {
-    background-color: #fff;
     box-shadow: 2px 0px 8px 0px rgba(29, 35, 41, 0.05);
 
     .ant-menu-light {
diff --git a/src/components/view/TreeView.vue b/src/components/view/TreeView.vue
index 9621754..0be5228 100644
--- a/src/components/view/TreeView.vue
+++ b/src/components/view/TreeView.vue
@@ -345,6 +345,7 @@
     reloadTreeData (objData) {
       if (objData && objData[0].isDel) {
         this.treeVerticalData = this.treeVerticalData.filter(item => item.id !== objData[0].id)
+        this.treeVerticalData = this.treeVerticalData.filter(item => item.parentdomainid !== objData[0].id)
       } else {
         // data response from action
         let jsonResponse = this.getResponseJsonData(objData[0])
diff --git a/src/config/section/domain.js b/src/config/section/domain.js
index a507ab6..7fbd263 100644
--- a/src/config/section/domain.js
+++ b/src/config/section/domain.js
@@ -130,7 +130,6 @@
       listView: true,
       dataView: true,
       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
       },
diff --git a/src/locales/en.json b/src/locales/en.json
index b7af2d0..fa54ea1 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -25,6 +25,7 @@
 "error.release.dedicate.zone": "Failed to release dedicated zone",
 "error.session.expired": "Your session has expired.",
 "error.unable.to.reach.management.server": "Unable to reach Management Server",
+"error.unable.to.proceed": "Unable to proceed. Please contact your administrator",
 "error.unresolved.internet.name": "Your internet name cannot be resolved.",
 "firewall.close": "Firewall",
 "force.delete.domain.warning": "Warning: Choosing this option will cause the deletion of all child domains and all associated accounts and their resources.",
@@ -1465,6 +1466,7 @@
 "label.no.grouping": "(no grouping)",
 "label.no.isos": "No available ISOs",
 "label.no.items": "No Available Items",
+"label.no.matching.offering": "No matching offering found",
 "label.no.security.groups": "No Available Security Groups",
 "label.noderootdisksize": "Node root disk size (in GB)",
 "label.nodiskcache": "No disk cache",
diff --git a/src/permission.js b/src/permission.js
index 161a1b4..d22c00f 100644
--- a/src/permission.js
+++ b/src/permission.js
@@ -66,7 +66,8 @@
           .catch(() => {
             notification.error({
               message: 'Error',
-              description: i18n.t('message.error.discovering.feature')
+              description: i18n.t('message.error.discovering.feature'),
+              duration: 0
             })
             store.dispatch('Logout').then(() => {
               next({ path: '/user/login', query: { redirect: to.fullPath } })
diff --git a/src/store/modules/user.js b/src/store/modules/user.js
index 76271ff..aba087d 100644
--- a/src/store/modules/user.js
+++ b/src/store/modules/user.js
@@ -19,6 +19,7 @@
 import Vue from 'vue'
 import md5 from 'md5'
 import message from 'ant-design-vue/es/message'
+import notification from 'ant-design-vue/es/notification'
 import router from '@/router'
 import store from '@/store'
 import { login, logout, api } from '@/api'
@@ -112,6 +113,8 @@
           commit('SET_LDAP', {})
           commit('SET_CLOUDIAN', {})
 
+          notification.destroy()
+
           resolve()
         }).catch(error => {
           reject(error)
@@ -148,6 +151,8 @@
           api('listZones', { listall: true }).then(json => {
             const zones = json.listzonesresponse.zone || []
             commit('SET_ZONES', zones)
+          }).catch(error => {
+            reject(error)
           })
           api('listApis').then(response => {
             const apis = {}
diff --git a/src/style/vars.less b/src/style/vars.less
index 399afd6..b422e53 100644
--- a/src/style/vars.less
+++ b/src/style/vars.less
@@ -89,6 +89,10 @@
   box-shadow: 1px 1px 0px 0px #e8e8e8;
 }
 
+.sider.light {
+  background: @navigation-background-color;
+}
+
 .ant-menu {
   background: @navigation-background-color;
 }
diff --git a/src/utils/request.js b/src/utils/request.js
index ffc62eb..9371aee 100644
--- a/src/utils/request.js
+++ b/src/utils/request.js
@@ -37,16 +37,29 @@
       notification.error({ message: i18n.t('label.forbidden'), description: data.message })
     }
     if (response.status === 401) {
-      if (response.config && response.config.params && ['listIdps'].includes(response.config.params.command)) {
+      if (response.config && response.config.params && ['listIdps', 'cloudianIsEnabled'].includes(response.config.params.command)) {
         return
       }
+      for (const key in response.data) {
+        if (key.includes('response')) {
+          if (response.data[key].errortext.includes('not available for user')) {
+            notification.error({
+              message: 'Error',
+              description: response.data[key].errortext + ' ' + i18n.t('error.unable.to.proceed'),
+              duration: 0
+            })
+            return
+          }
+        }
+      }
       notification.error({
         message: i18n.t('label.unauthorized'),
         description: i18n.t('message.authorization.failed'),
-        key: 'http-401'
+        key: 'http-401',
+        duration: 0
       })
       store.dispatch('Logout').then(() => {
-        router.go(0)
+        router.push({ path: '/user/login', query: { redirect: router.history.current.fullPath } })
       })
     }
     if (response.status === 404) {
diff --git a/src/views/AutogenView.vue b/src/views/AutogenView.vue
index f908fb1..f607ea5 100644
--- a/src/views/AutogenView.vue
+++ b/src/views/AutogenView.vue
@@ -651,6 +651,10 @@
           }
         }
       }).catch(error => {
+        if ([401].includes(error.response.status)) {
+          return
+        }
+
         if (Object.keys(this.searchParams).length > 0) {
           this.itemCount = 0
           this.items = []
@@ -661,13 +665,6 @@
           return
         }
 
-        if ([401].includes(error.response.status)) {
-          store.dispatch('Logout').then(() => {
-            this.$router.push({ path: '/user/login', query: { redirect: this.$route.fullPath } })
-          })
-          return
-        }
-
         this.$notifyError(error)
 
         if ([405].includes(error.response.status)) {
@@ -895,6 +892,9 @@
       api(action.api, params).then(json => {
         this.handleResponse(json, resourceName, action, false)
       }).catch(error => {
+        if ([401].includes(error.response.status)) {
+          return
+        }
         this.$notifyError(error)
       })
     },
@@ -995,6 +995,10 @@
           }
           this.closeAction()
         }).catch(error => {
+          if ([401].includes(error.response.status)) {
+            return
+          }
+
           console.log(error)
           this.$notifyError(error)
         }).finally(f => {
diff --git a/src/views/compute/DeployVM.vue b/src/views/compute/DeployVM.vue
index a4b42bf..6ad55aa 100644
--- a/src/views/compute/DeployVM.vue
+++ b/src/views/compute/DeployVM.vue
@@ -226,6 +226,21 @@
                 </template>
               </a-step>
               <a-step
+                :title="$t('label.data.disk')"
+                :status="zoneSelected ? 'process' : 'wait'"
+                v-if="!template.deployasis && template.childtemplates && template.childtemplates.length > 0" >
+                <template slot="description">
+                  <div v-if="zoneSelected">
+                    <multi-disk-selection
+                      :items="template.childtemplates"
+                      :diskOfferings="options.diskOfferings"
+                      :zoneId="zoneId"
+                      @select-multi-disk-offering="updateMultiDiskOffering($event)" />
+                  </div>
+                </template>
+              </a-step>
+              <a-step
+                v-else
                 :title="tabKey == 'templateid' ? $t('label.data.disk') : $t('label.disk.size')"
                 :status="zoneSelected ? 'process' : 'wait'">
                 <template slot="description">
@@ -607,6 +622,7 @@
 import ComputeSelection from '@views/compute/wizard/ComputeSelection'
 import DiskOfferingSelection from '@views/compute/wizard/DiskOfferingSelection'
 import DiskSizeSelection from '@views/compute/wizard/DiskSizeSelection'
+import MultiDiskSelection from '@views/compute/wizard/MultiDiskSelection'
 import TemplateIsoSelection from '@views/compute/wizard/TemplateIsoSelection'
 import AffinityGroupSelection from '@views/compute/wizard/AffinityGroupSelection'
 import NetworkSelection from '@views/compute/wizard/NetworkSelection'
@@ -623,6 +639,7 @@
     AffinityGroupSelection,
     TemplateIsoSelection,
     DiskSizeSelection,
+    MultiDiskSelection,
     DiskOfferingSelection,
     InfoCard,
     ComputeOfferingSelection,
@@ -1040,7 +1057,11 @@
         }
       }
 
-      if (this.diskOffering) {
+      if (!this.template.deployasis && this.template.childtemplates && this.template.childtemplates.length > 0) {
+        this.vm.diskofferingid = ''
+        this.vm.diskofferingname = ''
+        this.vm.diskofferingsize = ''
+      } else if (this.diskOffering) {
         this.vm.diskofferingid = this.diskOffering.id
         this.vm.diskofferingname = this.diskOffering.displaytext
         this.vm.diskofferingsize = this.diskOffering.disksize
@@ -1072,6 +1093,7 @@
     })
     this.form.getFieldDecorator('computeofferingid', { initialValue: undefined, preserve: true })
     this.form.getFieldDecorator('diskofferingid', { initialValue: undefined, preserve: true })
+    this.form.getFieldDecorator('multidiskoffering', { initialValue: undefined, preserve: true })
     this.form.getFieldDecorator('affinitygroupids', { initialValue: [], preserve: true })
     this.form.getFieldDecorator('networkids', { initialValue: [], preserve: true })
     this.form.getFieldDecorator('keypair', { initialValue: undefined, preserve: true })
@@ -1286,6 +1308,11 @@
         diskofferingid: id
       })
     },
+    updateMultiDiskOffering (value) {
+      this.form.setFieldsValue({
+        multidiskoffering: value
+      })
+    },
     updateAffinityGroups (ids) {
       this.form.setFieldsValue({
         affinitygroupids: ids
@@ -1408,9 +1435,22 @@
           deployVmData['details[0].configurationId'] = this.selectedTemplateConfiguration.id
         }
         // step 4: select disk offering
-        deployVmData.diskofferingid = values.diskofferingid
-        if (values.size) {
-          deployVmData.size = values.size
+        if (!this.template.deployasis && this.template.childtemplates && this.template.childtemplates.length > 0) {
+          if (values.multidiskoffering) {
+            let i = 0
+            Object.entries(values.multidiskoffering).forEach(([disk, offering]) => {
+              const diskKey = `datadiskofferinglist[${i}].datadisktemplateid`
+              const offeringKey = `datadiskofferinglist[${i}].diskofferingid`
+              deployVmData[diskKey] = disk
+              deployVmData[offeringKey] = offering
+              i++
+            })
+          }
+        } else {
+          deployVmData.diskofferingid = values.diskofferingid
+          if (values.size) {
+            deployVmData.size = values.size
+          }
         }
         // step 5: select an affinity group
         deployVmData.affinitygroupids = (values.affinitygroupids || []).join(',')
diff --git a/src/views/compute/wizard/DiskOfferingSelection.vue b/src/views/compute/wizard/DiskOfferingSelection.vue
index 4578363..aff96a2 100644
--- a/src/views/compute/wizard/DiskOfferingSelection.vue
+++ b/src/views/compute/wizard/DiskOfferingSelection.vue
@@ -133,6 +133,9 @@
   },
   created () {
     this.initDataItem()
+    if (this.items) {
+      this.dataItems = this.dataItems.concat(this.items)
+    }
   },
   computed: {
     tableSource () {
diff --git a/src/views/compute/wizard/MultiDiskSelection.vue b/src/views/compute/wizard/MultiDiskSelection.vue
new file mode 100644
index 0000000..998fdbe
--- /dev/null
+++ b/src/views/compute/wizard/MultiDiskSelection.vue
@@ -0,0 +1,170 @@
+// 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
+      :loading="loading"
+      :columns="columns"
+      :dataSource="tableSource"
+      :rowKey="record => record.id"
+      :pagination="false"
+      :rowSelection="rowSelection"
+      :scroll="{ y: 225 }" >
+
+      <span slot="offering" slot-scope="text, record">
+        <a-select
+          v-if="validOfferings[record.id] && validOfferings[record.id].length > 0"
+          @change="updateOffering($event, record.id)"
+          :defaultValue="validOfferings[record.id][0].id">
+          <a-select-option v-for="offering in validOfferings[record.id]" :key="offering.id">
+            {{ offering.displaytext }}
+          </a-select-option>
+        </a-select>
+        <span v-else>
+          {{ $t('label.no.matching.offering') }}
+        </span>
+      </span>
+    </a-table>
+  </div>
+</template>
+
+<script>
+import { api } from '@/api'
+
+export default {
+  name: 'MultiDiskSelection',
+  props: {
+    items: {
+      type: Array,
+      default: () => []
+    },
+    zoneId: {
+      type: String,
+      default: () => ''
+    }
+  },
+  data () {
+    return {
+      columns: [
+        {
+          dataIndex: 'name',
+          title: this.$t('label.data.disk')
+        },
+        {
+          dataIndex: 'offering',
+          title: this.$t('label.data.disk.offering'),
+          scopedSlots: { customRender: 'offering' }
+        }
+      ],
+      loading: false,
+      selectedRowKeys: [],
+      diskOfferings: [],
+      validOfferings: {},
+      values: {}
+    }
+  },
+  computed: {
+    tableSource () {
+      return this.items.map(item => {
+        return {
+          id: item.id,
+          name: `${item.name} (${item.size} GB)`,
+          disabled: this.validOfferings[item.id] && this.validOfferings[item.id].length === 0
+        }
+      })
+    },
+    rowSelection () {
+      return {
+        type: 'checkbox',
+        selectedRowKeys: this.selectedRowKeys,
+        getCheckboxProps: record => ({
+          props: {
+            disabled: record.disabled
+          }
+        }),
+        onChange: (rows) => {
+          this.selectedRowKeys = rows
+          this.sendValues()
+        }
+      }
+    }
+  },
+  watch: {
+    items (newData, oldData) {
+      this.items = newData
+      this.selectedRowKeys = []
+      this.fetchDiskOfferings()
+    },
+    zoneId (newData) {
+      this.zoneId = newData
+      this.fetchDiskOfferings()
+    }
+  },
+  created () {
+    this.fetchDiskOfferings()
+  },
+  methods: {
+    fetchDiskOfferings () {
+      this.diskOfferings = []
+      this.loading = true
+      api('listDiskOfferings', {
+        zoneid: this.zoneId,
+        listall: true
+      }).then(response => {
+        this.diskOfferings = response.listdiskofferingsresponse.diskoffering || []
+        this.diskOfferings = this.diskOfferings.filter(x => !x.iscustomized)
+        this.orderDiskOfferings()
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    orderDiskOfferings () {
+      this.loading = true
+      this.validOfferings = {}
+      for (const item of this.items) {
+        this.validOfferings[item.id] = this.diskOfferings.filter(x => x.disksize >= item.size)
+      }
+      this.setDefaultValues()
+      this.loading = false
+    },
+    setDefaultValues () {
+      this.values = {}
+      for (const item of this.items) {
+        this.values[item.id] = this.validOfferings[item.id].length > 0 ? this.validOfferings[item.id][0].id : ''
+      }
+    },
+    updateOffering (value, templateid) {
+      this.values[templateid] = value
+      this.sendValues()
+    },
+    sendValues () {
+      const data = {}
+      this.selectedRowKeys.map(x => {
+        data[x] = this.values[x]
+      })
+      this.$emit('select-multi-disk-offering', data)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  .ant-table-wrapper {
+    margin: 2rem 0;
+  }
+</style>
diff --git a/src/views/iam/DomainActionForm.vue b/src/views/iam/DomainActionForm.vue
index 6cfecb7..378b3b7 100644
--- a/src/views/iam/DomainActionForm.vue
+++ b/src/views/iam/DomainActionForm.vue
@@ -157,6 +157,10 @@
       this.$pollJob({
         jobId,
         successMethod: result => {
+          if (this.action.api === 'deleteDomain') {
+            this.$set(this.resource, 'isDel', true)
+            this.parentUpdActionData(this.resource)
+          }
           this.parentFetchData()
           if (action.response) {
             const description = action.response(result.jobresult)
@@ -250,9 +254,6 @@
           if (!hasJobId) {
             this.parentUpdActionData(json)
             this.parentFetchData()
-          } else {
-            this.$set(this.resource, 'isDel', true)
-            this.parentUpdActionData(this.resource)
           }
           this.parentCloseAction()
         }).catch(error => {
diff --git a/src/views/iam/DomainView.vue b/src/views/iam/DomainView.vue
index 287ca56..12ba40b 100644
--- a/src/views/iam/DomainView.vue
+++ b/src/views/iam/DomainView.vue
@@ -175,19 +175,16 @@
         this.resource = domains[0] || {}
         this.treeSelected = domains[0] || {}
       }).catch(error => {
+        if ([401].includes(error.response.status)) {
+          return
+        }
+
         this.$notification.error({
           message: this.$t('message.request.failed'),
           description: error.response.headers['x-description'],
           duration: 0
         })
 
-        if ([401].includes(error.response.status)) {
-          store.dispatch('Logout').then(() => {
-            this.$router.push({ path: '/user/login', query: { redirect: this.$route.fullPath } })
-          })
-          return
-        }
-
         if ([405].includes(error.response.status)) {
           this.$router.push({ path: '/exception/403' })
         }