mirror of https://github.com/openspug/spug
U 优化主机分组支持全局搜索
parent
0b9ab1e379
commit
7872d8cb85
|
@ -5,7 +5,7 @@
|
|||
"dependencies": {
|
||||
"@ant-design/icons": "^4.3.0",
|
||||
"ace-builds": "^1.4.13",
|
||||
"antd": "^4.19.2",
|
||||
"antd": "4.21.4",
|
||||
"axios": "^0.21.0",
|
||||
"bizcharts": "^3.5.9",
|
||||
"history": "^4.10.1",
|
||||
|
@ -45,9 +45,9 @@
|
|||
"devDependencies": {
|
||||
"@babel/plugin-proposal-decorators": "^7.10.5",
|
||||
"customize-cra": "^1.0.0",
|
||||
"http-proxy-middleware": "0.19.2",
|
||||
"less": "^3.12.2",
|
||||
"less-loader": "^7.1.0",
|
||||
"react-app-rewired": "^2.1.6",
|
||||
"http-proxy-middleware": "0.19.2"
|
||||
"react-app-rewired": "^2.1.6"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,15 +39,29 @@ export function clsNames(...args) {
|
|||
return args.filter(x => x).join(' ')
|
||||
}
|
||||
|
||||
export function includes(s, key) {
|
||||
key = key.toLowerCase();
|
||||
if (Array.isArray(s)) {
|
||||
for (let i of s) {
|
||||
if (i && i.toLowerCase().includes(key)) return true
|
||||
function isInclude(s, keys) {
|
||||
if (!s) return false
|
||||
if (Array.isArray(keys)) {
|
||||
for (let k of keys) {
|
||||
k = k.toLowerCase()
|
||||
if (s.toLowerCase().includes(k)) return true
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
return s && s.toLowerCase().includes(key)
|
||||
let k = keys.toLowerCase()
|
||||
return s.toLowerCase().includes(k)
|
||||
}
|
||||
}
|
||||
|
||||
// 字符串包含判断
|
||||
export function includes(s, keys) {
|
||||
if (Array.isArray(s)) {
|
||||
for (let i of s) {
|
||||
if (isInclude(i, keys)) return true
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
return isInclude(s, keys)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import { observer } from 'mobx-react';
|
|||
import { Modal, Form, Input, Select, DatePicker, Button, message } from 'antd';
|
||||
import { LoadingOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
import HostSelector from './HostSelector';
|
||||
import hostStore from 'pages/host/store';
|
||||
import { http, history } from 'libs';
|
||||
import store from './store';
|
||||
import lds from 'lodash';
|
||||
|
@ -44,7 +43,6 @@ export default observer(function () {
|
|||
useEffect(() => {
|
||||
const {app_host_ids, host_ids} = store.record;
|
||||
setHostIds(lds.clone(host_ids || app_host_ids));
|
||||
if (!hostStore.records || hostStore.records.length === 0) hostStore.fetchRecords()
|
||||
fetchVersions()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
|
|
@ -7,7 +7,6 @@ import React, { useState, useEffect } from 'react';
|
|||
import { observer } from 'mobx-react';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import { Modal, Form, Input, Upload, DatePicker, message, Button } from 'antd';
|
||||
import hostStore from 'pages/host/store';
|
||||
import HostSelector from './HostSelector';
|
||||
import { http, clsNames, X_TOKEN } from 'libs';
|
||||
import styles from './index.module.less';
|
||||
|
@ -26,7 +25,6 @@ export default observer(function () {
|
|||
useEffect(() => {
|
||||
const {app_host_ids, host_ids, extra} = store.record;
|
||||
setHostIds(lds.clone(host_ids || app_host_ids));
|
||||
if (!hostStore.records || hostStore.records.length === 0) hostStore.fetchRecords();
|
||||
if (store.record.extra) setFileList([{...extra, uid: '0'}])
|
||||
}, [])
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Modal, Table, Button, Alert } from 'antd';
|
||||
import hostStore from 'pages/host/store';
|
||||
|
@ -7,6 +7,10 @@ import lds from 'lodash';
|
|||
export default observer(function (props) {
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState(props.host_ids || []);
|
||||
|
||||
useEffect(() => {
|
||||
hostStore.initial()
|
||||
}, [])
|
||||
|
||||
function handleClickRow(record) {
|
||||
const index = selectedRowKeys.indexOf(record.id);
|
||||
if (index !== -1) {
|
||||
|
|
|
@ -7,7 +7,6 @@ import React, { useState, useEffect } from 'react';
|
|||
import { observer } from 'mobx-react';
|
||||
import { Modal, Form, Input, Select, Button, message } from 'antd';
|
||||
import HostSelector from './HostSelector';
|
||||
import hostStore from 'pages/host/store';
|
||||
import { http, includes } from 'libs';
|
||||
import store from './store';
|
||||
import lds from 'lodash';
|
||||
|
@ -22,7 +21,6 @@ export default observer(function () {
|
|||
useEffect(() => {
|
||||
const {app_host_ids, host_ids} = store.record;
|
||||
setHostIds(lds.clone(host_ids || app_host_ids));
|
||||
if (!hostStore.records || hostStore.records.length === 0) hostStore.fetchRecords()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Input, Card, Tree, Dropdown, Menu, Switch, Tooltip, Spin, Modal } from
|
|||
import {
|
||||
FolderOutlined,
|
||||
FolderAddOutlined,
|
||||
FolderOpenOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
CopyOutlined,
|
||||
|
@ -24,11 +25,12 @@ import store from './store';
|
|||
import lds from 'lodash';
|
||||
|
||||
export default observer(function () {
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [loading, setLoading] = useState();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [draggable, setDraggable] = useState(false);
|
||||
const [action, setAction] = useState('');
|
||||
const [expands, setExpands] = useState();
|
||||
const [expands, setExpands] = useState([]);
|
||||
const [bakTreeData, setBakTreeData] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -36,10 +38,13 @@ export default observer(function () {
|
|||
}, [loading])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isReady) {
|
||||
const length = store.treeData.length
|
||||
if (length > 0 && length < 5 && expands === undefined) {
|
||||
if (length > 0 && length < 5) {
|
||||
const tmp = store.treeData.filter(x => x.children.length)
|
||||
setExpands(tmp.map(x => x.key))
|
||||
setIsReady(true)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [store.treeData])
|
||||
|
@ -50,10 +55,10 @@ export default observer(function () {
|
|||
<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="3" icon={<CopyOutlined/>} onClick={() => store.showSelector(true)}>添加主机</Menu.Item>
|
||||
<Menu.Item key="4" icon={<ScissorOutlined/>} onClick={() => store.showSelector(false)}>移动主机</Menu.Item>
|
||||
<Menu.Item key="5" icon={<CloseOutlined/>} danger onClick={handleRemoveHosts}>删除主机</Menu.Item>
|
||||
<Menu.Divider/>
|
||||
<Menu.Item key="6" icon={<DeleteOutlined/>} danger onClick={handleRemove}>删除此分组</Menu.Item>
|
||||
</Menu>
|
||||
)
|
||||
|
@ -66,7 +71,7 @@ export default observer(function () {
|
|||
.then(() => setAction(''))
|
||||
.finally(() => setLoading(false))
|
||||
} else {
|
||||
if (store.group.key === 0) store.treeData = bakTreeData
|
||||
if (store.group.key === 0) store.rawTreeData = bakTreeData
|
||||
setAction('')
|
||||
}
|
||||
}
|
||||
|
@ -75,9 +80,9 @@ export default observer(function () {
|
|||
const group = store.group;
|
||||
Modal.confirm({
|
||||
title: '操作确认',
|
||||
content: `批量删除【${group.title}】分组内的 ${group.all_host_ids.length} 个主机?`,
|
||||
content: `批量删除【${group.title}】分组内的 ${store.counter[group.key].size} 个主机?`,
|
||||
onOk: () => http.delete('/api/host/', {params: {group_id: group.key}})
|
||||
.then(store.initial)
|
||||
.then(store.fetchRecords)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -92,24 +97,34 @@ export default observer(function () {
|
|||
}
|
||||
|
||||
function handleAddRoot() {
|
||||
setBakTreeData(lds.cloneDeep(store.treeData));
|
||||
const current = {key: 0, parent_id: 0, title: ''};
|
||||
store.treeData.unshift(current);
|
||||
store.treeData = lds.cloneDeep(store.treeData);
|
||||
setBakTreeData(lds.cloneDeep(store.rawTreeData));
|
||||
const current = {key: 0, parent_id: 0, title: '', children: []};
|
||||
store.rawTreeData.unshift(current);
|
||||
store.rawTreeData = lds.cloneDeep(store.rawTreeData);
|
||||
store.group = current;
|
||||
setAction('edit')
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
setBakTreeData(lds.cloneDeep(store.treeData));
|
||||
const current = {key: 0, parent_id: store.group.key, title: ''};
|
||||
store.group.children.unshift(current);
|
||||
store.treeData = lds.cloneDeep(store.treeData);
|
||||
setBakTreeData(lds.cloneDeep(store.rawTreeData));
|
||||
const current = {key: 0, parent_id: store.group.key, title: '', children: []};
|
||||
const node = _find_node(store.rawTreeData, store.group.key)
|
||||
node.children.unshift(current)
|
||||
store.rawTreeData = lds.cloneDeep(store.rawTreeData);
|
||||
if (!expands.includes(store.group.key)) setExpands([store.group.key, ...expands]);
|
||||
store.group = current;
|
||||
setAction('edit')
|
||||
}
|
||||
|
||||
function _find_node(list, key) {
|
||||
let node = lds.find(list, {key})
|
||||
if (node) return node
|
||||
for (let item of list) {
|
||||
node = _find_node(item.children, key)
|
||||
if (node) return node
|
||||
}
|
||||
}
|
||||
|
||||
function handleDrag(v) {
|
||||
setLoading(true);
|
||||
const pos = v.node.pos.split('-');
|
||||
|
@ -138,6 +153,7 @@ export default observer(function () {
|
|||
size="small"
|
||||
style={{width: 'calc(100% - 24px)'}}
|
||||
defaultValue={nodeData.title}
|
||||
placeholder="请输入"
|
||||
suffix={loading ? <LoadingOutlined/> : <span/>}
|
||||
onClick={e => e.stopPropagation()}
|
||||
onBlur={handleSubmit}
|
||||
|
@ -146,9 +162,13 @@ export default observer(function () {
|
|||
} else if (action === 'del' && nodeData.key === store.group.key) {
|
||||
return <LoadingOutlined style={{marginLeft: '4px'}}/>
|
||||
} else {
|
||||
const extend = nodeData.all_host_ids && nodeData.all_host_ids.length ? `(${nodeData.all_host_ids.length})` : null
|
||||
const length = store.counter[nodeData.key]?.size
|
||||
return (
|
||||
<span style={{lineHeight: '24px'}}>{nodeData.title}{extend}</span>
|
||||
<div className={styles.treeNode}>
|
||||
{expands.includes(nodeData.key) ? <FolderOpenOutlined/> : <FolderOutlined/>}
|
||||
<div className={styles.title}>{nodeData.title}</div>
|
||||
{length ? <div className={styles.number}>{length}</div> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -165,7 +185,7 @@ export default observer(function () {
|
|||
onChange={setDraggable}
|
||||
checkedChildren="排版"
|
||||
unCheckedChildren="浏览"/>
|
||||
<Tooltip title="排版模式下,可通过拖拽分组实现快速排序。">
|
||||
<Tooltip title="排版模式下,可通过拖拽分组实现快速排序,右键点击分组进行分组管理。">
|
||||
<QuestionCircleOutlined style={{marginLeft: 8, color: '#999'}}/>
|
||||
</Tooltip>
|
||||
</AuthFragment>)}>
|
||||
|
@ -176,6 +196,7 @@ export default observer(function () {
|
|||
trigger={['contextMenu']}
|
||||
onVisibleChange={v => v || setVisible(v)}>
|
||||
<Tree.DirectoryTree
|
||||
showIcon={false}
|
||||
autoExpandParent
|
||||
expandAction="doubleClick"
|
||||
draggable={draggable}
|
||||
|
|
|
@ -6,67 +6,69 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Modal, Row, Col, Tree, Table, Button, Space, Input } from 'antd';
|
||||
import { includes } from 'libs';
|
||||
import store from './store';
|
||||
import { FolderOpenOutlined, FolderOutlined } from '@ant-design/icons';
|
||||
import hStore from './store';
|
||||
import store from './store2';
|
||||
import styles from './index.module.less';
|
||||
|
||||
|
||||
export default observer(function (props) {
|
||||
const [isReady, setIsReady] = useState(false)
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [group, setGroup] = useState({});
|
||||
const [dataSource, setDataSource] = useState([]);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||
const [fKey, setFKey] = useState();
|
||||
const [expands, setExpands] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!store.treeData.length) {
|
||||
store.initial()
|
||||
.then(() => setGroup(store.treeData[0] || {}))
|
||||
} else {
|
||||
setGroup(store.treeData[0] || {})
|
||||
}
|
||||
store.onlySelf = props.onlySelf;
|
||||
hStore.initial().then(() => {
|
||||
store.rawRecords = hStore.rawRecords;
|
||||
store.rawTreeData = hStore.rawTreeData;
|
||||
store.group = store.treeData[0]
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isReady) {
|
||||
const length = store.treeData.length
|
||||
if (length > 0 && length < 5) {
|
||||
const tmp = store.treeData.filter(x => x.children.length)
|
||||
setExpands(tmp.map(x => x.key))
|
||||
setIsReady(true)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [store.treeData])
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedRowKeys(props.selectedRowKeys || [])
|
||||
}, [props.selectedRowKeys])
|
||||
|
||||
useEffect(() => {
|
||||
let records = store.records;
|
||||
if (group.key) records = records.filter(x => group.self_host_ids.includes(x.id));
|
||||
if (fKey) records = records.filter(x => includes(x.name, fKey) || includes(x.hostname, fKey));
|
||||
setDataSource(records)
|
||||
}, [group, fKey])
|
||||
|
||||
function treeRender(nodeData) {
|
||||
return (
|
||||
<span style={{lineHeight: '24px'}}>
|
||||
{nodeData.title}{nodeData.self_host_ids && nodeData.self_host_ids.length ? `(${nodeData.self_host_ids.length})` : null}
|
||||
</span>
|
||||
)
|
||||
if (props.oneGroup) {
|
||||
setSelectedRowKeys([])
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [store.group])
|
||||
|
||||
function handleClickRow(record) {
|
||||
let tmp = [...selectedRowKeys]
|
||||
const index = tmp.indexOf(record.id);
|
||||
if (index !== -1) {
|
||||
tmp.splice(index, 1)
|
||||
} else if (props.onlyOne) {
|
||||
tmp = [record.id]
|
||||
} else {
|
||||
tmp.push(record.id)
|
||||
let tmp = new Set(selectedRowKeys)
|
||||
if (!tmp.delete(record.id)) {
|
||||
if (props.onlyOne) tmp.clear()
|
||||
tmp.add(record.id)
|
||||
}
|
||||
setSelectedRowKeys(tmp)
|
||||
setSelectedRowKeys([...tmp])
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (props.onOk) {
|
||||
setLoading(true);
|
||||
let res
|
||||
const selectedRows = store.records.filter(x => selectedRowKeys.includes(x.id))
|
||||
const selectedRows = store.rawRecords.filter(x => selectedRowKeys.includes(x.id))
|
||||
if (props.onlyOne) {
|
||||
res = props.onOk(group, selectedRowKeys[0], selectedRows[0])
|
||||
res = props.onOk(store.group, selectedRowKeys[0], selectedRows[0])
|
||||
} else {
|
||||
res = props.onOk(group, selectedRowKeys, selectedRows);
|
||||
res = props.onOk(store.group, selectedRowKeys, selectedRows);
|
||||
}
|
||||
if (res && res.then) {
|
||||
res.then(props.onCancel, () => setLoading(false))
|
||||
|
@ -77,9 +79,33 @@ export default observer(function (props) {
|
|||
}
|
||||
}
|
||||
|
||||
function handleChangeGrp(node) {
|
||||
setGroup(node);
|
||||
if (props.oneGroup) setSelectedRowKeys([])
|
||||
function handleExpand(keys, {_, node}) {
|
||||
if (node.children.length > 0) {
|
||||
setExpands(keys)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectAll(selected) {
|
||||
let tmp = new Set(selectedRowKeys)
|
||||
for (let item of store.dataSource) {
|
||||
if (selected) {
|
||||
tmp.add(item.id)
|
||||
} else {
|
||||
tmp.delete(item.id)
|
||||
}
|
||||
}
|
||||
setSelectedRowKeys([...tmp])
|
||||
}
|
||||
|
||||
function treeRender(nodeData) {
|
||||
const length = store.counter[nodeData.key]?.size
|
||||
return (
|
||||
<div className={styles.treeNode}>
|
||||
{expands.includes(nodeData.key) ? <FolderOpenOutlined/> : <FolderOutlined/>}
|
||||
<div className={styles.title}>{nodeData.title}</div>
|
||||
{length ? <div className={styles.number}>{length}</div> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -95,25 +121,29 @@ export default observer(function (props) {
|
|||
<Row gutter={12}>
|
||||
<Col span={6}>
|
||||
<Tree.DirectoryTree
|
||||
defaultExpandAll={store.treeData.length > 0 && store.treeData.length < 5}
|
||||
showIcon={false}
|
||||
autoExpandParent
|
||||
expandAction="doubleClick"
|
||||
selectedKeys={[group.key]}
|
||||
selectedKeys={[store.group.key]}
|
||||
expandedKeys={expands}
|
||||
treeData={store.treeData}
|
||||
titleRender={treeRender}
|
||||
onSelect={(_, {node}) => handleChangeGrp(node)}
|
||||
onExpand={handleExpand}
|
||||
onSelect={(_, {node}) => store.group = node}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={18}>
|
||||
<div style={{display: 'flex', justifyContent: 'space-between', marginBottom: 12}}>
|
||||
<Input allowClear style={{width: 260}} placeholder="输入检索" onChange={e => setFKey(e.target.value)}/>
|
||||
<Input allowClear style={{width: 260}} placeholder="输入名称/IP检索"
|
||||
onChange={e => store.f_word = e.target.value}/>
|
||||
<Space hidden={selectedRowKeys.length === 0}>
|
||||
<div>已选择 {selectedRowKeys.length} 台主机</div>
|
||||
<Button type="link" onClick={() => setSelectedRowKeys([])}>取消选择</Button>
|
||||
<Button type="link" style={{paddingRight: 0}} onClick={() => setSelectedRowKeys([])}>取消选择</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
rowKey="id"
|
||||
dataSource={dataSource}
|
||||
dataSource={store.dataSource}
|
||||
pagination={false}
|
||||
scroll={{y: 480}}
|
||||
onRow={record => {
|
||||
|
@ -123,11 +153,12 @@ export default observer(function (props) {
|
|||
}}
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
hideSelectAll: props.onlyOne,
|
||||
onSelect: handleClickRow,
|
||||
onSelectAll: (_, __, changeRows) => changeRows.map(x => handleClickRow(x))
|
||||
onSelectAll: handleSelectAll
|
||||
}}>
|
||||
<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="IP地址" dataIndex="hostname" sorter={(a, b) => a.name.localeCompare(b.name)}/>
|
||||
<Table.Column hide ellipsis title="备注信息" dataIndex="desc"/>
|
||||
</Table>
|
||||
</Col>
|
||||
|
|
|
@ -40,7 +40,7 @@ function ComTable() {
|
|||
function IpAddress(props) {
|
||||
if (props.ip && props.ip.length > 0) {
|
||||
return (
|
||||
<div>{props.ip[0]}<span style={{color: '#999'}}>({props.isPublic ? '公' : '私有'})</span></div>
|
||||
<div>{props.ip[0]}<span style={{color: '#999'}}>({props.isPublic ? '公' : '私'})</span></div>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
|
@ -51,7 +51,8 @@ function ComTable() {
|
|||
<TableCard
|
||||
tKey="hi"
|
||||
rowKey="id"
|
||||
title={<Input placeholder="输入名称/IP检索" style={{maxWidth: 250}} onChange={e => store.f_word = e.target.value}/>}
|
||||
title={<Input allowClear value={store.f_word} placeholder="输入名称/IP检索" style={{maxWidth: 250}}
|
||||
onChange={e => store.f_word = e.target.value}/>}
|
||||
loading={store.isFetching}
|
||||
dataSource={store.dataSource}
|
||||
onReload={store.fetchRecords}
|
||||
|
|
|
@ -50,7 +50,11 @@ export default observer(function () {
|
|||
{store.cloudImport && <CloudImport/>}
|
||||
{store.syncVisible && <BatchSync/>}
|
||||
{store.selectorVisible &&
|
||||
<Selector oneGroup={!store.addByCopy} onCancel={() => store.selectorVisible = false} onOk={store.updateGroup}/>}
|
||||
<Selector
|
||||
onlySelf={!store.addByCopy}
|
||||
onCancel={() => store.selectorVisible = false}
|
||||
onOk={store.updateGroup}
|
||||
/>}
|
||||
</AuthDiv>
|
||||
);
|
||||
})
|
||||
|
|
|
@ -26,8 +26,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.selector :global(.ant-modal-footer) {
|
||||
.selector {
|
||||
:global(.ant-modal-footer) {
|
||||
border-top: none
|
||||
}
|
||||
}
|
||||
|
||||
.formAddress1 {
|
||||
|
@ -62,11 +64,27 @@
|
|||
|
||||
.group {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:global(.ant-tree-node-content-wrapper) {
|
||||
.treeNode {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.title {
|
||||
margin-left: 8px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
word-break: break-all;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.number {
|
||||
width: 30px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,15 +3,13 @@
|
|||
* Copyright (c) <spug.dev@gmail.com>
|
||||
* Released under the AGPL-3.0 License.
|
||||
*/
|
||||
import { observable, computed } from 'mobx';
|
||||
import { observable, computed, toJS } from 'mobx';
|
||||
import { message } from 'antd';
|
||||
import { http, includes } from 'libs';
|
||||
import lds from 'lodash';
|
||||
|
||||
class Store {
|
||||
counter = {};
|
||||
@observable records = null;
|
||||
@observable treeData = [];
|
||||
@observable rawTreeData = [];
|
||||
@observable rawRecords = [];
|
||||
@observable groups = {};
|
||||
@observable group = {};
|
||||
@observable record = {};
|
||||
|
@ -29,24 +27,61 @@ class Store {
|
|||
@observable f_word;
|
||||
@observable f_status = '';
|
||||
|
||||
@computed get records() {
|
||||
let records = this.rawRecords;
|
||||
if (this.f_word) {
|
||||
records = records.filter(x => {
|
||||
if (includes(x.name, this.f_word)) return true
|
||||
if (x.public_ip_address && includes(x.public_ip_address[0], this.f_word)) return true
|
||||
return !!(x.private_ip_address && includes(x.private_ip_address[0], this.f_word));
|
||||
});
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
@computed get dataSource() {
|
||||
let records = [];
|
||||
if (this.group.all_host_ids) records = this.records ? this.records.filter(x => this.group.all_host_ids.includes(x.id)) : [];
|
||||
if (this.f_word) records = records.filter(x => includes(x.name, this.f_word) || includes(x.public_ip_address, this.f_word) || includes(x.private_ip_address, this.f_word));
|
||||
if (this.group.key) {
|
||||
const host_ids = this.counter[this.group.key]
|
||||
records = this.records.filter(x => host_ids && host_ids.has(x.id));
|
||||
}
|
||||
if (this.f_status !== '') records = records.filter(x => this.f_status === x.is_verified);
|
||||
return records
|
||||
}
|
||||
|
||||
@computed get counter() {
|
||||
const counter = {}
|
||||
for (let host of this.records) {
|
||||
for (let id of host.group_ids) {
|
||||
if (counter[id]) {
|
||||
counter[id].add(host.id)
|
||||
} else {
|
||||
counter[id] = new Set([host.id])
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let item of this.rawTreeData) {
|
||||
this._handler_counter(item, counter)
|
||||
}
|
||||
return counter
|
||||
}
|
||||
|
||||
@computed get treeData() {
|
||||
let treeData = toJS(this.rawTreeData)
|
||||
if (this.f_word) {
|
||||
treeData = this._handle_filter_group(treeData)
|
||||
}
|
||||
return treeData
|
||||
}
|
||||
|
||||
fetchRecords = () => {
|
||||
this.isFetching = true;
|
||||
return http.get('/api/host/')
|
||||
.then(res => {
|
||||
const tmp = {};
|
||||
this.records = res;
|
||||
this.records.map(item => tmp[item.id] = item);
|
||||
this.rawRecords = res;
|
||||
this.rawRecords.map(item => tmp[item.id] = item);
|
||||
this.idMap = tmp;
|
||||
this._makeCounter();
|
||||
this.refreshCounter()
|
||||
})
|
||||
.finally(() => this.isFetching = false)
|
||||
};
|
||||
|
@ -61,22 +96,22 @@ class Store {
|
|||
return http.get('/api/host/group/')
|
||||
.then(res => {
|
||||
this.groups = res.groups;
|
||||
this.refreshCounter(res.treeData)
|
||||
this.rawTreeData = res.treeData
|
||||
})
|
||||
.finally(() => this.grpFetching = false)
|
||||
}
|
||||
|
||||
initial = () => {
|
||||
if (this.rawRecords.length > 0) return Promise.resolve()
|
||||
this.isFetching = true;
|
||||
this.grpFetching = true;
|
||||
return http.all([http.get('/api/host/'), http.get('/api/host/group/')])
|
||||
.then(http.spread((res1, res2) => {
|
||||
this.records = res1;
|
||||
this.records.map(item => this.idMap[item.id] = item);
|
||||
this.group = res2.treeData[0] || {};
|
||||
this.rawRecords = res1;
|
||||
this.rawRecords.map(item => this.idMap[item.id] = item);
|
||||
this.groups = res2.groups;
|
||||
this._makeCounter();
|
||||
this.refreshCounter(res2.treeData)
|
||||
this.rawTreeData = res2.treeData;
|
||||
this.group = this.treeData[0];
|
||||
}))
|
||||
.finally(() => {
|
||||
this.isFetching = false;
|
||||
|
@ -112,39 +147,24 @@ class Store {
|
|||
this.selectorVisible = true;
|
||||
}
|
||||
|
||||
refreshCounter = (treeData) => {
|
||||
treeData = treeData || lds.cloneDeep(this.treeData);
|
||||
if (treeData.length && this.records !== null) {
|
||||
for (let item of treeData) {
|
||||
this._refreshCounter(item)
|
||||
}
|
||||
this.treeData = treeData
|
||||
}
|
||||
}
|
||||
|
||||
_refreshCounter = (item) => {
|
||||
item.all_host_ids = item.self_host_ids = this.counter[item.key] || [];
|
||||
_handler_counter = (item, counter) => {
|
||||
if (!counter[item.key]) counter[item.key] = new Set()
|
||||
for (let child of item.children) {
|
||||
const ids = this._refreshCounter(child)
|
||||
item.all_host_ids = item.all_host_ids.concat(ids)
|
||||
this._handler_counter(child, counter)
|
||||
counter[child.key].forEach(x => counter[item.key].add(x))
|
||||
}
|
||||
item.all_host_ids = Array.from(new Set(item.all_host_ids));
|
||||
if (this.group.key === item.key) this.group = item;
|
||||
return item.all_host_ids
|
||||
}
|
||||
|
||||
_makeCounter = () => {
|
||||
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]
|
||||
_handle_filter_group = (treeData) => {
|
||||
const data = []
|
||||
for (let item of treeData) {
|
||||
const host_ids = this.counter[item.key]
|
||||
if (host_ids.size > 0 || item.key === this.group.key) {
|
||||
item.children = this._handle_filter_group(item.children)
|
||||
data.push(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.counter = counter
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||
* Copyright (c) <spug.dev@gmail.com>
|
||||
* Released under the AGPL-3.0 License.
|
||||
*/
|
||||
import { observable, computed, toJS } from 'mobx';
|
||||
import { includes } from 'libs';
|
||||
|
||||
class Store {
|
||||
@observable rawTreeData = [];
|
||||
@observable rawRecords = [];
|
||||
@observable group = {};
|
||||
@observable onlySelf = false;
|
||||
|
||||
@observable f_word;
|
||||
|
||||
@computed get records() {
|
||||
let records = this.rawRecords;
|
||||
if (this.f_word) {
|
||||
records = records.filter(x => {
|
||||
if (includes(x.name, this.f_word)) return true
|
||||
if (x.public_ip_address && includes(x.public_ip_address[0], this.f_word)) return true
|
||||
return !!(x.private_ip_address && includes(x.private_ip_address[0], this.f_word));
|
||||
});
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
@computed get dataSource() {
|
||||
let records = [];
|
||||
if (this.group.key) {
|
||||
const host_ids = this.counter[this.group.key]
|
||||
records = this.records.filter(x => host_ids && host_ids.has(x.id));
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
@computed get counter() {
|
||||
const counter = {}
|
||||
for (let host of this.records) {
|
||||
for (let id of host.group_ids) {
|
||||
if (counter[id]) {
|
||||
counter[id].add(host.id)
|
||||
} else {
|
||||
counter[id] = new Set([host.id])
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!this.onlySelf) {
|
||||
for (let item of this.rawTreeData) {
|
||||
this._handler_counter(item, counter)
|
||||
}
|
||||
}
|
||||
return counter
|
||||
}
|
||||
|
||||
@computed get treeData() {
|
||||
let treeData = toJS(this.rawTreeData)
|
||||
if (this.f_word) {
|
||||
treeData = this._handle_filter_group(treeData)
|
||||
}
|
||||
return treeData
|
||||
}
|
||||
|
||||
_handler_counter = (item, counter) => {
|
||||
if (!counter[item.key]) counter[item.key] = new Set()
|
||||
for (let child of item.children) {
|
||||
this._handler_counter(child, counter)
|
||||
counter[child.key].forEach(x => counter[item.key].add(x))
|
||||
}
|
||||
}
|
||||
|
||||
_handle_filter_group = (treeData) => {
|
||||
const data = []
|
||||
for (let item of treeData) {
|
||||
const host_ids = this.counter[item.key]
|
||||
if (host_ids?.size > 0 || item.key === this.group.key) {
|
||||
item.children = this._handle_filter_group(item.children)
|
||||
data.push(item)
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
export default new Store()
|
|
@ -29,9 +29,10 @@ export default function () {
|
|||
appStore.records = [];
|
||||
requestStore.records = [];
|
||||
requestStore.deploys = [];
|
||||
hostStore.records = null;
|
||||
hostStore.rawRecords = [];
|
||||
hostStore.rawTreeData = [];
|
||||
hostStore.groups = {};
|
||||
hostStore.treeData = [];
|
||||
hostStore.group = {};
|
||||
execStore.hosts = [];
|
||||
}, [])
|
||||
|
||||
|
|
|
@ -15,8 +15,8 @@ import hostStore from '../host/store';
|
|||
|
||||
export default observer(function () {
|
||||
useEffect(() => {
|
||||
hostStore.initial()
|
||||
store.targets = store.record.id ? store.record['targets'] : [undefined];
|
||||
if (!hostStore.records || hostStore.records.length === 0) hostStore.fetchRecords()
|
||||
}, [])
|
||||
return (
|
||||
<Modal
|
||||
|
|
|
@ -30,7 +30,7 @@ export default observer(function () {
|
|||
filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
|
||||
onChange={v => store.editTarget(index, v)}>
|
||||
<Select.Option value="local" disabled={store.targets.includes('local')}>本机</Select.Option>
|
||||
{hostStore.records.map(item => (
|
||||
{hostStore.rawRecords.map(item => (
|
||||
<Select.Option key={item.id} value={item.id} disabled={store.targets.includes(item.id)}>
|
||||
{`${item.name}(${item['hostname']}:${item['port']})`}
|
||||
</Select.Option>
|
||||
|
|
|
@ -17,9 +17,7 @@ export default observer(function () {
|
|||
const [groups, setGroups] = useState([...store.record.group_perms]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hostStore.treeData.length === 0) {
|
||||
hostStore.initial()
|
||||
}
|
||||
}, [])
|
||||
|
||||
function handleSubmit() {
|
||||
|
@ -63,7 +61,7 @@ export default observer(function () {
|
|||
value={id}
|
||||
showSearch={false}
|
||||
treeNodeLabelProp="name"
|
||||
treeData={hostStore.treeData}
|
||||
treeData={hostStore.rawTreeData}
|
||||
onChange={value => handleChange(index, value)}
|
||||
placeholder="请选择分组"/>
|
||||
{groups.length > 1 && (
|
||||
|
|
Loading…
Reference in New Issue