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: {
'/_search-center/': {
target: 'http://localhost:2900',
target: 'http://localhost:9000',
changeOrigin: true,
// pathRewrite: { '^/server': '' },
},

View File

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

View File

@ -24,7 +24,7 @@ export default class GlobalHeader extends PureComponent {
this.triggerResizeEvent();
};
render() {
const { collapsed, isMobile, logo } = this.props;
const { collapsed, isMobile, logo, clusterVisible, clusterList } = this.props;
return (
<div className={styles.header}>
{isMobile && (
@ -37,9 +37,34 @@ export default class GlobalHeader extends PureComponent {
type={collapsed ? 'menu-unfold' : 'menu-fold'}
onClick={this.toggle}
/>
<DropdownSelect defaultValue="Select cluster"
onChange={(item)=>{}}
data={['cluster1', 'cluster2','cluster3', 'cluster4','cluster5', 'cluster6']}/>
<DropdownSelect defaultValue={{name:"Select cluster"}}
labelField="name"
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} />
</div>
);

View File

@ -114,6 +114,25 @@ class HeaderView extends PureComponent {
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() {
const { isMobile, handleMenuCollapse, setting } = this.props;
const { navTheme, layout, fixedHeader } = setting;
@ -139,6 +158,8 @@ class HeaderView extends PureComponent {
onNoticeClear={this.handleNoticeClear}
onMenuClick={this.handleMenuClick}
onNoticeVisibleChange={this.handleNoticeVisibleChange}
onFetchClusterList={this.handleFetchClusterList}
handleSaveGlobalState={this.handleSaveGlobalState}
{...this.props}
/>
)}
@ -158,4 +179,6 @@ export default connect(({ user, global, setting, loading }) => ({
fetchingNotices: loading.effects['global/fetchNotices'],
notices: global.notices,
setting,
clusterVisible: global.clusterVisible,
clusterList: global.clusterList,
}))(HeaderView);

View File

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

View File

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

View File

@ -1,4 +1,8 @@
import { queryNotices } from '@/services/api';
import {message} from "antd";
import {searchClusterConfig} from "@/services/clusterConfig";
import {formatESSearchResult} from '@/lib/elasticsearch/util';
export default {
namespace: 'global',
@ -6,6 +10,9 @@ export default {
state: {
collapsed: false,
notices: [],
clusterVisible: true,
clusterList: [],
selectedCluster: '',
},
effects: {
@ -31,6 +38,29 @@ export default {
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: {
@ -52,12 +82,43 @@ export default {
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: {
setup({ history }) {
setup({ history, dispatch }) {
// Subscribe history(url) change, trigger `load` action if pathname is `/`
return history.listen(({ pathname, search }) => {
let clusterVisible = true;
if(pathname.startsWith("/system")){
clusterVisible = false;
}
dispatch({
type: 'saveData',
payload: {
clusterVisible,
}
})
if (typeof window.ga !== 'undefined') {
window.ga('send', 'pageview', pathname + search);
}

View File

@ -1,8 +1,26 @@
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 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 {
state = {
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() {
return (<Card>
<Table
bordered
dataSource={this.state.data}
columns={this.clusterColumns}
rowKey="id"
/>
return (<Card title={this.props.selectedCluster?this.props.selectedCluster.name:''}>
<Row gutter={[16,16]}>
<Col xs={24} sm={12} md={12} lg={8} >
<Card title="Summary" size={"small"}>
<Descriptions column={1} bordered colon={false} className={styles.overview}>
<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>)
}
}

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 {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 styles from './Form.less';
@ -42,15 +42,23 @@ class ClusterForm extends React.Component{
}
//console.log(values);
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') {
dispatch({
type: 'clusterConfig/addCluster',
payload: newVals,
}).then(function (rel){
if(rel){
message.success("添加成功")
router.push('/system/cluster');
}
});
@ -61,6 +69,7 @@ class ClusterForm extends React.Component{
payload: newVals,
}).then(function (rel){
if(rel){
message.success("修改成功")
router.push('/system/cluster');
}
});
@ -124,34 +133,21 @@ class ClusterForm extends React.Component{
</Form.Item>
<Form.Item label="ES 用户名">
{getFieldDecorator('username', {
initialValue: editValue.username,
initialValue: editValue.basic_auth.username,
rules: [
],
})(<Input autoComplete='off' />)}
</Form.Item>
<Form.Item label="ES 密码" hasFeedback>
{getFieldDecorator('password', {
initialValue: editValue.password,
initialValue: editValue.basic_auth.password,
rules: [
{
validator: this.validateToNextPassword,
},
],
})(<Input.Password />)}
</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="排序权重">
{getFieldDecorator('order', {
initialValue: editValue.order,
initialValue: editValue.order || 0,
})(<InputNumber />)}
</Form.Item>
<Form.Item label="描述">

View File

@ -1,5 +1,5 @@
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 {connect} from "dva";
@ -18,12 +18,15 @@ class Index extends React.Component {
key: 'endpoint',
},{
title: '用户名',
dataIndex: 'username',
dataIndex: 'basic_auth.username',
key: 'username',
},{
title: '密码',
dataIndex: 'password',
dataIndex: 'basic_auth.password',
key: 'password',
render: (val) =>{
return "******";
}
},{
title: '排序权重',
dataIndex: 'order',
@ -60,7 +63,9 @@ class Index extends React.Component {
})
}
componentDidMount() {
this.fetchData({})
if(typeof this.props.clusterConfig.data === 'undefined') {
this.fetchData({})
}
}
handleSearchClick = ()=>{
@ -77,6 +82,10 @@ class Index extends React.Component {
payload: {
id: record.id
}
}).then((result)=>{
if(result){
message.success("删除成功");
}
});
}
@ -92,7 +101,7 @@ class Index extends React.Component {
handleNewClick = () => {
this.saveData({
editMode: 'NEW',
editValue: {},
editValue: {basic_auth: {}},
})
}
handleEditClick = (record)=>{

View File

@ -27,6 +27,28 @@ export default {
message.error(res.error)
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;
},
*updateCluster({payload}, {call, put, select}) {
@ -35,6 +57,27 @@ export default {
message.error(res.error)
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;
},
*deleteCluster({payload}, {call, put, select}) {
@ -51,7 +94,16 @@ export default {
type: 'saveData',
payload: {
data,
total: total -1,
total: {
...total,
value: total.value + 1
}
}
})
yield put({
type: 'global/removeCluster',
payload: {
id: payload.id
}
})
return res;

View File

@ -9,7 +9,9 @@ export async function createClusterConfig(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',
body: params,
});
@ -23,7 +25,7 @@ export async function deleteClusterConfig(params) {
}
export async function searchClusterConfig(params) {
let url = `${pathPrefix}/system/cluster`;
let url = `${pathPrefix}/system/cluster/_search`;
let args = buildQueryArgs({
name: params.name,
enabled: params.enabled