mirror of https://github.com/openspug/spug
add host group
parent
d11ee1258d
commit
591a4d1659
|
@ -0,0 +1,96 @@
|
||||||
|
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||||
|
# Copyright: (c) <spug.dev@gmail.com>
|
||||||
|
# Released under the AGPL-3.0 License.
|
||||||
|
from django.views.generic import View
|
||||||
|
from django.db.models import F
|
||||||
|
from libs import json_response, JsonParser, Argument
|
||||||
|
from apps.host.models import Group
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_children(data):
|
||||||
|
if data:
|
||||||
|
sub_data = dict()
|
||||||
|
for item in Group.objects.filter(parent_id__in=data.keys()):
|
||||||
|
tmp = item.to_view()
|
||||||
|
sub_data[item.id] = tmp
|
||||||
|
data[item.parent_id]['children'].append(tmp)
|
||||||
|
return fetch_children(sub_data)
|
||||||
|
|
||||||
|
|
||||||
|
def merge_children(data, prefix, childes):
|
||||||
|
for item in childes:
|
||||||
|
name = f'{prefix}/{item["title"]}'
|
||||||
|
if item['children']:
|
||||||
|
merge_children(data, name, item['children'])
|
||||||
|
else:
|
||||||
|
data.append({'id': item['key'], 'name': name})
|
||||||
|
|
||||||
|
|
||||||
|
class GroupView(View):
|
||||||
|
def get(self, request):
|
||||||
|
data, data2 = dict(), []
|
||||||
|
for item in Group.objects.filter(parent_id=0):
|
||||||
|
data[item.id] = item.to_view()
|
||||||
|
fetch_children(data)
|
||||||
|
if not data:
|
||||||
|
grp = Group.objects.create(name='Default', sort_id=1)
|
||||||
|
data[grp.id] = grp.to_view()
|
||||||
|
|
||||||
|
merge_children(data2, '', data.values())
|
||||||
|
return json_response({'treeData': list(data.values()), 'groups': data2})
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
form, error = JsonParser(
|
||||||
|
Argument('id', type=int, required=False),
|
||||||
|
Argument('parent_id', type=int, default=0),
|
||||||
|
Argument('name', help='请输入分组名称')
|
||||||
|
).parse(request.body)
|
||||||
|
if error is None:
|
||||||
|
if form.id:
|
||||||
|
Group.objects.filter(pk=form.id).update(name=form.name)
|
||||||
|
else:
|
||||||
|
group = Group.objects.create(**form)
|
||||||
|
group.sort_id = group.id
|
||||||
|
group.save()
|
||||||
|
return json_response(error=error)
|
||||||
|
|
||||||
|
def patch(self, request):
|
||||||
|
form, error = JsonParser(
|
||||||
|
Argument('s_id', type=int, help='参数错误'),
|
||||||
|
Argument('d_id', type=int, help='参数错误'),
|
||||||
|
Argument('action', type=int, help='参数错误')
|
||||||
|
).parse(request.body)
|
||||||
|
if error is None:
|
||||||
|
src = Group.objects.get(pk=form.s_id)
|
||||||
|
dst = Group.objects.get(pk=form.d_id)
|
||||||
|
if form.action == 0:
|
||||||
|
src.parent_id = dst.id
|
||||||
|
src.save()
|
||||||
|
return json_response()
|
||||||
|
src.parent_id = dst.parent_id
|
||||||
|
if src.sort_id > dst.sort_id:
|
||||||
|
if form.action == -1:
|
||||||
|
dst = Group.objects.filter(sort_id__gt=dst.sort_id).last()
|
||||||
|
Group.objects.filter(sort_id__lt=src.sort_id, sort_id__gte=dst.sort_id).update(sort_id=F('sort_id') + 1)
|
||||||
|
else:
|
||||||
|
if form.action == 1:
|
||||||
|
dst = Group.objects.filter(sort_id__lt=dst.sort_id).first()
|
||||||
|
Group.objects.filter(sort_id__lte=dst.sort_id, sort_id__gt=src.sort_id).update(sort_id=F('sort_id') - 1)
|
||||||
|
src.sort_id = dst.sort_id
|
||||||
|
src.save()
|
||||||
|
return json_response(error=error)
|
||||||
|
|
||||||
|
def delete(self, request):
|
||||||
|
form, error = JsonParser(
|
||||||
|
Argument('id', type=int, help='参数错误')
|
||||||
|
).parse(request.GET)
|
||||||
|
if error is None:
|
||||||
|
group = Group.objects.filter(pk=form.id).first()
|
||||||
|
if not group:
|
||||||
|
return json_response(error='未找到指定分组')
|
||||||
|
if Group.objects.filter(parent_id=group.id).exists():
|
||||||
|
return json_response(error='请移除子分组后再尝试删除')
|
||||||
|
if group.hosts.exists():
|
||||||
|
return json_response(error='请移除分组下的主机后再尝试删除')
|
||||||
|
group.delete()
|
||||||
|
return json_response(error=error)
|
|
@ -30,9 +30,40 @@ class Host(models.Model, ModelMixin):
|
||||||
pkey = pkey or self.private_key
|
pkey = pkey or self.private_key
|
||||||
return SSH(self.hostname, self.port, self.username, pkey)
|
return SSH(self.hostname, self.port, self.username, pkey)
|
||||||
|
|
||||||
|
def to_view(self):
|
||||||
|
tmp = self.to_dict()
|
||||||
|
tmp['group_ids'] = []
|
||||||
|
return tmp
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Host %r>' % self.name
|
return '<Host %r>' % self.name
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'hosts'
|
db_table = 'hosts'
|
||||||
ordering = ('-id',)
|
ordering = ('-id',)
|
||||||
|
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
def to_view(self):
|
||||||
|
return {
|
||||||
|
'key': self.id,
|
||||||
|
'title': self.name,
|
||||||
|
'children': []
|
||||||
|
}
|
||||||
|
|
||||||
|
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'
|
||||||
|
|
|
@ -3,10 +3,12 @@
|
||||||
# Released under the AGPL-3.0 License.
|
# Released under the AGPL-3.0 License.
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import *
|
from apps.host.views import *
|
||||||
|
from apps.host.group import GroupView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', HostView.as_view()),
|
path('', HostView.as_view()),
|
||||||
|
path('group/', GroupView.as_view()),
|
||||||
path('import/', post_import),
|
path('import/', post_import),
|
||||||
path('parse/', post_parse),
|
path('parse/', post_parse),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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
|
from apps.host.models import Host, HostGroupRel
|
||||||
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
|
||||||
|
@ -25,10 +25,10 @@ class HostView(View):
|
||||||
if not request.user.has_host_perm(host_id):
|
if not request.user.has_host_perm(host_id):
|
||||||
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 = 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)}
|
||||||
zones = [x['zone'] for x in hosts.order_by('zone').values('zone').distinct()]
|
for rel in HostGroupRel.objects.all():
|
||||||
perms = [x.id for x in hosts] if request.user.is_supper else request.user.host_perms
|
hosts[rel.host_id]['group_ids'].append(rel.group_id)
|
||||||
return json_response({'zones': zones, 'hosts': [x.to_dict() for x in hosts], 'perms': perms})
|
return json_response(list(hosts.values()))
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
form, error = JsonParser(
|
form, error = JsonParser(
|
||||||
|
|
|
@ -4,10 +4,31 @@
|
||||||
* Released under the AGPL-3.0 License.
|
* Released under the AGPL-3.0 License.
|
||||||
*/
|
*/
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Table, Space, Divider, Popover, Checkbox, Button } from 'antd';
|
import { Table, Space, Divider, Popover, Checkbox, Button, Input, Select } from 'antd';
|
||||||
import { ReloadOutlined, SettingOutlined, FullscreenOutlined } from '@ant-design/icons';
|
import { ReloadOutlined, SettingOutlined, FullscreenOutlined, SearchOutlined } from '@ant-design/icons';
|
||||||
import styles from './index.module.less';
|
import styles from './index.module.less';
|
||||||
|
|
||||||
|
function Search(props) {
|
||||||
|
let keys = props.keys || [''];
|
||||||
|
keys = keys.map(x => x.split('/'));
|
||||||
|
const [key, setKey] = useState(keys[0][0]);
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
allowClear
|
||||||
|
style={{width: '280px'}}
|
||||||
|
placeholder="输入检索"
|
||||||
|
prefix={<SearchOutlined style={{color: '#c0c0c0'}}/>}
|
||||||
|
onChange={e => props.onChange(key, e.target.value)}
|
||||||
|
addonBefore={(
|
||||||
|
<Select value={key} onChange={setKey}>
|
||||||
|
{keys.map(item => (
|
||||||
|
<Select.Option key={item[0]} value={item[0]}>{item[1]}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function Footer(props) {
|
function Footer(props) {
|
||||||
const actions = props.actions || [];
|
const actions = props.actions || [];
|
||||||
const length = props.selected.length;
|
const length = props.selected.length;
|
||||||
|
@ -145,4 +166,5 @@ function TableCard(props) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TableCard.Search = Search;
|
||||||
export default TableCard
|
export default TableCard
|
|
@ -0,0 +1,150 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||||
|
* Copyright (c) <spug.dev@gmail.com>
|
||||||
|
* Released under the AGPL-3.0 License.
|
||||||
|
*/
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { Input, Card, Tree, Dropdown, Menu, Switch, message } from 'antd';
|
||||||
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
|
import styles from './index.module.css';
|
||||||
|
import { http } from 'libs';
|
||||||
|
import store from './store';
|
||||||
|
import lds from 'lodash';
|
||||||
|
|
||||||
|
export default observer(function () {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [draggable, setDraggable] = useState(false);
|
||||||
|
const [action, setAction] = useState('');
|
||||||
|
const [expands, setExpands] = useState([]);
|
||||||
|
const [treeData, setTreeData] = useState();
|
||||||
|
const [bakTreeData, setBakTreeData] = useState();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loading) store.fetchGroups()
|
||||||
|
}, [loading])
|
||||||
|
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
if (!store.group.title) {
|
||||||
|
return message.error('请输入分组名称')
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
const {key, parent_id, title} = store.group;
|
||||||
|
http.post('/api/host/group/', {id: key || undefined, parent_id, name: title})
|
||||||
|
.then(() => setAction(''))
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRemove() {
|
||||||
|
setAction('del');
|
||||||
|
setLoading(true);
|
||||||
|
http.delete('/api/host/group/', {params: {id: store.group.key}})
|
||||||
|
.finally(() => {
|
||||||
|
setAction('');
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddRoot() {
|
||||||
|
setBakTreeData(lds.cloneDeep(treeData));
|
||||||
|
const current = {key: 0, parent_id: 0, title: ''};
|
||||||
|
treeData.unshift(current);
|
||||||
|
setTreeData(lds.cloneDeep(treeData));
|
||||||
|
store.group = current;
|
||||||
|
setAction('edit')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAdd() {
|
||||||
|
setBakTreeData(lds.cloneDeep(treeData));
|
||||||
|
const current = {key: 0, parent_id: store.group.key, title: ''};
|
||||||
|
store.group.children.unshift(current);
|
||||||
|
setTreeData(lds.cloneDeep(treeData));
|
||||||
|
if (!expands.includes(store.group.key)) setExpands([store.group.key, ...expands]);
|
||||||
|
store.group = current;
|
||||||
|
setAction('edit')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrag(v) {
|
||||||
|
setLoading(true);
|
||||||
|
const pos = v.node.pos.split('-');
|
||||||
|
const dropPosition = v.dropPosition - Number(pos[pos.length - 1]);
|
||||||
|
http.patch('/api/host/group/', {s_id: v.dragNode.key, d_id: v.node.key, action: dropPosition})
|
||||||
|
.then(() => setLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBlur() {
|
||||||
|
if (store.group.key === 0) {
|
||||||
|
setTreeData(bakTreeData)
|
||||||
|
}
|
||||||
|
setAction('')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleExpand(keys, {_, node}) {
|
||||||
|
if (node.children.length > 0) {
|
||||||
|
setExpands(keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function treeRender(nodeData) {
|
||||||
|
if (action === 'edit' && nodeData.key === store.group.key) {
|
||||||
|
return <Input
|
||||||
|
autoFocus
|
||||||
|
size="small"
|
||||||
|
style={{width: 'calc(100% - 24px)'}}
|
||||||
|
defaultValue={nodeData.title}
|
||||||
|
suffix={loading ? <LoadingOutlined/> : <span/>}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onChange={e => store.group.title = e.target.value}
|
||||||
|
onPressEnter={handleSubmit}/>
|
||||||
|
} else if (action === 'del' && nodeData.key === store.group.key) {
|
||||||
|
return <LoadingOutlined style={{marginLeft: '4px'}}/>
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<span style={{lineHeight: '24px'}}>
|
||||||
|
{nodeData.title}{nodeData.host_ids && nodeData.host_ids.length ? `(${nodeData.host_ids.length})` : null}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title="分组列表"
|
||||||
|
loading={store.grpFetching}
|
||||||
|
extra={<Switch checked={draggable} onChange={setDraggable} checkedChildren="拖拽" unCheckedChildren="浏览"/>}>
|
||||||
|
<Dropdown
|
||||||
|
overlay={menus}
|
||||||
|
visible={visible}
|
||||||
|
trigger={['contextMenu']}
|
||||||
|
onVisibleChange={v => v || setVisible(v)}>
|
||||||
|
<Tree.DirectoryTree
|
||||||
|
className={styles.dragBox}
|
||||||
|
autoExpandParent
|
||||||
|
draggable={draggable}
|
||||||
|
treeData={store.treeData}
|
||||||
|
titleRender={treeRender}
|
||||||
|
expandedKeys={expands}
|
||||||
|
selectedKeys={[store.group.key]}
|
||||||
|
onSelect={(_, {node}) => store.group = node}
|
||||||
|
onExpand={handleExpand}
|
||||||
|
onDrop={handleDrag}
|
||||||
|
onRightClick={v => {
|
||||||
|
store.group = v.node;
|
||||||
|
setVisible(true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
})
|
|
@ -36,22 +36,12 @@ class ComTable extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let data = store.permRecords;
|
|
||||||
if (store.f_name) {
|
|
||||||
data = data.filter(item => item['name'].toLowerCase().includes(store.f_name.toLowerCase()))
|
|
||||||
}
|
|
||||||
if (store.f_zone) {
|
|
||||||
data = data.filter(item => item['zone'].toLowerCase().includes(store.f_zone.toLowerCase()))
|
|
||||||
}
|
|
||||||
if (store.f_host) {
|
|
||||||
data = data.filter(item => item['hostname'].toLowerCase().includes(store.f_host.toLowerCase()))
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<TableCard
|
<TableCard
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
title="主机列表"
|
title={<TableCard.Search keys={['f_name/主机名称', 'f_host/连接地址']} onChange={(k, v) => store[k] = v}/>}
|
||||||
loading={store.isFetching}
|
loading={store.isFetching}
|
||||||
dataSource={data}
|
dataSource={store.dataSource}
|
||||||
onReload={store.fetchRecords}
|
onReload={store.fetchRecords}
|
||||||
actions={[
|
actions={[
|
||||||
<AuthButton
|
<AuthButton
|
||||||
|
@ -72,11 +62,10 @@ class ComTable extends React.Component {
|
||||||
showTotal: total => `共 ${total} 条`,
|
showTotal: total => `共 ${total} 条`,
|
||||||
pageSizeOptions: ['10', '20', '50', '100']
|
pageSizeOptions: ['10', '20', '50', '100']
|
||||||
}}>
|
}}>
|
||||||
<Table.Column title="类别" dataIndex="zone"/>
|
<Table.Column showSorterTooltip={false} title="主机名称" dataIndex="name" sorter={(a, b) => a.name.localeCompare(b.name)}/>
|
||||||
<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 title="连接地址" dataIndex="hostname" sorter={(a, b) => a.name.localeCompare(b.name)}/>
|
||||||
<Table.Column width={100} title="端口" dataIndex="port"/>
|
<Table.Column hide width={100} title="端口" dataIndex="port"/>
|
||||||
<Table.Column ellipsis title="备注信息" dataIndex="desc"/>
|
<Table.Column hide ellipsis title="备注信息" dataIndex="desc"/>
|
||||||
{hasPermission('host.host.edit|host.host.del|host.host.console') && (
|
{hasPermission('host.host.edit|host.host.del|host.host.console') && (
|
||||||
<Table.Column width={200} title="操作" render={info => (
|
<Table.Column width={200} title="操作" render={info => (
|
||||||
<Action>
|
<Action>
|
||||||
|
|
|
@ -5,8 +5,9 @@
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { Input, Select } from 'antd';
|
import { Row, Col } from 'antd';
|
||||||
import { SearchForm, AuthDiv, Breadcrumb } from 'components';
|
import { AuthDiv, Breadcrumb } from 'components';
|
||||||
|
import Group from './Group';
|
||||||
import ComTable from './Table';
|
import ComTable from './Table';
|
||||||
import ComForm from './Form';
|
import ComForm from './Form';
|
||||||
import ComImport from './Import';
|
import ComImport from './Import';
|
||||||
|
@ -19,22 +20,16 @@ export default observer(function () {
|
||||||
<Breadcrumb.Item>首页</Breadcrumb.Item>
|
<Breadcrumb.Item>首页</Breadcrumb.Item>
|
||||||
<Breadcrumb.Item>主机管理</Breadcrumb.Item>
|
<Breadcrumb.Item>主机管理</Breadcrumb.Item>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
<SearchForm>
|
|
||||||
<SearchForm.Item span={6} title="主机类别">
|
<Row gutter={12}>
|
||||||
<Select allowClear placeholder="请选择" value={store.f_zone} onChange={v => store.f_zone = v}>
|
<Col span={6}>
|
||||||
{store.zones.map(item => (
|
<Group/>
|
||||||
<Select.Option value={item} key={item}>{item}</Select.Option>
|
</Col>
|
||||||
))}
|
<Col span={18}>
|
||||||
</Select>
|
<ComTable/>
|
||||||
</SearchForm.Item>
|
</Col>
|
||||||
<SearchForm.Item span={6} title="主机别名">
|
</Row>
|
||||||
<Input allowClear value={store.f_name} onChange={e => store.f_name = e.target.value} placeholder="请输入"/>
|
|
||||||
</SearchForm.Item>
|
|
||||||
<SearchForm.Item span={6} title="连接地址">
|
|
||||||
<Input allowClear value={store.f_host} onChange={e => store.f_host = e.target.value} placeholder="请输入"/>
|
|
||||||
</SearchForm.Item>
|
|
||||||
</SearchForm>
|
|
||||||
<ComTable/>
|
|
||||||
{store.formVisible && <ComForm/>}
|
{store.formVisible && <ComForm/>}
|
||||||
{store.importVisible && <ComImport/>}
|
{store.importVisible && <ComImport/>}
|
||||||
</AuthDiv>
|
</AuthDiv>
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
.dragBox :global(.ant-tree-node-content-wrapper) {
|
||||||
|
border-top: 2px transparent solid;
|
||||||
|
border-bottom: 2px transparent solid;
|
||||||
|
}
|
|
@ -3,41 +3,88 @@
|
||||||
* Copyright (c) <spug.dev@gmail.com>
|
* Copyright (c) <spug.dev@gmail.com>
|
||||||
* Released under the AGPL-3.0 License.
|
* Released under the AGPL-3.0 License.
|
||||||
*/
|
*/
|
||||||
import { observable } from "mobx";
|
import { observable, computed } from 'mobx';
|
||||||
import http from 'libs/http';
|
import http from 'libs/http';
|
||||||
|
|
||||||
class Store {
|
class Store {
|
||||||
@observable records = [];
|
@observable records = [];
|
||||||
@observable zones = [];
|
@observable treeData = [];
|
||||||
@observable permRecords = [];
|
@observable groups = [];
|
||||||
|
@observable group = {};
|
||||||
@observable record = {};
|
@observable record = {};
|
||||||
@observable idMap = {};
|
@observable idMap = {};
|
||||||
|
@observable grpFetching = true;
|
||||||
@observable isFetching = false;
|
@observable isFetching = false;
|
||||||
@observable formVisible = false;
|
@observable formVisible = false;
|
||||||
@observable importVisible = false;
|
@observable importVisible = false;
|
||||||
|
|
||||||
@observable f_name;
|
@observable f_name;
|
||||||
@observable f_zone;
|
|
||||||
@observable f_host;
|
@observable f_host;
|
||||||
|
|
||||||
|
@computed get dataSource() {
|
||||||
|
let records = [];
|
||||||
|
if (this.group.host_ids) records = this.records.filter(x => this.group.host_ids.includes(x.id));
|
||||||
|
if (this.f_name) records = records.filter(x => x.name.toLowerCase().includes(this.f_name.toLowerCase()));
|
||||||
|
if (this.f_host) records = records.filter(x => x.hostname.toLowerCase().includes(this.f_host.toLowerCase()));
|
||||||
|
return records
|
||||||
|
}
|
||||||
|
|
||||||
fetchRecords = () => {
|
fetchRecords = () => {
|
||||||
this.isFetching = true;
|
this.isFetching = true;
|
||||||
return http.get('/api/host/')
|
return http.get('/api/host/')
|
||||||
.then(({hosts, zones, perms}) => {
|
.then(res => {
|
||||||
this.records = hosts;
|
this.records = res;
|
||||||
this.zones = zones;
|
for (let item of this.records) {
|
||||||
this.permRecords = hosts.filter(item => perms.includes(item.id));
|
|
||||||
for (let item of hosts) {
|
|
||||||
this.idMap[item.id] = item
|
this.idMap[item.id] = item
|
||||||
}
|
}
|
||||||
|
this._updateGroupCount()
|
||||||
})
|
})
|
||||||
.finally(() => this.isFetching = false)
|
.finally(() => this.isFetching = false)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fetchGroups = () => {
|
||||||
|
this.grpFetching = true;
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
showForm = (info = {}) => {
|
showForm = (info = {}) => {
|
||||||
this.formVisible = true;
|
this.formVisible = true;
|
||||||
this.record = info
|
this.record = info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateCount = (counter, item) => {
|
||||||
|
let host_ids = counter[item.key] || [];
|
||||||
|
for (let child of item.children) {
|
||||||
|
host_ids = host_ids.concat(this._updateCount(counter, child))
|
||||||
|
}
|
||||||
|
item.host_ids = Array.from(new Set(host_ids));
|
||||||
|
return item.host_ids
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Store()
|
export default new Store()
|
||||||
|
|
Loading…
Reference in New Issue