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 2019/12/06 20:25:33 UTC

[cloudstack-primate] branch master updated: domain: implement tree-view based domain list view (#53)

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 3a7901a  domain: implement tree-view based domain list view (#53)
3a7901a is described below

commit 3a7901ad7c3a2c7ed9ee145626d02dc2d6876d7c
Author: Hoang Nguyen <ho...@unitech.vn>
AuthorDate: Sat Dec 7 03:25:25 2019 +0700

    domain: implement tree-view based domain list view (#53)
    
    Fixes #27
    
    Signed-off-by: Rohit Yadav <ro...@shapeblue.com>
---
 src/components/header/UserMenu.vue   |  14 +-
 src/components/view/InfoCard.vue     |   6 +-
 src/components/view/ResourceView.vue |   3 +-
 src/components/view/TreeView.vue     | 590 +++++++++++++++++++++++++++++++++++
 src/config/router.js                 |   4 +-
 src/config/section/iam.js            |  25 +-
 src/views/AutogenView.vue            | 112 +++++--
 7 files changed, 718 insertions(+), 36 deletions(-)

diff --git a/src/components/header/UserMenu.vue b/src/components/header/UserMenu.vue
index b53476c..1f33a0f 100644
--- a/src/components/header/UserMenu.vue
+++ b/src/components/header/UserMenu.vue
@@ -27,25 +27,19 @@
       </span>
       <a-menu slot="overlay" class="user-menu-wrapper">
         <a-menu-item class="user-menu-item" key="0">
-          <router-link :to="{ name: 'account' }">
+          <router-link :to="{ path: '/accountuser/' + $store.getters.userInfo.id }">
             <a-icon class="user-menu-item-icon" type="user"/>
-            <span class="user-menu-item-name">Account</span>
+            <span class="user-menu-item-name">Profile</span>
           </router-link>
         </a-menu-item>
-        <a-menu-item class="user-menu-item" key="2" disabled>
-          <router-link :to="{ name: 'account' }">
-            <a-icon class="user-menu-item-icon" type="setting"/>
-            <span class="user-menu-item-name">Settings</span>
-          </router-link>
-        </a-menu-item>
-        <a-menu-item class="user-menu-item" key="3" disabled>
+        <a-menu-item class="user-menu-item" key="1" disabled>
           <a :href="docBase" target="_blank">
             <a-icon class="user-menu-item-icon" type="question-circle-o"></a-icon>
             <span class="user-menu-item-name">Help</span>
           </a>
         </a-menu-item>
         <a-menu-divider/>
-        <a-menu-item class="user-menu-item" key="4">
+        <a-menu-item class="user-menu-item" key="2">
           <a href="javascript:;" @click="handleLogout">
             <a-icon class="user-menu-item-icon" type="logout"/>
             <span class="user-menu-item-name">Logout</span>
diff --git a/src/components/view/InfoCard.vue b/src/components/view/InfoCard.vue
index e4f973f..b8f4ab5 100644
--- a/src/components/view/InfoCard.vue
+++ b/src/components/view/InfoCard.vue
@@ -17,7 +17,7 @@
 
 <template>
   <a-spin :spinning="loading">
-    <a-card class="spin-content" :bordered="true" :title="title">
+    <a-card class="spin-content" :bordered="bordered" :title="title">
       <div>
         <div class="resource-details">
           <div class="avatar">
@@ -466,6 +466,10 @@ export default {
     title: {
       type: String,
       default: ''
+    },
+    bordered: {
+      type: Boolean,
+      default: true
     }
   },
   data () {
diff --git a/src/components/view/ResourceView.vue b/src/components/view/ResourceView.vue
index 7b16251..23ae8cc 100644
--- a/src/components/view/ResourceView.vue
+++ b/src/components/view/ResourceView.vue
@@ -35,7 +35,8 @@
           <a-tab-pane
             v-for="tab in tabs"
             :tab="$t(tab.name)"
-            :key="tab.name">
+            :key="tab.name"
+            v-if="'show' in tab ? tab.show(resource, $route) : true">
             <component :is="tab.component" :resource="resource" :loading="loading" />
           </a-tab-pane>
         </a-tabs>
diff --git a/src/components/view/TreeView.vue b/src/components/view/TreeView.vue
new file mode 100644
index 0000000..e68b17c
--- /dev/null
+++ b/src/components/view/TreeView.vue
@@ -0,0 +1,590 @@
+// 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>
+  <resource-layout>
+    <a-spin :spinning="loading" slot="left">
+      <a-card :bordered="false">
+        <a-input-search
+          size="default"
+          placeholder="Search"
+          v-model="searchQuery"
+          @search="onSearch"
+        >
+          <a-icon slot="prefix" type="search" />
+        </a-input-search>
+        <a-spin :spinning="loadingSearch">
+          <a-tree
+            showLine
+            v-if="treeViewData.length > 0"
+            class="list-tree-view"
+            :treeData="treeViewData"
+            :loadData="onLoadData"
+            :expandAction="false"
+            :showIcon="true"
+            :defaultSelectedKeys="defaultSelected"
+            :checkStrictly="true"
+            @select="onSelect"
+            @expand="onExpand"
+            :defaultExpandedKeys="arrExpand">
+            <a-icon slot="parent" type="folder" />
+            <a-icon slot="leaf" type="block" />
+          </a-tree>
+        </a-spin>
+      </a-card>
+    </a-spin>
+    <a-spin :spinning="detailLoading" slot="right">
+      <a-card
+        class="spin-content"
+        :bordered="true"
+        style="width:100%">
+        <a-tabs
+          style="width: 100%"
+          :animated="false"
+          :defaultActiveKey="tabs[0].name"
+          @change="onTabChange" >
+          <a-tab-pane
+            v-for="tab in tabs"
+            :tab="$t(tab.name)"
+            :key="tab.name"
+            v-if="checkShowTabDetail(tab.name)">
+            <component
+              :is="tab.component"
+              :resource="resource"
+              :items="items"
+              :tab="tabActive"
+              :loading="loading"
+              :bordered="false" />
+          </a-tab-pane>
+        </a-tabs>
+      </a-card>
+    </a-spin>
+  </resource-layout>
+</template>
+
+<script>
+import store from '@/store'
+import { api } from '@/api'
+import DetailsTab from '@/components/view/DetailsTab'
+import ResourceView from '@/components/view/ResourceView'
+import ResourceLayout from '@/layouts/ResourceLayout'
+
+export default {
+  name: 'TreeView',
+  components: {
+    ResourceLayout,
+    ResourceView
+  },
+  props: {
+    treeData: {
+      type: Array,
+      required: true
+    },
+    treeSelected: {
+      type: Object,
+      required: true
+    },
+    tabs: {
+      type: Array,
+      default () {
+        return [{
+          name: 'details',
+          component: DetailsTab
+        }]
+      }
+    },
+    loadedKeys: {
+      type: Array,
+      default () {
+        return []
+      }
+    },
+    loading: {
+      type: Boolean,
+      default: false
+    },
+    actionData: {
+      type: Array,
+      default () {
+        return []
+      }
+    }
+  },
+  data () {
+    return {
+      detailLoading: false,
+      loadingSearch: false,
+      tabActive: 'details',
+      selectedTreeKey: '',
+      resource: {},
+      defaultSelected: [],
+      treeVerticalData: [],
+      treeViewData: [],
+      oldTreeViewData: [],
+      apiList: '',
+      apiChildren: '',
+      apiDetail: '',
+      metaName: '',
+      page: 1,
+      pageSize: 20,
+      items: [],
+      showSetting: false,
+      oldSearchQuery: '',
+      searchQuery: '',
+      arrExpand: [],
+      rootKey: ''
+    }
+  },
+  created: function () {
+    this.metaName = this.$route.meta.name
+    this.apiList = this.$route.meta.permission[0] ? this.$route.meta.permission[0] : ''
+    this.apiChildren = this.$route.meta.permission[1] ? this.$route.meta.permission[1] : ''
+  },
+  watch: {
+    loading () {
+      this.detailLoading = this.loading
+    },
+    treeData () {
+      if (this.oldTreeViewData.length === 0) {
+        this.treeViewData = this.treeData
+        this.treeVerticalData = this.treeData
+      }
+
+      if (this.treeViewData.length > 0) {
+        this.oldTreeViewData = this.treeViewData
+        this.rootKey = this.treeViewData[0].key
+      }
+    },
+    treeSelected () {
+      if (Object.keys(this.treeSelected).length === 0) {
+        return
+      }
+
+      if (Object.keys(this.resource).length > 0) {
+        this.selectedTreeKey = this.resource.key
+        this.$emit('change-resource', this.resource)
+
+        // set default expand
+        if (this.defaultSelected.length > 1) {
+          const arrSelected = this.defaultSelected
+          this.defaultSelected = []
+          this.defaultSelected.push(arrSelected[0])
+        }
+
+        return
+      }
+
+      this.resource = this.treeSelected
+      this.resource = this.createResourceData(this.resource)
+      this.selectedTreeKey = this.treeSelected.key
+      this.defaultSelected.push(this.selectedTreeKey)
+
+      // set default expand
+      if (this.defaultSelected.length > 1) {
+        const arrSelected = this.defaultSelected
+        this.defaultSelected = []
+        this.defaultSelected.push(arrSelected[0])
+      }
+    },
+    actionData (newData, oldData) {
+      if (!newData || newData.length === 0) {
+        return
+      }
+
+      this.reloadTreeData(newData)
+    }
+  },
+  methods: {
+    onLoadData (treeNode) {
+      if (this.searchQuery !== '' && treeNode.eventKey !== this.rootKey) {
+        return new Promise(resolve => {
+          resolve()
+        })
+      }
+
+      const params = {
+        listAll: true,
+        id: treeNode.eventKey
+      }
+
+      return new Promise(resolve => {
+        api(this.apiChildren, params).then(json => {
+          const dataResponse = this.getResponseJsonData(json)
+          const dataGenerate = this.generateTreeData(dataResponse)
+          treeNode.dataRef.children = dataGenerate
+
+          if (this.treeVerticalData.length === 0) {
+            this.treeVerticalData = this.treeViewData
+          }
+
+          this.treeViewData = [...this.treeViewData]
+          this.oldTreeViewData = this.treeViewData
+
+          for (let i = 0; i < dataGenerate.length; i++) {
+            const resource = this.treeVerticalData.filter(item => item.id === dataGenerate[i].id)
+
+            if (!resource || resource.length === 0) {
+              this.treeVerticalData.push(dataGenerate[i])
+            } else {
+              this.treeVerticalData.filter((item, index) => {
+                if (item.id === dataGenerate[i].id) {
+                  // replace all value of tree data
+                  Object.keys(dataGenerate[i]).forEach((value, idx) => {
+                    this.$set(this.treeVerticalData[index], value, dataGenerate[i][value])
+                  })
+                }
+              })
+            }
+          }
+
+          resolve()
+        })
+      })
+    },
+    onSelect (selectedKeys, event) {
+      if (!event.selected) {
+        setTimeout(() => { event.node.$refs.selectHandle.click() })
+        return
+      }
+
+      // check item tree selected, set selectedTreeKey
+      if (selectedKeys && selectedKeys[0]) {
+        this.selectedTreeKey = selectedKeys[0]
+      }
+
+      this.getDetailResource(this.selectedTreeKey)
+    },
+    onExpand (treeExpand) {
+      this.arrExpand = treeExpand
+    },
+    onSearch (value) {
+      if (this.searchQuery === '' && this.oldSearchQuery === '') {
+        return
+      }
+
+      this.searchQuery = value
+      this.newTreeData = this.treeViewData
+      this.treeVerticalData = this.newTreeData
+
+      // set parameter for the request
+      const params = {}
+      params.listall = true
+
+      // Check the search query to set params and variables using reset data
+      if (this.searchQuery !== '') {
+        this.oldSearchQuery = this.searchQuery
+        params.keyword = this.searchQuery
+      } else if (this.metaName === 'domain') {
+        this.oldSearchQuery = ''
+        params.id = this.$store.getters.userInfo.domainid
+      }
+
+      this.arrExpand = []
+      this.treeViewData = []
+      this.loadingSearch = true
+
+      api(this.apiList, params).then(json => {
+        const listDomains = this.getResponseJsonData(json)
+        this.treeVerticalData = this.treeVerticalData.concat(listDomains)
+
+        if (!listDomains || listDomains.length === 0) {
+          return
+        }
+
+        if (listDomains[0].id === this.rootKey) {
+          const rootDomain = this.generateTreeData(listDomains)
+          this.treeViewData = rootDomain
+          return
+        }
+
+        this.recursiveTreeData(listDomains)
+
+        if (this.treeViewData && this.treeViewData[0]) {
+          this.defaultSelected = []
+          this.defaultSelected.push(this.treeViewData[0].key)
+          this.resource = this.treeViewData[0]
+          this.$emit('change-resource', this.resource)
+        }
+
+        // check treeViewData, set to expand first children
+        if (this.treeViewData &&
+            this.treeViewData[0] &&
+            this.treeViewData[0].children &&
+            this.treeViewData[0].children.length > 0
+        ) {
+          this.arrExpand.push(this.treeViewData[0].children[0].key)
+        }
+      }).finally(() => {
+        this.loadingSearch = false
+      })
+    },
+    onTabChange (key) {
+      this.tabActive = key
+    },
+    reloadTreeData (objData) {
+      // data response from action
+      let jsonResponse = this.getResponseJsonData(objData[0])
+      jsonResponse = this.createResourceData(jsonResponse)
+
+      // resource for check create or edit
+      const resource = this.treeVerticalData.filter(item => item.id === jsonResponse.id)
+
+      // when edit
+      if (resource && resource[0]) {
+        this.treeVerticalData.filter((item, index) => {
+          if (item.id === jsonResponse.id) {
+            // replace all value of tree data
+            Object.keys(jsonResponse).forEach((value, idx) => {
+              this.$set(this.treeVerticalData[index], value, jsonResponse[value])
+            })
+          }
+        })
+      } else {
+        // when create
+        let resourceExists = true
+
+        // check is searching data
+        if (this.searchQuery !== '') {
+          resourceExists = jsonResponse.title.indexOf(this.searchQuery) > -1
+        }
+
+        // push new resource to tree data
+        if (this.resource.haschild && resourceExists) {
+          this.treeVerticalData.push(jsonResponse)
+        }
+
+        // set resource is currently active as a parent
+        this.treeVerticalData.filter((item, index) => {
+          if (item.id === this.resource.id) {
+            this.$set(this.treeVerticalData[index], 'isLeaf', false)
+            this.$set(this.treeVerticalData[index], 'haschild', true)
+          }
+        })
+      }
+
+      this.recursiveTreeData(this.treeVerticalData)
+    },
+    getDetailResource (selectedKey) {
+      // set api name and parameter
+      const apiName = this.$route.meta.permission[0]
+      const params = {}
+
+      // set id to parameter
+      params.id = selectedKey
+      params.listAll = true
+      params.page = 1
+      params.pageSize = 1
+
+      api(apiName, params).then(json => {
+        const jsonResponse = this.getResponseJsonData(json)
+
+        // check json response is empty
+        if (!jsonResponse || jsonResponse.length === 0) {
+          this.resource = []
+        } else {
+          this.resource = jsonResponse[0]
+          this.resource = this.createResourceData(this.resource)
+          // set all value of resource tree data
+          this.treeVerticalData.filter((item, index) => {
+            if (item.id === this.resource.id) {
+              this.treeVerticalData[index] = this.resource
+            }
+          })
+        }
+
+        // emit change resource to parent
+        this.$emit('change-resource', this.resource)
+      })
+    },
+    getResponseJsonData (json) {
+      let responseName
+      let objectName
+      for (const key in json) {
+        if (key.includes('response')) {
+          responseName = key
+          break
+        }
+      }
+
+      for (const key in json[responseName]) {
+        if (key === 'count') {
+          continue
+        }
+
+        objectName = key
+        break
+      }
+      return json[responseName][objectName]
+    },
+    checkShowTabDetail (tabKey) {
+      // get tab item from the route
+      const itemTab = this.tabs.filter(item => item.name === tabKey)
+
+      // check tab item not exists
+      if (!itemTab || !itemTab[0]) {
+        return false
+      }
+
+      // get permission from the route
+      const permission = itemTab[0].permission ? itemTab[0].permission[0] : ''
+
+      // check permission not exists
+      if (!permission || permission === '') {
+        return true
+      }
+
+      // Check the permissions to see the tab for a user
+      if (!Object.prototype.hasOwnProperty.call(store.getters.apis, permission)) {
+        return false
+      }
+
+      return true
+    },
+    generateTreeData (jsonData) {
+      if (!jsonData || jsonData.length === 0) {
+        return []
+      }
+
+      for (let i = 0; i < jsonData.length; i++) {
+        jsonData[i] = this.createResourceData(jsonData[i])
+      }
+
+      return jsonData
+    },
+    createResourceData (resource) {
+      if (!resource || Object.keys(resource) === 0) {
+        return {}
+      }
+
+      Object.keys(resource).forEach((value, idx) => {
+        if (resource[value] === 'Unlimited') {
+          this.$set(resource, value, '-1')
+        }
+      })
+      this.$set(resource, 'title', resource.name)
+      this.$set(resource, 'key', resource.id)
+      resource.slots = {
+        icon: 'parent'
+      }
+
+      if (!resource.haschild) {
+        this.$set(resource, 'isLeaf', true)
+        resource.slots = {
+          icon: 'leaf'
+        }
+      }
+
+      return resource
+    },
+    recursiveTreeData (treeData) {
+      const maxLevel = Math.max.apply(Math, treeData.map((o) => { return o.level }))
+      const items = treeData.filter(item => item.level <= maxLevel)
+      this.treeViewData = this.getNestedChildren(items, 0, maxLevel)
+      this.oldTreeViewData = this.treeViewData
+    },
+    getNestedChildren (dataItems, level, maxLevel, id) {
+      if (level > maxLevel) {
+        return
+      }
+
+      let items = []
+
+      if (!id || id === '') {
+        items = dataItems.filter(item => item.level === level)
+      } else {
+        items = dataItems.filter(item => {
+          let parentKey = ''
+          const arrKeys = Object.keys(item)
+          for (let i = 0; i < arrKeys.length; i++) {
+            if (arrKeys[i].indexOf('parent') > -1 && arrKeys[i].indexOf('id') > -1) {
+              parentKey = arrKeys[i]
+              break
+            }
+          }
+
+          return parentKey ? item[parentKey] === id : item.level === level
+        })
+      }
+
+      if (items.length === 0) {
+        return this.getNestedChildren(dataItems, (level + 1), maxLevel)
+      }
+
+      for (let i = 0; i < items.length; i++) {
+        items[i] = this.createResourceData(items[i])
+
+        if (items[i].haschild) {
+          items[i].children = this.getNestedChildren(dataItems, (level + 1), maxLevel, items[i].key)
+        }
+      }
+
+      return items
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.list-tree-view {
+  overflow-y: hidden;
+}
+/deep/.ant-tree.ant-tree-directory {
+  li.ant-tree-treenode-selected {
+    span.ant-tree-switcher {
+      color: rgba(0, 0, 0, 0.65);
+    }
+    span.ant-tree-node-content-wrapper.ant-tree-node-selected > span {
+      color: rgba(0, 0, 0, 0.65);
+      background-color: #bae7ff;
+    }
+    span.ant-tree-node-content-wrapper::before {
+      background: #ffffff;
+    }
+  }
+
+  .ant-tree-child-tree {
+    li.ant-tree-treenode-selected {
+      span.ant-tree-switcher {
+        color: rgba(0, 0, 0, 0.65);
+      }
+      span.ant-tree-node-content-wrapper::before {
+        background: #ffffff;
+      }
+    }
+  }
+}
+
+/deep/.ant-tree li span.ant-tree-switcher.ant-tree-switcher-noop {
+  display: none;
+}
+
+/deep/.ant-tree-node-content-wrapper-open > span:first-child,
+/deep/.ant-tree-node-content-wrapper-close > span:first-child {
+  display: none;
+}
+
+/deep/.ant-tree-icon__customize {
+  color: rgba(0, 0, 0, 0.45);
+  background: #fff;
+  padding-right: 5px;
+}
+
+/deep/.ant-tree li .ant-tree-node-content-wrapper {
+  padding-left: 0;
+  margin-left: 3px;
+}
+</style>
diff --git a/src/config/router.js b/src/config/router.js
index 51a61e9..62c353b 100644
--- a/src/config/router.js
+++ b/src/config/router.js
@@ -62,7 +62,9 @@ export function generateRouterMap (section) {
           columns: child.columns,
           details: child.details,
           related: child.related,
-          actions: child.actions
+          actions: child.actions,
+          treeView: child.treeView ? child.treeView : false,
+          tabs: child.treeView ? child.tabs : {}
         },
         component: component,
         hideChildrenInMenu: true,
diff --git a/src/config/section/iam.js b/src/config/section/iam.js
index 5b0b2ef..351a871 100644
--- a/src/config/section/iam.js
+++ b/src/config/section/iam.js
@@ -167,7 +167,7 @@ export default {
       name: 'domain',
       title: 'Domains',
       icon: 'block',
-      permission: ['listDomains'],
+      permission: ['listDomains', 'listDomainChildren'],
       resourceType: 'Domain',
       columns: ['name', 'state', 'path', 'parentdomainname', 'level'],
       details: ['name', 'id', 'path', 'parentdomainname', 'level', 'networkdomain', 'iptotal', 'vmtotal', 'volumetotal', 'vmlimit', 'iplimit', 'volumelimit', 'snapshotlimit', 'templatelimit', 'vpclimit', 'cpulimit', 'memorylimit', 'networklimit', 'primarystoragelimit', 'secondarystoragelimit'],
@@ -176,18 +176,37 @@ export default {
         title: 'Accounts',
         param: 'domainid'
       }],
+      tabs: [
+        {
+          name: 'Domain',
+          component: () => import('@/components/view/InfoCard.vue'),
+          show: (record, route) => { return route.path === '/domain' }
+        },
+        {
+          name: 'details',
+          component: () => import('@/components/view/DetailsTab.vue')
+        }
+      ],
+      treeView: true,
       actions: [
         {
           api: 'createDomain',
           icon: 'plus',
           label: 'label.add.domain',
           listView: true,
-          args: ['parentdomainid', 'name', 'networkdomain', 'domainid']
+          dataView: true,
+          args: ['parentdomainid', 'name', 'networkdomain', 'domainid'],
+          mapping: {
+            parentdomainid: {
+              value: (record) => { return record.id }
+            }
+          }
         },
         {
           api: 'updateDomain',
           icon: 'edit',
           label: 'label.action.edit.domain',
+          listView: true,
           dataView: true,
           args: ['name', 'networkdomain']
         },
@@ -195,6 +214,7 @@ export default {
           api: 'updateResourceCount',
           icon: 'sync',
           label: 'label.action.update.resource.count',
+          listView: true,
           dataView: true,
           args: ['domainid'],
           mapping: {
@@ -207,6 +227,7 @@ export default {
           api: 'deleteDomain',
           icon: 'delete',
           label: 'label.delete.domain',
+          listView: true,
           dataView: true,
           show: (record) => { return record.level !== 0 },
           args: ['cleanup']
diff --git a/src/views/AutogenView.vue b/src/views/AutogenView.vue
index 4e07402..9c999d5 100644
--- a/src/views/AutogenView.vue
+++ b/src/views/AutogenView.vue
@@ -45,8 +45,8 @@
               </template>
               <a-button
                 v-if="action.api in $store.getters.apis &&
-                  ((!dataView && (action.listView || action.groupAction && selectedRowKeys.length > 0)) ||
-                  (dataView && action.dataView && ('show' in action ? action.show(resource) : true)))"
+                  ((!dataView && (action.listView || action.groupAction && selectedRowKeys.length > 0)) || (dataView && action.dataView)) &&
+                  ('show' in action ? action.show(resource) : true)"
                 :icon="action.icon"
                 :type="action.icon === 'delete' ? 'danger' : (action.icon === 'plus' ? 'primary' : 'default')"
                 shape="circle"
@@ -59,7 +59,7 @@
               style="width: unset"
               placeholder="Search"
               v-model="searchQuery"
-              v-if="!dataView"
+              v-if="!dataView && !treeView"
               @search="onSearch" />
           </span>
         </a-col>
@@ -191,14 +191,18 @@
       </a-modal>
     </div>
 
-    <div v-if="dataView">
-      <resource-view :resource="resource" :loading="loading" :tabs="$route.meta.tabs" />
+    <div v-if="dataView && !treeView">
+      <resource-view
+        :resource="resource"
+        :loading="loading"
+        :tabs="$route.meta.tabs" />
     </div>
     <div class="row-element" v-else>
       <list-view
         :loading="loading"
         :columns="columns"
-        :items="items" />
+        :items="items"
+        v-if="!treeView" />
       <a-pagination
         class="row-element"
         size="small"
@@ -209,7 +213,16 @@
         :pageSizeOptions="['10', '20', '40', '80', '100']"
         @change="changePage"
         @showSizeChange="changePageSize"
-        showSizeChanger />
+        showSizeChanger
+        v-if="!treeView" />
+      <tree-view
+        v-if="treeView"
+        :treeData="treeData"
+        :treeSelected="treeSelected"
+        :loading="loading"
+        :tabs="$route.meta.tabs"
+        @change-resource="changeResource"
+        :actionData="actionData"/>
     </div>
   </div>
 </template>
@@ -225,6 +238,7 @@ import ChartCard from '@/components/widgets/ChartCard'
 import Status from '@/components/widgets/Status'
 import ListView from '@/components/view/ListView'
 import ResourceView from '@/components/view/ResourceView'
+import TreeView from '@/components/view/TreeView'
 import { genericCompare } from '@/utils/sort.js'
 
 export default {
@@ -234,6 +248,7 @@ export default {
     ChartCard,
     ResourceView,
     ListView,
+    TreeView,
     Status
   },
   mixins: [mixinDevice],
@@ -253,7 +268,11 @@ export default {
       currentAction: {},
       showAction: false,
       dataView: false,
-      actions: []
+      treeView: false,
+      actions: [],
+      treeData: [],
+      treeSelected: {},
+      actionData: []
     }
   },
   computed: {
@@ -291,6 +310,8 @@ export default {
       this.columns = []
       this.columnKeys = []
       this.items = []
+      this.treeData = []
+      this.treeSelected = {}
       var params = { listall: true }
       if (Object.keys(this.$route.query).length > 0) {
         Object.assign(params, this.$route.query)
@@ -302,9 +323,12 @@ export default {
         params.keyword = this.searchQuery
       }
 
+      this.treeView = this.$route && this.$route.meta && this.$route.meta.treeView
+
       if (this.$route && this.$route.params && this.$route.params.id) {
         this.resource = {}
         this.dataView = true
+        this.treeView = false
       } else {
         this.dataView = false
       }
@@ -358,8 +382,16 @@ export default {
           params.name = this.$route.params.id
         }
       }
-      params.page = this.page
-      params.pagesize = this.pageSize
+
+      if (!this.treeView) {
+        params.page = this.page
+        params.pagesize = this.pageSize
+      } else {
+        const domainId = this.$store.getters.userInfo.domainid
+        params.id = domainId
+        delete params.treeView
+      }
+
       api(this.apiName, params).then(json => {
         var responseName
         var objectName
@@ -381,33 +413,47 @@ export default {
         if (!this.items || this.items.length === 0) {
           this.items = []
         }
-        for (let idx = 0; idx < this.items.length; idx++) {
-          this.items[idx].key = idx
-          for (const key in customRender) {
-            const func = customRender[key]
-            if (func && typeof func === 'function') {
-              this.items[idx][key] = func(this.items[idx])
+        if (this.treeView) {
+          this.treeData = this.generateTreeData(this.items)
+        } else {
+          for (let idx = 0; idx < this.items.length; idx++) {
+            this.items[idx].key = idx
+            for (const key in customRender) {
+              const func = customRender[key]
+              if (func && typeof func === 'function') {
+                this.items[idx][key] = func(this.items[idx])
+              }
+            }
+            if (this.$route.path.startsWith('/ssh')) {
+              this.items[idx].id = this.items[idx].name
             }
-          }
-          if (this.$route.path.startsWith('/ssh')) {
-            this.items[idx].id = this.items[idx].name
           }
         }
         if (this.items.length > 0) {
           this.resource = this.items[0]
+          this.treeSelected = this.treeView ? this.items[0] : {}
         } else {
           this.resource = {}
+          this.treeSelected = {}
         }
       }).catch(error => {
-        // handle error
         this.$notification.error({
           message: 'Request Failed',
           description: error.response.headers['x-description'],
           duration: 0
         })
-        if (error.response.status === 431) {
+
+        if ([401, 405].includes(error.response.status)) {
+          this.$router.push({ path: '/exception/403' })
+        }
+
+        if ([430, 431, 432].includes(error.response.status)) {
           this.$router.push({ path: '/exception/404' })
         }
+
+        if ([530, 531, 532, 533, 534, 535, 536, 537].includes(error.response.status)) {
+          this.$router.push({ path: '/exception/500' })
+        }
       }).finally(f => {
         this.loading = false
       })
@@ -422,6 +468,7 @@ export default {
       this.currentAction = {}
     },
     execAction (action) {
+      this.actionData = []
       if (action.component && action.api && !action.popup) {
         this.$router.push({ name: action.api })
         return
@@ -581,6 +628,11 @@ export default {
 
           var hasJobId = false
           api(this.currentAction.api, params).then(json => {
+            // set action data for reload tree-view
+            if (this.treeView) {
+              this.actionData.push(json)
+            }
+
             for (const obj in json) {
               if (obj.includes('response')) {
                 for (const res in json[obj]) {
@@ -630,6 +682,24 @@ export default {
         this.loading = false
         this.selectedRowKeys = []
       }, 1000)
+    },
+    generateTreeData (treeData) {
+      const result = []
+      const rootItem = treeData
+
+      rootItem[0].title = rootItem[0].title ? rootItem[0].title : rootItem[0].name
+      rootItem[0].key = rootItem[0].id ? rootItem[0].id : 0
+
+      if (!rootItem[0].haschild) {
+        rootItem[0].isLeaf = true
+      }
+
+      result.push(rootItem[0])
+      return result
+    },
+    changeResource (resource) {
+      this.treeSelected = resource
+      this.resource = this.treeSelected
     }
   }
 }