Skip to content

Commit 5fad4d1

Browse files
committed
feat(ipam): add drag-and-drop subnet move in tree view
1 parent f8ab88f commit 5fad4d1

9 files changed

Lines changed: 199 additions & 12 deletions

File tree

cmdb-api/api/lib/cmdb/ipam/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class OperateTypeEnum(BaseEnum):
1818
DELETE_SUBNET = "5"
1919
ASSIGN_ADDRESS = "6"
2020
REVOKE_ADDRESS = "7"
21+
MOVE_SUBNET = "8"
2122

2223

2324
class SubnetBuiltinAttributes(BaseEnum):

cmdb-api/api/lib/cmdb/ipam/subnet.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ def __init__(self):
2727
404, ErrFormat.ipam_subnet_model_not_found.format(BuiltinModelEnum.IPAM_SUBNET))
2828

2929
self.type_id = self.ci_type.id
30+
self.scope_type = CITypeCache.get(BuiltinModelEnum.IPAM_SCOPE)
31+
self.scope_type_id = self.scope_type.id if self.scope_type else None
3032

3133
def scan_rules(self, oneagent_id, last_update_at=None):
3234
result = []
@@ -272,6 +274,66 @@ def update(self, _id, **kwargs):
272274

273275
return _id
274276

277+
def move(self, _id, target_parent_id=None):
278+
if target_parent_id in ('', 'null'):
279+
target_parent_id = None
280+
elif target_parent_id is not None:
281+
target_parent_id = int(target_parent_id)
282+
283+
current_subnet = CIManager.get_ci_by_id(_id, need_children=False)
284+
if current_subnet['ci_type'] != BuiltinModelEnum.IPAM_SUBNET:
285+
abort(400, ErrFormat.ipam_subnet_not_found)
286+
287+
if target_parent_id == _id:
288+
abort(400, 'Cannot move subnet under itself')
289+
290+
if target_parent_id is not None and target_parent_id in CIRelationManager.recursive_children(_id):
291+
abort(400, 'Cannot move subnet under its descendant')
292+
293+
parent_type_ids = [self.type_id] + ([self.scope_type_id] if self.scope_type_id else [])
294+
current_parent_relations = CIRelation.get_by(only_query=True).join(
295+
CI, CI.id == CIRelation.first_ci_id).filter(
296+
CIRelation.second_ci_id == _id).filter(
297+
CIRelation.ancestor_ids.is_(None)).filter(CI.type_id.in_(parent_type_ids)).all()
298+
299+
if len(current_parent_relations) > 1:
300+
abort(400, 'Subnet has multiple parents')
301+
302+
current_parent_relation = current_parent_relations[0] if current_parent_relations else None
303+
current_parent_id = current_parent_relation.first_ci_id if current_parent_relation else None
304+
305+
if current_parent_id == target_parent_id:
306+
return _id
307+
308+
target_parent = None
309+
if target_parent_id is not None:
310+
target_parent = CIManager.get_ci_by_id(target_parent_id, need_children=False)
311+
if target_parent['ci_type'] != BuiltinModelEnum.IPAM_SCOPE:
312+
abort(400, 'Target node must be a scope or root')
313+
314+
self.validate_cidr(target_parent_id, current_subnet.get(SubnetBuiltinAttributes.CIDR), _id)
315+
316+
if current_parent_relation is not None:
317+
CIRelationManager.delete(current_parent_relation.id, apply_async=False, valid=False)
318+
319+
if target_parent_id is not None:
320+
CIRelationManager.add(target_parent_id, _id, valid=False, apply_async=False)
321+
322+
current_parent_name = 'root'
323+
if current_parent_id is not None:
324+
current_parent = CIManager.get_ci_by_id(current_parent_id, need_children=False)
325+
current_parent_name = current_parent.get('name') or current_parent.get(SubnetBuiltinAttributes.CIDR) or 'root'
326+
327+
target_parent_name = 'root'
328+
if target_parent is not None:
329+
target_parent_name = target_parent.get('name') or target_parent.get(SubnetBuiltinAttributes.CIDR) or 'root'
330+
331+
OperateHistoryManager().add(operate_type=OperateTypeEnum.MOVE_SUBNET,
332+
cidr=current_subnet.get(SubnetBuiltinAttributes.CIDR),
333+
description='{} -> {}'.format(current_parent_name, target_parent_name))
334+
335+
return _id
336+
275337
@classmethod
276338
def delete(cls, _id):
277339
if CIRelation.get_by(only_query=True).filter(CIRelation.first_ci_id == _id).first():

cmdb-api/api/views/cmdb/ipam/subnet.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ def delete(self, _id):
5151
return self.jsonify(id=SubnetManager().delete(_id))
5252

5353

54+
class SubnetMoveView(APIView):
55+
url_prefix = "/ipam/subnet/<int:_id>/move"
56+
57+
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
58+
app_cli.op.read, app_cli.admin_name)
59+
def put(self, _id):
60+
return self.jsonify(id=SubnetManager().move(_id, request.values.get('target_parent_id')))
61+
62+
5463
class SubnetScopeView(APIView):
5564
url_prefix = ("/ipam/scope", "/ipam/scope/<int:_id>")
5665

cmdb-ui/src/modules/cmdb/api/ipam.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ export function deleteIPAMSubnet(id) {
3737
})
3838
}
3939

40+
export function moveIPAMSubnet(id, data) {
41+
return axios({
42+
url: `/v0.1/ipam/subnet/${id}/move`,
43+
method: 'PUT',
44+
data
45+
})
46+
}
47+
4048
export function postIPAMScope(data) {
4149
return axios({
4250
url: '/v0.1/ipam/scope',

cmdb-ui/src/modules/cmdb/lang/en.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,6 +1069,7 @@ if __name__ == "__main__":
10691069
deleteCatalog: 'Delete Catalog',
10701070
updateSubnet: 'Update Subnet',
10711071
deleteSubnet: 'Delete Subnet',
1072+
moveSubnet: 'Move Subnet',
10721073
revokeAddress: 'Revoke Address',
10731074
operateTime: 'Operate Time',
10741075
operateUser: 'Operate User',

cmdb-ui/src/modules/cmdb/lang/zh.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,7 @@ if __name__ == "__main__":
10651065
deleteCatalog: '删除目录',
10661066
updateSubnet: '修改子网',
10671067
deleteSubnet: '删除子网',
1068+
moveSubnet: '移动子网',
10681069
revokeAddress: '地址回收',
10691070
operateTime: '操作时间',
10701071
operateUser: '操作人',

cmdb-ui/src/modules/cmdb/views/ipam/components/ipamTree.vue

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,23 @@
1313
:treeData="filterTreeData"
1414
:selectedKeys="treeKey ? [treeKey] : []"
1515
:defaultExpandedKeys="treeKey ? [treeKey] : []"
16+
:draggable="!searchValue"
17+
:allowDrop="allowDrop"
18+
@dragstart="handleDragStart"
19+
@drop="handleDrop"
1620
>
1721
<template #title="treeNodeData">
1822
<div
19-
class="ipam-tree-node"
23+
:class="[
24+
'ipam-tree-node',
25+
treeNodeData.isSubnet && !searchValue ? 'ipam-tree-node-draggable' : ''
26+
]"
2027
@click="clickTreeNode(treeNodeData)"
2128
>
29+
<OpsMoveIcon
30+
v-if="treeNodeData.isSubnet && !searchValue"
31+
class="ipam-tree-node-drag-icon"
32+
/>
2233
<ops-icon
2334
:type="treeNodeData.icon"
2435
class="ipam-tree-node-icon"
@@ -102,14 +113,16 @@
102113

103114
<script>
104115
import _ from 'lodash'
105-
import { deleteIPAMSubnet, deleteIPAMScope } from '@/modules/cmdb/api/ipam.js'
116+
import { deleteIPAMSubnet, deleteIPAMScope, moveIPAMSubnet } from '@/modules/cmdb/api/ipam.js'
117+
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
106118
107119
import SubnetForm from './subnetForm.vue'
108120
import CatalogForm from './catalogForm.vue'
109121
110122
export default {
111123
name: 'IPAMTree',
112124
components: {
125+
OpsMoveIcon,
113126
SubnetForm,
114127
CatalogForm
115128
},
@@ -146,6 +159,21 @@ export default {
146159
}
147160
},
148161
methods: {
162+
findNodeByKey(nodes, key) {
163+
for (const node of nodes) {
164+
if (String(node.key) === String(key)) {
165+
return node
166+
}
167+
if (node.children) {
168+
const foundNode = this.findNodeByKey(node.children, key)
169+
if (foundNode) {
170+
return foundNode
171+
}
172+
}
173+
}
174+
return null
175+
},
176+
149177
handleTreeDataBySearch(data) {
150178
const isMatch = data?.title?.indexOf?.(this.searchValue) !== -1
151179
if (!data?.children?.length) {
@@ -203,6 +231,50 @@ export default {
203231
204232
clickTreeNode(node) {
205233
this.$emit('updateTreeKey', node.key)
234+
},
235+
236+
allowDrop({ dropNode, dropPosition }) {
237+
if (this.searchValue || dropPosition !== 0) {
238+
return false
239+
}
240+
241+
const targetNode = this.findNodeByKey(this.treeData, dropNode?.eventKey)
242+
return targetNode?.key === 'all' || !targetNode?.isSubnet
243+
},
244+
245+
handleDragStart(info) {
246+
const dragNode = this.findNodeByKey(this.treeData, info?.node?.eventKey)
247+
if (!dragNode?.isSubnet) {
248+
const event = info && info.event
249+
if (event && event.preventDefault) {
250+
event.preventDefault()
251+
}
252+
if (event && event.stopPropagation) {
253+
event.stopPropagation()
254+
}
255+
return false
256+
}
257+
},
258+
259+
async handleDrop(info) {
260+
const dragNode = this.findNodeByKey(this.treeData, info?.dragNode?.eventKey)
261+
const targetNode = this.findNodeByKey(this.treeData, info?.node?.eventKey)
262+
263+
if (!dragNode?.isSubnet || !targetNode) {
264+
return
265+
}
266+
267+
const targetParentId = targetNode.key === 'all' ? null : targetNode.key
268+
if (`${dragNode.parentId || ''}` === `${targetParentId || ''}`) {
269+
return
270+
}
271+
272+
await moveIPAMSubnet(dragNode.key, {
273+
target_parent_id: targetParentId
274+
})
275+
276+
this.$message.success(this.$t('editSuccess'))
277+
this.refreshData()
206278
}
207279
}
208280
}
@@ -223,16 +295,25 @@ export default {
223295
height: 100%;
224296
225297
/deep/ .ant-tree {
298+
.ant-tree-switcher {
299+
display: inline-flex;
300+
align-items: center;
301+
justify-content: center;
302+
width: 18px;
303+
height: 32px;
304+
line-height: 32px;
305+
}
306+
226307
.ant-tree-node-content-wrapper {
227-
width: calc(100% - 24px);
308+
width: calc(100% - 18px);
228309
padding: 0px;
229310
display: inline-block;
230311
height: fit-content;
231312
232313
.ant-tree-title {
233314
display: inline-block;
234315
width: 100%;
235-
padding: 0 6px;
316+
padding: 0 4px 0 2px;
236317
}
237318
}
238319
@@ -263,6 +344,16 @@ export default {
263344
flex-shrink: 0;
264345
}
265346
347+
&-drag-icon {
348+
width: 12px;
349+
height: 12px;
350+
margin-right: 1px;
351+
flex-shrink: 0;
352+
color: #A5A9BC;
353+
opacity: 0;
354+
transition: opacity 0.2s ease;
355+
}
356+
266357
&-title {
267358
margin-left: 6px;
268359
font-size: 14px;
@@ -306,10 +397,22 @@ export default {
306397
}
307398
308399
&:hover {
400+
.ipam-tree-node-drag-icon {
401+
opacity: 1;
402+
}
403+
309404
.ipam-tree-node-action {
310405
display: inline-block;
311406
}
312407
}
408+
409+
&-draggable {
410+
cursor: grab;
411+
412+
&:active {
413+
cursor: grabbing;
414+
}
415+
}
313416
}
314417
}
315418
</style>

cmdb-ui/src/modules/cmdb/views/ipam/index.vue

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -173,17 +173,14 @@ export default {
173173
}
174174
175175
const allCount = treeData.reduce((acc, cur) => acc + cur.count, 0)
176-
const rootShowSubnetBtn = treeData.every((item) => item.ci_type === SUB_NET_CITYPE_NAME)
177-
const rootShowCatalogBtn = treeData.every((item) => item.ci_type === SCOPE_CITYPE_NAME)
178-
179176
treeData.unshift({
180177
key: 'all',
181178
title: this.$t('all'),
182179
count: allCount,
183180
icon: 'veops-entire_network_',
184181
iconColor: '#2F54EB',
185-
showCatalogBtn: rootShowCatalogBtn,
186-
showSubnetBtn: rootShowSubnetBtn,
182+
showCatalogBtn: true,
183+
showSubnetBtn: true,
187184
parentId: '',
188185
class: 'ipam-tree-node-all'
189186
})
@@ -217,14 +214,13 @@ export default {
217214
return this.handleTreeData(item, type2name, key)
218215
})
219216
220-
const showSubnetBtn = children.every((item) => item.ci_type === SUB_NET_CITYPE_NAME)
221217
return {
222218
key,
223219
title,
224220
icon,
225221
iconColor,
226-
showCatalogBtn: !isSubnet && !showSubnetBtn,
227-
showSubnetBtn: showSubnetBtn,
222+
showCatalogBtn: !isSubnet,
223+
showSubnetBtn: true,
228224
isSubnet,
229225
parentId,
230226
...data,

cmdb-ui/src/modules/cmdb/views/ipam/modules/history/operation/constants.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const OPERATE_TYPE = {
77
DELETE_SUBNET: '5',
88
ASSIGN_ADDRESS: '6',
99
REVOKE_ADDRESS: '7',
10+
MOVE_SUBNET: '8',
1011
}
1112

1213
export const OPERATE_TYPE_TEXT = {
@@ -18,6 +19,7 @@ export const OPERATE_TYPE_TEXT = {
1819
[OPERATE_TYPE.DELETE_SUBNET]: 'cmdb.ipam.deleteSubnet',
1920
[OPERATE_TYPE.ASSIGN_ADDRESS]: 'cmdb.ipam.addressAssign',
2021
[OPERATE_TYPE.REVOKE_ADDRESS]: 'cmdb.ipam.revokeAddress',
22+
[OPERATE_TYPE.MOVE_SUBNET]: 'cmdb.ipam.moveSubnet',
2123
}
2224

2325
export const OPERATE_TYPE_COLOR = {
@@ -53,4 +55,8 @@ export const OPERATE_TYPE_COLOR = {
5355
color: '#0AA5A8',
5456
backgroundColor: '#E8FFFB'
5557
},
58+
[OPERATE_TYPE.MOVE_SUBNET]: {
59+
color: '#722ED1',
60+
backgroundColor: '#F9F0FF'
61+
},
5662
}

0 commit comments

Comments
 (0)