| // 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> | |
| <template #left> | |
| <a-spin :spinning="loading"> | |
| <a-card :bordered="false"> | |
| <a-input-search | |
| size="default" | |
| :placeholder="$t('label.search')" | |
| v-model:value="searchQuery" | |
| @search="onSearch" | |
| > | |
| <template #prefix><search-outlined /></template> | |
| </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" | |
| :selectedKeys="defaultSelected" | |
| :checkStrictly="true" | |
| @select="onSelect" | |
| @expand="onExpand" | |
| :expandedKeys="arrExpand"> | |
| <template #icon="{data}"> | |
| <block-outlined v-if="data.isLeaf" /> | |
| </template> | |
| </a-tree> | |
| </a-spin> | |
| </a-card> | |
| </a-spin> | |
| </template> | |
| <template #right> | |
| <a-spin :spinning="detailLoading"> | |
| <a-card | |
| class="spin-content" | |
| :bordered="true" | |
| style="width:100%"> | |
| <a-tabs | |
| style="width: 100%" | |
| :animated="false" | |
| :defaultActiveKey="tabs[0].name" | |
| @change="onTabChange" > | |
| <template v-for="tab in tabs" :tab="$t('label.' + tab.name)" :key="tab.name"> | |
| <a-tab-pane :tab="$t('label.' + tab.name)" :key="tab.name" v-if="checkShowTabDetail(tab)"> | |
| <keep-alive> | |
| <component | |
| v-if="tab.resourceType" | |
| :is="tab.component" | |
| :resource="resource" | |
| :resourceType="tab.resourceType" | |
| :loading="loading" | |
| :tab="tabActive" | |
| :bordered="false" /> | |
| <component | |
| v-else | |
| :is="tab.component" | |
| :resource="resource" | |
| :items="items" | |
| :tab="tabActive" | |
| :loading="loading" | |
| :bordered="false" /> | |
| </keep-alive> | |
| </a-tab-pane> | |
| </template> | |
| </a-tabs> | |
| </a-card> | |
| </a-spin> | |
| </template> | |
| </resource-layout> | |
| </template> | |
| <script> | |
| import store from '@/store' | |
| import { callAPI } from '@/api' | |
| import DetailsTab from '@/components/view/DetailsTab' | |
| import ResourceView from '@/components/view/ResourceView' | |
| import ResourceLayout from '@/layouts/ResourceLayout' | |
| import eventBus from '@/config/eventBus' | |
| 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 | |
| }, | |
| treeStore: { | |
| type: Object, | |
| default () { | |
| return {} | |
| } | |
| }, | |
| treeDeletedKey: { | |
| type: String, | |
| default: null | |
| } | |
| }, | |
| provide: function () { | |
| return { | |
| parentFetchData: null, | |
| parentToggleLoading: null | |
| } | |
| }, | |
| 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] : '' | |
| eventBus.on('refresh-domain-icon', () => { | |
| this.getDetailResource(this.selectedTreeKey) | |
| }) | |
| }, | |
| watch: { | |
| loading () { | |
| this.detailLoading = this.loading | |
| this.treeViewData = [] | |
| this.arrExpand = [] | |
| if (!this.loading) { | |
| this.treeViewData = this.treeData.slice() | |
| this.treeVerticalData = this.treeData.slice() | |
| if (this.treeViewData.length > 0) { | |
| this.oldTreeViewData = this.treeViewData.slice() | |
| this.rootKey = this.treeViewData[0].key | |
| } | |
| } | |
| }, | |
| treeSelected: { | |
| deep: true, | |
| handler () { | |
| if (Object.keys(this.treeSelected).length === 0) { | |
| return | |
| } | |
| const resourceKeys = Object.keys(this.resource) | |
| if (resourceKeys.length > 0) { | |
| this.selectedTreeKey = this.resource.key | |
| this.$emit('change-resource', this.resource) | |
| if (resourceKeys.filter(x => x.endsWith('limit')).length === 0) { | |
| setTimeout(() => { this.getDetailResource(this.resource.id) }) | |
| } | |
| // 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]) | |
| } | |
| } | |
| }, | |
| treeVerticalData: { | |
| deep: true, | |
| handler () { | |
| if (!this.treeStore.isExpand) { | |
| return | |
| } | |
| if (this.treeStore.expands && this.treeStore.expands.length > 0) { | |
| for (const expandKey of this.treeStore.expands) { | |
| if (this.arrExpand.includes(expandKey)) { | |
| continue | |
| } | |
| const keyVisible = this.treeVerticalData.findIndex(item => item.key === expandKey) | |
| if (keyVisible > -1) this.arrExpand.push(expandKey) | |
| } | |
| } | |
| } | |
| } | |
| }, | |
| methods: { | |
| onLoadData (treeNode) { | |
| if (this.searchQuery !== '' && treeNode.eventKey !== this.rootKey) { | |
| return new Promise(resolve => { | |
| resolve() | |
| }) | |
| } | |
| const params = { | |
| listAll: true, | |
| id: treeNode.eventKey, | |
| showicon: true | |
| } | |
| return new Promise(resolve => { | |
| callAPI(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.treeVerticalData[index][value] = dataGenerate[i][value] | |
| }) | |
| } | |
| }) | |
| } | |
| } | |
| this.handleSelectResource(treeNode.eventKey) | |
| resolve() | |
| }) | |
| }) | |
| }, | |
| async handleSelectResource (treeKey) { | |
| if (this.treeDeletedKey && this.treeDeletedKey === this.treeStore?.treeSelected?.key) { | |
| const treeSelectedKey = this.treeStore.treeSelected.parentdomainid | |
| if (treeSelectedKey === this.rootKey) { | |
| this.resource = this.treeVerticalData[0] | |
| this.selectedTreeKey = treeSelectedKey | |
| this.defaultSelected = [treeSelectedKey] | |
| this.$emit('change-resource', this.resource) | |
| await this.setTreeStore(false, false, this.resource) | |
| return | |
| } | |
| const resourceIndex = await this.treeVerticalData.findIndex(item => item.id === treeSelectedKey) | |
| if (resourceIndex > -1) { | |
| const resource = await this.getDetailResource(treeSelectedKey) | |
| this.resource = await this.createResourceData(resource) | |
| this.selectedTreeKey = treeSelectedKey | |
| this.defaultSelected = [treeSelectedKey] | |
| this.$emit('change-resource', this.resource) | |
| await this.setTreeStore(false, false, this.resource) | |
| return | |
| } | |
| } | |
| const treeSelectedKey = this.treeStore.treeSelected.key | |
| const resourceIndex = await this.treeVerticalData.findIndex(item => item.id === treeSelectedKey) | |
| if (resourceIndex > -1) { | |
| this.selectedTreeKey = treeSelectedKey | |
| this.defaultSelected = [treeSelectedKey] | |
| this.resource = this.treeStore.treeSelected | |
| this.$emit('change-resource', this.resource) | |
| } | |
| }, | |
| async onSelect (selectedKeys, event) { | |
| if (!event.selected) { | |
| return | |
| } | |
| // check item tree selected, set selectedTreeKey | |
| if (selectedKeys && selectedKeys[0]) { | |
| this.selectedTreeKey = selectedKeys[0] | |
| } | |
| this.defaultSelected = [] | |
| this.defaultSelected.push(this.selectedTreeKey) | |
| const resource = await this.getDetailResource(this.selectedTreeKey) | |
| this.resource = await this.createResourceData(resource) | |
| const index = this.treeVerticalData.findIndex(item => item.key === this.selectedTreeKey) | |
| this.treeVerticalData[index] = this.resource | |
| this.$emit('change-resource', this.resource) | |
| await this.setTreeStore(false, false, this.resource) | |
| }, | |
| onExpand (treeExpand) { | |
| const treeStore = this.treeStore | |
| this.arrExpand = treeExpand | |
| treeStore.isExpand = true | |
| treeStore.expands = this.arrExpand | |
| treeStore.treeSelected = this.resource | |
| this.$emit('change-tree-store', treeStore) | |
| }, | |
| 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 | |
| this.$emit('change-tree-store', {}) | |
| callAPI(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 | |
| this.defaultSelected = [] | |
| this.defaultSelected.push(this.treeViewData[0].key) | |
| this.resource = this.treeViewData[0] | |
| this.$emit('change-resource', this.resource) | |
| 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 | |
| }, | |
| setTreeStore (arrExpand, isExpand, resource) { | |
| const treeStore = this.treeStore | |
| if (arrExpand) treeStore.expands = arrExpand | |
| if (isExpand) treeStore.isExpand = true | |
| if (resource) treeStore.treeSelected = resource | |
| this.$emit('change-tree-store', treeStore) | |
| }, | |
| getDetailResource (selectedKey) { | |
| return new Promise(resolve => { | |
| // 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.showicon = true | |
| params.page = 1 | |
| params.pageSize = 1 | |
| this.detailLoading = true | |
| callAPI(apiName, params).then(json => { | |
| const jsonResponse = this.getResponseJsonData(json) | |
| resolve(jsonResponse[0]) | |
| }).catch(() => { | |
| resolve() | |
| }).finally(() => { | |
| this.detailLoading = false | |
| }) | |
| }) | |
| }, | |
| getResponseJsonData (json) { | |
| let responseName | |
| let objectName | |
| let hasJobId = false | |
| for (const key in json) { | |
| if (key.includes('response')) { | |
| for (const res in json[key]) { | |
| if (res === 'jobid') { | |
| hasJobId = true | |
| break | |
| } | |
| } | |
| responseName = key | |
| break | |
| } | |
| } | |
| for (const key in json[responseName]) { | |
| if (key === 'count') { | |
| continue | |
| } | |
| objectName = key | |
| break | |
| } | |
| if (hasJobId) { | |
| return {} | |
| } | |
| return json[responseName][objectName] | |
| }, | |
| checkShowTabDetail (tab) { | |
| if ('show' in tab) { | |
| return tab.show(this.resource, this.$route, store.getters.userInfo) | |
| } | |
| // get permission from the route | |
| const permission = tab.permission ? tab.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') { | |
| resource.value = '-1' | |
| } | |
| }) | |
| resource.title = resource.name | |
| resource.key = resource.id | |
| if (resource?.icon) { | |
| resource.resourceIcon = resource.icon | |
| delete resource.icon | |
| } | |
| if (!resource.haschild) { | |
| resource.isLeaf = true | |
| } | |
| 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> | |
| :deep(.ant-tree).list-tree-view { | |
| overflow-y: hidden; | |
| margin-top: 10px; | |
| } | |
| :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-show-line) .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) { | |
| padding-right: 5px; | |
| background: transparent; | |
| } | |
| :deep(.ant-tree) { | |
| .ant-tree-treenode { | |
| .ant-tree-node-content-wrapper { | |
| margin-left: 0; | |
| margin-bottom: 2px; | |
| &:before { | |
| border-left: 1px solid #d9d9d9; | |
| content: " "; | |
| height: 100%; | |
| height: calc(100% - 15px); | |
| left: 12px; | |
| margin: 22px 0 0; | |
| position: absolute; | |
| width: 1px; | |
| top: 0; | |
| } | |
| } | |
| &.ant-tree-treenode-switcher-close, &.ant-tree-treenode-switcher-open, &.ant-tree-treenode-leaf-last { | |
| .ant-tree-node-content-wrapper:before { | |
| display: none; | |
| } | |
| } | |
| } | |
| } | |
| :deep(.ant-tree-show-line) .ant-tree-indent-unit:before { | |
| bottom: -2px; | |
| top: -5px; | |
| } | |
| </style> |