add host group migrate

pull/289/head
vapao 2020-12-20 00:26:29 +08:00
parent 8917b1fe76
commit cc22de094b
8 changed files with 196 additions and 50 deletions

View File

@ -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'])

View File

@ -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'

View File

@ -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):

View File

@ -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 (
<Drawer
width={500}
title={store.record.name}
title={host.name}
placement="right"
onClose={() => store.detailVisible = false}
visible={store.detailVisible}>
<Descriptions bordered size="small" title={<span style={{fontWeight: 500}}>基本信息</span>} column={1}>
<Descriptions.Item label="主机名称">{host.name}</Descriptions.Item>
<Descriptions.Item label="连接地址">{host.username}@{host.hostname}</Descriptions.Item>
<Descriptions.Item label="连接端口">{host.port}</Descriptions.Item>
<Descriptions.Item label="独立密钥">{host.pkey ? '是' : '否'}</Descriptions.Item>
<Descriptions.Item label="描述信息">{host.desc}</Descriptions.Item>
<Descriptions.Item label="所属分组">
<List >
<List.Item>腾讯云/华北区</List.Item>
<List.Item>腾讯云/测试环境/电商商城系统</List.Item>
<List.Item>腾讯云/测试环境/订单后台系统</List.Item>
</List>
</Descriptions.Item>
</Descriptions>
</Drawer>
)
})

View File

@ -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 = (
<Menu onClick={() => setVisible(false)}>
<Menu.Item key="0" onClick={handleAddRoot}>新建根分组</Menu.Item>
<Menu.Item key="1" onClick={handleAdd}>新建子分组</Menu.Item>
<Menu.Item key="2" onClick={() => setAction('edit')}>重命名</Menu.Item>
<Menu.Item key="3" danger onClick={handleRemove}>删除分组</Menu.Item>
<Menu.Item key="0" icon={<FolderOutlined/>} onClick={handleAddRoot}>新建根分组</Menu.Item>
<Menu.Item key="1" icon={<FolderAddOutlined/>} onClick={handleAdd}>新建子分组</Menu.Item>
<Menu.Item key="2" icon={<EditOutlined/>} onClick={() => setAction('edit')}>重命名</Menu.Item>
<Menu.Divider/>
<Menu.Item key="3" icon={<CopyOutlined/>} onClick={() => store.showSelector(true)}>添加至分组</Menu.Item>
<Menu.Item key="4" icon={<ScissorOutlined/>} onClick={() => store.showSelector(false)}>移动至分组</Menu.Item>
<Menu.Divider/>
<Menu.Item key="5" icon={<DeleteOutlined/>} danger onClick={handleRemove}>删除此分组</Menu.Item>
</Menu>
)
@ -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]}

View File

@ -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 (
<span style={{lineHeight: '24px'}}>
{nodeData.title}{nodeData.host_ids && nodeData.host_ids.length ? `${nodeData.host_ids.length}` : null}
</span>
)
}
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 (
<Modal
visible
width="70%"
title={props.title || '主机列表'}
onOk={handleSubmit}
confirmLoading={loading}
onCancel={props.onCancel}>
<Row gutter={12}>
<Col span={6}>
<Tree.DirectoryTree
selectedKeys={[group.key]}
className={styles.dragBox}
treeData={store.selfTreeData}
titleRender={treeRender}
onSelect={(_, {node}) => setGroup(node)}
/>
</Col>
<Col span={18}>
<Table
rowKey="id"
dataSource={dataSource}
onRow={record => {
return {
onClick: () => handleClickRow(record)
}
}}
rowSelection={{
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys)
}}>
<Table.Column title="主机名称" dataIndex="name" sorter={(a, b) => a.name.localeCompare(b.name)}/>
<Table.Column title="连接地址" dataIndex="hostname" sorter={(a, b) => a.name.localeCompare(b.name)}/>
<Table.Column hide ellipsis title="备注信息" dataIndex="desc"/>
</Table>
</Col>
</Row>
</Modal>
)
})

View File

@ -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 () {
<Detail/>
{store.formVisible && <ComForm/>}
{store.importVisible && <ComImport/>}
{store.selectorVisible && <Selector onCancel={() => store.selectorVisible = false} onOk={store.updateGroup}/>}
</AuthDiv>
);
})

View File

@ -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
}
}