From cc22de094b9ef276c3f140ee94eda43f9d1b1b7c Mon Sep 17 00:00:00 2001 From: vapao Date: Sun, 20 Dec 2020 00:26:29 +0800 Subject: [PATCH] add host group migrate --- spug_api/apps/host/group.py | 3 +- spug_api/apps/host/models.py | 10 +-- spug_api/apps/host/views.py | 20 +++--- spug_web/src/pages/host/Detail.js | 20 +++++- spug_web/src/pages/host/Group.js | 22 +++++-- spug_web/src/pages/host/Selector.js | 95 +++++++++++++++++++++++++++++ spug_web/src/pages/host/index.js | 2 + spug_web/src/pages/host/store.js | 74 +++++++++++++++------- 8 files changed, 196 insertions(+), 50 deletions(-) create mode 100644 spug_web/src/pages/host/Selector.js diff --git a/spug_api/apps/host/group.py b/spug_api/apps/host/group.py index f11f894..de23da6 100644 --- a/spug_api/apps/host/group.py +++ b/spug_api/apps/host/group.py @@ -18,8 +18,9 @@ def fetch_children(data): def merge_children(data, prefix, childes): + prefix = f'{prefix}/' if prefix else '' for item in childes: - name = f'{prefix}/{item["title"]}' + name = f'{prefix}{item["title"]}' item['name'] = name if item['children']: merge_children(data, name, item['children']) diff --git a/spug_api/apps/host/models.py b/spug_api/apps/host/models.py index e235a55..3d48fa9 100644 --- a/spug_api/apps/host/models.py +++ b/spug_api/apps/host/models.py @@ -46,7 +46,7 @@ class Group(models.Model, ModelMixin): name = models.CharField(max_length=20) parent_id = models.IntegerField(default=0) sort_id = models.IntegerField(default=0) - hosts = models.ManyToManyField(Host, through='HostGroupRel', related_name='groups') + hosts = models.ManyToManyField(Host, related_name='groups') def to_view(self): return { @@ -59,11 +59,3 @@ class Group(models.Model, ModelMixin): class Meta: db_table = 'host_groups' ordering = ('-sort_id',) - - -class HostGroupRel(models.Model): - group = models.ForeignKey(Group, on_delete=models.CASCADE) - host = models.ForeignKey(Host, on_delete=models.CASCADE) - - class Meta: - db_table = 'host_group_rel' diff --git a/spug_api/apps/host/views.py b/spug_api/apps/host/views.py index 07de7c8..cc454ec 100644 --- a/spug_api/apps/host/views.py +++ b/spug_api/apps/host/views.py @@ -6,7 +6,7 @@ from django.db.models import F from django.http.response import HttpResponseBadRequest from libs import json_response, JsonParser, Argument from apps.setting.utils import AppSetting -from apps.host.models import Host, HostGroupRel +from apps.host.models import Host, Group from apps.app.models import Deploy from apps.schedule.models import Task from apps.monitor.models import Detection @@ -26,7 +26,7 @@ class HostView(View): return json_response(error='无权访问该主机,请联系管理员') return json_response(Host.objects.get(pk=host_id)) hosts = {x.id: x.to_view() for x in Host.objects.filter(deleted_by_id__isnull=True)} - for rel in HostGroupRel.objects.all(): + for rel in Group.hosts.through.objects.all(): hosts[rel.host_id]['group_ids'].append(rel.group_id) return json_response(list(hosts.values())) @@ -61,15 +61,17 @@ class HostView(View): def patch(self, request): form, error = JsonParser( - Argument('id', type=int, required=False), - Argument('zone', help='请输入主机类别') + Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择主机'), + Argument('s_group_id', type=int, help='参数错误'), + Argument('t_group_id', type=int, help='参数错误'), + Argument('is_copy', type=bool, help='参数错误'), ).parse(request.body) if error is None: - host = Host.objects.filter(pk=form.id).first() - if not host: - return json_response(error='未找到指定主机') - count = Host.objects.filter(zone=host.zone, deleted_by_id__isnull=True).update(zone=form.zone) - return json_response(count) + s_group = Group.objects.get(pk=form.s_group_id) + t_group = Group.objects.get(pk=form.t_group_id) + t_group.hosts.add(*form.host_ids) + if not form.is_copy: + s_group.hosts.remove(*form.host_ids) return json_response(error=error) def delete(self, request): diff --git a/spug_web/src/pages/host/Detail.js b/spug_web/src/pages/host/Detail.js index f50e728..5b8fc61 100644 --- a/spug_web/src/pages/host/Detail.js +++ b/spug_web/src/pages/host/Detail.js @@ -1,17 +1,31 @@ import React from 'react'; import { observer } from 'mobx-react'; -import { Drawer} from 'antd'; +import { Drawer, Descriptions, List } from 'antd'; import store from './store'; export default observer(function () { + const host = store.record; return ( store.detailVisible = false} visible={store.detailVisible}> - + 基本信息} column={1}> + {host.name} + {host.username}@{host.hostname} + {host.port} + {host.pkey ? '是' : '否'} + {host.desc} + + + 腾讯云/华北区 + 腾讯云/测试环境/电商商城系统 + 腾讯云/测试环境/订单后台系统 + + + ) }) \ No newline at end of file diff --git a/spug_web/src/pages/host/Group.js b/spug_web/src/pages/host/Group.js index 743240d..ccf6a48 100644 --- a/spug_web/src/pages/host/Group.js +++ b/spug_web/src/pages/host/Group.js @@ -6,6 +6,14 @@ import React, { useState, useEffect } from 'react'; import { observer } from 'mobx-react'; import { Input, Card, Tree, Dropdown, Menu, Switch, message } from 'antd'; +import { + FolderOutlined, + FolderAddOutlined, + EditOutlined, + DeleteOutlined, + CopyOutlined, + ScissorOutlined +} from '@ant-design/icons'; import { LoadingOutlined } from '@ant-design/icons'; import styles from './index.module.css'; import { http } from 'libs'; @@ -27,10 +35,14 @@ export default observer(function () { const menus = ( setVisible(false)}> - 新建根分组 - 新建子分组 - setAction('edit')}>重命名 - 删除分组 + } onClick={handleAddRoot}>新建根分组 + } onClick={handleAdd}>新建子分组 + } onClick={() => setAction('edit')}>重命名 + + } onClick={() => store.showSelector(true)}>添加至分组 + } onClick={() => store.showSelector(false)}>移动至分组 + + } danger onClick={handleRemove}>删除此分组 ) @@ -132,7 +144,7 @@ export default observer(function () { className={styles.dragBox} autoExpandParent draggable={draggable} - treeData={store.treeData} + treeData={store.allTreeData} titleRender={treeRender} expandedKeys={expands} selectedKeys={[store.group.key]} diff --git a/spug_web/src/pages/host/Selector.js b/spug_web/src/pages/host/Selector.js new file mode 100644 index 0000000..9882a7d --- /dev/null +++ b/spug_web/src/pages/host/Selector.js @@ -0,0 +1,95 @@ +import React, { useEffect, useState } from 'react'; +import { observer } from 'mobx-react'; +import { Modal, Row, Col, Tree, Table } from 'antd'; +import styles from './index.module.css'; +import store from './store'; + +export default observer(function (props) { + const [loading, setLoading] = useState(false); + const [group, setGroup] = useState({}); + const [dataSource, setDataSource] = useState([]); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + + useEffect(() => { + if (!store.selfTreeData.length) { + store.fetchRecords() + store.fetchGroups() + .then(() => setGroup(store.selfTreeData[0])) + } else { + setGroup(store.selfTreeData[0]) + } + }, []) + + useEffect(() => { + if (group.key) { + const records = store.records.filter(x => group.host_ids.includes(x.id)); + setDataSource(records) + } + }, [group]) + + function treeRender(nodeData) { + return ( + + {nodeData.title}{nodeData.host_ids && nodeData.host_ids.length ? `(${nodeData.host_ids.length})` : null} + + ) + } + + function handleClickRow(record) { + const index = selectedRowKeys.indexOf(record.id); + if (index !== -1) { + selectedRowKeys.splice(index, 1) + } else { + selectedRowKeys.push(record.id) + } + setSelectedRowKeys([...selectedRowKeys]) + } + + function handleSubmit() { + if (props.onOk) { + setLoading(true) + props.onOk(group, selectedRowKeys) + .then(props.onCancel, () => setLoading(false)) + } + } + + return ( + + + + setGroup(node)} + /> + + + { + return { + onClick: () => handleClickRow(record) + } + }} + rowSelection={{ + selectedRowKeys, + onChange: (keys) => setSelectedRowKeys(keys) + }}> + a.name.localeCompare(b.name)}/> + a.name.localeCompare(b.name)}/> + +
+ +
+
+ ) +}) \ No newline at end of file diff --git a/spug_web/src/pages/host/index.js b/spug_web/src/pages/host/index.js index a419faa..a68d155 100644 --- a/spug_web/src/pages/host/index.js +++ b/spug_web/src/pages/host/index.js @@ -12,6 +12,7 @@ import ComTable from './Table'; import ComForm from './Form'; import ComImport from './Import'; import Detail from './Detail'; +import Selector from './Selector'; import store from './store'; export default observer(function () { @@ -34,6 +35,7 @@ export default observer(function () { {store.formVisible && } {store.importVisible && } + {store.selectorVisible && store.selectorVisible = false} onOk={store.updateGroup}/>} ); }) diff --git a/spug_web/src/pages/host/store.js b/spug_web/src/pages/host/store.js index 8365888..281ee8d 100644 --- a/spug_web/src/pages/host/store.js +++ b/spug_web/src/pages/host/store.js @@ -4,7 +4,9 @@ * Released under the AGPL-3.0 License. */ import { observable, computed } from 'mobx'; +import { message } from 'antd'; import http from 'libs/http'; +import lds from 'lodash'; class Store { @observable records = []; @@ -13,15 +15,47 @@ class Store { @observable group = {}; @observable record = {}; @observable idMap = {}; + @observable addByCopy = true; @observable grpFetching = true; @observable isFetching = false; @observable formVisible = false; @observable importVisible = false; @observable detailVisible = false; + @observable selectorVisible = false; @observable f_name; @observable f_host; + @computed get counter() { + const counter = {}; + for (let host of this.records) { + for (let id of host.group_ids) { + if (counter[id]) { + counter[id].push(host.id) + } else { + counter[id] = [host.id] + } + } + } + return counter + } + + @computed get selfTreeData() { + const treeData = lds.cloneDeep(this.treeData); + for (let item of treeData) { + this._updateCounter(item, true) + } + return treeData + } + + @computed get allTreeData() { + const treeData = lds.cloneDeep(this.treeData); + for (let item of treeData) { + this._updateCounter(item, false) + } + return treeData + } + @computed get dataSource() { let records = []; if (this.group.host_ids) records = this.records.filter(x => this.group.host_ids.includes(x.id)); @@ -38,23 +72,30 @@ class Store { for (let item of this.records) { this.idMap[item.id] = item } - this._updateGroupCount() }) .finally(() => this.isFetching = false) }; fetchGroups = () => { this.grpFetching = true; - http.get('/api/host/group/') + return http.get('/api/host/group/') .then(res => { this.treeData = res.treeData; this.groups = res.groups; if (!this.group.key) this.group = this.treeData[0]; - this._updateGroupCount() }) .finally(() => this.grpFetching = false) } + updateGroup = (group, host_ids) => { + const form = {host_ids, s_group_id: group.key, t_group_id: this.group.key, is_copy: this.addByCopy}; + return http.patch('/api/host/', form) + .then(() => { + message.success('操作成功'); + this.fetchRecords() + }) + } + showForm = (info = {}) => { this.formVisible = true; this.record = info @@ -65,31 +106,18 @@ class Store { this.detailVisible = true; } - _updateGroupCount = () => { - if (this.treeData.length && this.records.length) { - const counter = {}; - for (let host of this.records) { - for (let id of host.group_ids) { - if (counter[id]) { - counter[id].push(host.id) - } else { - counter[id] = [host.id] - } - } - } - for (let item of this.treeData) { - this._updateCount(counter, item) - } - } + showSelector = (addByCopy) => { + this.addByCopy = addByCopy; + this.selectorVisible = true; } - _updateCount = (counter, item) => { - let host_ids = counter[item.key] || []; + _updateCounter = (item, isSelf) => { + let host_ids = this.counter[item.key] || []; for (let child of item.children) { - host_ids = host_ids.concat(this._updateCount(counter, child)) + const ids = this._updateCounter(child, isSelf) + if (!isSelf) host_ids = host_ids.concat(ids) } item.host_ids = Array.from(new Set(host_ids)); - if (item.key === this.group.key) this.group = item; return item.host_ids } }