U 优化主机分组支持全局搜索

pull/517/head
vapao 2022-07-07 09:03:31 +08:00
parent 0b9ab1e379
commit 7872d8cb85
17 changed files with 336 additions and 144 deletions

View File

@ -5,7 +5,7 @@
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.3.0", "@ant-design/icons": "^4.3.0",
"ace-builds": "^1.4.13", "ace-builds": "^1.4.13",
"antd": "^4.19.2", "antd": "4.21.4",
"axios": "^0.21.0", "axios": "^0.21.0",
"bizcharts": "^3.5.9", "bizcharts": "^3.5.9",
"history": "^4.10.1", "history": "^4.10.1",
@ -45,9 +45,9 @@
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-decorators": "^7.10.5", "@babel/plugin-proposal-decorators": "^7.10.5",
"customize-cra": "^1.0.0", "customize-cra": "^1.0.0",
"http-proxy-middleware": "0.19.2",
"less": "^3.12.2", "less": "^3.12.2",
"less-loader": "^7.1.0", "less-loader": "^7.1.0",
"react-app-rewired": "^2.1.6", "react-app-rewired": "^2.1.6"
"http-proxy-middleware": "0.19.2"
} }
} }

View File

@ -39,15 +39,29 @@ export function clsNames(...args) {
return args.filter(x => x).join(' ') return args.filter(x => x).join(' ')
} }
export function includes(s, key) { function isInclude(s, keys) {
key = key.toLowerCase(); if (!s) return false
if (Array.isArray(s)) { if (Array.isArray(keys)) {
for (let i of s) { for (let k of keys) {
if (i && i.toLowerCase().includes(key)) return true k = k.toLowerCase()
if (s.toLowerCase().includes(k)) return true
} }
return false return false
} else { } 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)
} }
} }

View File

@ -8,7 +8,6 @@ import { observer } from 'mobx-react';
import { Modal, Form, Input, Select, DatePicker, Button, message } from 'antd'; import { Modal, Form, Input, Select, DatePicker, Button, message } from 'antd';
import { LoadingOutlined, SyncOutlined } from '@ant-design/icons'; import { LoadingOutlined, SyncOutlined } from '@ant-design/icons';
import HostSelector from './HostSelector'; import HostSelector from './HostSelector';
import hostStore from 'pages/host/store';
import { http, history } from 'libs'; import { http, history } from 'libs';
import store from './store'; import store from './store';
import lds from 'lodash'; import lds from 'lodash';
@ -44,7 +43,6 @@ export default observer(function () {
useEffect(() => { useEffect(() => {
const {app_host_ids, host_ids} = store.record; const {app_host_ids, host_ids} = store.record;
setHostIds(lds.clone(host_ids || app_host_ids)); setHostIds(lds.clone(host_ids || app_host_ids));
if (!hostStore.records || hostStore.records.length === 0) hostStore.fetchRecords()
fetchVersions() fetchVersions()
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])

View File

@ -7,7 +7,6 @@ import React, { useState, useEffect } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { UploadOutlined } from '@ant-design/icons'; import { UploadOutlined } from '@ant-design/icons';
import { Modal, Form, Input, Upload, DatePicker, message, Button } from 'antd'; import { Modal, Form, Input, Upload, DatePicker, message, Button } from 'antd';
import hostStore from 'pages/host/store';
import HostSelector from './HostSelector'; import HostSelector from './HostSelector';
import { http, clsNames, X_TOKEN } from 'libs'; import { http, clsNames, X_TOKEN } from 'libs';
import styles from './index.module.less'; import styles from './index.module.less';
@ -26,7 +25,6 @@ export default observer(function () {
useEffect(() => { useEffect(() => {
const {app_host_ids, host_ids, extra} = store.record; const {app_host_ids, host_ids, extra} = store.record;
setHostIds(lds.clone(host_ids || app_host_ids)); 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'}]) if (store.record.extra) setFileList([{...extra, uid: '0'}])
}, []) }, [])

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Modal, Table, Button, Alert } from 'antd'; import { Modal, Table, Button, Alert } from 'antd';
import hostStore from 'pages/host/store'; import hostStore from 'pages/host/store';
@ -7,6 +7,10 @@ import lds from 'lodash';
export default observer(function (props) { export default observer(function (props) {
const [selectedRowKeys, setSelectedRowKeys] = useState(props.host_ids || []); const [selectedRowKeys, setSelectedRowKeys] = useState(props.host_ids || []);
useEffect(() => {
hostStore.initial()
}, [])
function handleClickRow(record) { function handleClickRow(record) {
const index = selectedRowKeys.indexOf(record.id); const index = selectedRowKeys.indexOf(record.id);
if (index !== -1) { if (index !== -1) {

View File

@ -7,7 +7,6 @@ import React, { useState, useEffect } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Modal, Form, Input, Select, Button, message } from 'antd'; import { Modal, Form, Input, Select, Button, message } from 'antd';
import HostSelector from './HostSelector'; import HostSelector from './HostSelector';
import hostStore from 'pages/host/store';
import { http, includes } from 'libs'; import { http, includes } from 'libs';
import store from './store'; import store from './store';
import lds from 'lodash'; import lds from 'lodash';
@ -22,7 +21,6 @@ export default observer(function () {
useEffect(() => { useEffect(() => {
const {app_host_ids, host_ids} = store.record; const {app_host_ids, host_ids} = store.record;
setHostIds(lds.clone(host_ids || app_host_ids)); 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 // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])

View File

@ -9,6 +9,7 @@ import { Input, Card, Tree, Dropdown, Menu, Switch, Tooltip, Spin, Modal } from
import { import {
FolderOutlined, FolderOutlined,
FolderAddOutlined, FolderAddOutlined,
FolderOpenOutlined,
EditOutlined, EditOutlined,
DeleteOutlined, DeleteOutlined,
CopyOutlined, CopyOutlined,
@ -24,11 +25,12 @@ import store from './store';
import lds from 'lodash'; import lds from 'lodash';
export default observer(function () { export default observer(function () {
const [isReady, setIsReady] = useState(false);
const [loading, setLoading] = useState(); const [loading, setLoading] = useState();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [draggable, setDraggable] = useState(false); const [draggable, setDraggable] = useState(false);
const [action, setAction] = useState(''); const [action, setAction] = useState('');
const [expands, setExpands] = useState(); const [expands, setExpands] = useState([]);
const [bakTreeData, setBakTreeData] = useState(); const [bakTreeData, setBakTreeData] = useState();
useEffect(() => { useEffect(() => {
@ -36,10 +38,13 @@ export default observer(function () {
}, [loading]) }, [loading])
useEffect(() => { useEffect(() => {
const length = store.treeData.length if (!isReady) {
if (length > 0 && length < 5 && expands === undefined) { const length = store.treeData.length
const tmp = store.treeData.filter(x => x.children.length) if (length > 0 && length < 5) {
setExpands(tmp.map(x => x.key)) 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 // eslint-disable-next-line react-hooks/exhaustive-deps
}, [store.treeData]) }, [store.treeData])
@ -50,10 +55,10 @@ export default observer(function () {
<Menu.Item key="1" icon={<FolderAddOutlined/>} onClick={handleAdd}>新建子分组</Menu.Item> <Menu.Item key="1" icon={<FolderAddOutlined/>} onClick={handleAdd}>新建子分组</Menu.Item>
<Menu.Item key="2" icon={<EditOutlined/>} onClick={() => setAction('edit')}>重命名</Menu.Item> <Menu.Item key="2" icon={<EditOutlined/>} onClick={() => setAction('edit')}>重命名</Menu.Item>
<Menu.Divider/> <Menu.Divider/>
<Menu.Item key="3" icon={<CopyOutlined/>} onClick={() => store.showSelector(true)}>添加至分组</Menu.Item> <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="4" icon={<ScissorOutlined/>} onClick={() => store.showSelector(false)}>移动主机</Menu.Item>
<Menu.Divider/>
<Menu.Item key="5" icon={<CloseOutlined/>} danger onClick={handleRemoveHosts}>删除主机</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.Item key="6" icon={<DeleteOutlined/>} danger onClick={handleRemove}>删除此分组</Menu.Item>
</Menu> </Menu>
) )
@ -66,7 +71,7 @@ export default observer(function () {
.then(() => setAction('')) .then(() => setAction(''))
.finally(() => setLoading(false)) .finally(() => setLoading(false))
} else { } else {
if (store.group.key === 0) store.treeData = bakTreeData if (store.group.key === 0) store.rawTreeData = bakTreeData
setAction('') setAction('')
} }
} }
@ -75,9 +80,9 @@ export default observer(function () {
const group = store.group; const group = store.group;
Modal.confirm({ Modal.confirm({
title: '操作确认', 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}}) 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() { function handleAddRoot() {
setBakTreeData(lds.cloneDeep(store.treeData)); setBakTreeData(lds.cloneDeep(store.rawTreeData));
const current = {key: 0, parent_id: 0, title: ''}; const current = {key: 0, parent_id: 0, title: '', children: []};
store.treeData.unshift(current); store.rawTreeData.unshift(current);
store.treeData = lds.cloneDeep(store.treeData); store.rawTreeData = lds.cloneDeep(store.rawTreeData);
store.group = current; store.group = current;
setAction('edit') setAction('edit')
} }
function handleAdd() { function handleAdd() {
setBakTreeData(lds.cloneDeep(store.treeData)); setBakTreeData(lds.cloneDeep(store.rawTreeData));
const current = {key: 0, parent_id: store.group.key, title: ''}; const current = {key: 0, parent_id: store.group.key, title: '', children: []};
store.group.children.unshift(current); const node = _find_node(store.rawTreeData, store.group.key)
store.treeData = lds.cloneDeep(store.treeData); node.children.unshift(current)
store.rawTreeData = lds.cloneDeep(store.rawTreeData);
if (!expands.includes(store.group.key)) setExpands([store.group.key, ...expands]); if (!expands.includes(store.group.key)) setExpands([store.group.key, ...expands]);
store.group = current; store.group = current;
setAction('edit') 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) { function handleDrag(v) {
setLoading(true); setLoading(true);
const pos = v.node.pos.split('-'); const pos = v.node.pos.split('-');
@ -138,6 +153,7 @@ export default observer(function () {
size="small" size="small"
style={{width: 'calc(100% - 24px)'}} style={{width: 'calc(100% - 24px)'}}
defaultValue={nodeData.title} defaultValue={nodeData.title}
placeholder="请输入"
suffix={loading ? <LoadingOutlined/> : <span/>} suffix={loading ? <LoadingOutlined/> : <span/>}
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
onBlur={handleSubmit} onBlur={handleSubmit}
@ -146,9 +162,13 @@ export default observer(function () {
} else if (action === 'del' && nodeData.key === store.group.key) { } else if (action === 'del' && nodeData.key === store.group.key) {
return <LoadingOutlined style={{marginLeft: '4px'}}/> return <LoadingOutlined style={{marginLeft: '4px'}}/>
} else { } 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 ( 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} onChange={setDraggable}
checkedChildren="排版" checkedChildren="排版"
unCheckedChildren="浏览"/> unCheckedChildren="浏览"/>
<Tooltip title="排版模式下,可通过拖拽分组实现快速排序。"> <Tooltip title="排版模式下,可通过拖拽分组实现快速排序,右键点击分组进行分组管理。">
<QuestionCircleOutlined style={{marginLeft: 8, color: '#999'}}/> <QuestionCircleOutlined style={{marginLeft: 8, color: '#999'}}/>
</Tooltip> </Tooltip>
</AuthFragment>)}> </AuthFragment>)}>
@ -176,6 +196,7 @@ export default observer(function () {
trigger={['contextMenu']} trigger={['contextMenu']}
onVisibleChange={v => v || setVisible(v)}> onVisibleChange={v => v || setVisible(v)}>
<Tree.DirectoryTree <Tree.DirectoryTree
showIcon={false}
autoExpandParent autoExpandParent
expandAction="doubleClick" expandAction="doubleClick"
draggable={draggable} draggable={draggable}

View File

@ -6,67 +6,69 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Modal, Row, Col, Tree, Table, Button, Space, Input } from 'antd'; import { Modal, Row, Col, Tree, Table, Button, Space, Input } from 'antd';
import { includes } from 'libs'; import { FolderOpenOutlined, FolderOutlined } from '@ant-design/icons';
import store from './store'; import hStore from './store';
import store from './store2';
import styles from './index.module.less'; import styles from './index.module.less';
export default observer(function (props) { export default observer(function (props) {
const [isReady, setIsReady] = useState(false)
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [group, setGroup] = useState({});
const [dataSource, setDataSource] = useState([]);
const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const [fKey, setFKey] = useState(); const [expands, setExpands] = useState([]);
useEffect(() => { useEffect(() => {
if (!store.treeData.length) { store.onlySelf = props.onlySelf;
store.initial() hStore.initial().then(() => {
.then(() => setGroup(store.treeData[0] || {})) store.rawRecords = hStore.rawRecords;
} else { store.rawTreeData = hStore.rawTreeData;
setGroup(store.treeData[0] || {}) 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(() => { useEffect(() => {
setSelectedRowKeys(props.selectedRowKeys || []) setSelectedRowKeys(props.selectedRowKeys || [])
}, [props.selectedRowKeys]) }, [props.selectedRowKeys])
useEffect(() => { useEffect(() => {
let records = store.records; if (props.oneGroup) {
if (group.key) records = records.filter(x => group.self_host_ids.includes(x.id)); setSelectedRowKeys([])
if (fKey) records = records.filter(x => includes(x.name, fKey) || includes(x.hostname, fKey)); }
setDataSource(records) // eslint-disable-next-line react-hooks/exhaustive-deps
}, [group, fKey]) }, [store.group])
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>
)
}
function handleClickRow(record) { function handleClickRow(record) {
let tmp = [...selectedRowKeys] let tmp = new Set(selectedRowKeys)
const index = tmp.indexOf(record.id); if (!tmp.delete(record.id)) {
if (index !== -1) { if (props.onlyOne) tmp.clear()
tmp.splice(index, 1) tmp.add(record.id)
} else if (props.onlyOne) {
tmp = [record.id]
} else {
tmp.push(record.id)
} }
setSelectedRowKeys(tmp) setSelectedRowKeys([...tmp])
} }
function handleSubmit() { function handleSubmit() {
if (props.onOk) { if (props.onOk) {
setLoading(true); setLoading(true);
let res 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) { if (props.onlyOne) {
res = props.onOk(group, selectedRowKeys[0], selectedRows[0]) res = props.onOk(store.group, selectedRowKeys[0], selectedRows[0])
} else { } else {
res = props.onOk(group, selectedRowKeys, selectedRows); res = props.onOk(store.group, selectedRowKeys, selectedRows);
} }
if (res && res.then) { if (res && res.then) {
res.then(props.onCancel, () => setLoading(false)) res.then(props.onCancel, () => setLoading(false))
@ -77,9 +79,33 @@ export default observer(function (props) {
} }
} }
function handleChangeGrp(node) { function handleExpand(keys, {_, node}) {
setGroup(node); if (node.children.length > 0) {
if (props.oneGroup) setSelectedRowKeys([]) 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 ( return (
@ -95,25 +121,29 @@ export default observer(function (props) {
<Row gutter={12}> <Row gutter={12}>
<Col span={6}> <Col span={6}>
<Tree.DirectoryTree <Tree.DirectoryTree
defaultExpandAll={store.treeData.length > 0 && store.treeData.length < 5} showIcon={false}
autoExpandParent
expandAction="doubleClick" expandAction="doubleClick"
selectedKeys={[group.key]} selectedKeys={[store.group.key]}
expandedKeys={expands}
treeData={store.treeData} treeData={store.treeData}
titleRender={treeRender} titleRender={treeRender}
onSelect={(_, {node}) => handleChangeGrp(node)} onExpand={handleExpand}
onSelect={(_, {node}) => store.group = node}
/> />
</Col> </Col>
<Col span={18}> <Col span={18}>
<div style={{display: 'flex', justifyContent: 'space-between', marginBottom: 12}}> <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}> <Space hidden={selectedRowKeys.length === 0}>
<div>已选择 {selectedRowKeys.length} 台主机</div> <div>已选择 {selectedRowKeys.length} 台主机</div>
<Button type="link" onClick={() => setSelectedRowKeys([])}>取消选择</Button> <Button type="link" style={{paddingRight: 0}} onClick={() => setSelectedRowKeys([])}>取消选择</Button>
</Space> </Space>
</div> </div>
<Table <Table
rowKey="id" rowKey="id"
dataSource={dataSource} dataSource={store.dataSource}
pagination={false} pagination={false}
scroll={{y: 480}} scroll={{y: 480}}
onRow={record => { onRow={record => {
@ -123,11 +153,12 @@ export default observer(function (props) {
}} }}
rowSelection={{ rowSelection={{
selectedRowKeys, selectedRowKeys,
hideSelectAll: props.onlyOne,
onSelect: handleClickRow, 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="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.Column hide ellipsis title="备注信息" dataIndex="desc"/>
</Table> </Table>
</Col> </Col>

View File

@ -40,7 +40,7 @@ function ComTable() {
function IpAddress(props) { function IpAddress(props) {
if (props.ip && props.ip.length > 0) { if (props.ip && props.ip.length > 0) {
return ( 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 { } else {
return null return null
@ -51,7 +51,8 @@ function ComTable() {
<TableCard <TableCard
tKey="hi" tKey="hi"
rowKey="id" 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} loading={store.isFetching}
dataSource={store.dataSource} dataSource={store.dataSource}
onReload={store.fetchRecords} onReload={store.fetchRecords}

View File

@ -50,7 +50,11 @@ export default observer(function () {
{store.cloudImport && <CloudImport/>} {store.cloudImport && <CloudImport/>}
{store.syncVisible && <BatchSync/>} {store.syncVisible && <BatchSync/>}
{store.selectorVisible && {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> </AuthDiv>
); );
}) })

View File

@ -26,8 +26,10 @@
} }
} }
.selector :global(.ant-modal-footer) { .selector {
border-top: none :global(.ant-modal-footer) {
border-top: none
}
} }
.formAddress1 { .formAddress1 {
@ -62,11 +64,27 @@
.group { .group {
height: 100%; 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; overflow: hidden;
text-overflow: ellipsis; 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;
} }
} }

View File

@ -3,15 +3,13 @@
* 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, computed } from 'mobx'; import { observable, computed, toJS } from 'mobx';
import { message } from 'antd'; import { message } from 'antd';
import { http, includes } from 'libs'; import { http, includes } from 'libs';
import lds from 'lodash';
class Store { class Store {
counter = {}; @observable rawTreeData = [];
@observable records = null; @observable rawRecords = [];
@observable treeData = [];
@observable groups = {}; @observable groups = {};
@observable group = {}; @observable group = {};
@observable record = {}; @observable record = {};
@ -29,24 +27,61 @@ class Store {
@observable f_word; @observable f_word;
@observable f_status = ''; @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() { @computed get dataSource() {
let records = []; 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.group.key) {
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)); 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); if (this.f_status !== '') records = records.filter(x => this.f_status === x.is_verified);
return records 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 = () => { fetchRecords = () => {
this.isFetching = true; this.isFetching = true;
return http.get('/api/host/') return http.get('/api/host/')
.then(res => { .then(res => {
const tmp = {}; const tmp = {};
this.records = res; this.rawRecords = res;
this.records.map(item => tmp[item.id] = item); this.rawRecords.map(item => tmp[item.id] = item);
this.idMap = tmp; this.idMap = tmp;
this._makeCounter();
this.refreshCounter()
}) })
.finally(() => this.isFetching = false) .finally(() => this.isFetching = false)
}; };
@ -61,22 +96,22 @@ class Store {
return http.get('/api/host/group/') return http.get('/api/host/group/')
.then(res => { .then(res => {
this.groups = res.groups; this.groups = res.groups;
this.refreshCounter(res.treeData) this.rawTreeData = res.treeData
}) })
.finally(() => this.grpFetching = false) .finally(() => this.grpFetching = false)
} }
initial = () => { initial = () => {
if (this.rawRecords.length > 0) return Promise.resolve()
this.isFetching = true; this.isFetching = true;
this.grpFetching = true; this.grpFetching = true;
return http.all([http.get('/api/host/'), http.get('/api/host/group/')]) return http.all([http.get('/api/host/'), http.get('/api/host/group/')])
.then(http.spread((res1, res2) => { .then(http.spread((res1, res2) => {
this.records = res1; this.rawRecords = res1;
this.records.map(item => this.idMap[item.id] = item); this.rawRecords.map(item => this.idMap[item.id] = item);
this.group = res2.treeData[0] || {};
this.groups = res2.groups; this.groups = res2.groups;
this._makeCounter(); this.rawTreeData = res2.treeData;
this.refreshCounter(res2.treeData) this.group = this.treeData[0];
})) }))
.finally(() => { .finally(() => {
this.isFetching = false; this.isFetching = false;
@ -112,39 +147,24 @@ class Store {
this.selectorVisible = true; this.selectorVisible = true;
} }
refreshCounter = (treeData) => { _handler_counter = (item, counter) => {
treeData = treeData || lds.cloneDeep(this.treeData); if (!counter[item.key]) counter[item.key] = new Set()
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] || [];
for (let child of item.children) { for (let child of item.children) {
const ids = this._refreshCounter(child) this._handler_counter(child, counter)
item.all_host_ids = item.all_host_ids.concat(ids) 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 = () => { _handle_filter_group = (treeData) => {
const counter = {}; const data = []
for (let host of this.records) { for (let item of treeData) {
for (let id of host.group_ids) { const host_ids = this.counter[item.key]
if (counter[id]) { if (host_ids.size > 0 || item.key === this.group.key) {
counter[id].push(host.id) item.children = this._handle_filter_group(item.children)
} else { data.push(item)
counter[id] = [host.id]
}
} }
} }
this.counter = counter return data
} }
} }

View File

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

View File

@ -29,9 +29,10 @@ export default function () {
appStore.records = []; appStore.records = [];
requestStore.records = []; requestStore.records = [];
requestStore.deploys = []; requestStore.deploys = [];
hostStore.records = null; hostStore.rawRecords = [];
hostStore.rawTreeData = [];
hostStore.groups = {}; hostStore.groups = {};
hostStore.treeData = []; hostStore.group = {};
execStore.hosts = []; execStore.hosts = [];
}, []) }, [])

View File

@ -15,8 +15,8 @@ import hostStore from '../host/store';
export default observer(function () { export default observer(function () {
useEffect(() => { useEffect(() => {
hostStore.initial()
store.targets = store.record.id ? store.record['targets'] : [undefined]; store.targets = store.record.id ? store.record['targets'] : [undefined];
if (!hostStore.records || hostStore.records.length === 0) hostStore.fetchRecords()
}, []) }, [])
return ( return (
<Modal <Modal

View File

@ -30,7 +30,7 @@ export default observer(function () {
filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0} filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
onChange={v => store.editTarget(index, v)}> onChange={v => store.editTarget(index, v)}>
<Select.Option value="local" disabled={store.targets.includes('local')}>本机</Select.Option> <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)}> <Select.Option key={item.id} value={item.id} disabled={store.targets.includes(item.id)}>
{`${item.name}(${item['hostname']}:${item['port']})`} {`${item.name}(${item['hostname']}:${item['port']})`}
</Select.Option> </Select.Option>

View File

@ -17,9 +17,7 @@ export default observer(function () {
const [groups, setGroups] = useState([...store.record.group_perms]); const [groups, setGroups] = useState([...store.record.group_perms]);
useEffect(() => { useEffect(() => {
if (hostStore.treeData.length === 0) { hostStore.initial()
hostStore.initial()
}
}, []) }, [])
function handleSubmit() { function handleSubmit() {
@ -63,7 +61,7 @@ export default observer(function () {
value={id} value={id}
showSearch={false} showSearch={false}
treeNodeLabelProp="name" treeNodeLabelProp="name"
treeData={hostStore.treeData} treeData={hostStore.rawTreeData}
onChange={value => handleChange(index, value)} onChange={value => handleChange(index, value)}
placeholder="请选择分组"/> placeholder="请选择分组"/>
{groups.length > 1 && ( {groups.length > 1 && (