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": {
"@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"
}
}

View File

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

View File

@ -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
}, [])

View File

@ -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'}])
}, [])

View File

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

View File

@ -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
}, [])

View File

@ -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(() => {
const length = store.treeData.length
if (length > 0 && length < 5 && expands === undefined) {
const tmp = store.treeData.filter(x => x.children.length)
setExpands(tmp.map(x => x.key))
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])
@ -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}

View File

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

View File

@ -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}

View File

@ -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>
);
})

View File

@ -26,8 +26,10 @@
}
}
.selector :global(.ant-modal-footer) {
border-top: none
.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;
}
}

View File

@ -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
}
}

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 = [];
requestStore.records = [];
requestStore.deploys = [];
hostStore.records = null;
hostStore.rawRecords = [];
hostStore.rawTreeData = [];
hostStore.groups = {};
hostStore.treeData = [];
hostStore.group = {};
execStore.hosts = [];
}, [])

View File

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

View File

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

View File

@ -17,9 +17,7 @@ export default observer(function () {
const [groups, setGroups] = useState([...store.record.group_perms]);
useEffect(() => {
if (hostStore.treeData.length === 0) {
hostStore.initial()
}
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 && (