modify cluster overview and alerting

This commit is contained in:
liugq 2021-10-08 14:53:47 +08:00
parent 879f529514
commit 947b0bfa04
20 changed files with 439 additions and 99 deletions

View File

@ -0,0 +1,87 @@
package index_management
import (
"fmt"
httprouter "infini.sh/framework/core/api/router"
"infini.sh/framework/core/elastic"
"infini.sh/framework/core/orm"
"infini.sh/framework/core/util"
"infini.sh/framework/modules/elastic/common"
"net/http"
log "github.com/cihub/seelog"
)
func (handler APIHandler) ElasticsearchOverviewAction(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
var (
totalNode int
totalStoreSize int
hosts = map[string]struct{}{}
)
elastic.WalkConfigs(func(key, value interface{})bool{
if handler.Config.Elasticsearch == key {
return true
}
data, err := handler.getLatestClusterMonitorData(key)
if err != nil{
log.Error(err)
}
val, err := data.GetValue("cluster_stats.nodes.count.total")
if err != nil {
log.Warn(err)
}
if num, ok := val.(float64); ok {
totalNode += int(num)
}
val, err = data.GetValue("index_stats._all.total.store.size_in_bytes")
if err != nil {
log.Warn(err)
}
if num, ok := val.(float64); ok {
totalStoreSize += int(num)
}
val, err = data.GetValue("agent.ip")
if err != nil {
log.Warn(err)
}
if ip, ok := val.(string); ok {
hosts[ip] = struct{}{}
}
return true
})
resBody := util.MapStr{
"total_node": totalNode,
"total_store_size_in_bytes": totalStoreSize,
"total_host": len(hosts),
//"hosts": hosts,
}
handler.WriteJSON(w, resBody, http.StatusOK)
}
func (handler APIHandler) getLatestClusterMonitorData(clusterID interface{}) (util.MapStr, error){
client := elastic.GetClient(handler.Config.Elasticsearch)
queryDSLTpl := `{
"size": 1,
"query": {
"match": {
"elasticsearch": "%s"
}
},
"sort": [
{
"cluster_stats.timestamp": {
"order": "desc"
}
}
]
}`
queryDSL := fmt.Sprintf(queryDSLTpl, clusterID)
searchRes, err := client.SearchWithRawQueryDSL(orm.GetIndexName(common.MonitoringItem{}), []byte(queryDSL))
if err != nil {
return nil, err
}
if len(searchRes.Hits.Hits) == 0 {
return nil, nil
}
return searchRes.Hits.Hits[0].Source, nil
}

View File

@ -17,6 +17,8 @@ func Init(cfg *config.AppConfig) {
} }
var pathPrefix = "/_search-center/" var pathPrefix = "/_search-center/"
//ui.HandleUIMethod(api.POST, "/api/get_indices",index_management.API1) //ui.HandleUIMethod(api.POST, "/api/get_indices",index_management.API1)
ui.HandleUIMethod(api.GET, path.Join(pathPrefix, "elasticsearch/overview"), handler.ElasticsearchOverviewAction)
ui.HandleUIMethod(api.GET, path.Join(pathPrefix, "dict/_search"), handler.GetDictListAction) ui.HandleUIMethod(api.GET, path.Join(pathPrefix, "dict/_search"), handler.GetDictListAction)
ui.HandleUIMethod(api.POST, path.Join(pathPrefix, "dict/*id"), handler.CreateDictItemAction) ui.HandleUIMethod(api.POST, path.Join(pathPrefix, "dict/*id"), handler.CreateDictItemAction)
//ui.HandleUIMethod(api.GET, "/api/dict/:id",handler.GetDictItemAction) //ui.HandleUIMethod(api.GET, "/api/dict/:id",handler.GetDictItemAction)

View File

@ -679,14 +679,14 @@ func ExecuteMonitor(w http.ResponseWriter, req *http.Request, ps httprouter.Para
} }
monitorCtx, err := createMonitorContext(&trigger, resBody, &sm, IfaceMap{}) monitorCtx, err := createMonitorContext(&trigger, resBody, &sm, IfaceMap{})
if err != nil { if err != nil {
triggerResult["error"] = err triggerResult["error"] = err.Error()
triggerResults[trigger.ID] = triggerResult triggerResults[trigger.ID] = triggerResult
continue continue
} }
isTrigger, err := resolveTriggerResult(&trigger, monitorCtx) isTrigger, err := resolveTriggerResult(&trigger, monitorCtx)
triggerResult["triggered"] = isTrigger triggerResult["triggered"] = isTrigger
if err != nil { if err != nil {
triggerResult["error"] = err triggerResult["error"] = err.Error()
} }
if trigger.ID == "" { if trigger.ID == "" {
trigger.ID = util.GetUUID() trigger.ID = util.GetUUID()

View File

@ -214,9 +214,9 @@ export default [
}, },
] ]
}, },
//
//
// //search //search
// { // {
// path: '/search', // path: '/search',
// name: 'search', // name: 'search',

View File

@ -65,7 +65,7 @@ const SendRequestButton = (props: any) => {
interface ConsoleInputProps { interface ConsoleInputProps {
clusterID: string, clusterID: string,
initialText: string, initialText: string | undefined,
} }
const DEFAULT_INPUT_VALUE = `GET _search const DEFAULT_INPUT_VALUE = `GET _search
@ -198,7 +198,7 @@ const ConsoleInputUI = ({clusterID, initialText}:ConsoleInputProps) => {
); );
}; };
const ConsoleInput = ({clusterID}:ConsoleInputProps)=>{ const ConsoleInput = ({clusterID}:{clusterID:string})=>{
const { done, error, retry } = useDataInit(); const { done, error, retry } = useDataInit();
const { currentTextObject } = useEditorReadContext(); const { currentTextObject } = useEditorReadContext();
return done ? <ConsoleInputUI clusterID={clusterID} initialText={currentTextObject?.text}/>: <></> return done ? <ConsoleInputUI clusterID={clusterID} initialText={currentTextObject?.text}/>: <></>

View File

@ -35,7 +35,7 @@ export class Settings {
constructor(private readonly storage: Storage) {} constructor(private readonly storage: Storage) {}
getFontSize() { getFontSize() {
return this.storage.get('font_size', 14); return this.storage.get('font_size', 12);
} }
setFontSize(size: any) { setFontSize(size: any) {

19
web/src/lib/format.js Normal file
View File

@ -0,0 +1,19 @@
const unitArr = Array("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB");
export const formatter = {
bytes: (value) => {
if (isNaN(value) || null == value || value === ''||value==0) {
return "0B";
}
var index = 0;
var srcsize = parseFloat(value);
index = Math.floor(Math.log(srcsize) / Math.log(1024));
var size = srcsize / Math.pow(1024, index);
size = size.toFixed(1);
return {
size,
unit: unitArr[index],
};
},
}

View File

@ -145,6 +145,7 @@ export default {
'alert.trigger.edit.action.field.name': 'Action name', 'alert.trigger.edit.action.field.name': 'Action name',
'alert.trigger.edit.action.field.destination': 'Destination', 'alert.trigger.edit.action.field.destination': 'Destination',
'alert.trigger.edit.action.field.message': 'Message', 'alert.trigger.edit.action.field.message': 'Message',
'alert.trigger.edit.action.field.message.info.paragraph1': 'You have access to a "_ctx" variable in your yaml scripts and action templates.',
'alert.trigger.edit.action.field.message_subject': 'Message subject', 'alert.trigger.edit.action.field.message_subject': 'Message subject',
'alert.trigger.edit.action.field.message_preview': 'Message preview', 'alert.trigger.edit.action.field.message_preview': 'Message preview',
'alert.trigger.edit.action.send_test_message': 'Send test message', 'alert.trigger.edit.action.send_test_message': 'Send test message',

View File

@ -143,7 +143,8 @@ export default {
'alert.trigger.edit.action.button.add_action': '添加通知', 'alert.trigger.edit.action.button.add_action': '添加通知',
'alert.trigger.edit.action.field.name': '通知名称', 'alert.trigger.edit.action.field.name': '通知名称',
'alert.trigger.edit.action.field.destination': '渠道选择', 'alert.trigger.edit.action.field.destination': '渠道选择',
'alert.trigger.edit.action.field.message': '消息配置', 'alert.trigger.edit.action.field.message': '消息',
'alert.trigger.edit.action.field.message.info.paragraph1': '您可以在 yaml 脚本中和通知模版中访问 "_ctx" 变量',
'alert.trigger.edit.action.field.message_subject': '消息标题', 'alert.trigger.edit.action.field.message_subject': '消息标题',
'alert.trigger.edit.action.field.message_preview': '消息预览', 'alert.trigger.edit.action.field.message_preview': '消息预览',
'alert.trigger.edit.action.send_test_message': '发送测试消息', 'alert.trigger.edit.action.send_test_message': '发送测试消息',

View File

@ -16,6 +16,7 @@
import React from 'react'; import React from 'react';
import { EuiLink, EuiText, EuiTitle } from '@elastic/eui'; import { EuiLink, EuiText, EuiTitle } from '@elastic/eui';
import { URL } from '../../../utils/constants'; import { URL } from '../../../utils/constants';
import {formatMessage} from 'umi/locale';
const message = () => ({ const message = () => ({
flyoutProps: { flyoutProps: {
@ -27,14 +28,14 @@ const message = () => ({
header: ( header: (
<EuiTitle size="m" style={{ fontSize: '25px' }}> <EuiTitle size="m" style={{ fontSize: '25px' }}>
<h2> <h2>
<strong>Message</strong> <strong>{formatMessage({id:"alert.trigger.edit.action.field.message"})}</strong>
</h2> </h2>
</EuiTitle> </EuiTitle>
), ),
body: ( body: (
<EuiText style={{ fontSize: '14px' }}> <EuiText style={{ fontSize: '14px' }}>
<p> <p>
{`You have access to a "_ctx" variable in your yaml scripts and action quicktemplate templates.`} {formatMessage({id:"alert.trigger.edit.action.field.message.info.paragraph1"})}
</p> </p>
{/* <h3>Learn More</h3> {/* <h3>Learn More</h3>
<ul> <ul>

View File

@ -34,7 +34,7 @@ const CONTEXT_VARIABLES = JSON.stringify(
const triggerCondition = context => ({ const triggerCondition = context => ({
flyoutProps: { flyoutProps: {
'aria-labelledby': 'triggerConditionFlyout', 'aria-labelledby': 'triggerConditionFlyout',
maxWidth: 500, maxWidth: 540,
size: 'm', size: 'm',
}, },
headerProps: { hasBorder: true }, headerProps: { hasBorder: true },
@ -50,6 +50,16 @@ const triggerCondition = context => ({
<EuiText style={{ fontSize: '14px' }}> <EuiText style={{ fontSize: '14px' }}>
<p>{formatMessage({id:'alert.trigger.edit.define.field.condition.info.paragraph1'})}</p> <p>{formatMessage({id:'alert.trigger.edit.define.field.condition.info.paragraph1'})}</p>
<p> <p>
逻辑判断操作包括 or, and, not, in, range, equals, contains, regexp其中 or, and, not 支持嵌套
<EuiCodeBlock language="yaml">
{`and:
- range:
_ctx.results.[0].hits.total.value.gt: 0
- contains:
_ctx.results.[0].hits.hits.[0]._source.field: value`}
</EuiCodeBlock>
</p>
<p>
{formatMessage({id:'alert.trigger.edit.define.field.condition.info.paragraph2'})} {formatMessage({id:'alert.trigger.edit.define.field.condition.info.paragraph2'})}
</p> </p>
</EuiText> </EuiText>

View File

@ -5,6 +5,7 @@ import DestinationsList from './pages/Destinations/containers/DestinationsList';
import {Fetch} from '../../components/kibana/core/public/http/fetch'; import {Fetch} from '../../components/kibana/core/public/http/fetch';
import {ScopedHistory} from '../../components/kibana/core/public/application/scoped_history'; import {ScopedHistory} from '../../components/kibana/core/public/application/scoped_history';
import {notification} from 'antd'; import {notification} from 'antd';
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
const Destination = ({httpClient, notifications, history})=> { const Destination = ({httpClient, notifications, history})=> {
return ( return (
@ -85,6 +86,8 @@ export default (props)=>{
}, [props.history]) }, [props.history])
return ( return (
<Destination httpClient={httpClient} notifications={notifications} history={history} /> <PageHeaderWrapper>
<Destination httpClient={httpClient} notifications={notifications} history={history} />
</PageHeaderWrapper>
) )
} }

View File

@ -6,6 +6,7 @@ import {useMemo} from 'react';
import {ScopedHistory} from '../../components/kibana/core/public/application/scoped_history'; import {ScopedHistory} from '../../components/kibana/core/public/application/scoped_history';
import {notification} from 'antd'; import {notification} from 'antd';
import {connect} from 'dva' import {connect} from 'dva'
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
const httpClient = new Fetch({ const httpClient = new Fetch({
basePath:{ basePath:{
@ -49,15 +50,17 @@ const AlertingUI = (props)=>{
}, [props.history]) }, [props.history])
return ( return (
<CoreContext.Provider key={props.selectedCluster.id} <PageHeaderWrapper>
value={{ http: httpClient, isDarkMode, notifications: notifications }} <CoreContext.Provider key={props.selectedCluster.id}
> value={{ http: httpClient, isDarkMode, notifications: notifications }}
<Router history={history}> >
<div style={{background:'#fff'}}> <Router history={history}>
<Main title="Alerting" {...props} /> <div style={{background:'#fff'}}>
</div> <Main title="Alerting" {...props} />
</Router> </div>
</CoreContext.Provider> </Router>
</CoreContext.Provider>
</PageHeaderWrapper>
) )
} }

View File

@ -4,6 +4,7 @@ import {useState, useEffect} from 'react';
import './alertoverview.scss'; import './alertoverview.scss';
import { formatMessage } from 'umi/locale'; import { formatMessage } from 'umi/locale';
export const AlertOverview = (props: any)=>{ export const AlertOverview = (props: any)=>{
const {httpClient, history} = props; const {httpClient, history} = props;
const [data, setData] = useState({ const [data, setData] = useState({
@ -16,8 +17,7 @@ export const AlertOverview = (props: any)=>{
totalAlerts: 0, totalAlerts: 0,
}); });
const getAlerts = _.debounce( const getAlerts = (from, size, search, sortField, sortDirection, severityLevel, alertState, monitorIds, type) => {
(from, size, search, sortField, sortDirection, severityLevel, alertState, monitorIds, type) => {
let params = { let params = {
from, from,
size, size,
@ -31,41 +31,42 @@ export const AlertOverview = (props: any)=>{
if(monitorIds){ if(monitorIds){
params["monitorIds"]= monitorIds; params["monitorIds"]= monitorIds;
} }
httpClient.get('/alerting/alerts', { query: params }).then((resp:any) => { return httpClient.get('/alerting/alerts', { query: params })
if (resp.ok) { }
const useData = (pageSize: number, page: number, type: string):[any,any] => {
const [size, setSize] = useState(pageSize || 10);
const [pageIndex, setPageIndex] = useState(page || 1);
const [alertData, setAlertData] = useState({
data: [],
total: 0,
});
useEffect(()=>{
const from = (pageIndex - 1) * size;
const fetchAlerts = async (from: number, size: number)=>{
const resp = await getAlerts(from, size,"", "start_time", "desc", "ALL", "ALL","", type);
if(resp.ok){
const { alerts, totalAlerts } = resp; const { alerts, totalAlerts } = resp;
if(type == 'ALERT_HISTORY'){ setAlertData({
setHistoryData({ ...alertData,
alerts, data: alerts,
totalAlerts, total: totalAlerts,
}); })
return;
}
setData({
alerts,
totalAlerts,
});
} else {
console.log('error getting alerts:', resp);
} }
}); }
}, fetchAlerts(from,size);
500, }, [pageIndex, size, type]);
{ leading: true } const changePage = (pageIndex: number) => {
); setPageIndex(pageIndex);
}
return [alertData, changePage];
}
const pageSize = 10; const pageSize = 10;
useEffect(()=>{ const [alerts, onAlertPageChange] = useData(pageSize, 1, "ALERT");
getAlerts(0, pageSize, "", "start_time", "desc", "ALL", "ALL","", "ALERT"); const [historyAlerts, onAlertHistoryPageChange] = useData(pageSize, 1, "ALERT_HISTORY");
getAlerts(0, pageSize, "", "start_time", "desc", "ALL", "ALL","", "ALERT_HISTORY")
},[])
const onPageChangeGen = (type:string) => {
return (pageIndex: number)=>{
const from = (pageIndex - 1) * pageSize;
getAlerts(from, pageSize, "", "start_time", "desc", "ALL", "ALL","", type)
}
}
const onItemClick = (item: any)=>{ const onItemClick = (item: any)=>{
@ -75,21 +76,21 @@ export const AlertOverview = (props: any)=>{
return ( return (
<div className="alert-overview"> <div className="alert-overview">
<div className="left"> <div className="left">
<AlertList dataSource={data.alerts} <AlertList dataSource={alerts.data as any}
title={formatMessage({id:'alert.overview.alertlist.title'})} title={formatMessage({id:'alert.overview.alertlist.title'})}
onItemClick={onItemClick} onItemClick={onItemClick}
pagination={{ pagination={{
pageSize, pageSize,
total: data.totalAlerts, total: alerts.total,
onChange: onPageChangeGen('ALERT'), onChange: onAlertPageChange,
}}/> }}/>
<AlertList dataSource={historyData.alerts} <AlertList dataSource={historyAlerts.data}
title={formatMessage({id:'alert.overview.alertlist-history.title'})} title={formatMessage({id:'alert.overview.alertlist-history.title'})}
onItemClick={onItemClick} onItemClick={onItemClick}
pagination={{ pagination={{
pageSize, pageSize,
total: historyData.totalAlerts, total: historyAlerts.total,
onChange: onPageChangeGen('ALERT_HISTORY'), onChange: onAlertHistoryPageChange,
}}/> }}/>
</div> </div>
{/* <div className="right"> {/* <div className="right">

View File

@ -16,7 +16,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import _ from 'lodash'; import _ from 'lodash';
import queryString from 'query-string'; import queryString from 'query-string';
import { EuiBasicTable, EuiButton, EuiHorizontalRule, EuiIcon,RIGHT_ALIGNMENT,EuiButtonIcon } from '@elastic/eui'; import { EuiBasicTable, EuiButton, EuiHorizontalRule, EuiIcon,RIGHT_ALIGNMENT,EuiButtonIcon,EuiDescriptionList } from '@elastic/eui';
import ContentPanel from '../../../components/ContentPanel'; import ContentPanel from '../../../components/ContentPanel';
import DashboardEmptyPrompt from '../components/DashboardEmptyPrompt'; import DashboardEmptyPrompt from '../components/DashboardEmptyPrompt';
@ -55,20 +55,6 @@ export default class Dashboard extends Component {
sortField, sortField,
} = this.getURLQueryParams(); } = this.getURLQueryParams();
const tableColumns = [...columns, {
align: RIGHT_ALIGNMENT,
width: '40px',
isExpander: true,
render: (item) => {
const {itemIdToExpandedRowMap} = this.state;
(
<EuiButtonIcon
onClick={() => toggleDetails(item)}
aria-label={itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'}
iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'}
/>
)},
}]
this.state = { this.state = {
alerts: [], alerts: [],
@ -83,7 +69,7 @@ export default class Dashboard extends Component {
sortField, sortField,
totalAlerts: 0, totalAlerts: 0,
itemIdToExpandedRowMap:{}, itemIdToExpandedRowMap:{},
columns: tableColumns, columns: columns,
}; };
} }
@ -305,6 +291,27 @@ export default class Dashboard extends Component {
this.setState({ page }); this.setState({ page });
}; };
toggleDetails = (item)=>{
const id = `${item.id}-${item.version}`;
const itemIdToExpandedRowMapValues = { ...this.state.itemIdToExpandedRowMap };
if (itemIdToExpandedRowMapValues[id]) {
delete itemIdToExpandedRowMapValues[id];
} else {
const listItems = [
{
title: 'error',
description: `${item.error_message}`,
}
]
itemIdToExpandedRowMapValues[id] = (
<EuiDescriptionList listItems={listItems} />
);
}
this.setState({
itemIdToExpandedRowMap:itemIdToExpandedRowMapValues,
});
}
render() { render() {
const { const {
alerts, alerts,
@ -316,9 +323,28 @@ export default class Dashboard extends Component {
sortDirection, sortDirection,
sortField, sortField,
totalAlerts, totalAlerts,
columns,
itemIdToExpandedRowMap,
} = this.state; } = this.state;
const { monitorIds, detectorIds, onCreateTrigger } = this.props; const { monitorIds, detectorIds, onCreateTrigger } = this.props;
const tableColumns = [...columns, {
align: RIGHT_ALIGNMENT,
width: '40px',
isExpander: true,
render: (item) => {
if(item.state != 'ERROR'){
return null;
}
return (
<EuiButtonIcon
onClick={() => this.toggleDetails(item)}
aria-label={itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'}
iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'}
/>
)},
}]
const pagination = { const pagination = {
pageIndex: page, pageIndex: page,
pageSize: size, pageSize: size,
@ -376,6 +402,7 @@ export default class Dashboard extends Component {
<EuiBasicTable <EuiBasicTable
itemIdToExpandedRowMap={this.state.itemIdToExpandedRowMap} itemIdToExpandedRowMap={this.state.itemIdToExpandedRowMap}
isExpandable={true} isExpandable={true}
hasActions={true}
items={alerts} items={alerts}
/* /*
* If using just ID, doesn't update selectedItems when doing acknowledge * If using just ID, doesn't update selectedItems when doing acknowledge
@ -383,7 +410,7 @@ export default class Dashboard extends Component {
* $id-$version will correctly remove selected items * $id-$version will correctly remove selected items
* */ * */
itemId={(item) => `${item.id}-${item.version}`} itemId={(item) => `${item.id}-${item.version}`}
columns={this.state.columns} columns={tableColumns}
pagination={pagination} pagination={pagination}
sorting={sorting} sorting={sorting}
isSelectable={true} isSelectable={true}

View File

@ -85,7 +85,7 @@ export const columns = [
render: (state, alert) => { render: (state, alert) => {
const stateText = const stateText =
typeof state !== 'string' ? DEFAULT_EMPTY_DATA : _.capitalize(state.toLowerCase()); typeof state !== 'string' ? DEFAULT_EMPTY_DATA : _.capitalize(state.toLowerCase());
return state === ALERT_STATE.ERROR ? `${stateText}: ${alert.error_message}` : (stateOptions[state] || stateText); return (stateOptions[state] || stateText); // state === ALERT_STATE.ERROR ? `${stateText}: ${alert.error_message}` :
}, },
}, },
{ {

View File

@ -98,6 +98,17 @@ export default class Triggers extends Component {
sortable: true, sortable: true,
truncateText: false, truncateText: false,
}, },
{
actions: [{
name: 'Edit',
icon: 'documentEdit',
type: 'icon',
description: formatMessage({ id: 'form.button.edit' }),
onClick: (item)=>{
this.props.onEditTrigger(item);
},
}],
},
]; ];
const sorting = { sort: { field, direction } }; const sorting = { sort: { field, direction } };
@ -110,9 +121,9 @@ export default class Triggers extends Component {
titleSize="s" titleSize="s"
bodyStyles={{ padding: 'initial' }} bodyStyles={{ padding: 'initial' }}
actions={[ actions={[
<EuiButton onClick={this.onEdit} disabled={selectedItems.length !== 1}> // <EuiButton onClick={this.onEdit} disabled={selectedItems.length !== 1}>
{formatMessage({ id: 'form.button.edit' })} // {formatMessage({ id: 'form.button.edit' })}
</EuiButton>, // </EuiButton>,
<EuiButton onClick={this.onDelete} disabled={!selectedItems.length}> <EuiButton onClick={this.onDelete} disabled={!selectedItems.length}>
{formatMessage({ id: 'form.button.delete' })} {formatMessage({ id: 'form.button.delete' })}
</EuiButton>, </EuiButton>,

View File

@ -1,8 +1,11 @@
import React from 'react'; import React from 'react';
import {List,Card,Row,Icon,Col} from "antd"; import {List,Card,Row,Icon,Col, Table} from "antd";
import styles from "./Overview.less"; import styles from "./Overview.less";
import {connect} from "dva"; import {connect} from "dva";
import {formatGigNumber} from "@/utils/utils"; import {formatGigNumber} from "@/utils/utils";
import {HealthStatusCircle} from '@/components/infini/health_status_circle';
import {formatter} from '@/lib/format';
import Link from 'umi/link';
const tabList = [ const tabList = [
@ -10,19 +13,21 @@ const tabList = [
key: 'tabCluster', key: 'tabCluster',
tab: '集群', tab: '集群',
}, },
{ // {
key: 'tabHost', // key: 'tabHost',
tab: '主机', // tab: '主机',
}, // },
{ // {
key: 'tabNode', // key: 'tabNode',
tab: '节点', // tab: '节点',
}, // },
]; ];
@connect(({global}) => ({ @connect(({clusterOverview, global}) => ({
clusterList: clusterOverview.clusterList,
clusterStatus: global.clusterStatus,
overview: clusterOverview.overview,
})) }))
@ -30,22 +35,134 @@ class Overview extends React.Component {
state = { state = {
tabkey: 'tabCluster', tabkey: 'tabCluster',
}; };
clusterColumns = [{
title: '集群名称',
dataIndex: 'name',
key: 'name',
render:(val, item)=>{
return <Link to={`/cluster/metrics/elasticsearch/${item.id}`}>{val}</Link>;
}
},{
title: '健康状态',
dataIndex: 'id',
key: 'health_status',
render: (val)=>{
const {clusterStatus} = this.props;
if(!clusterStatus || !clusterStatus[val]){
return
}
const isAvailable = clusterStatus[val].available;
if(!isAvailable){
return <Icon type="close-circle" style={{width:14, height:14, color:'red',borderRadius: 14, boxShadow: '0px 0px 5px #555'}}/>
}
const status = clusterStatus[val].health?.status;
return <HealthStatusCircle status={status}/>
}
},{
title: '所属业务',
dataIndex: 'business',
key: 'business',
render: ()=>{
return 'eu-de-1'
}
},
{
title: '所属部门',
dataIndex: 'business_department',
key: 'business_department',
render: ()=>{
return '部门X'
}
}, {
title: '部署环境',
dataIndex: 'deploy_env',
key: 'deploy_env',
render: ()=>{
return 'PROD'
}
},{
title: '程序版本',
dataIndex: 'version',
key: 'elasticsearch_version',
// render: (data)=>{
// return
// }
},{
title: '节点数',
dataIndex: 'id',
key: 'number_of_nodes',
render: (val)=>{
const {clusterStatus} = this.props;
if(!clusterStatus || !clusterStatus[val]){
return
}
return clusterStatus[val].health?.number_of_nodes;
}
},{
title: '集群地址',
dataIndex: 'host',
key: 'host',
},
{
title: '监控启用状态',
dataIndex: 'monitored',
key: 'monitored',
render: (val) => {
return val? '启用': '关闭';
}
}]
onOperationTabChange = key => { onOperationTabChange = key => {
this.setState({ tabkey: key }); this.setState({ tabkey: key });
}; };
getClusterList = (params)=>{
const {dispatch} = this.props;
dispatch({
type: 'clusterOverview/fetchClusterList',
payload: params,
})
}
getOverviewData = (params)=>{
const {dispatch} = this.props;
dispatch({
type: 'clusterOverview/fetchOverview',
})
}
componentDidMount() {
this.getClusterList({size:20})
this.getOverviewData();
}
render() { render() {
const { tabkey } = this.state; const { tabkey } = this.state;
const {clusterList, overview} = this.props;
const contentList = { const contentList = {
tabCluster: ( tabCluster: (
<Card title="集群列表" style={{ marginBottom: 24 }} bordered={false}> <Card style={{ marginBottom: 24 }} bordered={false}>
<div> {/* <div>
<Icon type="frown-o" /> <Icon type="frown-o" />
暂无数据 暂无数据
</div> </div> */}
<Table
bordered
columns={this.clusterColumns}
dataSource={clusterList?.data}
rowKey='id'
pagination={{
total: clusterList?.total?.value,
pageSize: 20,
onChange: (page, pageSize)=>{
this.getClusterList({
size: pageSize,
from: (page - 1)*pageSize,
})
}
}}
/>
</Card> </Card>
), ),
tabHost: ( tabHost: (
@ -65,6 +182,7 @@ class Overview extends React.Component {
</Card> </Card>
), ),
}; };
const totalStoreSize = formatter.bytes(overview?.total_store_size_in_bytes || 0);
return ( return (
<div> <div>
@ -76,7 +194,7 @@ class Overview extends React.Component {
> >
<Card.Meta title='集群总数' className={styles.title} /> <Card.Meta title='集群总数' className={styles.title} />
<div> <div>
<span className={styles.total}>1</span> <span className={styles.total}>{clusterList?.total?.value}</span>
</div> </div>
</Card> </Card>
</Col> </Col>
@ -87,7 +205,7 @@ class Overview extends React.Component {
> >
<Card.Meta title='主机总数' className={styles.title} /> <Card.Meta title='主机总数' className={styles.title} />
<div> <div>
<span className={styles.total}>1</span> <span className={styles.total}>{overview?.total_host}</span>
</div> </div>
</Card> </Card>
</Col> </Col>
@ -98,7 +216,7 @@ class Overview extends React.Component {
> >
<Card.Meta title='节点总数' className={styles.title} /> <Card.Meta title='节点总数' className={styles.title} />
<div> <div>
<span className={styles.total}>1</span> <span className={styles.total}>{overview?.total_node}</span>
</div> </div>
</Card> </Card>
</Col> </Col>
@ -109,7 +227,7 @@ class Overview extends React.Component {
> >
<Card.Meta title='存储空间' className={styles.title} /> <Card.Meta title='存储空间' className={styles.title} />
<div> <div>
<span className={styles.total}>100</span><span className={styles.unit}>GB</span> <span className={styles.total}>{totalStoreSize.size}</span><span className={styles.unit}>{totalStoreSize.unit}</span>
</div> </div>
</Card> </Card>
</Col> </Col>

View File

@ -0,0 +1,48 @@
import {searchClusterConfig} from "@/services/cluster";
import {getOverview} from "@/services/overview";
import {message} from "antd";
import {formatESSearchResult} from '@/lib/elasticsearch/util';
export default {
namespace: 'clusterOverview',
state: {
},
effects:{
*fetchClusterList({payload}, {call, put, select}){
let res = yield call(searchClusterConfig, payload);
if(res.error){
message.error(res.error)
return false;
}
res = formatESSearchResult(res);
yield put({
type: 'saveData',
payload: {
clusterList: res,
}
})
},
*fetchOverview({}, {call, put}){
let res = yield call(getOverview);
if(res.error){
message.error(res.error)
return false;
}
yield put({
type: 'saveData',
payload: {
overview: res,
}
})
}
},
reducers:{
saveData(state, {payload}){
return {
...state,
...payload
};
},
}
};

View File

@ -0,0 +1,8 @@
import request from '@/utils/request';
import {pathPrefix} from './common';
export async function getOverview(params) {
return request(`${pathPrefix}/elasticsearch/overview`, {
method: 'GET'
});
}