Compare commits

...

6 Commits

7 changed files with 246 additions and 118 deletions

View File

@@ -142,5 +142,6 @@
"src/**/*.{js,vue}": [
"eslint --fix"
]
}
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

View File

@@ -11,6 +11,7 @@
import AssetTreeTable from '@/components/Apps/AssetTreeTable'
import { AccountInfoFormatter, DetailFormatter } from '@/components/Table/TableFormatters'
import { connectivityMeta } from '@/components/Apps/AccountListTable/const'
import { setUrlParam } from '@/utils/common/index'
export default {
name: 'GrantedAssets',
@@ -34,7 +35,7 @@ export default {
}
const initialUrl = vm.tableConfig.initialUrl
const nodeId = node.meta.data.id
const url = initialUrl.replace('/assets/', `/nodes/${nodeId}/assets/`)
const url = setUrlParam(initialUrl, 'node_id', nodeId)
vm.tableConfig.url = url
}
},
@@ -70,7 +71,7 @@ export default {
showMenu: false,
showRefresh: true,
showAssets: false,
showSearch: false,
showSearch: true,
url: this.tableUrl,
// ?assets=0不显示资产. =1显示资产
treeUrl: this.treeUrl,
@@ -78,6 +79,9 @@ export default {
callback: {
onSelected: (event, node) => vm.onSelected(node, vm),
refresh: vm.refreshObjectAssetPermission
},
async: {
enable: false
}
},
tableConfig: {

View File

@@ -1,16 +1,67 @@
<template>
<div>
<div class="treebox">
<div v-if="treeSetting.showSearch">
<div v-if="treeSetting.showSearch" @click="focusTreeSearchInput">
<el-input
v-show="showTreeSearch"
ref="treeSearchInput"
v-model="treeSearchValue"
:placeholder="$tc('Search')"
class="fixed-tree-search"
prefix-icon="fa fa-search"
:placeholder="treeSearchInputPlaceholder"
size="mini"
@input="treeSearchHandle"
>
<template #prepend>
<template v-if="!isSearchTypeDropdownEnabled">
<el-tooltip
effect="dark"
placement="top"
:content="currentTreeSearchTypeTooltip"
:open-delay="300"
>
<span style="cursor: pointer;" @click.stop="focusTreeSearchInput">
<i class="fa fa-search" />
<span class="search-label">{{ treeSearchTypeLabel }}</span>
</span>
</el-tooltip>
</template>
<template v-else>
<el-dropdown trigger="hover" @command="onSearchTypeChange">
<el-tooltip
effect="dark"
placement="top"
:content="currentTreeSearchTypeTooltip"
:open-delay="1000"
>
<span @click.stop="focusTreeSearchInput">
<i class="fa fa-search" />
<span class="search-label">{{ treeSearchTypeLabel }}</span>
<i class="el-icon-arrow-down" />
</span>
</el-tooltip>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-for="(item, type) in treeSearchTypeOptions"
:key="type"
:command="type"
:class="{ 'is-active': treeSearchType === type }"
>
<el-tooltip
effect="dark"
placement="right"
:content="item.tooltip"
:open-delay="300"
>
<span>{{ item.label }}</span>
</el-tooltip>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</template>
<span slot="suffix">
<i
class="el-icon-close"
@@ -47,6 +98,7 @@ import '@ztree/ztree_v3/js/jquery.ztree.exhide.min.js'
import '@/styles/ztree.css'
import '@/styles/ztree_icon.scss'
import axiosRetry from 'axios-retry'
import { setUrlParam } from '@/utils/common'
const defaultObject = {
type: Object,
@@ -66,18 +118,52 @@ export default {
rMenu: '',
init: false,
loading: false,
showTreeSearch: false,
treeSearchValue: ''
showTreeSearch: true,
treeSearchValue: '',
treeSearchType: 'asset',
treeSearchTypeOptions: {},
treeSearchTypeSupportOptions: {
node: {
label: this.$t('Node'),
placeholder: this.$t('Search node'),
tooltip: this.$t('Search by node name'),
search_key: 'search_node'
},
asset: {
label: this.$t('Asset'),
placeholder: this.$t('Search asset'),
tooltip: this.$t('Search by asset name or address'),
search_key: 'search_asset'
}
},
treeType: '' // asset | node
}
},
computed: {
treeSetting() {
return this.setting
},
isSearchTypeDropdownEnabled() {
return Object.keys(this.treeSearchTypeOptions).length > 1
},
currentTreeSearchType() {
return this.treeSearchTypeOptions[this.treeSearchType]
},
currentTreeSearchTypeTooltip() {
return this.currentTreeSearchType?.tooltip || ''
},
treeSearchTypeLabel() {
return this.currentTreeSearchType?.label || ''
},
treeSearchInputPlaceholder() {
return this.currentTreeSearchType?.placeholder || this.$t('Search')
}
},
mounted() {
window.refresh = this.refresh
window.onSearch = this.onSearch
this.initTreeType()
this.initTreeSearchTypeOptions()
this.initTree().then(() => {
this.$nextTick(() => {
this.updateTreeHeight()
@@ -90,6 +176,36 @@ export default {
window.removeEventListener('resize', this.updateTreeHeight)
},
methods: {
initTreeType() {
let treeType = this.treeSetting.treeType
if (!treeType) {
treeType = this.treeSetting.async?.enable ? 'asset' : 'node'
}
this.treeType = treeType
},
initTreeSearchTypeOptions() {
if (this.treeType === 'asset') {
// 资产树支持异步搜索节点和资产
this.treeSearchTypeOptions = this.treeSearchTypeSupportOptions
// 默认搜索资产
this.treeSearchType = 'asset'
} else {
// 节点树只支持搜索节点
this.treeSearchTypeOptions = Object.fromEntries(
Object.entries(this.treeSearchTypeSupportOptions)
.filter(([key]) => key === 'node')
)
// 默认搜索节点
this.treeSearchType = 'node'
}
},
onSearchTypeChange(type) {
this.treeSearchType = type
this.focusTreeSearchInput()
},
focusTreeSearchInput() {
this.$refs.treeSearchInput.focus()
},
onMenuClick(menu) {
if (menu.disabled) {
return
@@ -116,19 +232,14 @@ export default {
const zTreeRect = tree.getBoundingClientRect()
tree.style.height = `calc(100vh - ${zTreeRect.top}px - 30px - 25px)`
}, 100),
async initTree(refresh = false) {
async initTree(refresh = false, iTreeUrl = '') {
const vm = this
let treeUrl
this.loading = true
if (refresh && this.treeSetting.treeUrl.indexOf('/perms/') !== -1 &&
this.treeSetting.treeUrl.indexOf('rebuild_tree') === -1
) {
treeUrl = (this.treeSetting.treeUrl.indexOf('?') === -1)
? `${this.treeSetting.treeUrl}?rebuild_tree=1`
: `${this.treeSetting.treeUrl}&rebuild_tree=1`
} else {
let treeUrl = iTreeUrl
if (!treeUrl) {
treeUrl = this.treeSetting.treeUrl
}
treeUrl = setUrlParam(treeUrl, 'tree_type', this.treeType)
if (refresh) {
$.fn.zTree.destroy(this.iZTreeID)
@@ -221,25 +332,31 @@ export default {
searchInput.oninput = e => this.treeSearchHandle((e.target.value || ''))
},
treeSearchHandle: _.debounce(function(value) {
if (this.treeSetting.async.enable) {
this.filterAssetsServer(value)
if (this.treeSetting.async?.enable) {
this.searchFromServer(value)
} else {
this.filterTree(value)
this.searchFromLocal(value)
}
}, 600),
getCheckedNodes: function() {
return this.zTree.getCheckedNodes(true)
},
recurseParent(node) {
const parentNode = node.getParentNode()
if (parentNode && parentNode.pId) {
return [parentNode, ...this.recurseParent(parentNode)]
} else if (parentNode) {
return [parentNode]
} else {
if (!parentNode) {
return []
}
const allParents = []
if (parentNode) {
allParents.push(parentNode)
if (parentNode.pId) {
allParents.push(...this.recurseParent(parentNode))
}
}
return allParents
},
recurseChildren(node) {
if (!node.isParent) {
return []
@@ -248,49 +365,27 @@ export default {
if (!children) {
return []
}
let allChildren = []
const allChildren = []
children.forEach((n) => {
allChildren = [...children, ...this.recurseChildren(n)]
allChildren.push(n)
allChildren.push(...this.recurseChildren(n))
})
return allChildren
},
groupBy(array, filter) {
const groups = {}
array.forEach(function(o) {
const group = JSON.stringify(filter(o))
groups[group] = groups[group] || []
groups[group].push(o)
})
return Object.keys(groups).map(function(group) {
return groups[group]
})
},
filterTree(keyword, tree = this.zTree) {
searchFromLocal(keyword, tree = this.zTree) {
if (!this.zTree) return
const searchNode = tree.getNodesByFilter((node) => node.id === 'search')
if (searchNode) tree.removeNode(searchNode[0])
const nodes = tree.transformToArray(tree.getNodes())
const allNodes = tree.transformToArray(tree.getNodes())
if (!keyword) {
tree.showNodes(nodes)
tree.showNodes(allNodes)
tree.expandAll(false)
return
}
if (!keyword) {
if (tree.hiddenNodes) {
tree.showNodes(tree.hiddenNodes)
tree.hiddenNodes = null
}
if (tree.expandNodes) {
tree.expandNodes.forEach((node) => {
if (node.id !== nodes[0].id) {
tree.expandNode(node, false)
}
})
tree.expandNodes = null
}
return null
}
let shouldShow = []
const matchedNodes = tree.getNodesByFilter((node) => {
return node.name.toLowerCase().indexOf(keyword.toLowerCase()) > -1
})
@@ -300,67 +395,54 @@ export default {
const assetsAmount = matchedNodes.length
name = `${name} (${assetsAmount})`
const newNode = { id: 'search', name: name, isParent: false, open: false }
tree.addNodes(null, newNode)
const addedNodes = tree.addNodes(null, newNode)
// 隐藏所有节点,只显示搜索节点
tree.hideNodes(allNodes)
tree.showNodes(addedNodes)
return
}
// 获取应该展示的节点,以及应该展开的节点
let shouldShow = []
let shouldExpandNodes = []
let shouldCollapseNodes = []
matchedNodes.forEach((node) => {
const parents = this.recurseParent(node)
const children = this.recurseChildren(node)
shouldShow = [...shouldShow, ...parents, ...children, node]
})
// 应该显示匹配节点本身、其祖先节点和子孙节点
shouldShow.push(node)
shouldShow.push(...parents)
shouldShow.push(...children)
tree.hiddenNodes = nodes
tree.expandNodes = shouldShow
tree.hideNodes(nodes)
// 应该展开匹配节点的父节点,不展开匹配节点的子孙节点
shouldExpandNodes.push(...parents)
// 应该折叠匹配节点的子孙节点
shouldCollapseNodes.push(node)
shouldCollapseNodes.push(...children)
})
shouldShow = Array.from(new Set(shouldShow))
shouldExpandNodes = Array.from(new Set(shouldExpandNodes))
shouldCollapseNodes = Array.from(new Set(shouldCollapseNodes))
// 隐藏所有节点,显示应该显示的节点
tree.hideNodes(allNodes)
tree.showNodes(shouldShow)
for (const node of shouldShow) {
if (node.isParent) {
tree.expandNode(node, true)
}
// 展开应该展开的节点
for (const node of shouldExpandNodes) {
tree.expandNode(node, true)
}
// 折叠应该折叠的节点
for (const node of shouldCollapseNodes) {
tree.expandNode(node, false)
}
},
filterAssetsServer(keyword) {
if (!this.zTree) return
let searchNode = this.zTree.getNodesByFilter((node) => node.id === 'search')
if (searchNode) {
this.zTree.removeChildNodes(searchNode[0])
this.zTree.removeNode(searchNode[0])
}
const treeNodes = this.zTree.getNodes()
if (!keyword) {
if (treeNodes.length !== 0) {
this.zTree.showNodes(treeNodes)
}
return
}
if (treeNodes.length !== 0) {
this.zTree.hideNodes(treeNodes)
}
let treeUrl = this.treeSetting.searchUrl ? this.treeSetting.searchUrl : this.treeSetting.treeUrl
const filterField = treeUrl.includes('?') ? `&search=${keyword}` : `?search=${keyword}`
if (treeUrl.indexOf('assets/nodes/children/tree') > -1) {
treeUrl = treeUrl + '&all=all'
}
const searchUrl = `${treeUrl}${filterField}`
this.$axios.get(searchUrl).then(nodes => {
let name = this.$t('Search')
const assetsAmount = nodes.length
name = `${name} (${assetsAmount})`
const newNode = { id: 'search', name: name, isParent: true, open: true, zAsync: true }
searchNode = this.zTree.addNodes(null, newNode)[0]
searchNode.zAsync = true
this.rootNodeAddDom(searchNode)
const nodesGroupByOrg = this.groupBy(nodes, (node) => {
return node.meta?.data?.org_name
})
for (const item of nodesGroupByOrg) {
this.zTree.addNodes(searchNode, item)
}
searchNode.open = true
})
searchFromServer(keyword) {
// 直接用搜索 API 返回的数据重新初始化树
const treeUrl = this.treeSetting.searchUrl ? this.treeSetting.searchUrl : this.treeSetting.treeUrl
const searchTypeKey = this.treeSearchTypeOptions[this.treeSearchType]?.search_key || 'search'
const searchUrl = setUrlParam(treeUrl, searchTypeKey, keyword)
this.initTree(true, searchUrl)
}
}
@@ -624,12 +706,19 @@ div.rMenu li {
.fixed-tree-search {
margin-bottom: 10px;
border: 1px solid;
border-radius: 3px;
&:hover,
&:focus-within {
border-color: var(--color-primary);
}
& ::v-deep .el-input__inner {
border-radius: 4px;
border: none;
background: #fafafa;
padding-right: 32px;
color: var(--color-text-primary)
color: var(--color-text-primary);
}
& ::v-deep .el-input__suffix {
@@ -653,6 +742,37 @@ div.rMenu li {
& ::v-deep .el-input__suffix-inner {
line-height: 30px;
}
& ::v-deep .el-input-group__prepend {
padding-left: 5px;
padding-right: 3px;
border: none;
color: #999;
* {
color: inherit;
}
align-items: center;
background: #fafafa;
.el-icon-arrow-down {
display: inline-block;
transition: transform 0.8s ease; /* 动画关键 */
}
:hover {
.el-icon-arrow-down {
transform: rotate(180deg); /* 顺时针 180° */
}
}
.search-label {
margin-left: 1px;
margin-right: 1px;
}
}
}
::v-deep .el-dropdown-menu__item.is-active {
color: var(--color-primary);
font-weight: 500;
}
.icon-refresh {

View File

@@ -61,7 +61,7 @@ export default {
updateSuccessNextRoute: this.updateSuccessNextRoute,
hasDetailInMsg: false,
fields: [
[this.$t('Basic'), ['name', 'address', 'platform', 'nodes']],
[this.$t('Basic'), ['name', 'address', 'platform', 'node']],
[this.$t('Protocol'), ['protocols']],
[this.$t('Account'), ['accounts']],
[this.$t('Other'), ['directory_services', 'zone', 'labels', 'is_active', 'comment']]
@@ -73,8 +73,8 @@ export default {
const values = _.cloneDeep(validValues)
const submitMethod = id ? 'put' : 'post'
if (values.nodes && values.nodes.length === 0) {
delete values['nodes']
if (!values.node) {
delete values['node']
}
if (submitMethod === 'put') {
@@ -143,15 +143,14 @@ export default {
},
async setInitial() {
const { defaultConfig } = this
const { node } = this.$route.query
const nodesInitial = node ? [node] : []
const { node_id } = this.$route.query
const platformId = this.platformID || 'Linux'
const url = `/api/v1/assets/platforms/${platformId}/`
this.platform = await this.$axios.get(url)
const initial = {
labels: [],
is_active: true,
nodes: nodesInitial,
node: node_id,
platform: parseInt(this.platform.id),
protocols: []
}

View File

@@ -46,6 +46,9 @@ export default {
url: '/api/v1/assets/assets/',
showMenu: !this.$store.getters.currentOrgIsRoot,
showDefaultMenu: true,
async: {
enable: false
},
menu: [
]
},

View File

@@ -148,9 +148,10 @@ export const assetFieldsMeta = (vm, category, type) => {
return vm.platform.ds_enabled === false
}
},
nodes: {
node: {
rules: [rules.RequiredChange],
el: {
multiple: false,
ajax: {
url: '/api/v1/assets/nodes/',
transformOption: item => {

View File

@@ -19,7 +19,7 @@ export default {
data() {
return {
treeUrl: `/api/v1/perms/users/${this.object.id}/nodes/children/tree/`,
tableUrl: `/api/v1/perms/users/${this.object.id}/assets/?all=1`,
tableUrl: `/api/v1/perms/users/${this.object.id}/assets/`,
actions: {
has: false
},