domain: implement tree-view based domain list view (#53)

Fixes #27

Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
Hoang Nguyen 2019-12-07 03:25:25 +07:00 committed by Rohit Yadav
parent 3a82cc535f
commit c9a75e2f30
7 changed files with 718 additions and 36 deletions

View File

@ -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>

View File

@ -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 () {

View File

@ -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>

View File

@ -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>

View File

@ -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,

View File

@ -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']

View File

@ -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
}
}
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,6 +413,9 @@ export default {
if (!this.items || this.items.length === 0) {
this.items = []
}
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) {
@ -393,21 +428,32 @@ export default {
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
}
}
}