Compare commits

...

6 Commits

7 changed files with 246 additions and 118 deletions

View File

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

View File

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

View File

@@ -1,16 +1,67 @@
<template> <template>
<div> <div>
<div class="treebox"> <div class="treebox">
<div v-if="treeSetting.showSearch"> <div v-if="treeSetting.showSearch" @click="focusTreeSearchInput">
<el-input <el-input
v-show="showTreeSearch" v-show="showTreeSearch"
ref="treeSearchInput"
v-model="treeSearchValue" v-model="treeSearchValue"
:placeholder="$tc('Search')"
class="fixed-tree-search" class="fixed-tree-search"
prefix-icon="fa fa-search" :placeholder="treeSearchInputPlaceholder"
size="mini" size="mini"
@input="treeSearchHandle" @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"> <span slot="suffix">
<i <i
class="el-icon-close" 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.css'
import '@/styles/ztree_icon.scss' import '@/styles/ztree_icon.scss'
import axiosRetry from 'axios-retry' import axiosRetry from 'axios-retry'
import { setUrlParam } from '@/utils/common'
const defaultObject = { const defaultObject = {
type: Object, type: Object,
@@ -66,18 +118,52 @@ export default {
rMenu: '', rMenu: '',
init: false, init: false,
loading: false, loading: false,
showTreeSearch: false, showTreeSearch: true,
treeSearchValue: '' 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: { computed: {
treeSetting() { treeSetting() {
return this.setting 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() { mounted() {
window.refresh = this.refresh window.refresh = this.refresh
window.onSearch = this.onSearch window.onSearch = this.onSearch
this.initTreeType()
this.initTreeSearchTypeOptions()
this.initTree().then(() => { this.initTree().then(() => {
this.$nextTick(() => { this.$nextTick(() => {
this.updateTreeHeight() this.updateTreeHeight()
@@ -90,6 +176,36 @@ export default {
window.removeEventListener('resize', this.updateTreeHeight) window.removeEventListener('resize', this.updateTreeHeight)
}, },
methods: { 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) { onMenuClick(menu) {
if (menu.disabled) { if (menu.disabled) {
return return
@@ -116,19 +232,14 @@ export default {
const zTreeRect = tree.getBoundingClientRect() const zTreeRect = tree.getBoundingClientRect()
tree.style.height = `calc(100vh - ${zTreeRect.top}px - 30px - 25px)` tree.style.height = `calc(100vh - ${zTreeRect.top}px - 30px - 25px)`
}, 100), }, 100),
async initTree(refresh = false) { async initTree(refresh = false, iTreeUrl = '') {
const vm = this const vm = this
let treeUrl
this.loading = true this.loading = true
if (refresh && this.treeSetting.treeUrl.indexOf('/perms/') !== -1 && let treeUrl = iTreeUrl
this.treeSetting.treeUrl.indexOf('rebuild_tree') === -1 if (!treeUrl) {
) {
treeUrl = (this.treeSetting.treeUrl.indexOf('?') === -1)
? `${this.treeSetting.treeUrl}?rebuild_tree=1`
: `${this.treeSetting.treeUrl}&rebuild_tree=1`
} else {
treeUrl = this.treeSetting.treeUrl treeUrl = this.treeSetting.treeUrl
} }
treeUrl = setUrlParam(treeUrl, 'tree_type', this.treeType)
if (refresh) { if (refresh) {
$.fn.zTree.destroy(this.iZTreeID) $.fn.zTree.destroy(this.iZTreeID)
@@ -221,25 +332,31 @@ export default {
searchInput.oninput = e => this.treeSearchHandle((e.target.value || '')) searchInput.oninput = e => this.treeSearchHandle((e.target.value || ''))
}, },
treeSearchHandle: _.debounce(function(value) { treeSearchHandle: _.debounce(function(value) {
if (this.treeSetting.async.enable) { if (this.treeSetting.async?.enable) {
this.filterAssetsServer(value) this.searchFromServer(value)
} else { } else {
this.filterTree(value) this.searchFromLocal(value)
} }
}, 600), }, 600),
getCheckedNodes: function() { getCheckedNodes: function() {
return this.zTree.getCheckedNodes(true) return this.zTree.getCheckedNodes(true)
}, },
recurseParent(node) { recurseParent(node) {
const parentNode = node.getParentNode() const parentNode = node.getParentNode()
if (parentNode && parentNode.pId) { if (!parentNode) {
return [parentNode, ...this.recurseParent(parentNode)]
} else if (parentNode) {
return [parentNode]
} else {
return [] return []
} }
const allParents = []
if (parentNode) {
allParents.push(parentNode)
if (parentNode.pId) {
allParents.push(...this.recurseParent(parentNode))
}
}
return allParents
}, },
recurseChildren(node) { recurseChildren(node) {
if (!node.isParent) { if (!node.isParent) {
return [] return []
@@ -248,49 +365,27 @@ export default {
if (!children) { if (!children) {
return [] return []
} }
let allChildren = [] const allChildren = []
children.forEach((n) => { children.forEach((n) => {
allChildren = [...children, ...this.recurseChildren(n)] allChildren.push(n)
allChildren.push(...this.recurseChildren(n))
}) })
return allChildren return allChildren
}, },
groupBy(array, filter) {
const groups = {} searchFromLocal(keyword, tree = this.zTree) {
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) {
if (!this.zTree) return if (!this.zTree) return
const searchNode = tree.getNodesByFilter((node) => node.id === 'search') const searchNode = tree.getNodesByFilter((node) => node.id === 'search')
if (searchNode) tree.removeNode(searchNode[0]) if (searchNode) tree.removeNode(searchNode[0])
const nodes = tree.transformToArray(tree.getNodes())
const allNodes = tree.transformToArray(tree.getNodes())
if (!keyword) { if (!keyword) {
tree.showNodes(nodes) tree.showNodes(allNodes)
tree.expandAll(false)
return 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) => { const matchedNodes = tree.getNodesByFilter((node) => {
return node.name.toLowerCase().indexOf(keyword.toLowerCase()) > -1 return node.name.toLowerCase().indexOf(keyword.toLowerCase()) > -1
}) })
@@ -300,67 +395,54 @@ export default {
const assetsAmount = matchedNodes.length const assetsAmount = matchedNodes.length
name = `${name} (${assetsAmount})` name = `${name} (${assetsAmount})`
const newNode = { id: 'search', name: name, isParent: false, open: false } 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) => { matchedNodes.forEach((node) => {
const parents = this.recurseParent(node) const parents = this.recurseParent(node)
const children = this.recurseChildren(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 shouldExpandNodes.push(...parents)
tree.hideNodes(nodes) // 应该折叠匹配节点的子孙节点
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) tree.showNodes(shouldShow)
for (const node of shouldShow) { // 展开应该展开的节点
if (node.isParent) { for (const node of shouldExpandNodes) {
tree.expandNode(node, true) 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 searchFromServer(keyword) {
const filterField = treeUrl.includes('?') ? `&search=${keyword}` : `?search=${keyword}` // 直接用搜索 API 返回的数据重新初始化树
if (treeUrl.indexOf('assets/nodes/children/tree') > -1) { const treeUrl = this.treeSetting.searchUrl ? this.treeSetting.searchUrl : this.treeSetting.treeUrl
treeUrl = treeUrl + '&all=all' const searchTypeKey = this.treeSearchTypeOptions[this.treeSearchType]?.search_key || 'search'
} const searchUrl = setUrlParam(treeUrl, searchTypeKey, keyword)
const searchUrl = `${treeUrl}${filterField}` this.initTree(true, searchUrl)
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
})
} }
} }
@@ -624,12 +706,19 @@ div.rMenu li {
.fixed-tree-search { .fixed-tree-search {
margin-bottom: 10px; margin-bottom: 10px;
border: 1px solid;
border-radius: 3px;
&:hover,
&:focus-within {
border-color: var(--color-primary);
}
& ::v-deep .el-input__inner { & ::v-deep .el-input__inner {
border-radius: 4px; border: none;
background: #fafafa; background: #fafafa;
padding-right: 32px; padding-right: 32px;
color: var(--color-text-primary) color: var(--color-text-primary);
} }
& ::v-deep .el-input__suffix { & ::v-deep .el-input__suffix {
@@ -653,6 +742,37 @@ div.rMenu li {
& ::v-deep .el-input__suffix-inner { & ::v-deep .el-input__suffix-inner {
line-height: 30px; 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 { .icon-refresh {

View File

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

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ export default {
data() { data() {
return { return {
treeUrl: `/api/v1/perms/users/${this.object.id}/nodes/children/tree/`, 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: { actions: {
has: false has: false
}, },