modified system cluster and global cluster selector

This commit is contained in:
silenceqi 2021-02-20 20:36:39 +08:00
parent 523ba2fbec
commit c981e6537d
16 changed files with 386 additions and 80 deletions

View File

@ -69,7 +69,7 @@ export default {
// }, // },
proxy: { proxy: {
'/_search-center/': { '/_search-center/': {
target: 'http://localhost:2900', target: 'http://localhost:9000',
changeOrigin: true, changeOrigin: true,
// pathRewrite: { '^/server': '' }, // pathRewrite: { '^/server': '' },
}, },

View File

@ -304,7 +304,7 @@ export default [
}, },
{ {
path: '/system/cluster/edit', path: '/system/cluster/edit',
name: 'edit-cluster', name: 'editCluster',
component: './System/Cluster/Form', component: './System/Cluster/Form',
hideInMenu: true hideInMenu: true
}, },

View File

@ -0,0 +1,75 @@
export default {
// 'post /_search-center/system/cluster/_search': function(req, res){
// res.send({
// "took": 0,
// "timed_out": false,
// "hits": {
// "total": {
// "relation": "eq",
// "value": 1
// },
// "max_score": 1,
// "hits": [
// {
// "_index": ".infini-search-center_cluster",
// "_type": "_doc",
// "_id": "c0oc4kkgq9s8qss2uk50",
// "_source": {
// "basic_auth": {
// "password": "123",
// "username": "medcl"
// },
// "created": "2021-02-20T16:03:30.867084+08:00",
// "description": "xx业务集群1",
// "enabled": false,
// "endpoint": "http://localhost:9200",
// "name": "cluster1",
// "updated": "2021-02-20T16:03:30.867084+08:00"
// }
// }
// ]
// }
// })
// },
// 'post /_search-center/system/cluster': function(req, res){
// res.send({
// "_id": "c0obhd4gq9s7akom0o60",
// "_source": {
// "name": "cluster1",
// "endpoint": "http://localhost:9200",
// "basic_auth": {
// "username": "medcl",
// "password": "123"
// },
// "description": "xx业务集群1",
// "enabled": false,
// "created": "2021-02-20T15:12:50.984062+08:00",
// "updated": "2021-02-20T15:12:50.984062+08:00"
// },
// "result": "created"
// });
// },
// 'put /_search-center/system/cluster/:id': function(req, res){
// res.send({
// "_id": "c0obhd4gq9s7akom0o60",
// "_source": {
// "basic_auth": {
// "password": "456",
// "username": "medcl"
// },
// "description": "xx业务集群2",
// "endpoint": "http://localhost:9201",
// "name": "cluster2",
// "updated": "2021-02-20T15:25:12.159789+08:00"
// },
// "result": "updated"
// });
// },
//
// 'delete /_search-center/system/cluster/:id': function(req, res){
// res.send({
// "_id": "c0obk7cgq9s7hi05aou0",
// "result": "deleted"
// });
// }
}

View File

@ -6,7 +6,6 @@ import styles from './DropdownSelect.less';
class DropdownSelect extends React.Component{ class DropdownSelect extends React.Component{
state={ state={
value: this.props.defaultValue, value: this.props.defaultValue,
data: this.props.data || [],
loading: false, loading: false,
hasMore: true, hasMore: true,
} }
@ -24,26 +23,23 @@ class DropdownSelect extends React.Component{
} }
componentDidMount(){ componentDidMount(){
let data = []; let me = this;
for(let i = 1; i<=28; i++){ this.fetchData().then((data)=>{
data.push('cluster'+i) let hasMore = true;
} if(data.length < this.props.size){
this.setState({ hasMore = false;
data: data, }
me.setState({
hasMore
})
}) })
} }
fetchData = ()=>{ fetchData = ()=>{
let me = this; let me = this;
return new Promise(resolve => { const {fetchData, size} = this.props;
setTimeout(() => { let data = this.props.data || [];
let start = me.state.data.length; let from = data.length;
let data =[] return fetchData(from, size);
for(let i = start + 1; i<start+11; i++){
data.push('cluster'+i)
}
resolve(data)
}, 2000)
});
} }
handleInfiniteOnLoad = (page) => { handleInfiniteOnLoad = (page) => {
@ -51,28 +47,24 @@ class DropdownSelect extends React.Component{
this.setState({ this.setState({
loading: true, loading: true,
}) })
if (data.length > 50) {
message.warning('No more data');
this.setState({
hasMore: false,
loading: false,
});
return;
}
this.fetchData().then((newdata)=>{ this.fetchData().then((newdata)=>{
data = data.concat(newdata); let newState = {
this.setState({
data,
loading: false, loading: false,
}); };
if(newdata.length < this.props.size){
message.info("no more data");
newState.hasMore = false;
}
this.setState(newState);
}); });
} }
render(){ render(){
let me = this; let me = this;
const menu = (<div className={styles.dropmenu}> const {labelField} = this.props;
<div className={styles.infiniteContainer}> const menu = (<div className={styles.dropmenu} style={{width: this.props.width}}>
<div className={styles.infiniteContainer} style={{height: this.props.height}}>
<InfiniteScroll <InfiniteScroll
initialLoad={false} initialLoad={false}
loadMore={this.handleInfiniteOnLoad} loadMore={this.handleInfiniteOnLoad}
@ -84,10 +76,10 @@ class DropdownSelect extends React.Component{
gutter: 8, gutter: 8,
column: 4, column: 4,
}} }}
dataSource={this.state.data} dataSource={this.props.data}
renderItem={item => ( renderItem={item => (
<List.Item key={item}> <List.Item key={item[labelField]}>
<Button onClick={()=>{this.handleItemClick(item)}} className={styles.btnitem}>{item}</Button> <Button onClick={()=>{this.handleItemClick(item)}} className={styles.btnitem}>{item[labelField]}</Button>
</List.Item> </List.Item>
)} )}
> >
@ -106,9 +98,11 @@ class DropdownSelect extends React.Component{
)} )}
</div>); </div>);
return( return(
<Dropdown overlay={menu} placement="bottomLeft"> this.props.visible ?
<Button className={styles['btn-ds']}>{this.state.value} <Icon style={{float:'right', marginTop:3}} type="caret-down"/></Button> (<Dropdown overlay={menu} placement="bottomLeft">
</Dropdown> <Button className={styles['btn-ds']}>{this.state.value[labelField]} <Icon style={{float: 'right', marginTop: 3}}
type="caret-down"/></Button>
</Dropdown>) : ""
) )
} }

View File

@ -32,7 +32,7 @@
.infiniteContainer { .infiniteContainer {
overflow: auto; overflow: auto;
overflow-x: hidden; overflow-x: hidden;
height: 300px; max-height: 300px;
} }
.loadingContainer { .loadingContainer {
position: absolute; position: absolute;

View File

@ -24,7 +24,7 @@ export default class GlobalHeader extends PureComponent {
this.triggerResizeEvent(); this.triggerResizeEvent();
}; };
render() { render() {
const { collapsed, isMobile, logo } = this.props; const { collapsed, isMobile, logo, clusterVisible, clusterList } = this.props;
return ( return (
<div className={styles.header}> <div className={styles.header}>
{isMobile && ( {isMobile && (
@ -37,9 +37,34 @@ export default class GlobalHeader extends PureComponent {
type={collapsed ? 'menu-unfold' : 'menu-fold'} type={collapsed ? 'menu-unfold' : 'menu-fold'}
onClick={this.toggle} onClick={this.toggle}
/> />
<DropdownSelect defaultValue="Select cluster" <DropdownSelect defaultValue={{name:"Select cluster"}}
onChange={(item)=>{}} labelField="name"
data={['cluster1', 'cluster2','cluster3', 'cluster4','cluster5', 'cluster6']}/> visible={clusterVisible}
onChange={(item)=>{
this.props.handleSaveGlobalState({
selectedCluster: item
})
}}
size={56}
fetchData={
this.props.onFetchClusterList
// (from, size)=>{
// return new Promise(resolve => {
// setTimeout(() => {
// let start = from;
// let data =[]
// for(let i = start + 1; i<start+size+1; i++){
// if(start+size > 56){
// break;
// }
// data.push('cluster'+i)
// }
// resolve(data)
// }, 2000)
// });
// }
}
data={clusterList}/>
<RightContent {...this.props} /> <RightContent {...this.props} />
</div> </div>
); );

View File

@ -114,6 +114,25 @@ class HeaderView extends PureComponent {
this.ticking = false; this.ticking = false;
}; };
handleFetchClusterList = (from, size) => {
const { dispatch } = this.props;
return dispatch({
type: 'global/fetchClusterList',
payload: {
from,
size,
}
});
};
handleSaveGlobalState = (newState) => {
const { dispatch } = this.props;
return dispatch({
type: 'global/saveData',
payload: newState
});
}
render() { render() {
const { isMobile, handleMenuCollapse, setting } = this.props; const { isMobile, handleMenuCollapse, setting } = this.props;
const { navTheme, layout, fixedHeader } = setting; const { navTheme, layout, fixedHeader } = setting;
@ -139,6 +158,8 @@ class HeaderView extends PureComponent {
onNoticeClear={this.handleNoticeClear} onNoticeClear={this.handleNoticeClear}
onMenuClick={this.handleMenuClick} onMenuClick={this.handleMenuClick}
onNoticeVisibleChange={this.handleNoticeVisibleChange} onNoticeVisibleChange={this.handleNoticeVisibleChange}
onFetchClusterList={this.handleFetchClusterList}
handleSaveGlobalState={this.handleSaveGlobalState}
{...this.props} {...this.props}
/> />
)} )}
@ -158,4 +179,6 @@ export default connect(({ user, global, setting, loading }) => ({
fetchingNotices: loading.effects['global/fetchNotices'], fetchingNotices: loading.effects['global/fetchNotices'],
notices: global.notices, notices: global.notices,
setting, setting,
clusterVisible: global.clusterVisible,
clusterList: global.clusterList,
}))(HeaderView); }))(HeaderView);

View File

@ -120,6 +120,7 @@ export default {
'menu.system': 'SYSTEM', 'menu.system': 'SYSTEM',
'menu.system.cluster': 'CLUSTER', 'menu.system.cluster': 'CLUSTER',
'menu.system.editCluster': 'EDIT CLUSTER',
'menu.system.settings': 'SETTINGS', 'menu.system.settings': 'SETTINGS',
'menu.system.settings.global': 'GLOBAL', 'menu.system.settings.global': 'GLOBAL',
'menu.system.settings.gateway': 'GATEWAY', 'menu.system.settings.gateway': 'GATEWAY',

View File

@ -127,6 +127,7 @@ export default {
'menu.system': '系统管理', 'menu.system': '系统管理',
'menu.system.cluster': '集群管理', 'menu.system.cluster': '集群管理',
'menu.system.editCluster': '集群编辑',
'menu.system.settings': '系统设置', 'menu.system.settings': '系统设置',
'menu.system.settings.global': '全局设置', 'menu.system.settings.global': '全局设置',
'menu.system.settings.gateway': '网关设置', 'menu.system.settings.gateway': '网关设置',

View File

@ -1,4 +1,8 @@
import { queryNotices } from '@/services/api'; import { queryNotices } from '@/services/api';
import {message} from "antd";
import {searchClusterConfig} from "@/services/clusterConfig";
import {formatESSearchResult} from '@/lib/elasticsearch/util';
export default { export default {
namespace: 'global', namespace: 'global',
@ -6,6 +10,9 @@ export default {
state: { state: {
collapsed: false, collapsed: false,
notices: [], notices: [],
clusterVisible: true,
clusterList: [],
selectedCluster: '',
}, },
effects: { effects: {
@ -31,6 +38,29 @@ export default {
payload: count, payload: count,
}); });
}, },
*fetchClusterList({payload}, {call, put, select}){
let res = yield call(searchClusterConfig, payload);
if(res.error){
message.error(res.error)
return false;
}
res = formatESSearchResult(res)
let clusterList = yield select(state => state.global.clusterList);
let data = res.data.map((item)=>{
return {
name: item.name,
id: item.id,
};
})
yield put({
type: 'saveData',
payload: {
clusterList: clusterList.concat(data)
}
})
return data;
},
}, },
reducers: { reducers: {
@ -52,12 +82,43 @@ export default {
notices: state.notices.filter(item => item.type !== payload), notices: state.notices.filter(item => item.type !== payload),
}; };
}, },
saveData(state, {payload}){
return {
...state,
...payload,
}
},
removeCluster(state, {payload}){
return {
...state,
clusterList: state.clusterList.filter(item => item.id !== payload.id)
}
},
addCluster(state, {payload}){
state.clusterList.push(payload)
return state;
},
updateCluster(state, {payload}){
let idx = state.clusterList.findIndex(item => item.id === payload.id);
idx > -1 && (state.clusterList[idx].name = payload.name);
return state;
}
}, },
subscriptions: { subscriptions: {
setup({ history }) { setup({ history, dispatch }) {
// Subscribe history(url) change, trigger `load` action if pathname is `/` // Subscribe history(url) change, trigger `load` action if pathname is `/`
return history.listen(({ pathname, search }) => { return history.listen(({ pathname, search }) => {
let clusterVisible = true;
if(pathname.startsWith("/system")){
clusterVisible = false;
}
dispatch({
type: 'saveData',
payload: {
clusterVisible,
}
})
if (typeof window.ga !== 'undefined') { if (typeof window.ga !== 'undefined') {
window.ga('send', 'pageview', pathname + search); window.ga('send', 'pageview', pathname + search);
} }

View File

@ -1,8 +1,26 @@
import React, {Fragment} from 'react'; import React, {Fragment} from 'react';
import {Card, Divider, Popconfirm, Table} from "antd"; import {Card, Divider, Popconfirm, Row, Col, Table, Descriptions} from "antd";
import {Link} from "umi" import {Link} from "umi"
import moment from "moment"; import moment from "moment";
import styles from "./Overview.less";
import {connect} from "dva";
let HealthCircle = (props)=>{
return (
<div style={{
background: props.color,
width: 12,
height: 12,
borderRadius: 12,
display: "inline-block",
marginRight: 3,
}}></div>
)
}
@connect(({global}) => ({
selectedCluster: global.selectedCluster
}))
class Overview extends React.Component { class Overview extends React.Component {
state = { state = {
data: [{id:"JFpIbacZQamv9hkgQEDZ2Q", name:"single-es", endpoint:"http://localhost:9200", health: "green", version: "7.10.0", uptime:"320883955"}] data: [{id:"JFpIbacZQamv9hkgQEDZ2Q", name:"single-es", endpoint:"http://localhost:9200", health: "green", version: "7.10.0", uptime:"320883955"}]
@ -39,13 +57,43 @@ class Overview extends React.Component {
} }
]; ];
render() { render() {
return (<Card> return (<Card title={this.props.selectedCluster?this.props.selectedCluster.name:''}>
<Table <Row gutter={[16,16]}>
bordered <Col xs={24} sm={12} md={12} lg={8} >
dataSource={this.state.data} <Card title="Summary" size={"small"}>
columns={this.clusterColumns} <Descriptions column={1} bordered colon={false} className={styles.overview}>
rowKey="id" <Descriptions.Item label="Health"><HealthCircle color="green"/>Healthy</Descriptions.Item>
/> <Descriptions.Item label="Version">7.10.0</Descriptions.Item>
<Descriptions.Item label="Uptime">3 </Descriptions.Item>
<Descriptions.Item label="License">Basic</Descriptions.Item>
</Descriptions>
</Card>
</Col>
<Col xs={24} sm={12} md={12} lg={8}>
<Card title="Nodes:2" size={"small"}>
<Descriptions column={1} bordered colon={false} size="small" className={styles.overview}>
<Descriptions.Item label="Disk Available">
83.21%
<p className={styles.light}>775.1 GB / 931.5 GB</p>
</Descriptions.Item>
<Descriptions.Item label="JVM Heap">
27.60%
<p className={styles.light}>565.3 GB / 2.0 GB</p>
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
<Col xs={24} sm={12} md={12} lg={8}>
<Card title="Indices:27" size={"small"}>
<Descriptions column={1} bordered colon={false} className={styles.overview}>
<Descriptions.Item label="Documents">20,812,087</Descriptions.Item>
<Descriptions.Item label="Disk Usage">1.1 GB</Descriptions.Item>
<Descriptions.Item label="Primary Shards">28</Descriptions.Item>
<Descriptions.Item label="Replica Shards">26</Descriptions.Item>
</Descriptions>
</Card>
</Col>
</Row>,
</Card>) </Card>)
} }
} }

View File

@ -0,0 +1,19 @@
.overview{
:global(.ant-descriptions-row){
border-bottom: none;
}
:global(.ant-descriptions-item-label){
border-right: none;
background-color: #fff;
font-weight: bold;
}
:global(.ant-descriptions-view){
border: none;
}
:global(.ant-descriptions-item-content){
color: #333;
}
.light{
color: #666;
}
}

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import {Card, Form, Icon, Input, InputNumber, Button, Switch} from 'antd'; import {Card, Form, Icon, Input, InputNumber, Button, Switch, message} from 'antd';
import router from 'umi/router'; import router from 'umi/router';
import styles from './Form.less'; import styles from './Form.less';
@ -42,15 +42,23 @@ class ClusterForm extends React.Component{
} }
//console.log(values); //console.log(values);
let newVals = { let newVals = {
...values name: values.name,
endpoint: values.endpoint,
basic_auth: {
username: values.username,
password: values.password,
},
description: values.description,
enabled: values.enabled,
order: values.order,
} }
delete(newVals['confirm']);
if(clusterConfig.editMode === 'NEW') { if(clusterConfig.editMode === 'NEW') {
dispatch({ dispatch({
type: 'clusterConfig/addCluster', type: 'clusterConfig/addCluster',
payload: newVals, payload: newVals,
}).then(function (rel){ }).then(function (rel){
if(rel){ if(rel){
message.success("添加成功")
router.push('/system/cluster'); router.push('/system/cluster');
} }
}); });
@ -61,6 +69,7 @@ class ClusterForm extends React.Component{
payload: newVals, payload: newVals,
}).then(function (rel){ }).then(function (rel){
if(rel){ if(rel){
message.success("修改成功")
router.push('/system/cluster'); router.push('/system/cluster');
} }
}); });
@ -124,34 +133,21 @@ class ClusterForm extends React.Component{
</Form.Item> </Form.Item>
<Form.Item label="ES 用户名"> <Form.Item label="ES 用户名">
{getFieldDecorator('username', { {getFieldDecorator('username', {
initialValue: editValue.username, initialValue: editValue.basic_auth.username,
rules: [ rules: [
], ],
})(<Input autoComplete='off' />)} })(<Input autoComplete='off' />)}
</Form.Item> </Form.Item>
<Form.Item label="ES 密码" hasFeedback> <Form.Item label="ES 密码" hasFeedback>
{getFieldDecorator('password', { {getFieldDecorator('password', {
initialValue: editValue.password, initialValue: editValue.basic_auth.password,
rules: [ rules: [
{
validator: this.validateToNextPassword,
},
], ],
})(<Input.Password />)} })(<Input.Password />)}
</Form.Item> </Form.Item>
<Form.Item label="ES 确认密码" hasFeedback>
{getFieldDecorator('confirm', {
initialValue: editValue.password,
rules: [
{
validator: this.compareToFirstPassword,
},
],
})(<Input.Password onBlur={this.handleConfirmBlur} />)}
</Form.Item>
<Form.Item label="排序权重"> <Form.Item label="排序权重">
{getFieldDecorator('order', { {getFieldDecorator('order', {
initialValue: editValue.order, initialValue: editValue.order || 0,
})(<InputNumber />)} })(<InputNumber />)}
</Form.Item> </Form.Item>
<Form.Item label="描述"> <Form.Item label="描述">

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import {Button, Card, Col, Divider, Form, Input, Row, Table, Switch, Icon, Popconfirm} from "antd"; import {Button, Card, Col, Divider, Form, Input, Row, Table, Switch, Icon, Popconfirm, message} from "antd";
import Link from "umi/link"; import Link from "umi/link";
import {connect} from "dva"; import {connect} from "dva";
@ -18,12 +18,15 @@ class Index extends React.Component {
key: 'endpoint', key: 'endpoint',
},{ },{
title: '用户名', title: '用户名',
dataIndex: 'username', dataIndex: 'basic_auth.username',
key: 'username', key: 'username',
},{ },{
title: '密码', title: '密码',
dataIndex: 'password', dataIndex: 'basic_auth.password',
key: 'password', key: 'password',
render: (val) =>{
return "******";
}
},{ },{
title: '排序权重', title: '排序权重',
dataIndex: 'order', dataIndex: 'order',
@ -60,7 +63,9 @@ class Index extends React.Component {
}) })
} }
componentDidMount() { componentDidMount() {
this.fetchData({}) if(typeof this.props.clusterConfig.data === 'undefined') {
this.fetchData({})
}
} }
handleSearchClick = ()=>{ handleSearchClick = ()=>{
@ -77,6 +82,10 @@ class Index extends React.Component {
payload: { payload: {
id: record.id id: record.id
} }
}).then((result)=>{
if(result){
message.success("删除成功");
}
}); });
} }
@ -92,7 +101,7 @@ class Index extends React.Component {
handleNewClick = () => { handleNewClick = () => {
this.saveData({ this.saveData({
editMode: 'NEW', editMode: 'NEW',
editValue: {}, editValue: {basic_auth: {}},
}) })
} }
handleEditClick = (record)=>{ handleEditClick = (record)=>{

View File

@ -27,6 +27,28 @@ export default {
message.error(res.error) message.error(res.error)
return false; return false;
} }
let {data, total} = yield select(state => state.clusterConfig);
data.unshift({
...res._source,
id: res._id,
});
yield put({
type: 'saveData',
payload: {
data,
total: {
...total,
value: total.value + 1
},
}
})
yield put({
type: 'global/addCluster',
payload: {
id: res._id,
name: res._source.name,
}
})
return res; return res;
}, },
*updateCluster({payload}, {call, put, select}) { *updateCluster({payload}, {call, put, select}) {
@ -35,6 +57,27 @@ export default {
message.error(res.error) message.error(res.error)
return false; return false;
} }
let {data} = yield select(state => state.clusterConfig);
let idx = data.findIndex((item)=>{
return item.id === res._id;
});
data[idx] = {
...data[idx],
...res._source
};
yield put({
type: 'saveData',
payload: {
data
}
})
yield put({
type: 'global/updateCluster',
payload: {
id: res._id,
name: res._source.name,
}
})
return res; return res;
}, },
*deleteCluster({payload}, {call, put, select}) { *deleteCluster({payload}, {call, put, select}) {
@ -51,7 +94,16 @@ export default {
type: 'saveData', type: 'saveData',
payload: { payload: {
data, data,
total: total -1, total: {
...total,
value: total.value + 1
}
}
})
yield put({
type: 'global/removeCluster',
payload: {
id: payload.id
} }
}) })
return res; return res;

View File

@ -9,7 +9,9 @@ export async function createClusterConfig(params) {
} }
export async function updateClusterConfig(params) { export async function updateClusterConfig(params) {
return request(`${pathPrefix}/system/cluster/${params.id}`, { let id = params.id;
delete(params['id']);
return request(`${pathPrefix}/system/cluster/${id}`, {
method: 'PUT', method: 'PUT',
body: params, body: params,
}); });
@ -23,7 +25,7 @@ export async function deleteClusterConfig(params) {
} }
export async function searchClusterConfig(params) { export async function searchClusterConfig(params) {
let url = `${pathPrefix}/system/cluster`; let url = `${pathPrefix}/system/cluster/_search`;
let args = buildQueryArgs({ let args = buildQueryArgs({
name: params.name, name: params.name,
enabled: params.enabled enabled: params.enabled