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): def merge_children(data, prefix, childes):
prefix = f'{prefix}/' if prefix else ''
for item in childes: for item in childes:
name = f'{prefix}/{item["title"]}' name = f'{prefix}{item["title"]}'
item['name'] = name item['name'] = name
if item['children']: if item['children']:
merge_children(data, name, 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) name = models.CharField(max_length=20)
parent_id = models.IntegerField(default=0) parent_id = models.IntegerField(default=0)
sort_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): def to_view(self):
return { return {
@ -59,11 +59,3 @@ class Group(models.Model, ModelMixin):
class Meta: class Meta:
db_table = 'host_groups' db_table = 'host_groups'
ordering = ('-sort_id',) 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 django.http.response import HttpResponseBadRequest
from libs import json_response, JsonParser, Argument from libs import json_response, JsonParser, Argument
from apps.setting.utils import AppSetting 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.app.models import Deploy
from apps.schedule.models import Task from apps.schedule.models import Task
from apps.monitor.models import Detection from apps.monitor.models import Detection
@ -26,7 +26,7 @@ class HostView(View):
return json_response(error='无权访问该主机,请联系管理员') return json_response(error='无权访问该主机,请联系管理员')
return json_response(Host.objects.get(pk=host_id)) 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)} 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) hosts[rel.host_id]['group_ids'].append(rel.group_id)
return json_response(list(hosts.values())) return json_response(list(hosts.values()))
@ -61,15 +61,17 @@ class HostView(View):
def patch(self, request): def patch(self, request):
form, error = JsonParser( form, error = JsonParser(
Argument('id', type=int, required=False), Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择主机'),
Argument('zone', help='请输入主机类别') Argument('s_group_id', type=int, help='参数错误'),
Argument('t_group_id', type=int, help='参数错误'),
Argument('is_copy', type=bool, help='参数错误'),
).parse(request.body) ).parse(request.body)
if error is None: if error is None:
host = Host.objects.filter(pk=form.id).first() s_group = Group.objects.get(pk=form.s_group_id)
if not host: t_group = Group.objects.get(pk=form.t_group_id)
return json_response(error='未找到指定主机') t_group.hosts.add(*form.host_ids)
count = Host.objects.filter(zone=host.zone, deleted_by_id__isnull=True).update(zone=form.zone) if not form.is_copy:
return json_response(count) s_group.hosts.remove(*form.host_ids)
return json_response(error=error) return json_response(error=error)
def delete(self, request): def delete(self, request):

View File

@ -1,17 +1,31 @@
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Drawer} from 'antd'; import { Drawer, Descriptions, List } from 'antd';
import store from './store'; import store from './store';
export default observer(function () { export default observer(function () {
const host = store.record;
return ( return (
<Drawer <Drawer
width={500} width={500}
title={store.record.name} title={host.name}
placement="right" placement="right"
onClose={() => store.detailVisible = false} onClose={() => store.detailVisible = false}
visible={store.detailVisible}> 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> </Drawer>
) )
}) })

View File

@ -6,6 +6,14 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Input, Card, Tree, Dropdown, Menu, Switch, message } from 'antd'; 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 { LoadingOutlined } from '@ant-design/icons';
import styles from './index.module.css'; import styles from './index.module.css';
import { http } from 'libs'; import { http } from 'libs';
@ -27,10 +35,14 @@ export default observer(function () {
const menus = ( const menus = (
<Menu onClick={() => setVisible(false)}> <Menu onClick={() => setVisible(false)}>
<Menu.Item key="0" onClick={handleAddRoot}>新建根分组</Menu.Item> <Menu.Item key="0" icon={<FolderOutlined/>} onClick={handleAddRoot}>新建根分组</Menu.Item>
<Menu.Item key="1" onClick={handleAdd}>新建子分组</Menu.Item> <Menu.Item key="1" icon={<FolderAddOutlined/>} onClick={handleAdd}>新建子分组</Menu.Item>
<Menu.Item key="2" onClick={() => setAction('edit')}>重命名</Menu.Item> <Menu.Item key="2" icon={<EditOutlined/>} onClick={() => setAction('edit')}>重命名</Menu.Item>
<Menu.Item key="3" danger onClick={handleRemove}>删除分组</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> </Menu>
) )
@ -132,7 +144,7 @@ export default observer(function () {
className={styles.dragBox} className={styles.dragBox}
autoExpandParent autoExpandParent
draggable={draggable} draggable={draggable}
treeData={store.treeData} treeData={store.allTreeData}
titleRender={treeRender} titleRender={treeRender}
expandedKeys={expands} expandedKeys={expands}
selectedKeys={[store.group.key]} 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 ComForm from './Form';
import ComImport from './Import'; import ComImport from './Import';
import Detail from './Detail'; import Detail from './Detail';
import Selector from './Selector';
import store from './store'; import store from './store';
export default observer(function () { export default observer(function () {
@ -34,6 +35,7 @@ export default observer(function () {
<Detail/> <Detail/>
{store.formVisible && <ComForm/>} {store.formVisible && <ComForm/>}
{store.importVisible && <ComImport/>} {store.importVisible && <ComImport/>}
{store.selectorVisible && <Selector onCancel={() => store.selectorVisible = false} onOk={store.updateGroup}/>}
</AuthDiv> </AuthDiv>
); );
}) })

View File

@ -4,7 +4,9 @@
* Released under the AGPL-3.0 License. * Released under the AGPL-3.0 License.
*/ */
import { observable, computed } from 'mobx'; import { observable, computed } from 'mobx';
import { message } from 'antd';
import http from 'libs/http'; import http from 'libs/http';
import lds from 'lodash';
class Store { class Store {
@observable records = []; @observable records = [];
@ -13,15 +15,47 @@ class Store {
@observable group = {}; @observable group = {};
@observable record = {}; @observable record = {};
@observable idMap = {}; @observable idMap = {};
@observable addByCopy = true;
@observable grpFetching = true; @observable grpFetching = true;
@observable isFetching = false; @observable isFetching = false;
@observable formVisible = false; @observable formVisible = false;
@observable importVisible = false; @observable importVisible = false;
@observable detailVisible = false; @observable detailVisible = false;
@observable selectorVisible = false;
@observable f_name; @observable f_name;
@observable f_host; @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() { @computed get dataSource() {
let records = []; let records = [];
if (this.group.host_ids) records = this.records.filter(x => this.group.host_ids.includes(x.id)); 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) { for (let item of this.records) {
this.idMap[item.id] = item this.idMap[item.id] = item
} }
this._updateGroupCount()
}) })
.finally(() => this.isFetching = false) .finally(() => this.isFetching = false)
}; };
fetchGroups = () => { fetchGroups = () => {
this.grpFetching = true; this.grpFetching = true;
http.get('/api/host/group/') return http.get('/api/host/group/')
.then(res => { .then(res => {
this.treeData = res.treeData; this.treeData = res.treeData;
this.groups = res.groups; this.groups = res.groups;
if (!this.group.key) this.group = this.treeData[0]; if (!this.group.key) this.group = this.treeData[0];
this._updateGroupCount()
}) })
.finally(() => this.grpFetching = false) .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 = {}) => { showForm = (info = {}) => {
this.formVisible = true; this.formVisible = true;
this.record = info this.record = info
@ -65,31 +106,18 @@ class Store {
this.detailVisible = true; this.detailVisible = true;
} }
_updateGroupCount = () => { showSelector = (addByCopy) => {
if (this.treeData.length && this.records.length) { this.addByCopy = addByCopy;
const counter = {}; this.selectorVisible = true;
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)
}
}
} }
_updateCount = (counter, item) => { _updateCounter = (item, isSelf) => {
let host_ids = counter[item.key] || []; let host_ids = this.counter[item.key] || [];
for (let child of item.children) { 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)); item.host_ids = Array.from(new Set(host_ids));
if (item.key === this.group.key) this.group = item;
return item.host_ids return item.host_ids
} }
} }