mirror of
https://github.com/jumpserver/lina.git
synced 2026-01-14 03:46:26 +00:00
Compare commits
6 Commits
v5
...
v5_refacto
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65392f1d0c | ||
|
|
777b139fde | ||
|
|
cccd2cbb7a | ||
|
|
f75a4007f6 | ||
|
|
a9c1e06706 | ||
|
|
b66b779e79 |
@@ -142,5 +142,6 @@
|
||||
"src/**/*.{js,vue}": [
|
||||
"eslint --fix"
|
||||
]
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: []
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ export default {
|
||||
url: '/api/v1/assets/assets/',
|
||||
showMenu: !this.$store.getters.currentOrgIsRoot,
|
||||
showDefaultMenu: true,
|
||||
async: {
|
||||
enable: false
|
||||
},
|
||||
menu: [
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user