mirror of https://github.com/openspug/spug
add host group migrate
parent
8917b1fe76
commit
cc22de094b
|
@ -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'])
|
||||||
|
|
|
@ -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'
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
})
|
})
|
|
@ -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]}
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
})
|
|
@ -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>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue