mirror of https://github.com/jumpserver/jumpserver
[Feature] tree增删功能
parent
1ac30ed09c
commit
2f06a2b1d3
|
@ -13,7 +13,7 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from rest_framework import generics
|
from rest_framework import generics, mixins
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework_bulk import BulkModelViewSet
|
from rest_framework_bulk import BulkModelViewSet
|
||||||
|
@ -21,6 +21,7 @@ from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView
|
||||||
from rest_framework.pagination import LimitOffsetPagination
|
from rest_framework.pagination import LimitOffsetPagination
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.db.models import Q, Count
|
from django.db.models import Q, Count
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from common.mixins import CustomFilterMixin
|
from common.mixins import CustomFilterMixin
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
|
@ -311,21 +312,41 @@ class LabelViewSet(BulkModelViewSet):
|
||||||
return super().list(request, *args, **kwargs)
|
return super().list(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class TreeViewApi(APIView):
|
class NodeViewSet(BulkModelViewSet):
|
||||||
|
queryset = Node.objects.all()
|
||||||
|
permission_classes = (IsSuperUser,)
|
||||||
|
serializer_class = serializers.NodeSerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def perform_create(self, serializer):
|
||||||
return Node.objects.all()
|
child_id = Node.get_root_node().get_next_child_id()
|
||||||
|
serializer.validated_data["id"] = child_id
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
def get(self, request):
|
|
||||||
data = []
|
class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
|
||||||
for node in self.get_queryset():
|
queryset = Node.objects.all()
|
||||||
parent = ":".join(node.id.split(":")[:-1])
|
permission_classes = (IsSuperUser,)
|
||||||
d = {
|
serializer_class = serializers.NodeSerializer
|
||||||
"id": node.id,
|
instance = None
|
||||||
"pId": parent,
|
|
||||||
"name": node.name
|
def post(self, request, *args, **kwargs):
|
||||||
}
|
if not request.data.get("name"):
|
||||||
if node.id == "0":
|
request.data["name"] = _("New node {}").format(
|
||||||
d["open"] = True
|
Node.get_root_node().get_next_child_id().split(":")[-1]
|
||||||
data.append(d)
|
)
|
||||||
return Response(data)
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
instance = self.get_object()
|
||||||
|
name = request.data.get("name")
|
||||||
|
node = instance.create_child(name=name)
|
||||||
|
return Response({"id": node.id, "name": node.name}, status=201)
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
instance = self.get_object()
|
||||||
|
if self.request.query_params.get("all"):
|
||||||
|
children = instance.get_all_children()
|
||||||
|
else:
|
||||||
|
children = instance.get_children()
|
||||||
|
response = [{"id": node.id, "name": node.name} for node in children]
|
||||||
|
return Response(response, status=200)
|
||||||
|
|
|
@ -4,8 +4,8 @@ from rest_framework import serializers
|
||||||
from rest_framework_bulk.serializers import BulkListSerializer
|
from rest_framework_bulk.serializers import BulkListSerializer
|
||||||
|
|
||||||
from common.mixins import BulkSerializerMixin
|
from common.mixins import BulkSerializerMixin
|
||||||
from .models import AssetGroup, Asset, Cluster, AdminUser, SystemUser, Label
|
from .models import AssetGroup, Asset, Cluster, AdminUser, SystemUser, Label, Node
|
||||||
from .const import ADMIN_USER_CONN_CACHE_KEY, SYSTEM_USER_CONN_CACHE_KEY
|
from .const import ADMIN_USER_CONN_CACHE_KEY
|
||||||
|
|
||||||
|
|
||||||
class AssetGroupSerializer(BulkSerializerMixin, serializers.ModelSerializer):
|
class AssetGroupSerializer(BulkSerializerMixin, serializers.ModelSerializer):
|
||||||
|
@ -316,3 +316,26 @@ class LabelDistinctSerializer(serializers.ModelSerializer):
|
||||||
def get_value(obj):
|
def get_value(obj):
|
||||||
labels = Label.objects.filter(name=obj["name"])
|
labels = Label.objects.filter(name=obj["name"])
|
||||||
return ', '.join([label.value for label in labels])
|
return ', '.join([label.value for label in labels])
|
||||||
|
|
||||||
|
|
||||||
|
class NodeSerializer(serializers.ModelSerializer):
|
||||||
|
parent = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Node
|
||||||
|
fields = ['id', 'name', 'parent']
|
||||||
|
list_serializer_class = BulkListSerializer
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_parent(obj):
|
||||||
|
if obj.id == "0":
|
||||||
|
return "#"
|
||||||
|
if not obj.id.startswith("0"):
|
||||||
|
return "0"
|
||||||
|
return ":".join(obj.id.split(":")[:-1])
|
||||||
|
|
||||||
|
def get_fields(self):
|
||||||
|
fields = super().get_fields()
|
||||||
|
field = fields["id"]
|
||||||
|
field.required = False
|
||||||
|
return fields
|
||||||
|
|
|
@ -13,8 +13,26 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
.ztree li span.button.add {
|
div#rMenu {
|
||||||
margin-left:2px; margin-right: -1px; background-position:-144px 0; vertical-align:top; *vertical-align:middle
|
position:absolute;
|
||||||
|
visibility:hidden;
|
||||||
|
text-align: left;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
float: left;
|
||||||
|
padding: 5px 0;
|
||||||
|
margin: 2px 0 0;
|
||||||
|
list-style: none;
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
div#rMenu li{
|
||||||
|
margin: 1px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
{#list-style: none outside none;#}
|
||||||
|
}
|
||||||
|
.dropdown a:hover {
|
||||||
|
background-color: #f1f1f1
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
@ -28,8 +46,9 @@
|
||||||
<div class="ibox-content mailbox-content">
|
<div class="ibox-content mailbox-content">
|
||||||
<div class="file-manager">
|
<div class="file-manager">
|
||||||
<h5>Tree View</h5>
|
<h5>Tree View</h5>
|
||||||
<div id="treeDemo" class="ztree">
|
<div id="assetTree" class="ztree">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -83,9 +102,17 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
<div id="rMenu">
|
||||||
</div>
|
<ul class="dropdown-menu">
|
||||||
|
<li id="m_del" tabindex="-1" onclick="removeTreeNode();"><a>重命名</a></li>
|
||||||
|
<li id="m_add" tabindex="-1" onclick="addTreeNode();"><a>添加节点</a></li>
|
||||||
|
<li class="divider"></li>
|
||||||
|
<li id="m_del" tabindex="-1" onclick="removeTreeNode();"><a>删除节点</a></li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -93,6 +120,7 @@
|
||||||
<script src="{% static 'js/plugins/jstree/jstree.min.js' %}"></script>
|
<script src="{% static 'js/plugins/jstree/jstree.min.js' %}"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
var zTree, rMenu;
|
||||||
function initTable() {
|
function initTable() {
|
||||||
var options = {
|
var options = {
|
||||||
ele: $('#asset_list_table'),
|
ele: $('#asset_list_table'),
|
||||||
|
@ -142,123 +170,105 @@
|
||||||
return jumpserver.initServerSideDataTable(options);
|
return jumpserver.initServerSideDataTable(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
function beforeDrag(treeId, treeNodes) {
|
function OnRightClick(event, treeId, treeNode) {
|
||||||
showLog("Before drag");
|
if (!treeNode && event.target.tagName.toLowerCase() !== "button" && $(event.target).parents("a").length == 0) {
|
||||||
return false;
|
zTree.cancelSelectedNode();
|
||||||
}
|
showRMenu("root", event.clientX, event.clientY);
|
||||||
|
} else if (treeNode && !treeNode.noR) {
|
||||||
function beforeEditName(treeId, treeNode) {
|
|
||||||
showLog("Before edit name");
|
|
||||||
className = (className === "dark" ? "" : "dark");
|
|
||||||
showLog("[ " + getTime() + " beforeEditName ] " + treeNode.name);
|
|
||||||
var zTree = $.fn.zTree.getZTreeObj("treeDemo");
|
|
||||||
zTree.selectNode(treeNode);
|
zTree.selectNode(treeNode);
|
||||||
setTimeout(function () {
|
showRMenu("node", event.clientX, event.clientY);
|
||||||
if (confirm("进入节点 -- " + treeNode.name + " 的编辑状态吗?")) {
|
|
||||||
setTimeout(function () {
|
|
||||||
zTree.editName(treeNode);
|
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
}, 0);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function beforeRemove(treeId, treeNode) {
|
function showRMenu(type, x, y) {
|
||||||
showLog("[ " + getTime() + " beforeRemove ] " + treeNode.name);
|
$("#rMenu ul").show();
|
||||||
className = (className === "dark" ? "" : "dark");
|
if (type === "root") {
|
||||||
var zTree = $.fn.zTree.getZTreeObj("treeDemo");
|
$("#m_del").hide();
|
||||||
zTree.selectNode(treeNode);
|
$("#m_check").hide();
|
||||||
return confirm("确认删除 节点 -- " + treeNode.name + " 吗?");
|
$("#m_unCheck").hide();
|
||||||
|
} else {
|
||||||
|
$("#m_del").show();
|
||||||
|
$("#m_check").show();
|
||||||
|
$("#m_unCheck").show();
|
||||||
|
}
|
||||||
|
{#y += $("#page-wrapper")[0].scrollTop;#}
|
||||||
|
{#x += $("#page-wrapper")[0].scrollLeft;#}
|
||||||
|
x -= 220;
|
||||||
|
{#y -= 100;#}
|
||||||
|
{#y += document.body.scrollTop;#}
|
||||||
|
{#x += document.body.scrollLeft;#}
|
||||||
|
rMenu.css({"top":y+"px", "left":x+"px", "visibility":"visible"});
|
||||||
|
|
||||||
|
$("body").bind("mousedown", onBodyMouseDown);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRemove(e, treeId, treeNode) {
|
function beforeClick(treeId, treeNode, clickFlag) {
|
||||||
showLog("Remove node")
|
|
||||||
}
|
|
||||||
|
|
||||||
function beforeRename(treeId, treeNode, newName, isCancel) {
|
|
||||||
showLog((isCancel ? "<span style='color:red'>" : "") + "[ " + getTime() + " beforeRename ] " + treeNode.name + (isCancel ? "</span>" : ""));
|
|
||||||
className = (className === "dark" ? "" : "dark");
|
|
||||||
if (newName.length == 0) {
|
|
||||||
setTimeout(function () {
|
|
||||||
var zTree = $.fn.zTree.getZTreeObj("treeDemo");
|
|
||||||
zTree.cancelEditName();
|
|
||||||
alert("节点名称不能为空.");
|
|
||||||
}, 0);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRename(e, treeId, treeNode, isCancel) {
|
function onClick(event, treeId, treeNode, clickFlag) {
|
||||||
console.log("On remname");
|
showLog("On click");
|
||||||
{#showLog((isCancel ? "<span style='color:red'>" : "") + "[ " + getTime() + " onRename ] " + treeNode.name + (isCancel ? "</span>" : ""));#}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showRemoveBtn(treeId, treeNode) {
|
|
||||||
showLog("Show remove btn");
|
|
||||||
return !treeNode.isFirstNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showRenameBtn(treeId, treeNode) {
|
|
||||||
showLog("Show rename btn");
|
|
||||||
return !treeNode.isLastNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showLog(str) {
|
function showLog(str) {
|
||||||
console.log(str)
|
console.log(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTime() {
|
function hideRMenu() {
|
||||||
var now = new Date(),
|
if (rMenu) rMenu.css({"visibility": "hidden"});
|
||||||
h = now.getHours(),
|
$("body").unbind("mousedown", onBodyMouseDown);
|
||||||
m = now.getMinutes(),
|
|
||||||
s = now.getSeconds(),
|
|
||||||
ms = now.getMilliseconds();
|
|
||||||
return (h + ":" + m + ":" + s + " " + ms);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var newCount = 1;
|
function onBodyMouseDown(event){
|
||||||
|
if (!(event.target.id === "rMenu" || $(event.target).parents("#rMenu").length>0)) {
|
||||||
|
rMenu.css({"visibility" : "hidden"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function addHoverDom(treeId, treeNode) {
|
function addTreeNode() {
|
||||||
var sObj = $("#" + treeNode.tId + "_span");
|
hideRMenu();
|
||||||
if (treeNode.editNameFlag || $("#addBtn_" + treeNode.tId).length > 0) return;
|
var parentNode = zTree.getSelectedNodes()[0];
|
||||||
var addStr = "<span class='button add' id='addBtn_" + treeNode.tId
|
if (!parentNode){
|
||||||
+ "' title='add node' onfocus='this.blur();'></span>";
|
return
|
||||||
sObj.after(addStr);
|
}
|
||||||
var btn = $("#addBtn_" + treeNode.tId);
|
var url = "{% url 'api-assets:node-children' pk='0:0' %}".replace("0:0", parentNode.id );
|
||||||
if (btn) btn.bind("click", function () {
|
|
||||||
var zTree = $.fn.zTree.getZTreeObj("treeDemo");
|
$.post(url, {}, function (data, status){
|
||||||
zTree.addNodes(treeNode, {
|
if (status === "success") {
|
||||||
id: (100 + newCount),
|
var newNode = { name:data["name"], id:data["id"], pId: parentNode.id };
|
||||||
pId: treeNode.id,
|
newNode.checked = zTree.getSelectedNodes()[0].checked;
|
||||||
name: "new node" + (newCount++)
|
zTree.addNodes(parentNode, newNode);
|
||||||
|
} else {
|
||||||
|
alert("{% trans 'Create node failed' %}")
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return false;
|
}
|
||||||
|
|
||||||
|
function removeTreeNode() {
|
||||||
|
hideRMenu();
|
||||||
|
var current_node = zTree.getSelectedNodes()[0];
|
||||||
|
if (!current_node){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_node.children && current_node.children.length > 0) {
|
||||||
|
alert("{% trans 'Have child node, cancel' %}")
|
||||||
|
} else {
|
||||||
|
var url = "{% url 'api-assets:node-detail' pk='0:0' %}".replace("0:0", current_node.id );
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
method: "DELETE",
|
||||||
|
success: function () {
|
||||||
|
zTree.removeNode(current_node);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
function removeHoverDom(treeId, treeNode) {
|
|
||||||
showLog("Remove hove dom");
|
|
||||||
$("#addBtn_" + treeNode.tId).unbind().remove();
|
|
||||||
};
|
|
||||||
|
|
||||||
function selectAll() {
|
|
||||||
var zTree = $.fn.zTree.getZTreeObj("treeDemo");
|
|
||||||
zTree.setting.edit.editNameSelectAll = $("#selectAll").attr("checked");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function initTree() {
|
function initTree() {
|
||||||
var setting = {
|
var setting = {
|
||||||
view: {
|
view: {
|
||||||
addHoverDom: addHoverDom,
|
dblClickExpand: false
|
||||||
removeHoverDom: removeHoverDom,
|
|
||||||
selectedMulti: false
|
|
||||||
},
|
|
||||||
edit: {
|
|
||||||
enable: true,
|
|
||||||
editNameSelectAll: true,
|
|
||||||
showRemoveBtn: showRemoveBtn,
|
|
||||||
showRenameBtn: showRenameBtn
|
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
simpleData: {
|
simpleData: {
|
||||||
|
@ -266,23 +276,25 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
callback: {
|
callback: {
|
||||||
beforeDrag: beforeDrag,
|
onRightClick: OnRightClick,
|
||||||
beforeEditName: beforeEditName,
|
beforeClick: beforeClick,
|
||||||
beforeRemove: beforeRemove,
|
onClick: onClick
|
||||||
beforeRename: beforeRename,
|
|
||||||
onRemove: onRemove,
|
|
||||||
onRename: onRename
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var zNodes = [];
|
var zNodes = [];
|
||||||
$.get("{% url 'api-assets:tree-view' %}", function(data, status){
|
$.get("{% url 'api-assets:node-list' %}", function(data, status){
|
||||||
zNodes = data;
|
$.each(data, function (index, value) {
|
||||||
console.log(data);
|
value["pId"] = value["parent"];
|
||||||
console.log(status);
|
if (value["id"] === "0") {
|
||||||
$.fn.zTree.init($("#treeDemo"), setting, zNodes);
|
value["open"] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
zNodes = data;
|
||||||
|
$.fn.zTree.init($("#assetTree"), setting, zNodes);
|
||||||
|
zTree = $.fn.zTree.getZTreeObj("assetTree");
|
||||||
|
rMenu = $("#rMenu");
|
||||||
});
|
});
|
||||||
$("#selectAll").bind("click", selectAll);
|
|
||||||
}
|
}
|
||||||
$(document).ready(function(){
|
$(document).ready(function(){
|
||||||
initTable();
|
initTable();
|
||||||
|
|
|
@ -13,6 +13,7 @@ router.register(r'v1/clusters', api.ClusterViewSet, 'cluster')
|
||||||
router.register(r'v1/admin-user', api.AdminUserViewSet, 'admin-user')
|
router.register(r'v1/admin-user', api.AdminUserViewSet, 'admin-user')
|
||||||
router.register(r'v1/system-user', api.SystemUserViewSet, 'system-user')
|
router.register(r'v1/system-user', api.SystemUserViewSet, 'system-user')
|
||||||
router.register(r'v1/labels', api.LabelViewSet, 'label')
|
router.register(r'v1/labels', api.LabelViewSet, 'label')
|
||||||
|
router.register(r'v1/nodes', api.NodeViewSet, 'node')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^v1/assets-bulk/$', api.AssetListUpdateApi.as_view(), name='asset-bulk-update'),
|
url(r'^v1/assets-bulk/$', api.AssetListUpdateApi.as_view(), name='asset-bulk-update'),
|
||||||
|
@ -42,7 +43,7 @@ urlpatterns = [
|
||||||
api.SystemUserPushApi.as_view(), name='system-user-push'),
|
api.SystemUserPushApi.as_view(), name='system-user-push'),
|
||||||
url(r'^v1/system-user/(?P<pk>[0-9a-zA-Z\-]{36})/connective/$',
|
url(r'^v1/system-user/(?P<pk>[0-9a-zA-Z\-]{36})/connective/$',
|
||||||
api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'),
|
api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'),
|
||||||
url(r'^v1/tree/$', api.TreeViewApi.as_view(), name='tree-view')
|
url(r'^v1/nodes/(?P<pk>[0-9:]+)/children/$', api.NodeChildrenApi.as_view(), name='node-children'),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns += router.urls
|
urlpatterns += router.urls
|
||||||
|
|
Loading…
Reference in New Issue