init
9326
console/package-lock.json
generated
Normal file
45
console/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "mobx-ts-example",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"start": "webpack-dev-server",
|
||||
"daily-build": "rm -rf dist && NODE_ENV=production webpack",
|
||||
"pre-build": "rm -rf dist && NODE_ENV=production webpack",
|
||||
"prod-build": "rm -rf dist && NODE_ENV=production webpack"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@hot-loader/react-dom": "^16.8.6",
|
||||
"@types/echarts": "^4.1.9",
|
||||
"@types/react": "^16.8.8",
|
||||
"@types/react-dom": "^16.8.2",
|
||||
"@types/react-router-dom": "^4.3.1",
|
||||
"antd": "^3.16.1",
|
||||
"css-loader": "^2.1.0",
|
||||
"echarts": "^4.2.1",
|
||||
"file-loader": "^5.0.2",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"less": "^3.9.0",
|
||||
"less-loader": "^4.1.0",
|
||||
"mini-css-extract-plugin": "^0.6.0",
|
||||
"mobx": "^5.9.4",
|
||||
"mobx-react": "^5.4.3",
|
||||
"moment": "^2.24.0",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.1",
|
||||
"react": "^16.8.4",
|
||||
"react-hot-loader": "^4.8.4",
|
||||
"react-router-dom": "^5.0.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"terser-webpack-plugin": "^1.2.3",
|
||||
"ts-loader": "^5.3.3",
|
||||
"tsconfig-paths-webpack-plugin": "^3.2.0",
|
||||
"tslint": "^5.13.1",
|
||||
"tslint-react": "^3.6.0",
|
||||
"typescript": "^3.3.3333",
|
||||
"webpack": "^4.29.6",
|
||||
"webpack-cli": "^3.2.3",
|
||||
"webpack-dev-server": "^3.2.1"
|
||||
}
|
||||
}
|
||||
56
console/pom.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>kafka-manager</artifactId>
|
||||
<groupId>com.xiaojukeji.kafka</groupId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>kafka-manager-console</artifactId>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.github.eirslett</groupId>
|
||||
<artifactId>frontend-maven-plugin</artifactId>
|
||||
<version>1.6</version>
|
||||
<configuration>
|
||||
<installDirectory>target</installDirectory>
|
||||
<workingDirectory>./</workingDirectory>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>install node and npm</id>
|
||||
<goals>
|
||||
<goal>install-node-and-npm</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<nodeVersion>v8.12.0</nodeVersion>
|
||||
<npmVersion>6.4.1</npmVersion>
|
||||
<nodeDownloadRoot>http://npm.taobao.org/mirrors/node/</nodeDownloadRoot>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>npm install</id>
|
||||
<goals>
|
||||
<goal>npm</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<arguments>install</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>npm run prod-build</id>
|
||||
<goals>
|
||||
<goal>npm</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<arguments>run prod-build</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
BIN
console/src/assets/image/admin.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
console/src/assets/image/devops.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
5
console/src/assets/image/images.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare module '*.svg';
|
||||
declare module '*.png';
|
||||
declare module '*.jpg';
|
||||
declare module '*.jpeg';
|
||||
declare module '*.gif';
|
||||
BIN
console/src/assets/image/kafka-logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
console/src/assets/image/kafka-manager.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
console/src/assets/image/login-bg.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
console/src/assets/image/logo.ico
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
console/src/assets/image/normal.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
104
console/src/component/antd/index.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import message from 'antd/es/message';
|
||||
import 'antd/es/message/style';
|
||||
|
||||
import Input from 'antd/es/input';
|
||||
import 'antd/es/input/style';
|
||||
|
||||
import InputNumber from 'antd/es/input-number';
|
||||
import 'antd/es/input-number/style';
|
||||
|
||||
import Table from 'antd/es/table';
|
||||
import 'antd/es/table/style';
|
||||
|
||||
import Tabs from 'antd/es/tabs';
|
||||
import 'antd/es/tabs/style';
|
||||
|
||||
import Select from 'antd/es/select';
|
||||
import 'antd/es/select/style';
|
||||
|
||||
import DatePicker from 'antd/es/date-picker';
|
||||
import 'antd/es/date-picker/style';
|
||||
|
||||
import Button from 'antd/es/button';
|
||||
import 'antd/es/button/style';
|
||||
|
||||
import Modal from 'antd/es/modal';
|
||||
import 'antd/es/modal/style';
|
||||
|
||||
import Form from 'antd/es/form';
|
||||
import 'antd/es/form/style';
|
||||
|
||||
import Row from 'antd/es/row';
|
||||
import 'antd/es/row/style';
|
||||
|
||||
import Col from 'antd/es/col';
|
||||
import 'antd/es/col/style';
|
||||
|
||||
import Switch from 'antd/es/switch';
|
||||
import 'antd/es/switch/style';
|
||||
|
||||
import Alert from 'antd/es/alert';
|
||||
import 'antd/es/alert/style';
|
||||
|
||||
import { PaginationConfig, ColumnProps } from 'antd/es/table/interface';
|
||||
|
||||
import notification from 'antd/es/notification';
|
||||
import 'antd/es/notification/style';
|
||||
|
||||
import Tooltip from 'antd/es/tooltip';
|
||||
import 'antd/es/tooltip/style';
|
||||
|
||||
import Radio from 'antd/es/radio';
|
||||
import 'antd/es/radio';
|
||||
import { RadioChangeEvent } from 'antd/es/radio';
|
||||
|
||||
import Collapse from 'antd/es/collapse';
|
||||
import 'antd/es/collapse/style';
|
||||
|
||||
import Icon from 'antd/es/icon';
|
||||
import 'antd/es/icon/style';
|
||||
|
||||
import Dropdown from 'antd/es/dropdown';
|
||||
import 'antd/es/dropdown/style';
|
||||
|
||||
import Spin from 'antd/es/spin';
|
||||
import 'antd/es/spin/style';
|
||||
|
||||
import Drawer from 'antd/es/drawer';
|
||||
import 'antd/es/drawer/style';
|
||||
|
||||
import Checkbox from 'antd/es/checkbox';
|
||||
import 'antd/es/checkbox/style';
|
||||
|
||||
import Affix from 'antd/es/affix';
|
||||
import 'antd/es/affix/style';
|
||||
|
||||
export {
|
||||
PaginationConfig,
|
||||
notification,
|
||||
ColumnProps,
|
||||
DatePicker,
|
||||
message,
|
||||
Tooltip,
|
||||
Button,
|
||||
Select,
|
||||
Switch,
|
||||
Modal,
|
||||
Input,
|
||||
Table,
|
||||
Radio,
|
||||
Alert,
|
||||
Tabs,
|
||||
Form,
|
||||
Row,
|
||||
Col,
|
||||
RadioChangeEvent,
|
||||
InputNumber,
|
||||
Collapse,
|
||||
Icon,
|
||||
Dropdown,
|
||||
Spin,
|
||||
Drawer,
|
||||
Checkbox,
|
||||
Affix,
|
||||
};
|
||||
82
console/src/component/flow-table/index.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import { Table } from 'component/antd';
|
||||
|
||||
interface IFlow {
|
||||
key: string;
|
||||
avr: number;
|
||||
pre1: number;
|
||||
pre5: number;
|
||||
pre15: number;
|
||||
}
|
||||
|
||||
const flowColumns = [{
|
||||
title: '名称',
|
||||
dataIndex: 'key',
|
||||
key: 'name',
|
||||
sorter: (a: IFlow, b: IFlow) => a.key.charCodeAt(0) - b.key.charCodeAt(0),
|
||||
render(t: string) {
|
||||
return t === 'byteRejected' ? 'byteRejected(B/s)' : (t === 'byteIn' || t === 'byteOut' ? `${t}(KB/s)` : t);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '平均数',
|
||||
dataIndex: 'avr',
|
||||
key: 'partition_num',
|
||||
sorter: (a: IFlow, b: IFlow) => a.avr - b.avr,
|
||||
},
|
||||
{
|
||||
title: '前1分钟',
|
||||
dataIndex: 'pre1',
|
||||
key: 'byte_input',
|
||||
sorter: (a: IFlow, b: IFlow) => a.pre1 - b.pre1,
|
||||
},
|
||||
{
|
||||
title: '前5分钟',
|
||||
dataIndex: 'pre5',
|
||||
key: 'byte_output',
|
||||
sorter: (a: IFlow, b: IFlow) => a.pre5 - b.pre5,
|
||||
},
|
||||
{
|
||||
title: '前15分钟',
|
||||
dataIndex: 'pre15',
|
||||
key: 'message',
|
||||
sorter: (a: IFlow, b: IFlow) => a.pre15 - b.pre15,
|
||||
}];
|
||||
|
||||
export interface IFlowInfo {
|
||||
byteIn: number[];
|
||||
byteOut: number[];
|
||||
byteRejected: number[];
|
||||
failedFetchRequest: number[];
|
||||
failedProduceRequest: number[];
|
||||
messageIn: number[];
|
||||
[key: string]: number[];
|
||||
}
|
||||
|
||||
export class StatusGraghCom<T extends IFlowInfo> extends React.Component {
|
||||
public getData(): T {
|
||||
return null;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const statusData = this.getData();
|
||||
if (!statusData) return null;
|
||||
const data: any[] = [];
|
||||
Object.keys(statusData).map((key) => {
|
||||
const v = key === 'byteIn' || key === 'byteOut' ? statusData[key].map(i => (i / 1024).toFixed(2)) :
|
||||
statusData[key].map(i => i.toFixed(2));
|
||||
const obj = {
|
||||
key,
|
||||
avr: v[0],
|
||||
pre1: v[1],
|
||||
pre5: v[2],
|
||||
pre15: v[3],
|
||||
};
|
||||
data.push(obj);
|
||||
});
|
||||
return (
|
||||
<Table columns={flowColumns} dataSource={data} pagination={false} />
|
||||
);
|
||||
}
|
||||
}
|
||||
69
console/src/container/admin-consume/detail.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { PaginationConfig, Table } from 'component/antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import urlQuery from 'store/url-query';
|
||||
import { IConsumeInfo } from 'store/topic';
|
||||
import { consume } from 'store/consume';
|
||||
import { SearchAndFilter } from 'container/cluster-topic';
|
||||
import { modal } from 'store';
|
||||
|
||||
const pagination: PaginationConfig = {
|
||||
position: 'bottom',
|
||||
showQuickJumper: true,
|
||||
pageSize: 10,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
};
|
||||
|
||||
@observer
|
||||
export class AdminConsume extends SearchAndFilter {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
public columns = [{
|
||||
title: '消费组名称',
|
||||
dataIndex: 'consumerGroup',
|
||||
key: 'consumerGroup',
|
||||
width: '70%',
|
||||
sorter: (a: IConsumeInfo, b: IConsumeInfo) => a.consumerGroup.charCodeAt(0) - b.consumerGroup.charCodeAt(0),
|
||||
}, {
|
||||
title: 'location',
|
||||
dataIndex: 'location',
|
||||
key: 'location',
|
||||
width: '20%',
|
||||
render: (t: string) => t.toLowerCase(),
|
||||
}, {
|
||||
title: '操作',
|
||||
key: 'operation',
|
||||
width: '10%',
|
||||
render: (t: string, r: IConsumeInfo) => {
|
||||
return (
|
||||
<a onClick={modal.showConsumerTopic.bind(null, Object.assign({ clusterId: urlQuery.clusterId }, r))}>详情</a>);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
public componentDidMount() {
|
||||
consume.getConsumeInfo(urlQuery.clusterId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const data = consume.data.filter((d) => d.consumerGroup.includes(this.state.searchKey));
|
||||
return (
|
||||
<>
|
||||
<ul className="table-operation-bar">
|
||||
{this.renderSearch('请输入消费组名称')}
|
||||
</ul>
|
||||
<div style={{ marginTop: '48px' }}>
|
||||
<Table
|
||||
columns={this.columns}
|
||||
dataSource={data}
|
||||
pagination={pagination}
|
||||
rowKey="consumerGroup"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
8
console/src/container/admin-consume/index.less
Normal file
@@ -0,0 +1,8 @@
|
||||
.content-container .table-operation-bar.in-panel {
|
||||
position: static;
|
||||
text-align: right;
|
||||
.new-topic {
|
||||
margin-right: 0;
|
||||
margin-left: 30px;
|
||||
}
|
||||
}
|
||||
69
console/src/container/admin-consume/index.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { PaginationConfig, Table } from 'component/antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import urlQuery from 'store/url-query';
|
||||
import { IConsumeInfo } from 'store/topic';
|
||||
import { consume } from 'store/consume';
|
||||
import { SearchAndFilter } from 'container/cluster-topic';
|
||||
import { modal } from 'store';
|
||||
|
||||
const pagination: PaginationConfig = {
|
||||
position: 'bottom',
|
||||
showQuickJumper: true,
|
||||
pageSize: 10,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
};
|
||||
|
||||
@observer
|
||||
export class AdminConsume extends SearchAndFilter {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
public columns = [{
|
||||
title: '消费组名称',
|
||||
dataIndex: 'consumerGroup',
|
||||
key: 'consumerGroup',
|
||||
width: '70%',
|
||||
sorter: (a: IConsumeInfo, b: IConsumeInfo) => a.consumerGroup.charCodeAt(0) - b.consumerGroup.charCodeAt(0),
|
||||
}, {
|
||||
title: 'location',
|
||||
dataIndex: 'location',
|
||||
key: 'location',
|
||||
width: '20%',
|
||||
render: (t: string) => t.toLowerCase(),
|
||||
}, {
|
||||
title: '操作',
|
||||
key: 'operation',
|
||||
width: '10%',
|
||||
render: (t: string, r: IConsumeInfo) => {
|
||||
return (
|
||||
<a onClick={modal.showConsumerTopic.bind(null, Object.assign({ clusterId: urlQuery.clusterId }, r))}>详情</a>);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
public componentDidMount() {
|
||||
consume.getConsumeInfo(urlQuery.clusterId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const data = consume.data.filter((d) => d.consumerGroup.includes(this.state.searchKey));
|
||||
return (
|
||||
<div className="k-row">
|
||||
<ul className="table-operation-bar">
|
||||
{this.renderSearch('请输入消费组名称')}
|
||||
</ul>
|
||||
<div style={{ marginTop: '48px' }}>
|
||||
<Table
|
||||
columns={this.columns}
|
||||
dataSource={data}
|
||||
pagination={pagination}
|
||||
rowKey="consumerGroup"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
87
console/src/container/admin-controller/index.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Table, Tabs, PaginationConfig } from 'component/antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import urlQuery from 'store/url-query';
|
||||
import { controller } from 'store/controller';
|
||||
import { SearchAndFilter } from 'container/cluster-topic';
|
||||
import moment from 'moment';
|
||||
|
||||
const TabPane = Tabs.TabPane;
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'BrokerId',
|
||||
dataIndex: 'brokerId',
|
||||
key: 'brokerId',
|
||||
sorter: (a: any, b: any) => a.brokerNum - b.brokerNum,
|
||||
},
|
||||
{
|
||||
title: 'host',
|
||||
key: 'host',
|
||||
dataIndex: 'host',
|
||||
render: (r: string, t: any) => {
|
||||
return (
|
||||
<a href={`/admin/broker_detail?clusterId=${urlQuery.clusterId}&brokerId=${t.brokerId}`} target="_blank">{r}
|
||||
</a>
|
||||
); },
|
||||
},
|
||||
{
|
||||
title: '版本',
|
||||
dataIndex: 'controllerVersion',
|
||||
key: 'controllerVersion',
|
||||
},
|
||||
{
|
||||
title: '变更时间',
|
||||
dataIndex: 'controllerTimestamp',
|
||||
key: 'controllerTimestamp',
|
||||
sorter: (a: any, b: any) => a.controllerTimestamp - b.updacontrollerTimestampteTime,
|
||||
render: (t: number) => moment(t).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
];
|
||||
|
||||
const pagination: PaginationConfig = {
|
||||
position: 'bottom',
|
||||
showQuickJumper: true,
|
||||
pageSize: 10,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
};
|
||||
|
||||
@observer
|
||||
export class AdminController extends SearchAndFilter {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
controller.getController(urlQuery.clusterId);
|
||||
}
|
||||
|
||||
public renderController() {
|
||||
if (!controller.data) return null;
|
||||
const data = controller.data.filter((d) => d.host.includes(this.state.searchKey));
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
pagination={pagination}
|
||||
rowKey="controllerTimestamp"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<ul className="table-operation-bar">
|
||||
{this.renderSearch('请输入关键词')}
|
||||
</ul>
|
||||
<Tabs defaultActiveKey="1" type="card">
|
||||
<TabPane tab="Controller变更历史" key="1">
|
||||
{this.renderController()}
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
60
console/src/container/admin-home/cluster-detail.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import urlQuery from 'store/url-query';
|
||||
import { NetWorkFlow } from 'container/topic-detail/com';
|
||||
import { Tabs } from 'component/antd';
|
||||
import { broker } from 'store/broker';
|
||||
import { BrokerStatus } from 'container/broker-info/base-info';
|
||||
import { BrokerList } from 'container/broker-info/base-info';
|
||||
import { AdminConsume } from 'container/admin-consume';
|
||||
import { AdminRegion } from 'container/admin-region';
|
||||
import { AdminController } from 'container/admin-controller';
|
||||
import AdminTopic from 'container/admin-topic/index';
|
||||
import { handleTabKey } from 'lib/utils';
|
||||
|
||||
export class ClusterDetail extends React.Component {
|
||||
|
||||
public updateStatus = () => {
|
||||
broker.getBrokerNetwork(urlQuery.clusterId);
|
||||
}
|
||||
public componentDidMount() {
|
||||
this.updateStatus();
|
||||
}
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<Tabs type="card" activeKey={location.hash.substr(1) || '0'} onChange={handleTabKey}>
|
||||
<Tabs.TabPane tab="集群流量" key="0">
|
||||
<div className="k-row right-flow">
|
||||
<p className="k-title">历史流量</p>
|
||||
<NetWorkFlow clusterId={urlQuery.clusterId} />
|
||||
</div>
|
||||
<div className="k-row right-flow" style={{ marginTop: '50px' }}>
|
||||
<p className="k-title">实时流量</p>
|
||||
<span className="k-abs" onClick={this.updateStatus}>
|
||||
<i className="k-icon-shuaxin didi-theme" />刷新
|
||||
</span>
|
||||
<BrokerStatus />
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Topic管理" key="1">
|
||||
<AdminTopic />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Broker状态概览" key="2">
|
||||
<BrokerList />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="ConsumerGroup列表" key="3">
|
||||
<AdminConsume />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Region管理" key="4">
|
||||
<AdminRegion />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Controller变更历史" key="5">
|
||||
<AdminController />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
9
console/src/container/admin-home/index.less
Normal file
@@ -0,0 +1,9 @@
|
||||
.right-flow {
|
||||
.k-abs {
|
||||
right: 24px;
|
||||
cursor: pointer;
|
||||
& > i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
132
console/src/container/admin-home/index.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import * as React from 'react';
|
||||
import './index.less';
|
||||
|
||||
import { Table, Tabs, ColumnProps, PaginationConfig } from 'component/antd';
|
||||
|
||||
import { modal } from 'store';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { observer } from 'mobx-react';
|
||||
import { IClusterData } from 'types/base-type';
|
||||
|
||||
const TabPane = Tabs.TabPane;
|
||||
|
||||
const detailUrl ='/admin/cluster_detail?clusterId=';
|
||||
|
||||
const collectionColumns: Array<ColumnProps<IClusterData>> = [
|
||||
{
|
||||
title: '集群ID',
|
||||
dataIndex: 'clusterId',
|
||||
key: 'clusterId',
|
||||
sorter: (a: IClusterData, b: IClusterData) => a.clusterId - b.clusterId,
|
||||
},
|
||||
{
|
||||
title: '集群名称',
|
||||
key: 'clusterName',
|
||||
sorter: (a: IClusterData, b: IClusterData) => a.clusterName.charCodeAt(0) - b.clusterName.charCodeAt(0),
|
||||
render: (text, record) => {
|
||||
return <a href={`${detailUrl}${record.clusterId}`}>{record.clusterName}</a>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Topic 数',
|
||||
key: 'topicNum',
|
||||
sorter: (a: IClusterData, b: IClusterData) => a.topicNum - b.topicNum,
|
||||
render: (text, record) => {
|
||||
return <a href={`${detailUrl}${record.clusterId}#1`}>{record.topicNum}</a>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Broker 数量',
|
||||
dataIndex: 'brokerNum',
|
||||
key: 'brokerNum',
|
||||
sorter: (a: IClusterData, b: IClusterData) => a.brokerNum - b.brokerNum,
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<a
|
||||
href={
|
||||
`${detailUrl}${record.clusterId}&clusterName=${btoa(encodeURIComponent(record.clusterName))}#2`}
|
||||
>
|
||||
{record.brokerNum}
|
||||
</a>);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'ConsumerGroup 数',
|
||||
key: 'consumerGroupNum',
|
||||
sorter: (a: IClusterData, b: IClusterData) => a.consumerGroupNum - b.consumerGroupNum,
|
||||
render: (text, record) => {
|
||||
return <a href={`${detailUrl}${record.clusterId}#3`}>{record.consumerGroupNum}</a>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Region 数',
|
||||
key: 'regionNum',
|
||||
sorter: (a: IClusterData, b: IClusterData) => a.regionNum - b.regionNum,
|
||||
render: (text, record) => {
|
||||
return <a href={`${detailUrl}${record.clusterId}`}>{record.regionNum}</a>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'ControllerID',
|
||||
key: 'controllerId',
|
||||
sorter: (a: IClusterData, b: IClusterData) => a.controllerId - b.controllerId,
|
||||
render: (text, record) => {
|
||||
return <a href={`${detailUrl}${record.clusterId}#5`}>{record.controllerId}</a>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
key: 'operation',
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<span className="table-operation">
|
||||
<a onClick={modal.showModifyCluster.bind(null, record)}>修改</a>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const pagination: PaginationConfig = {
|
||||
position: 'bottom',
|
||||
showQuickJumper: true,
|
||||
pageSize: 10,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
};
|
||||
|
||||
@observer
|
||||
export class AdminHome extends React.Component {
|
||||
public renderList() {
|
||||
return (
|
||||
<Table
|
||||
columns={collectionColumns}
|
||||
dataSource={cluster.data.slice(1)}
|
||||
pagination={pagination}
|
||||
rowKey="clusterId"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
cluster.getClusters();
|
||||
cluster.getKafkaVersions();
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<ul className="table-operation-bar">
|
||||
<li className="new-topic" onClick={modal.showNewCluster}>
|
||||
<i className="k-icon-xinjian didi-theme"/>添加集群
|
||||
</li>
|
||||
</ul>
|
||||
<Tabs defaultActiveKey="1" type="card">
|
||||
<TabPane tab="集群列表" key="1">
|
||||
{this.renderList()}
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
138
console/src/container/admin-operation/index.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Table, Tabs, Modal, notification } from 'component/antd';
|
||||
import { PaginationConfig } from 'antd/es/table/interface';
|
||||
import { modal } from 'store';
|
||||
import { observer } from 'mobx-react';
|
||||
import { operation, ITask, taskMap } from 'store/operation';
|
||||
import { cluster } from 'store/cluster';
|
||||
import moment from 'moment';
|
||||
import { modifyTask } from 'lib/api';
|
||||
import { SearchAndFilter } from 'container/cluster-topic';
|
||||
import { tableFilter } from 'lib/utils';
|
||||
|
||||
const pagination: PaginationConfig = {
|
||||
position: 'bottom',
|
||||
showQuickJumper: true,
|
||||
pageSize: 10,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
};
|
||||
|
||||
@observer
|
||||
export class AdminOperation extends SearchAndFilter {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
filterClusterVisible: false,
|
||||
filterStatusVisible: false,
|
||||
};
|
||||
|
||||
public renderColumns = (data: ITask[]) => {
|
||||
const cluster = Object.assign({
|
||||
title: '集群名称',
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterId',
|
||||
filters: tableFilter<ITask>(data, 'clusterName'),
|
||||
onFilter: (value: string, record: ITask) => record.clusterName.indexOf(value) === 0,
|
||||
}, this.renderColumnsFilter('filterClusterVisible'));
|
||||
|
||||
const status = Object.assign({
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
filters: taskMap.map((ele, index) => ({ text: ele, value: index + '' })),
|
||||
onFilter: (value: string, record: ITask) => record.status === +value,
|
||||
render: (t: number) => <span className={t === 2 || t === 1 ? 'success' : t === 3 ? 'fail' : ''}>{taskMap[t]}</span>,
|
||||
}, this.renderColumnsFilter('filterStatusVisible'));
|
||||
|
||||
return [
|
||||
{
|
||||
title: '任务id',
|
||||
dataIndex: 'taskId',
|
||||
key: 'taskId',
|
||||
sorter: (a: ITask, b: ITask) => a.taskId - b.taskId,
|
||||
},
|
||||
cluster,
|
||||
{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
sorter: (a: ITask, b: ITask) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
dataIndex: 'operator',
|
||||
key: 'operator',
|
||||
sorter: (a: ITask, b: ITask) => a.operator.charCodeAt(0) - b.operator.charCodeAt(0),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'gmtCreate',
|
||||
key: 'gmtCreate',
|
||||
sorter: (a: ITask, b: ITask) => a.gmtCreate - b.gmtCreate,
|
||||
render: (t: number) => moment(t).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
status,
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
key: 'operation',
|
||||
width: 200,
|
||||
render: (text: string, record: ITask) => {
|
||||
const status: number = record.status;
|
||||
return (
|
||||
<span className="table-operation">
|
||||
<a onClick={modal.showTask.bind(null, record, 'detail')}>详情</a>
|
||||
{+status === 0 || +status === 1 ? <a onClick={modal.showTask.bind(null, record)}>修改</a> : null}
|
||||
{!status ? <a onClick={this.handleAction.bind(null, record.taskId, 'start')}>执行</a> : null}
|
||||
{!status ? <a onClick={this.handleAction.bind(null, record.taskId, 'cancel')}>撤销</a> : null}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public handleAction(taskId: number, type: string) {
|
||||
Modal.confirm({
|
||||
title: `确认${type === 'start' ? '执行Topic迁移任务' : '撤销任务' + taskId}`,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
modifyTask({ action: type, taskId }).then(() => {
|
||||
notification.success({ message: `${type === 'start' ? '执行' : '撤销'}任务成功` });
|
||||
operation.getTask();
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
operation.getTask();
|
||||
cluster.getClusters();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { searchKey } = this.state;
|
||||
const data: ITask[] = operation.tasks && searchKey ? operation.tasks.filter((d) => d.taskId === +searchKey) : operation.tasks;
|
||||
return (
|
||||
<>
|
||||
<ul className="table-operation-bar">
|
||||
<li className="new-topic" onClick={modal.showTask.bind(null, null)}>
|
||||
<i className="k-icon-xinjian didi-theme" />新建迁移任务
|
||||
</li>
|
||||
{this.renderSearch('请输入关键字')}
|
||||
</ul>
|
||||
<Tabs type="card">
|
||||
<Tabs.TabPane tab="迁移任务" key="0">
|
||||
<Table
|
||||
rowKey="taskId"
|
||||
columns={this.renderColumns(data)}
|
||||
dataSource={data}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
16
console/src/container/admin-order/index.less
Normal file
@@ -0,0 +1,16 @@
|
||||
.num-container {
|
||||
position: relative;
|
||||
.num {
|
||||
position: absolute;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
line-height: 15px;
|
||||
border-radius: 15px;
|
||||
background-color: #f5222d;
|
||||
color: #fff;
|
||||
display: inline-block;
|
||||
right: -18px;
|
||||
text-align: center;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
171
console/src/container/admin-order/index.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Table, Tabs } from 'component/antd';
|
||||
import { PaginationConfig } from 'antd/es/table/interface';
|
||||
import { modal } from 'store';
|
||||
import { observer } from 'mobx-react';
|
||||
import { order, tableStatusFilter } from 'store/order';
|
||||
import moment from 'moment';
|
||||
import { handleTabKey, tableFilter } from 'lib/utils';
|
||||
import { SearchAndFilter } from 'container/cluster-topic';
|
||||
import { IBaseOrder } from 'types/base-type';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const TabPane = Tabs.TabPane;
|
||||
|
||||
const pagination: PaginationConfig = {
|
||||
position: 'bottom',
|
||||
showQuickJumper: true,
|
||||
pageSize: 10,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
};
|
||||
|
||||
@observer
|
||||
export class AdminOrder extends SearchAndFilter {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
filterClusterVisible: false,
|
||||
filterStatusVisible: false,
|
||||
filterSVisible: false,
|
||||
filterCVisible: false,
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
order.getAdminOrder();
|
||||
}
|
||||
|
||||
public renderColumns = (data: IBaseOrder[], type: boolean) => {
|
||||
const cluster = Object.assign({
|
||||
title: '集群名称',
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
filters: tableFilter<IBaseOrder>(data, 'clusterName'),
|
||||
onFilter: (value: string, record: IBaseOrder) => record.clusterName.indexOf(value) === 0,
|
||||
}, this.renderColumnsFilter(type ? 'filterClusterVisible' : 'filterCVisible'));
|
||||
|
||||
const status = Object.assign({
|
||||
title: '审批状态',
|
||||
dataIndex: 'statusStr',
|
||||
key: 'statusStr',
|
||||
width: 100,
|
||||
filters: tableStatusFilter,
|
||||
onFilter: (value: string, record: IBaseOrder) => record.statusStr.indexOf(value) === 0,
|
||||
render: (t: string) => <span className={t === '通过' ? 'success' : t === '拒绝' ? 'fail' : ''}>{t}</span>,
|
||||
}, this.renderColumnsFilter(type ? 'filterStatusVisible' : 'filterSVisible'));
|
||||
return [
|
||||
{
|
||||
title: '工单 ID',
|
||||
dataIndex: 'orderId',
|
||||
key: 'orderId',
|
||||
sorter: (a: IBaseOrder, b: IBaseOrder) => a.orderId - b.orderId,
|
||||
},
|
||||
cluster,
|
||||
{
|
||||
title: 'Topic 名称',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
sorter: (a: IBaseOrder, b: IBaseOrder) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
},
|
||||
{
|
||||
title: '申请人',
|
||||
dataIndex: 'applicant',
|
||||
key: 'applicant',
|
||||
sorter: (a: IBaseOrder, b: IBaseOrder) => a.applicant.charCodeAt(0) - b.applicant.charCodeAt(0),
|
||||
},
|
||||
{
|
||||
title: '审批人',
|
||||
dataIndex: 'approver',
|
||||
key: 'approver',
|
||||
},
|
||||
{
|
||||
title: '申请时间',
|
||||
dataIndex: 'gmtCreate',
|
||||
key: 'gmtCreate',
|
||||
sorter: (a: IBaseOrder, b: IBaseOrder) => a.gmtCreate - b.gmtCreate,
|
||||
render: (t: number) => moment(t).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
status,
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
key: 'operation',
|
||||
width: 100,
|
||||
render: (text: string, r: IBaseOrder) => {
|
||||
if (!+location.hash.substr(1)) {
|
||||
return (
|
||||
<span className="table-operation">
|
||||
<a onClick={modal.showOrderApprove.bind(null, r, 'showOrderDetail')}>详情</a>
|
||||
{r.orderStatus === 0 ? <a onClick={modal.showOrderApprove.bind(null, r, 'showOrderApprove')}>审批</a> : null}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="table-operation">
|
||||
<a onClick={modal.showPartition.bind(null, r, 'showPartitionDetail')}>详情</a>
|
||||
{r.orderStatus === 0 ? <a onClick={modal.showPartition.bind(null, r, 'showPartition')}>审批</a> : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public renderTopic() {
|
||||
const data = order.adminTopicOrder.filter((d) => d.topicName.includes(this.state.searchKey));
|
||||
return (
|
||||
<Table
|
||||
columns={this.renderColumns(data, true)}
|
||||
dataSource={data}
|
||||
pagination={pagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public renderPartition() {
|
||||
const data = order.adminPartitionOrder.filter((d) => d.topicName.includes(this.state.searchKey));
|
||||
return (
|
||||
<Table
|
||||
columns={this.renderColumns(data, false)}
|
||||
dataSource={data}
|
||||
pagination={pagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const defaultKey = location.hash.substr(1) || '0';
|
||||
return (
|
||||
<>
|
||||
<ul className="table-operation-bar">
|
||||
{this.renderSearch('请输入topic名称')}
|
||||
</ul>
|
||||
<Tabs activeKey={defaultKey} type="card" onChange={handleTabKey}>
|
||||
<TabPane
|
||||
key="0"
|
||||
tab={
|
||||
<div className="num-container">
|
||||
{order.pendingTopic ? <span className="num">{order.pendingTopic}</span> : null}
|
||||
Topic 申请
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{this.renderTopic()}
|
||||
</TabPane>
|
||||
<TabPane
|
||||
tab={
|
||||
<div className="num-container">
|
||||
{order.pendingOrder ? <span className="num">{order.pendingOrder}</span> : null}
|
||||
扩容申请
|
||||
</div>
|
||||
}
|
||||
key="1"
|
||||
>
|
||||
{this.renderPartition()}
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
138
console/src/container/admin-region/index.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Table, Tabs, Modal, PaginationConfig, notification } from 'component/antd';
|
||||
import { modal } from 'store';
|
||||
import { observer } from 'mobx-react';
|
||||
import { IRegionData, region, statusMap, levelMap } from 'store/region';
|
||||
import urlQuery from 'store/url-query';
|
||||
import { SearchAndFilter } from 'container/cluster-topic';
|
||||
|
||||
const TabPane = Tabs.TabPane;
|
||||
|
||||
const handleDeleteRegion = (record: IRegionData) => {
|
||||
Modal.confirm({
|
||||
title: `确认删除 ${record.regionName} ?`,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
region.delRegion(record.regionId).then(() => {
|
||||
notification.success({ message: '删除成功' });
|
||||
region.getRegions(urlQuery.clusterId);
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const pagination: PaginationConfig = {
|
||||
position: 'bottom',
|
||||
showQuickJumper: true,
|
||||
pageSize: 10,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
};
|
||||
|
||||
@observer
|
||||
export class AdminRegion extends SearchAndFilter {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
filterStatus: false,
|
||||
filterLevel: false,
|
||||
};
|
||||
|
||||
public renderColumns = () => {
|
||||
const status = Object.assign({
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
filters: statusMap.map((ele, index) => ({ text: ele, value: index + '' })),
|
||||
render: (t: number) => <span className={!(t + 1) ? 'fail' : t === 1 ? '' : 'success'}>{statusMap[t + 1]}</span>,
|
||||
onFilter: (value: string, record: IRegionData) => record.status + 1 === +value,
|
||||
}, this.renderColumnsFilter('filterStatus'));
|
||||
|
||||
const level = Object.assign({
|
||||
title: '重要程度',
|
||||
dataIndex: 'level',
|
||||
key: 'level',
|
||||
filters: levelMap.map((ele, index) => ({ text: ele, value: index + '' })),
|
||||
render: (t: number) => {
|
||||
return levelMap[t];
|
||||
},
|
||||
onFilter: (value: string, record: IRegionData) => record.level === +value,
|
||||
}, this.renderColumnsFilter('filterLevel'));
|
||||
|
||||
return [
|
||||
{
|
||||
title: 'Region名称',
|
||||
dataIndex: 'regionName',
|
||||
key: 'regionName',
|
||||
},
|
||||
{
|
||||
title: 'BrokerList',
|
||||
key: 'brokerIdList',
|
||||
render: (text: string, record: IRegionData) => {
|
||||
return <span>{record.brokerIdList.join(', ')}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作者',
|
||||
dataIndex: 'operator',
|
||||
key: 'operator',
|
||||
},
|
||||
status,
|
||||
level,
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
key: 'operation',
|
||||
width: 200,
|
||||
render: (text: string, record: IRegionData) => {
|
||||
return (
|
||||
<span className="table-operation">
|
||||
<a onClick={modal.showRegion.bind(null, record)}>编辑</a>
|
||||
<a onClick={handleDeleteRegion.bind(null, record)}>删除</a>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
region.getRegions(urlQuery.clusterId);
|
||||
}
|
||||
|
||||
public renderRegion() {
|
||||
if (!region.data) return null;
|
||||
const data = region.data.filter((d) => d.regionName.includes(this.state.searchKey));
|
||||
return (
|
||||
<Table
|
||||
columns={this.renderColumns()}
|
||||
dataSource={data}
|
||||
pagination={pagination}
|
||||
rowKey="regionId"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<ul className="table-operation-bar">
|
||||
<li className="new-topic" onClick={modal.showRegion.bind(null, null)}>
|
||||
<i className="k-icon-xinjian didi-theme" />新增Region
|
||||
</li>
|
||||
{this.renderSearch('请输入关键词')}
|
||||
</ul>
|
||||
<Tabs defaultActiveKey="1" type="card">
|
||||
<TabPane tab="Region管理" key="1">
|
||||
{this.renderRegion()}
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
148
console/src/container/admin-topic/index.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Table, Tabs, Form, notification, Modal, Tooltip } from 'component/antd';
|
||||
import { PaginationConfig } from 'antd/es/table/interface';
|
||||
import { UserHome } from 'container/user-home';
|
||||
import { topic } from 'store/topic';
|
||||
import urlQuery from 'store/url-query';
|
||||
import { modal } from 'store';
|
||||
import { observer } from 'mobx-react';
|
||||
import { deleteTopic } from 'lib/api';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { ITopic } from 'types/base-type';
|
||||
|
||||
const pagination: PaginationConfig = {
|
||||
position: 'bottom',
|
||||
showQuickJumper: true,
|
||||
pageSize: 10,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
};
|
||||
|
||||
@observer
|
||||
class AdminTopic extends UserHome {
|
||||
|
||||
public cols = [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
width: 350,
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 250,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
sorter: (a: ITopic, b: ITopic) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
render: (t: string, r: ITopic) => {
|
||||
return <a href={`/user/topic_detail?clusterId=${r.clusterId}&topic=${r.topicName}`}>{t}</a>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '分区数',
|
||||
dataIndex: 'partitionNum',
|
||||
key: 'partitionNum',
|
||||
sorter: (a: ITopic, b: ITopic) => b.partitionNum - a.partitionNum,
|
||||
},
|
||||
{
|
||||
title: '副本数',
|
||||
dataIndex: 'replicaNum',
|
||||
key: 'replicaNum',
|
||||
sorter: (a: ITopic, b: ITopic) => b.replicaNum - a.replicaNum,
|
||||
},
|
||||
{
|
||||
title: '流入 (KB/s)',
|
||||
dataIndex: 'byteIn',
|
||||
key: 'byteIn',
|
||||
sorter: (a: ITopic, b: ITopic) => b.byteIn - a.byteIn,
|
||||
render: (t: number) => (t / 1024).toFixed(2),
|
||||
},
|
||||
{
|
||||
title: '流入(QPS)',
|
||||
dataIndex: 'produceRequest',
|
||||
key: 'produceRequest',
|
||||
width: 150,
|
||||
sorter: (a: ITopic, b: ITopic) => b.produceRequest - a.produceRequest,
|
||||
render: (t: number) => t.toFixed(2),
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
dataIndex: 'principals',
|
||||
key: 'principals',
|
||||
width: 120,
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 100,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
render: (t: string) => <Tooltip placement="top" title={t} >{t}</Tooltip>,
|
||||
sorter: (a: ITopic, b: ITopic) =>
|
||||
a.principals && b.principals ? a.principals.charCodeAt(0) - b.principals.charCodeAt(0) : (-1),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
key: 'operation',
|
||||
width: 200,
|
||||
render: (text: string, r: ITopic) => {
|
||||
return (
|
||||
<span className="table-operation">
|
||||
<a onClick={modal.showExpandAdmin.bind(null, r)}>扩分区</a>
|
||||
<a onClick={modal.showAdimTopic.bind(null, r)}>编辑</a>
|
||||
<a onClick={this.handleDelete.bind(null, r)}>删除</a>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
public handleDelete = ({ clusterId, topicName }: ITopic) => {
|
||||
const topicNameList = [topicName];
|
||||
Modal.confirm({
|
||||
title: `确认删除${topicName}?`,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
deleteTopic({ clusterId, topicNameList }).then(() => {
|
||||
notification.success({ message: '删除成功' });
|
||||
topic.getAdminTopics(urlQuery.clusterId);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public renderTable() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs type="card">
|
||||
<Tabs.TabPane tab="Topic管理" key="0">
|
||||
<Table pagination={pagination} columns={this.cols} dataSource={this.getData(topic.data)} rowKey="topicName" />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
cluster.getClusters();
|
||||
topic.getAdminTopics(urlQuery.clusterId);
|
||||
}
|
||||
|
||||
public renderClusterTopic() {
|
||||
return (
|
||||
<>
|
||||
{this.renderSearch('请输入Topic名称或者负责人')}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create({ name: 'aminTopic' })(AdminTopic);
|
||||
12
console/src/container/admin-usermanage/index.less
Normal file
@@ -0,0 +1,12 @@
|
||||
.u-container {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background: rgba(0,0,0,0.02);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
vertical-align: middle;
|
||||
line-height: 48px;
|
||||
padding: 0 12px;
|
||||
margin-top: 20px;
|
||||
position: relative;
|
||||
}
|
||||
97
console/src/container/admin-usermanage/index.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Table, notification, PaginationConfig, Modal } from 'component/antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import { users } from 'store/users';
|
||||
import { deleteUser } from 'lib/api';
|
||||
import { modal } from 'store';
|
||||
import { SearchAndFilter } from 'container/cluster-topic';
|
||||
import './index.less';
|
||||
|
||||
const pagination: PaginationConfig = {
|
||||
position: 'bottom',
|
||||
showQuickJumper: true,
|
||||
pageSize: 10,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
};
|
||||
|
||||
const handleForbidden = (record: any) => {
|
||||
Modal.confirm({
|
||||
title: `确认删除 ${record.username} ?`,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
deleteUser(record.username).then(() => {
|
||||
notification.success({ message: '删除成功' });
|
||||
users.getUsers();
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@observer
|
||||
export class UserManage extends SearchAndFilter {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
filterVisible: false,
|
||||
};
|
||||
|
||||
public renderColumns = () => {
|
||||
const role = Object.assign({
|
||||
title: '角色',
|
||||
key: 'role',
|
||||
dataIndex: 'role',
|
||||
filters: users.filterRole,
|
||||
onFilter: (value: string, record: any) => record.role.indexOf(value) === 0,
|
||||
}, this.renderColumnsFilter('filterVisible'));
|
||||
|
||||
return [
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
},
|
||||
role,
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
key: 'operation',
|
||||
render: (t: any, r: any) => {
|
||||
return (
|
||||
<span className="table-operation">
|
||||
<a onClick={modal.showNewUser.bind(null, r)}>修改</a>
|
||||
<a onClick={handleForbidden.bind(null, r)}>删除</a>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
users.getUsers();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const data = users.userData.filter((d) => d.username.includes(this.state.searchKey));
|
||||
return (
|
||||
<>
|
||||
<div className="u-container">
|
||||
<span>管理员授权</span>
|
||||
<ul className="table-operation-bar">
|
||||
<li className="new-topic" onClick={modal.showNewUser.bind(null, null)}>
|
||||
<i className="k-icon-xinjian didi-theme" />新建用户
|
||||
</li>
|
||||
{this.renderSearch('用户名称')}
|
||||
</ul>
|
||||
</div>
|
||||
<Table
|
||||
columns={this.renderColumns()}
|
||||
dataSource={data}
|
||||
pagination={pagination}
|
||||
rowKey="username"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
135
console/src/container/alarm/index.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Table, Tabs, Select, Input, notification, Modal } from 'component/antd';
|
||||
import { PaginationConfig } from 'antd/es/table/interface';
|
||||
import { modal } from 'store';
|
||||
import { observer } from 'mobx-react';
|
||||
import { alarm, IAlarm } from 'store/alarm';
|
||||
import { deleteAlarm } from 'lib/api';
|
||||
import { SearchAndFilter } from 'container/cluster-topic';
|
||||
import { getCookie } from 'lib/utils';
|
||||
import moment = require('moment');
|
||||
|
||||
const TabPane = Tabs.TabPane;
|
||||
const Search = Input.Search;
|
||||
|
||||
const handleDeleteAlarm = (record: IAlarm) => {
|
||||
Modal.confirm({
|
||||
title: `确认删除 ${record.alarmName} ?`,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
deleteAlarm(record.id).then(() => {
|
||||
notification.success({ message: '删除成功' });
|
||||
alarm.getAlarm();
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const pagination: PaginationConfig = {
|
||||
position: 'bottom',
|
||||
showQuickJumper: true,
|
||||
pageSize: 10,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
};
|
||||
|
||||
@observer
|
||||
export class Alarm extends SearchAndFilter {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
filterVisible: false,
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
alarm.getAlarm();
|
||||
alarm.getAlarmConstant();
|
||||
}
|
||||
|
||||
public renderColumns = () => {
|
||||
const status = Object.assign({
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
filters: [{ text: '已启用', value: '1' }, { text: '暂停', value: '0' }],
|
||||
onFilter: (value: string, record: IAlarm) => record.status === +value,
|
||||
render: (t: string) => <span className={t ? 'success' : ''}>{t ? '已启用' : '暂停'}</span>,
|
||||
}, this.renderColumnsFilter('filterVisible'));
|
||||
|
||||
return [
|
||||
{
|
||||
title: '配置名称',
|
||||
dataIndex: 'alarmName',
|
||||
key: 'alarmName',
|
||||
sorter: (a: IAlarm, b: IAlarm) => a.alarmName.charCodeAt(0) - b.alarmName.charCodeAt(0),
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
dataIndex: 'principalList',
|
||||
key: 'principalList',
|
||||
render: (t: string[]) => t.join(','),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'gmtCreate',
|
||||
key: 'gmtCreate',
|
||||
sorter: (a: IAlarm, b: IAlarm) => a.gmtCreate - b.gmtCreate,
|
||||
render: (t: number) => moment(t).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
status,
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
key: 'operation',
|
||||
width: 200,
|
||||
render: (text: string, record: IAlarm) => {
|
||||
return (
|
||||
<span className="table-operation">
|
||||
<a onClick={this.handleModify.bind(null, record)}>修改</a>
|
||||
<a onClick={handleDeleteAlarm.bind(null, record)}>删除</a>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public renderAlarm() {
|
||||
const data = alarm.data.filter((d) => {
|
||||
return d.alarmName.includes(this.state.searchKey) || d.principalList.includes(this.state.searchKey);
|
||||
});
|
||||
return (
|
||||
<Table
|
||||
columns={this.renderColumns()}
|
||||
dataSource={data}
|
||||
pagination={pagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public handleModify = (record: IAlarm) => {
|
||||
if (!getCookie('username') && !record.principalList.includes(getCookie('username'))) {
|
||||
notification.success({ message: '抱歉,没有修改权限' });
|
||||
return false;
|
||||
}
|
||||
modal.showAlarmModify(record);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<ul className="table-operation-bar">
|
||||
<li className="new-topic" onClick={modal.showAlarm.bind(null, null)}>
|
||||
<i className="k-icon-xinjian didi-theme" />添加告警配置
|
||||
</li>
|
||||
{this.renderSearch('请输入关键字')}
|
||||
</ul>
|
||||
<Tabs defaultActiveKey="1" type="card">
|
||||
<TabPane tab="告警列表" key="1">
|
||||
{this.renderAlarm()}
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
82
console/src/container/broker-detail/base-detail.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import * as React from 'react';
|
||||
import { TopicDetail } from 'container/topic-detail';
|
||||
import './index.less';
|
||||
import { broker, IBrokerNetworkInfo } from 'store/broker';
|
||||
import { observer } from 'mobx-react';
|
||||
import { StatusGraghCom } from 'component/flow-table';
|
||||
import urlQuery from 'store/url-query';
|
||||
import { NetWorkFlow } from 'container/topic-detail/com';
|
||||
|
||||
@observer
|
||||
export class OneBrokerStatus extends StatusGraghCom<IBrokerNetworkInfo> {
|
||||
public getData() {
|
||||
return broker.oneNetwork;
|
||||
}
|
||||
}
|
||||
|
||||
export class BrokerBaseDetail extends TopicDetail {
|
||||
public componentDidMount() {
|
||||
broker.getOneBrokerNetwork(urlQuery.clusterId, urlQuery.brokerId);
|
||||
broker.getBrokerTopicAnalyzer(urlQuery.clusterId, urlQuery.brokerId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<div className="k-row right-flow mb-24">
|
||||
<p className="k-title">基本信息</p>
|
||||
<Summary />
|
||||
</div>
|
||||
<div className="k-row right-flow mb-24">
|
||||
<p className="k-title">历史流量</p>
|
||||
<NetWorkFlow clusterId={urlQuery.clusterId} brokerId={urlQuery.brokerId} />
|
||||
</div>
|
||||
<div className="k-row right-flow">
|
||||
<p className="k-title">实时流量</p>
|
||||
<span className="k-abs"><i className="k-icon-shuaxin didi-theme" />刷新</span>
|
||||
<OneBrokerStatus />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
class Summary extends React.Component {
|
||||
public componentDidMount() {
|
||||
broker.getBrokerBaseInfo(urlQuery.clusterId, urlQuery.brokerId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="k-summary">
|
||||
<div className="k-row-1">
|
||||
<div>主机:{broker.brokerBaseInfo.host}</div>
|
||||
<div>启动时间:{broker.brokerBaseInfo.startTime}</div>
|
||||
</div>
|
||||
<div className="k-row-3">
|
||||
<div>
|
||||
<span>Topic数</span>
|
||||
<p>{broker.brokerBaseInfo.topicNum}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>分区数</span>
|
||||
<p>{broker.brokerBaseInfo.partitionCount}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>分区Leader</span>
|
||||
<p>{broker.brokerBaseInfo.leaderCount}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>Port</span>
|
||||
<p>{broker.brokerBaseInfo.port}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>JMX Port</span>
|
||||
<p>{broker.brokerBaseInfo.jmxPort}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
90
console/src/container/broker-detail/broker-index.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { broker } from 'store/broker';
|
||||
import urlQuery from 'store/url-query';
|
||||
import echarts from 'echarts/lib/echarts';
|
||||
import { getBrokerMetricOption } from 'lib/charts-config';
|
||||
import { MetricChartList as list } from './constant';
|
||||
import { Spin, DatePicker, notification, Button } from 'component/antd';
|
||||
import './index.less';
|
||||
|
||||
// 引入柱状图
|
||||
import 'echarts/lib/chart/line';
|
||||
// 引入提示框和标题组件
|
||||
import 'echarts/lib/component/tooltip';
|
||||
import 'echarts/lib/component/title';
|
||||
import 'echarts/lib/component/legend';
|
||||
import moment from 'moment';
|
||||
|
||||
@observer
|
||||
export class BrokerMetrics extends React.Component {
|
||||
public chartId: HTMLDivElement[] = [];
|
||||
public charts: echarts.ECharts[] = [];
|
||||
|
||||
public state = {
|
||||
loading: true,
|
||||
};
|
||||
|
||||
public renderCharts(startTime: moment.Moment, endTime: moment.Moment) {
|
||||
broker.getBrokerKeyMetrics(urlQuery.clusterId, urlQuery.brokerId,
|
||||
startTime.format('x'),
|
||||
endTime.format('x')).then(data => {
|
||||
getBrokerMetricOption(list, data).forEach((ele: object, index) => {
|
||||
if (ele) {
|
||||
this.charts[index] = echarts.init(this.chartId[index]);
|
||||
this.charts[index].setOption(ele);
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
this.charts[index] = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.renderCharts(broker.startTime, broker.endTime);
|
||||
}
|
||||
|
||||
public handleSearch = () => {
|
||||
const { startTime, endTime } = broker;
|
||||
if (startTime >= endTime) {
|
||||
notification.error({ message: '开始时间不能大于或等于结束时间' });
|
||||
return false;
|
||||
}
|
||||
this.setState({ loading: true });
|
||||
this.renderCharts(startTime, endTime);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const charts = list.map((item, index) => {
|
||||
if (!item.value) return <div className="emptyChart" key={index} ref={(id) => this.chartId.push(id)} />;
|
||||
return (
|
||||
<div key={index}>
|
||||
<p>{item.label}</p>
|
||||
<Spin spinning={this.state.loading}>
|
||||
<div style={{ height: 300, padding: '0px 10px' }} ref={(id) => this.chartId.push(id)} />
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<div className="status-graph">
|
||||
<ul className="k-toolbar topic-line-tool">
|
||||
<li>
|
||||
<span className="label">开始时间</span>
|
||||
<DatePicker showTime={true} value={broker.startTime} onChange={broker.changeStartTime} />
|
||||
</li>
|
||||
<li>
|
||||
<span className="label" >结束时间</span>
|
||||
<DatePicker showTime={true} value={broker.endTime} onChange={broker.changeEndTime} />
|
||||
</li>
|
||||
<li><Button type="primary" size="small" onClick={this.handleSearch}>查询</Button></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="charts-container" >
|
||||
{charts}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
109
console/src/container/broker-detail/broker-partition.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import React from 'react';
|
||||
import { Table, PaginationConfig } from 'component/antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import { broker, IPartitions } from 'store/broker';
|
||||
import { columsDefault } from './constant';
|
||||
import urlQuery from 'store/url-query';
|
||||
import './index.less';
|
||||
|
||||
const columns = [{
|
||||
title: 'Topic',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
}, {
|
||||
title: 'Leader',
|
||||
dataIndex: 'leaderPartitionList',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 250,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
render: (value: number[]) => {
|
||||
return value.map(i => <span key={i} className="p-params">{i}</span>);
|
||||
},
|
||||
}, {
|
||||
title: '副本',
|
||||
dataIndex: 'followerPartitionIdList',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 250,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
render: (value: number[]) => {
|
||||
return value.map(i => <span key={i} className="p-params">{i}</span>);
|
||||
},
|
||||
}, {
|
||||
title: '未同步副本',
|
||||
dataIndex: 'notUnderReplicatedPartitionIdList',
|
||||
render: (value: number[]) => {
|
||||
return value.map(i => <span key={i} className="p-params-unFinished">{i}</span>);
|
||||
},
|
||||
}, {
|
||||
title: '状态',
|
||||
dataIndex: 'underReplicated',
|
||||
render: (value: boolean) => {
|
||||
return value ? '已同步' : '未同步';
|
||||
},
|
||||
}, {
|
||||
title: '操作',
|
||||
render: (record: IPartitions) => {
|
||||
return (<a onClick={broker.handleOpen.bind(broker, record.topicName)}>查看详情</a>);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const pagination: PaginationConfig = {
|
||||
position: 'bottom',
|
||||
showQuickJumper: true,
|
||||
pageSize: 10,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
};
|
||||
|
||||
@observer
|
||||
export class BrokerPartition extends React.Component {
|
||||
|
||||
public componentDidMount() {
|
||||
broker.getPartitions(urlQuery.clusterId, urlQuery.brokerId);
|
||||
}
|
||||
|
||||
public getDescription = (value: any, record: IPartitions) => {
|
||||
return Object.keys(value).map((key: keyof IPartitions) => {
|
||||
return (
|
||||
<p key={key}><span>{value[key]}</span>{(record[key] as []).join(',')}
|
||||
(共{(record[key] as []).length}个)</p>);
|
||||
});
|
||||
}
|
||||
|
||||
public getMoreDetail = (record: IPartitions) => {
|
||||
return (
|
||||
<div className="p-description">
|
||||
<p><span>Topic: </span>{record.topicName}</p>
|
||||
<p><span>isUnderReplicated:</span>{record.underReplicated ? '已同步' : '未同步'}</p>
|
||||
{this.getDescription(columsDefault, record)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
expandIconAsCell={false}
|
||||
expandIconColumnIndex={-1}
|
||||
expandedRowRender={this.getMoreDetail}
|
||||
dataSource={broker.topicPartitionsInfo}
|
||||
expandedRowKeys={broker.openKeys}
|
||||
rowKey="topicName"
|
||||
pagination={pagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
64
console/src/container/broker-detail/constant.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
export const MetricChartList = [
|
||||
{
|
||||
value: 'requestHandlerIdlPercent',
|
||||
label: '请求处理器空闲百分比(%)',
|
||||
},
|
||||
{
|
||||
value: 'networkProcessorIdlPercent',
|
||||
label: '网络处理器空闲百分比(%)',
|
||||
},
|
||||
{
|
||||
value: 'requestQueueSize',
|
||||
label: '请求列表大小(个)',
|
||||
},
|
||||
{
|
||||
value: 'responseQueueSize',
|
||||
label: '响应列表大小(个)',
|
||||
},
|
||||
{
|
||||
value: 'logFlushTime',
|
||||
label: '刷日志时间(ms)',
|
||||
},
|
||||
{
|
||||
value: '',
|
||||
label: '',
|
||||
},
|
||||
{
|
||||
value: 'totalTimeProduceMean',
|
||||
label: 'produce请求时间-平均值(ms)',
|
||||
},
|
||||
{
|
||||
value: 'totalTimeFetchConsumerMean',
|
||||
label: 'fetch请求处理时间-平均值(ms)',
|
||||
},
|
||||
{
|
||||
value: 'totalTimeProduce99Th',
|
||||
label: 'produce请求时间-99分位(ms)',
|
||||
},
|
||||
{
|
||||
value: 'totalTimeFetchConsumer99Th',
|
||||
label: 'fetch请求处理时间-99分位(ms)',
|
||||
},
|
||||
{
|
||||
value: 'failProduceRequest',
|
||||
label: '每秒生产失败数(条/秒)',
|
||||
},
|
||||
{
|
||||
value: 'failFetchRequest',
|
||||
label: '每秒消费失败数(条/秒)',
|
||||
},
|
||||
];
|
||||
|
||||
export const columsDefault = {
|
||||
leaderPartitionList: 'leaderPartitions:',
|
||||
followerPartitionIdList: 'followerPartitions:',
|
||||
notUnderReplicatedPartitionIdList: 'notUnderReplicatedPartitions:',
|
||||
};
|
||||
|
||||
export const brokerMetrics = {
|
||||
bytesIn: 'Bytes In(MB/ 秒)',
|
||||
bytesOut: 'Bytes Out(MB/ 秒)',
|
||||
messagesIn: 'Messages In(条)',
|
||||
totalFetchRequests: 'Total Fetch Requests(QPS)',
|
||||
totalProduceRequests: 'Total Produce Requests(QPS)',
|
||||
};
|
||||
116
console/src/container/broker-detail/index.less
Normal file
@@ -0,0 +1,116 @@
|
||||
.k-summary {
|
||||
width: 100%;
|
||||
font-family: PingFangSC-Regular;
|
||||
background: #fff;
|
||||
.k-row-1 {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
border-bottom: solid 1px #e8e8e8;
|
||||
div {
|
||||
flex: 1;
|
||||
line-height: 48px;
|
||||
padding-left: 32px;
|
||||
font-size: 15px;
|
||||
color: rgba(0,0,0,0.85);
|
||||
}
|
||||
div + div {
|
||||
border-left: solid 1px #e8e8e8;
|
||||
}
|
||||
}
|
||||
.k-row-2 {
|
||||
width: 100%;
|
||||
padding: 24px 0;
|
||||
border-top: solid 1px #e8e8e8;
|
||||
border-bottom: solid 1px #e8e8e8;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
div {
|
||||
height: 58px;
|
||||
flex: 1;
|
||||
span {
|
||||
display: block;
|
||||
line-height: 22px;
|
||||
color: rgba(0,0,0,0.45);
|
||||
font-size: 14px;
|
||||
}
|
||||
p {
|
||||
line-height: 32px;
|
||||
margin-top: 4px;
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
color: rgba(0,0,0,0.85);
|
||||
}
|
||||
}
|
||||
div + div {
|
||||
border-left: solid 2px #e8e8e8;
|
||||
}
|
||||
}
|
||||
.k-row-3 {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
div {
|
||||
padding: 9px 0;
|
||||
flex: 1;
|
||||
span {
|
||||
display: block;
|
||||
line-height: 22px;
|
||||
color: rgba(0,0,0,0.45);
|
||||
font-size: 14px;
|
||||
}
|
||||
p {
|
||||
line-height: 22px;
|
||||
margin-top: 1px;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
color: rgba(0,0,0,0.85);
|
||||
}
|
||||
}
|
||||
.long-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
div + div {
|
||||
border-left: solid 2px #e8e8e8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.p-description {
|
||||
margin-left: 20px;
|
||||
span {
|
||||
display: inline-block;
|
||||
width: 150px;
|
||||
text-align: right;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.charts-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
min-width: 1200px;
|
||||
> div {
|
||||
width: 48%;
|
||||
margin: 15px 24px 15px 0px;
|
||||
background-color: #fff;
|
||||
padding: 15px 0px;
|
||||
&:hover {
|
||||
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
p {
|
||||
padding-left: 16px;
|
||||
font-weight: 700;
|
||||
height: 32px;
|
||||
border-bottom: 1px dashed #ccc;
|
||||
}
|
||||
}
|
||||
.emptyChart {
|
||||
background-color: transparent;
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
37
console/src/container/broker-detail/index.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import * as React from 'react';
|
||||
import { Tabs } from 'component/antd';
|
||||
import { BrokerBaseDetail } from './base-detail';
|
||||
import { TopicAnalysis } from './topic-analysis';
|
||||
import { BrokerTopicInfo } from './topic-info';
|
||||
import { BrokerPartition } from './broker-partition';
|
||||
import { BrokerMetrics } from './broker-index';
|
||||
import { handleTabKey } from 'lib/utils';
|
||||
|
||||
const TabPane = Tabs.TabPane;
|
||||
|
||||
export class BrokerDetail extends React.Component {
|
||||
public render() {
|
||||
return (
|
||||
<Tabs activeKey={location.hash.substr(1) || '1'} type="card" onChange={handleTabKey}>
|
||||
<TabPane tab="状态信息" key="1">
|
||||
<BrokerBaseDetail />
|
||||
</TabPane>
|
||||
<TabPane tab="Topic信息" key="2">
|
||||
<BrokerTopicInfo />
|
||||
</TabPane>
|
||||
<TabPane tab="Partition信息" key="3">
|
||||
<BrokerPartition />
|
||||
</TabPane>
|
||||
{/* <TabPane tab="状态图" key="4">
|
||||
<NetWorkFlow clusterId={urlQuery.clusterId} />
|
||||
</TabPane> */}
|
||||
<TabPane tab="Topic分析" key="5">
|
||||
<TopicAnalysis />
|
||||
</TabPane>
|
||||
<TabPane tab="Broker关键指标" key="6">
|
||||
<BrokerMetrics />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
}
|
||||
94
console/src/container/broker-detail/topic-analysis.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import * as React from 'react';
|
||||
import { Table } from 'component/antd';
|
||||
import urlQuery from 'store/url-query';
|
||||
import { broker, IBrokerMetrics } from 'store/broker';
|
||||
import { brokerMetrics } from './constant';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
const columns = [{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
},
|
||||
{
|
||||
title: 'Bytes In(MB/s)',
|
||||
dataIndex: 'bytesInRate',
|
||||
key: 'bytesInRate',
|
||||
render: (t: number, record: any) => `${record && record.bytesIn} (${+Math.ceil((t * 100))}%)`,
|
||||
},
|
||||
{
|
||||
title: 'Bytes Out(MB/s)',
|
||||
dataIndex: 'bytesOutRate',
|
||||
key: 'bytesOutRate',
|
||||
render: (t: number, record: any) => `${record && record.bytesOut} (${+Math.ceil((t * 100))}%)`,
|
||||
},
|
||||
{
|
||||
title: 'Message In(秒)',
|
||||
dataIndex: 'messagesInRate',
|
||||
key: 'messagesInRate',
|
||||
render: (t: number, record: any) => `${record && record.messagesIn} (${+Math.ceil((t * 100))}%)`,
|
||||
},
|
||||
{
|
||||
title: 'Total Fetch Requests(秒)',
|
||||
dataIndex: 'totalFetchRequestsRate',
|
||||
key: 'totalFetchRequestsRate',
|
||||
render: (t: number, record: any) => `${record && record.totalFetchRequests} (${+Math.ceil((t * 100))}%)`,
|
||||
},
|
||||
{
|
||||
title: 'Total Produce Requests(秒)',
|
||||
dataIndex: 'totalProduceRequestsRate',
|
||||
key: 'totalProduceRequestsRate',
|
||||
render: (t: number, record: any) => `${record && record.totalProduceRequests} (${+Math.ceil((t * 100))}%)`,
|
||||
}];
|
||||
@observer
|
||||
export class TopicAnalysis extends React.Component {
|
||||
public componentDidMount() {
|
||||
broker.getOneBrokerNetwork(urlQuery.clusterId, urlQuery.brokerId);
|
||||
broker.getBrokerTopicAnalyzer(urlQuery.clusterId, urlQuery.brokerId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<div className="k-row right-flow mb-24">
|
||||
<p className="k-title">Broker 状态</p>
|
||||
<BrokerStatus />
|
||||
</div>
|
||||
<div className="k-row right-flow">
|
||||
<p className="k-title">Topic 状态</p>
|
||||
<span className="k-abs didi-theme" style={{ fontSize: '14px' }}>说明:数值后的百分比表示“占Broker总量的百分比”</span>
|
||||
<Table
|
||||
rowKey="name"
|
||||
columns={columns}
|
||||
dataSource={broker.analyzerData.topicAnalysisVOList}
|
||||
pagination={false}
|
||||
/>;
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@observer
|
||||
class BrokerStatus extends React.Component {
|
||||
public render() {
|
||||
return (
|
||||
<div className="k-summary">
|
||||
<div className="k-row-3">
|
||||
<div>
|
||||
<span>Broker ID</span>
|
||||
<p>{urlQuery.brokerId}</p>
|
||||
</div>
|
||||
{broker.analyzerData ?
|
||||
Object.keys(brokerMetrics).map((i: keyof IBrokerMetrics) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<span className={brokerMetrics[i] && brokerMetrics[i].length > 25 ? 'long-text' : ''}>{brokerMetrics[i]}</span>
|
||||
<p>{broker.analyzerData[i] && broker.analyzerData[i].toFixed(2)}</p>
|
||||
</div>
|
||||
);
|
||||
}) : ''}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
102
console/src/container/broker-detail/topic-info.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
import { Table, PaginationConfig } from 'component/antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import { broker } from 'store/broker';
|
||||
import urlQuery from 'store/url-query';
|
||||
import moment from 'moment';
|
||||
import { ITopic } from 'types/base-type';
|
||||
import { SearchAndFilter } from 'container/cluster-topic';
|
||||
|
||||
const cloumns = [{
|
||||
title: 'Topic名称',
|
||||
key: 'topicName',
|
||||
width: 350,
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 250,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
sorter: (a: ITopic, b: ITopic) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
render: (t: string, r: ITopic) => {
|
||||
return (
|
||||
<a
|
||||
href={`/admin/topic_detail?clusterId=${urlQuery.clusterId}&topic=${r.topicName}`}
|
||||
target="_blank"
|
||||
>
|
||||
{r.topicName}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
title: '分区数',
|
||||
dataIndex: 'partitionNum',
|
||||
key: 'partitionNum',
|
||||
sorter: (a: ITopic, b: ITopic) => b.partitionNum - a.partitionNum,
|
||||
}, {
|
||||
title: '副本数',
|
||||
dataIndex: 'replicaNum',
|
||||
key: 'replicaNum',
|
||||
sorter: (a: ITopic, b: ITopic) => b.replicaNum - a.replicaNum,
|
||||
}, {
|
||||
title: '流入(KB/s)',
|
||||
dataIndex: 'byteIn',
|
||||
key: 'byteIn',
|
||||
sorter: (a: ITopic, b: ITopic) => b.byteIn - a.byteIn,
|
||||
render: (t: number) => (t / 1024).toFixed(2),
|
||||
}, {
|
||||
title: '流入(QPS)',
|
||||
dataIndex: 'produceRequest',
|
||||
key: 'produceRequest',
|
||||
sorter: (a: ITopic, b: ITopic) => b.produceRequest - a.produceRequest,
|
||||
render: (t: number) => t.toFixed(2),
|
||||
}, {
|
||||
title: '负责人',
|
||||
dataIndex: 'principals',
|
||||
key: 'principals',
|
||||
}, {
|
||||
title: '修改时间',
|
||||
dataIndex: 'updateTime',
|
||||
key: 'updateTime',
|
||||
sorter: (a: ITopic, b: ITopic) => a.updateTime - b.updateTime,
|
||||
render: (t: number) => moment(t).format('YYYY-MM-DD HH:mm:ss'),
|
||||
}, {
|
||||
}];
|
||||
|
||||
const pagination: PaginationConfig = {
|
||||
position: 'bottom',
|
||||
showQuickJumper: true,
|
||||
pageSize: 10,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
};
|
||||
|
||||
@observer
|
||||
export class BrokerTopicInfo extends SearchAndFilter {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
filterClusterVisible: false,
|
||||
filterStatusVisible: false,
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
broker.getBrokerTopic(urlQuery.clusterId, urlQuery.brokerId);
|
||||
}
|
||||
public render() {
|
||||
const { searchKey } = this.state;
|
||||
const data: ITopic[] = broker.topics.filter((d) => d.topicName.includes(searchKey) ||
|
||||
(d.principals && d.principals.includes(searchKey)));
|
||||
return (
|
||||
<>
|
||||
<div style={{ height: 45 }}>
|
||||
<ul className="table-operation-bar">
|
||||
{this.renderSearch('请输入Topic名称或者负责人')}
|
||||
</ul>
|
||||
</div>
|
||||
<Table columns={cloumns} dataSource={data} rowKey="topicName" pagination={pagination} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
245
console/src/container/broker-info/base-info.tsx
Normal file
@@ -0,0 +1,245 @@
|
||||
import * as React from 'react';
|
||||
import './index.less';
|
||||
import { Table, Modal, notification, PaginationConfig, Button } from 'component/antd';
|
||||
import { broker, IBroker, IBrokerNetworkInfo, IBrokerPartition } from 'store/broker';
|
||||
import { observer } from 'mobx-react';
|
||||
import { StatusGraghCom } from 'component/flow-table';
|
||||
import urlQuery from 'store/url-query';
|
||||
import moment from 'moment';
|
||||
import { deleteBroker } from 'lib/api';
|
||||
import { SearchAndFilter } from 'container/cluster-topic';
|
||||
|
||||
import './index.less';
|
||||
import { modal } from 'store';
|
||||
import { tableFilter } from 'lib/utils';
|
||||
|
||||
const pagination: PaginationConfig = {
|
||||
position: 'bottom',
|
||||
showQuickJumper: true,
|
||||
pageSize: 5,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
};
|
||||
|
||||
@observer
|
||||
export class BrokerStatus extends StatusGraghCom<IBrokerNetworkInfo> {
|
||||
public getData() {
|
||||
return broker.network;
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export class BrokerList extends SearchAndFilter {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
searchId: '',
|
||||
filterRegionVisible: false,
|
||||
filterStatusVisible: false,
|
||||
filterVisible: false,
|
||||
filterRVisible: false,
|
||||
};
|
||||
|
||||
public colPartition = (list: IBroker[]) => {
|
||||
const region = Object.assign({
|
||||
title: 'Region',
|
||||
dataIndex: 'regionName',
|
||||
key: 'regionName',
|
||||
filters: tableFilter<IBroker>(list, 'regionName'),
|
||||
onFilter: (value: string, record: IBroker) => record.regionName === value,
|
||||
}, this.renderColumnsFilter('filterRVisible'));
|
||||
|
||||
const status = Object.assign({
|
||||
title: '已同步',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
filters: [{ text: '是', value: '是' }, { text: '否', value: '否' }],
|
||||
onFilter: (value: string, record: IBrokerPartition) => record.status === value,
|
||||
}, this.renderColumnsFilter('filterVisible'));
|
||||
|
||||
return [{
|
||||
title: 'BrokerID',
|
||||
dataIndex: 'brokerId',
|
||||
key: 'brokerId',
|
||||
sorter: (a: IBrokerPartition, b: IBrokerPartition) => a.brokerId - b.brokerId,
|
||||
}, {
|
||||
title: '峰值(MB/s)',
|
||||
dataIndex: 'bytesInPerSec',
|
||||
key: 'bytesInPerSec',
|
||||
sorter: (a: IBrokerPartition, b: IBrokerPartition) => a.bytesInPerSec - b.bytesInPerSec,
|
||||
render: (t: number) => (t / (1024 * 1024)).toFixed(2),
|
||||
}, {
|
||||
title: '分区数量',
|
||||
dataIndex: 'partitionCount',
|
||||
key: 'partitionCount',
|
||||
sorter: (a: IBrokerPartition, b: IBrokerPartition) => a.partitionCount - b.partitionCount,
|
||||
}, {
|
||||
title: 'Leader数量',
|
||||
dataIndex: 'leaderCount',
|
||||
key: 'leaderCount',
|
||||
sorter: (a: IBrokerPartition, b: IBrokerPartition) => a.leaderCount - b.leaderCount,
|
||||
}, {
|
||||
title: '未同步副本数量',
|
||||
dataIndex: 'notUnderReplicatedPartitionCount',
|
||||
key: 'notUnderReplicatedPartitionCount',
|
||||
sorter: (a: IBrokerPartition, b: IBrokerPartition) => a.notUnderReplicatedPartitionCount - b.notUnderReplicatedPartitionCount,
|
||||
},
|
||||
status,
|
||||
region,
|
||||
];
|
||||
}
|
||||
|
||||
public colList = (list: IBroker[]) => {
|
||||
const region = Object.assign({
|
||||
title: 'Region',
|
||||
dataIndex: 'regionName',
|
||||
key: 'regionName',
|
||||
filters: tableFilter<IBroker>(list, 'regionName'),
|
||||
onFilter: (value: string, record: IBroker) => record.regionName === value,
|
||||
}, this.renderColumnsFilter('filterRegionVisible'));
|
||||
|
||||
const status = Object.assign({
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
filters: [{ text: '未使用', value: '未使用' }, { text: '使用中', value: '使用中' }],
|
||||
onFilter: (value: string, record: IBroker) => record.status === value,
|
||||
render: (t: number) => t ? '未使用' : '使用中',
|
||||
}, this.renderColumnsFilter('filterStatusVisible'));
|
||||
|
||||
return [{
|
||||
title: 'BrokerID',
|
||||
dataIndex: 'brokerId',
|
||||
key: 'brokerId',
|
||||
sorter: (a: IBroker, b: IBroker) => a.brokerId - b.brokerId,
|
||||
render: (t: string, record: IBroker) => {
|
||||
return (
|
||||
<a
|
||||
href={`/admin/broker_detail?clusterId=${urlQuery.clusterId}&brokerId=${record.brokerId}`}
|
||||
target="_blank"
|
||||
>
|
||||
{t}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
title: '主机',
|
||||
dataIndex: 'host',
|
||||
key: 'host',
|
||||
sorter: (a: IBroker, b: IBroker) => a.host.charCodeAt(0) - b.host.charCodeAt(0),
|
||||
}, {
|
||||
title: 'Port',
|
||||
dataIndex: 'port',
|
||||
key: 'port',
|
||||
sorter: (a: IBroker, b: IBroker) => a.port - b.port,
|
||||
}, {
|
||||
title: 'JMX Port',
|
||||
dataIndex: 'jmxPort',
|
||||
key: 'jmxPort',
|
||||
sorter: (a: IBroker, b: IBroker) => a.jmxPort - b.jmxPort,
|
||||
}, {
|
||||
title: '启动时间',
|
||||
dataIndex: 'startTime',
|
||||
key: 'startTime',
|
||||
sorter: (a: IBroker, b: IBroker) => a.startTime - b.startTime,
|
||||
render: (t: number) => moment(t).format('YYYY-MM-DD HH:mm:ss'),
|
||||
}, {
|
||||
title: '流入(KB/s)',
|
||||
dataIndex: 'byteIn',
|
||||
key: 'byteIn',
|
||||
sorter: (a: IBroker, b: IBroker) => b.byteIn - a.byteIn,
|
||||
render: (t: number) => (t / 1024).toFixed(2),
|
||||
}, {
|
||||
title: '流出(KB/s)',
|
||||
dataIndex: 'byteOut',
|
||||
key: 'byteOut',
|
||||
sorter: (a: IBroker, b: IBroker) => b.byteOut - a.byteOut,
|
||||
render: (t: number) => (t / 1024).toFixed(2),
|
||||
},
|
||||
region,
|
||||
status,
|
||||
{
|
||||
title: '操作',
|
||||
render: (text: string, record: IBroker) => {
|
||||
return (
|
||||
<>
|
||||
<span className="table-operation">
|
||||
<a
|
||||
href={`/admin/broker_detail?clusterId=${urlQuery.clusterId}&brokerId=${record.brokerId}`}
|
||||
target="_blank"
|
||||
>详情
|
||||
</a>
|
||||
<a
|
||||
onClick={!record.status ? () => { } : this.deleteBroker.bind(null, record)}
|
||||
style={!record.status ? { cursor: 'not-allowed', color: '#999' } : {}}
|
||||
>删除
|
||||
</a>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
},
|
||||
}];
|
||||
}
|
||||
|
||||
public deleteBroker = ({ brokerId }: IBroker) => {
|
||||
Modal.confirm({
|
||||
title: `确认删除${brokerId}?`,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
deleteBroker(urlQuery.clusterId, brokerId).then(() => {
|
||||
notification.success({ message: '删除成功' });
|
||||
broker.getBrokerList(urlQuery.clusterId);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
broker.getBrokerList(urlQuery.clusterId);
|
||||
broker.getBrokerPartition(urlQuery.clusterId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const dataList = this.state.searchKey !== '' ?
|
||||
broker.list.filter((d) => d.host.includes(this.state.searchKey) || d.brokerId === +this.state.searchKey)
|
||||
: broker.list;
|
||||
const dataPartitions = this.state.searchId !== '' ?
|
||||
broker.partitions.filter((d) => d.brokerId === +this.state.searchId) : broker.partitions;
|
||||
return (
|
||||
<>
|
||||
<div className="k-row">
|
||||
<ul className="k-tab">
|
||||
<li>Broker概览</li>
|
||||
<li className="k-tab-button">
|
||||
<Button type="primary" onClick={modal.showLeaderRebalance}>Leader Rebalance</Button>
|
||||
</li>
|
||||
{this.renderSearch('请输入主机或BrokerId')}
|
||||
</ul>
|
||||
<div style={this.state.searchKey ? { minHeight: 370 } : null}>
|
||||
<Table
|
||||
columns={this.colList(dataList)}
|
||||
dataSource={dataList}
|
||||
rowKey="brokerId"
|
||||
pagination={pagination}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="k-row" style={{ height: 400 }}>
|
||||
<ul className="k-tab">
|
||||
<li>Broker分区概览</li>
|
||||
<li className="k-tab-button">
|
||||
<Button type="primary" onClick={modal.showLeaderRebalance}>Leader Rebalance</Button>
|
||||
</li>
|
||||
{this.renderSearch('请输入BrokerId', 'searchId')}
|
||||
</ul>
|
||||
<Table
|
||||
columns={this.colPartition(dataPartitions)}
|
||||
dataSource={dataPartitions}
|
||||
rowKey="brokerId"
|
||||
pagination={pagination}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
79
console/src/container/broker-info/broker-overview.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import * as React from 'react';
|
||||
import { Select, Button, Tooltip } from 'component/antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import urlQuery from 'store/url-query';
|
||||
import './index.less';
|
||||
import moment = require('moment');
|
||||
import { modal } from 'store';
|
||||
import { selecOptions } from './constant';
|
||||
import { broker, IBrokerPartition, IOverviewKey } from 'store/broker';
|
||||
|
||||
@observer
|
||||
export class BrokerOverview extends React.Component {
|
||||
public renderSquare = (type: IOverviewKey) => {
|
||||
return broker.realPartitions.map((item: IBrokerPartition) => {
|
||||
const brokerDetail = (
|
||||
<div>
|
||||
<p><span>ID:</span>{item.brokerId}</p>
|
||||
<p><span>{selecOptions[type]}</span>{item[type]}</p>
|
||||
<p><span>主机: </span>{item.host}</p>
|
||||
<p><span>端口:</span>{item.port}</p>
|
||||
<p><span>jmx端口:</span>{item.jmxPort}</p>
|
||||
<p><span>启动时间:</span>{moment(item.startTime).format('YYYY-MM-DD HH:mm:ss')}</p>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Tooltip key={item.brokerId} placement="right" title={brokerDetail}>
|
||||
<a
|
||||
className={type === 'notUnderReplicatedPartitionCount' && item[type] ? 'finished' : ''}
|
||||
href={`/admin/broker_detail?clusterId=${urlQuery.clusterId}&brokerId=${item.brokerId}`}
|
||||
target="_blank"
|
||||
>{item[type]}
|
||||
</a>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<ul className="topic-line-tool">
|
||||
<li>
|
||||
<span className="label">Region</span>
|
||||
<Select defaultValue="all" style={{ width: '260px' }} onChange={broker.filterSquare}>
|
||||
<Select.Option value="all">全部</Select.Option>
|
||||
{broker.regionOption.map((i: any) => <Select.Option
|
||||
key={i.brokerId}
|
||||
value={i.regionName}
|
||||
>{i.regionName}
|
||||
</Select.Option>)}
|
||||
</Select>
|
||||
</li>
|
||||
<li>
|
||||
<span className="label">维度</span>
|
||||
<Select defaultValue={broker.viewType} style={{ width: '160px' }} onChange={broker.handleOverview}>
|
||||
{Object.keys(selecOptions)
|
||||
.map((i: IOverviewKey) => <Select.Option value={i} key={i}>{selecOptions[i]}</Select.Option>)}
|
||||
</Select>
|
||||
</li>
|
||||
<li>
|
||||
<Button type="primary" onClick={modal.showLeaderRebalance}>Leader Rebalance</Button>
|
||||
</li>
|
||||
{
|
||||
broker.viewType === 'notUnderReplicatedPartitionCount' ?
|
||||
<li className="introduce">
|
||||
<span className="common common-green" />
|
||||
<span className="label">同步</span>
|
||||
<span className="common common-red" />
|
||||
<span className="label">未同步</span>
|
||||
</li> : ''
|
||||
}
|
||||
</ul>
|
||||
<div className="square-container">
|
||||
{this.renderSquare(broker.viewType)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
5
console/src/container/broker-info/constant.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const selecOptions = {
|
||||
partitionCount: '分区数量',
|
||||
leaderCount: 'leader数量',
|
||||
notUnderReplicatedPartitionCount: '副本状态',
|
||||
};
|
||||
84
console/src/container/broker-info/index.less
Normal file
@@ -0,0 +1,84 @@
|
||||
.square-container {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
padding: 17px 20px;
|
||||
a {
|
||||
display: inline-block;
|
||||
width: 35px;
|
||||
height: 22px;
|
||||
background: rgba(255, 241, 240, 1);
|
||||
border-radius: 4px;
|
||||
line-height: 22px;
|
||||
margin-right: 20px;
|
||||
text-align: center;
|
||||
color: #f5222d;
|
||||
&.finished {
|
||||
background: rgba(47, 194, 91, 0.2);
|
||||
color: #2fc25b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.topic-line-tool {
|
||||
height: 48px;
|
||||
font-size: 14px;
|
||||
font-family: PingFangSC-Regular;
|
||||
font-weight: 400;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
background: rgba(250, 250, 250);
|
||||
li {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: 24px;
|
||||
line-height: 48px;
|
||||
span.label {
|
||||
padding-right: 10px;
|
||||
}
|
||||
&.introduce {
|
||||
float: right;
|
||||
span.common {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 8px;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
&-green {
|
||||
background: rgba(47, 194, 91, 1);
|
||||
}
|
||||
&-red {
|
||||
background: rgba(245, 34, 45, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.k-tab {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
padding: 0px 24px;
|
||||
font-size: 14px;
|
||||
font-family: PingFangSC-Medium;
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
.k-tab-button {
|
||||
position: absolute;
|
||||
right: 213px;
|
||||
}
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
&.ant-btn {
|
||||
color: #f38031;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
21
console/src/container/broker-info/index.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as React from 'react';
|
||||
import { Tabs } from 'component/antd';
|
||||
import { BrokerList } from './base-info';
|
||||
import { BrokerOverview } from './broker-overview';
|
||||
|
||||
const TabPane = Tabs.TabPane;
|
||||
|
||||
export class BrokerInfo extends React.Component {
|
||||
public render() {
|
||||
return (
|
||||
<Tabs defaultActiveKey="1" type="card">
|
||||
<TabPane tab="Broker状态概览" key="1">
|
||||
<BrokerList />
|
||||
</TabPane>
|
||||
{/* <TabPane tab="Broker状态总览" key="2">
|
||||
<BrokerOverview />
|
||||
</TabPane> */}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
}
|
||||
98
console/src/container/cluster-topic/index.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import * as React from 'react';
|
||||
import { Select, Input, Checkbox, Icon } from 'component/antd';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { IFiler } from 'types/base-type';
|
||||
|
||||
const Option = Select.Option;
|
||||
const Search = Input.Search;
|
||||
|
||||
interface IParams {
|
||||
filters: IFiler[];
|
||||
setSelectedKeys: (selectedKeys: string[]) => void;
|
||||
confirm?: () => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
[filter: string]: boolean | string;
|
||||
}
|
||||
|
||||
export class SearchAndFilter extends React.Component<any, IState> {
|
||||
public timer: number;
|
||||
|
||||
public renderCluster() {
|
||||
return (
|
||||
<li>
|
||||
<Select value={cluster.active} onChange={cluster.changeCluster}>
|
||||
{cluster.data.map((d) => <Option value={d.clusterId} key={d.clusterId}>{d.clusterName}</Option>)}
|
||||
</Select>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
public renderSearch(placeholder?: string, keyName: string = 'searchKey') {
|
||||
return (
|
||||
<li><Search placeholder={placeholder || '请输入Topic名称'} onChange={this.onSearchChange.bind(null, keyName)} /></li>
|
||||
);
|
||||
}
|
||||
|
||||
public onSearchChange = (keyName: string, e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const searchKey = e.target.value.trim();
|
||||
this.setState({
|
||||
[keyName]: searchKey,
|
||||
});
|
||||
}
|
||||
|
||||
public handleChange(params: IParams, e: []) {
|
||||
const { setSelectedKeys, confirm } = params;
|
||||
setSelectedKeys(e);
|
||||
confirm();
|
||||
}
|
||||
|
||||
public handleVisble = (type: string) => {
|
||||
if (this.timer) clearTimeout(this.timer);
|
||||
setTimeout(() => {
|
||||
this.setState({ [type]: true });
|
||||
});
|
||||
}
|
||||
|
||||
public handleUnVisble = (type: string) => {
|
||||
this.timer = setTimeout(() => {
|
||||
this.setState({ [type]: false });
|
||||
}, 100);
|
||||
}
|
||||
|
||||
public renderFilter = (type: string, params: IParams) => {
|
||||
const { filters } = params;
|
||||
return filters !== undefined ? (
|
||||
<ul
|
||||
onMouseOver={this.handleVisble.bind(null, type)}
|
||||
onMouseLeave={this.handleUnVisble.bind(null, type)}
|
||||
className="ant-dropdown-menu ant-dropdown-menu-vertical"
|
||||
>
|
||||
<Checkbox.Group onChange={this.handleChange.bind(null, params)}>
|
||||
{filters.map(i => <li key={i.value} className="ant-dropdown-menu-item">
|
||||
<Checkbox value={i.value} >{i.text}</Checkbox>
|
||||
</li>)}
|
||||
</Checkbox.Group>
|
||||
</ul>
|
||||
) : <div />;
|
||||
}
|
||||
|
||||
public renderFilterIcon = (type: string) => {
|
||||
return (
|
||||
<span
|
||||
onMouseOver={this.handleVisble.bind(null, type)}
|
||||
onMouseLeave={this.handleUnVisble.bind(null, type)}
|
||||
><Icon type="filter" theme="filled" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
public renderColumnsFilter = (type: string) => {
|
||||
return {
|
||||
filterIcon: this.renderFilterIcon.bind(null, type),
|
||||
filterDropdownVisible: this.state[type],
|
||||
filterDropdown: this.renderFilter.bind(null, type),
|
||||
};
|
||||
}
|
||||
}
|
||||
34
console/src/container/consumer/index.less
Normal file
@@ -0,0 +1,34 @@
|
||||
.ant-table-title {
|
||||
padding: 0px 0px 16px 0px;
|
||||
}
|
||||
|
||||
.consumer-container {
|
||||
.ant-table-title {
|
||||
position: absolute;
|
||||
top: -38px;
|
||||
}
|
||||
}
|
||||
|
||||
.group-title {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: rgb(105, 105, 105);
|
||||
> div {
|
||||
display: inline-block;
|
||||
border: 0.5px dashed rgba(0, 0, 0, 0.3);
|
||||
padding: 5px 8px;
|
||||
margin-right: 10px;
|
||||
span {
|
||||
color: #f38031;
|
||||
font-weight: 700;
|
||||
}
|
||||
&.group-select {
|
||||
width: 250px;
|
||||
border: none;
|
||||
.ant-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
111
console/src/container/consumer/index.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Table, Tabs, PaginationConfig, Select } from 'component/antd';
|
||||
import Url from 'lib/url-parser';
|
||||
import { topic, IGroupInfo } from 'store/topic';
|
||||
import { observer } from 'mobx-react';
|
||||
import { TopicDetail } from 'container/topic-detail';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const TabPane = Tabs.TabPane;
|
||||
|
||||
const parColumns = [
|
||||
{
|
||||
title: 'Partition ID',
|
||||
dataIndex: 'partitionId',
|
||||
key: 'partitionId',
|
||||
sorter: (a: IGroupInfo, b: IGroupInfo) => a.partitionId - b.partitionId,
|
||||
},
|
||||
{
|
||||
title: 'Consume ID',
|
||||
dataIndex: 'clientId',
|
||||
key: 'clientId',
|
||||
sorter: (a: IGroupInfo, b: IGroupInfo) => +a.clientId - +b.clientId,
|
||||
},
|
||||
{
|
||||
title: 'Consumer Offset',
|
||||
dataIndex: 'consumeOffset',
|
||||
key: 'consumeOffset',
|
||||
sorter: (a: IGroupInfo, b: IGroupInfo) => a.consumeOffset - b.consumeOffset,
|
||||
},
|
||||
{
|
||||
title: 'Partition Offset',
|
||||
dataIndex: 'partitionOffset',
|
||||
key: 'partitionOffset',
|
||||
sorter: (a: IGroupInfo, b: IGroupInfo) => a.partitionOffset - b.partitionOffset,
|
||||
},
|
||||
{
|
||||
title: 'Lag',
|
||||
dataIndex: 'lag',
|
||||
key: 'lag',
|
||||
sorter: (a: IGroupInfo, b: IGroupInfo) => a.lag - b.lag,
|
||||
},
|
||||
];
|
||||
|
||||
const pagination: PaginationConfig = {
|
||||
position: 'bottom',
|
||||
showQuickJumper: true,
|
||||
pageSize: 10,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
};
|
||||
|
||||
@observer
|
||||
export class Consumer extends TopicDetail {
|
||||
public clusterId: number;
|
||||
public topicName: string;
|
||||
public group: string;
|
||||
public location: string;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
this.topicName = url.search.topic;
|
||||
this.location = url.search.location;
|
||||
this.group = url.search.group;
|
||||
this.handleGroupChange(this.group + ',' + this.location);
|
||||
}
|
||||
|
||||
public handleGroupChange = (value: string) => {
|
||||
const { topicName, clusterId } = this;
|
||||
topic.getGroupInfo(topicName, clusterId, value.split(',')[0], value.split(',')[1]);
|
||||
}
|
||||
|
||||
public renderHeader = () => {
|
||||
return (
|
||||
<div className="group-title">
|
||||
consumerGroup:
|
||||
<div className="group-select">
|
||||
<Select defaultValue={this.group} onChange={this.handleGroupChange}>
|
||||
{topic.consumeInfo.map((d) => <Select.Option value={d.consumerGroup + ',' + d.location} key={d.consumerGroup}>
|
||||
{d.consumerGroup}</Select.Option>)}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public renderConsumer() {
|
||||
const data = this.state.searchKey ?
|
||||
topic.groupInfo.filter((g) => g.partitionId === Number(this.state.searchKey)) : topic.groupInfo;
|
||||
return (
|
||||
<Table
|
||||
rowKey="partitionId"
|
||||
columns={parColumns}
|
||||
dataSource={data}
|
||||
pagination={pagination}
|
||||
title={this.renderHeader}
|
||||
className={location.pathname.includes('admin') ? 'consumer-container' : ''}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public renderTab() {
|
||||
return (
|
||||
<TabPane tab="消费详情" key="2">
|
||||
{this.renderConsumer()}
|
||||
</TabPane>
|
||||
);
|
||||
}
|
||||
}
|
||||
31
console/src/container/drawer/index.less
Normal file
@@ -0,0 +1,31 @@
|
||||
.o-container {
|
||||
width: 100%;
|
||||
padding: 40px 0 0 10px;
|
||||
.b-list {
|
||||
button {
|
||||
margin: 0 5px 0 10px;
|
||||
}
|
||||
}
|
||||
.headLine {
|
||||
padding-top: 30px;
|
||||
margin-top: 30px;
|
||||
color: #f38031;
|
||||
border-top: 1px solid rgb(216, 216, 216);
|
||||
}
|
||||
.timeButton {
|
||||
position: relative;
|
||||
left: 260px;
|
||||
top: 40px;
|
||||
}
|
||||
.partionButton {
|
||||
float: right;
|
||||
margin: 20px 15px 0 0;
|
||||
}
|
||||
.b-item {
|
||||
margin-top: 20px;
|
||||
padding-left: 222px;
|
||||
button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
console/src/container/drawer/index.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { drawer } from 'store/drawer';
|
||||
|
||||
import ResetOffset from './reset-offset';
|
||||
import TopicSample from './topic-sample';
|
||||
|
||||
@observer
|
||||
export default class AllDrawerInOne extends React.Component {
|
||||
public render() {
|
||||
if (!drawer.id) return null;
|
||||
return (
|
||||
<>
|
||||
{drawer.id === 'showResetOffset' ? <ResetOffset /> : null}
|
||||
{drawer.id === 'showTopicSample' ? <TopicSample /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
118
console/src/container/drawer/reset-offset.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import * as React from 'react';
|
||||
import { Drawer, Form, Row, Button, Input, DatePicker, Col, Select, message, Alert } from 'component/antd';
|
||||
import { drawer } from 'store/drawer';
|
||||
import { topic } from 'store/topic';
|
||||
import { consume } from 'store/consume';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import './index.less';
|
||||
import moment = require('moment');
|
||||
|
||||
@observer
|
||||
class ResetOffset extends React.Component<any> {
|
||||
|
||||
public handleSubmit = (e: React.MouseEvent<any, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
this.props.form.validateFields((err: Error, values: any) => {
|
||||
if (err) return;
|
||||
const { timestamp } = values;
|
||||
consume.offsetPartition(Object.assign({ timestamp: +moment(timestamp).format('x') }, topic.currentGroup))
|
||||
.then(() => {
|
||||
message.success('重置时间成功');
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 200);
|
||||
});
|
||||
});
|
||||
}
|
||||
public submitPartiton = () => {
|
||||
consume.offsetPartition(topic.currentGroup, 1).then(() => {
|
||||
message.success('重置分区成功');
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
return (
|
||||
<Drawer
|
||||
title="重置消费偏移"
|
||||
width={520}
|
||||
closable={false}
|
||||
onClose={drawer.close}
|
||||
visible={true}
|
||||
destroyOnClose={true}
|
||||
>
|
||||
<Alert message="重置之前一定要停止消费任务!!!" type="warning" showIcon={true} />
|
||||
<div className="o-container">
|
||||
<Form labelAlign="left" onSubmit={this.handleSubmit} >
|
||||
<Row>
|
||||
<p style={{ fontSize: '14px', color: '#f38031' }}>重置到指定时间</p>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<Form.Item label="请选择时间" >
|
||||
{getFieldDecorator('timestamp', {
|
||||
rules: [{ required: true, message: '请选择时间' }],
|
||||
initialValue: moment(),
|
||||
})(
|
||||
<DatePicker showTime={true} style={{ width: '50%' }} />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item className="timeButton">
|
||||
<Button type="primary" htmlType="submit">
|
||||
重置
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="headLine">
|
||||
<p style={{ fontSize: '14px' }}>重置到指定分区</p>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item>
|
||||
<Row>
|
||||
<Col span={8}>partitionId</Col>
|
||||
<Col span={16}>partitionOffset</Col>
|
||||
</Row>
|
||||
{consume.offsetList.map((ele, index) => {
|
||||
return (
|
||||
<Row key={index} gutter={16}>
|
||||
<Col span={8}>
|
||||
<Select placeholder="请选择" onChange={consume.selectChange.bind(consume, index)}>
|
||||
{topic.groupInfo.map((r, k) => {
|
||||
return <Select.Option key={k} value={r.partitionId}>{r.partitionId}</Select.Option>;
|
||||
})}
|
||||
</Select></Col>
|
||||
<Col span={10}>
|
||||
<Input placeholder="请输入partition offset" onChange={consume.inputChange.bind(consume, index)} />
|
||||
</Col>
|
||||
<Col span={6} className="b-list">
|
||||
<Button type="dashed" icon="plus" onClick={consume.handleList.bind(null, null)} />
|
||||
<Button
|
||||
type="dashed"
|
||||
icon="minus"
|
||||
disabled={index === 0}
|
||||
onClick={consume.handleList.bind(null, index)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
<Row className="partionButton">
|
||||
<Button type="primary" onClick={this.submitPartiton}>重置</Button>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
</Row>
|
||||
</Form>
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create({ name: 'topicSample' })(ResetOffset);
|
||||
103
console/src/container/drawer/topic-sample.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import * as React from 'react';
|
||||
import { Drawer, Form, Row, Button, Input, InputNumber, notification } from 'component/antd';
|
||||
import { drawer } from 'store/drawer';
|
||||
|
||||
import './index.less';
|
||||
import { topic } from 'store/topic';
|
||||
|
||||
const topicFormItemLayout = {
|
||||
labelCol: {
|
||||
span: 5,
|
||||
},
|
||||
wrapperCol: {
|
||||
span: 14,
|
||||
},
|
||||
};
|
||||
|
||||
class TopicSample extends React.Component<any> {
|
||||
public state = {
|
||||
loading: false,
|
||||
};
|
||||
|
||||
public handleSubmit = (e: React.MouseEvent<any, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
this.props.form.validateFields((err: Error, values: any) => {
|
||||
if (err) return;
|
||||
const { offset, partitionId } = values;
|
||||
const bothExist = [offset, partitionId].filter(item => item === undefined || item === null).length;
|
||||
if (bothExist === 1) {
|
||||
notification.error({ message: '分区号和偏移量必须同时存在' });
|
||||
return false;
|
||||
}
|
||||
this.setState({ loading: true });
|
||||
topic.addSample(Object.assign(drawer.topicData, values)).then(() => this.setState({ loading: false }));
|
||||
});
|
||||
}
|
||||
|
||||
public cleanData = () => {
|
||||
topic.sampleData = null;
|
||||
drawer.close();
|
||||
}
|
||||
public render() {
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
return (
|
||||
<Drawer
|
||||
title="Topic 采样"
|
||||
width={620}
|
||||
closable={false}
|
||||
onClose={this.cleanData}
|
||||
visible={true}
|
||||
destroyOnClose={true}
|
||||
>
|
||||
<div className="o-container">
|
||||
<Form {...topicFormItemLayout} labelAlign="right" onSubmit={this.handleSubmit} >
|
||||
<Row>
|
||||
<Form.Item label="最大采样条数" >
|
||||
{getFieldDecorator('maxMsgNum', {
|
||||
rules: [{ required: true, message: '请输入最大采样条数' }],
|
||||
initialValue: 1,
|
||||
})(<Input />)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item label="最大采样时间">
|
||||
{getFieldDecorator('timeout', {
|
||||
rules: [{ required: true, message: '请输入最大采样时间' }],
|
||||
initialValue: 3000,
|
||||
})(<Input />)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item label="分区号">
|
||||
{getFieldDecorator('partitionId')(<InputNumber min={0} />)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item label="偏移量">
|
||||
{getFieldDecorator('offset')(<InputNumber min={0} />)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item className="b-item">
|
||||
<Button type="primary" htmlType="submit" loading={this.state.loading}>确认</Button>
|
||||
<Button type="primary" onClick={this.cleanData}>取消</Button>
|
||||
</Form.Item>
|
||||
</Row>
|
||||
</Form>
|
||||
{
|
||||
topic.sampleData ?
|
||||
topic.sampleData.map((i: any, index: number) => {
|
||||
return <Input.TextArea
|
||||
value={i.value}
|
||||
style={{ height: 120, width: 500, marginTop: 10 }}
|
||||
key={index}
|
||||
/>;
|
||||
}) : null
|
||||
}
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create({ name: 'topicSample' })(TopicSample);
|
||||
103
console/src/container/header/index.less
Normal file
@@ -0,0 +1,103 @@
|
||||
.kafka-header-container {
|
||||
height: 64px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, .1);
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
.left-content {
|
||||
width: 220px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
.mid-content {
|
||||
flex: 1;
|
||||
padding-left: 100px;
|
||||
a {
|
||||
color: #4A4A4A;
|
||||
&:hover {
|
||||
color: #f38031;
|
||||
}
|
||||
}
|
||||
span {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
line-height: 64px;
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
&.k-active {
|
||||
a {
|
||||
color: #f38031;
|
||||
}
|
||||
&:after {
|
||||
width: 100%;
|
||||
content: '';
|
||||
height: 4px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: #f38031;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.right-content {
|
||||
margin-right: 24px;
|
||||
position: relative;
|
||||
.kafka-avatar-icon {
|
||||
font-size: 24px;
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: -30px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.kafka-header-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
.left-content,
|
||||
.right-content {
|
||||
font-size: 0;
|
||||
& > span {
|
||||
display: inline-block;
|
||||
line-height: 64px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.kafka-header-icon {
|
||||
height: 45px;
|
||||
width: 45px;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.kafka-header-text {
|
||||
margin-left: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
font-family: PingFangSC-Medium;
|
||||
color: rgba(25,24,24,1);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.kafka-header-menu {
|
||||
width: 88px;
|
||||
background-color: #fff;
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
box-shadow: 0px 0px 4px 0px rgba(217,217,217);
|
||||
border-radius: 4px;
|
||||
li {
|
||||
text-align: center;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: rgba(236,111,38,0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
console/src/container/header/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import './index.less';
|
||||
import { getCookie, deleteCookie } from 'lib/utils';
|
||||
import { userLogoff } from 'lib/api';
|
||||
import { notification, Dropdown } from 'component/antd';
|
||||
import { users } from 'store/users';
|
||||
import logoUrl from '../../assets/image/kafka-logo.png';
|
||||
import devIcon from '../../assets/image/devops.png';
|
||||
import adminIcon from '../../assets/image/admin.png';
|
||||
import userIcon from '../../assets/image/normal.png';
|
||||
|
||||
interface IHeader {
|
||||
active: string;
|
||||
}
|
||||
|
||||
export const Header = (props: IHeader) => {
|
||||
const { active } = props;
|
||||
const username = getCookie('username');
|
||||
const role = Number(getCookie('role'));
|
||||
|
||||
const logoff = () => {
|
||||
userLogoff(username).then(() => {
|
||||
notification.success({ message: '退出成功' });
|
||||
deleteCookie(['username', 'role']);
|
||||
location.reload();
|
||||
});
|
||||
};
|
||||
|
||||
const menu = (
|
||||
<ul className="kafka-header-menu">
|
||||
{role ? <li> <a href="/admin/user_manage">管理</a></li> : ''}
|
||||
<li onClick={logoff}>退出</li>
|
||||
</ul>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="kafka-header-container">
|
||||
<div className="left-content">
|
||||
<img className="kafka-header-icon" src={logoUrl} alt="" />
|
||||
<span className="kafka-header-text">Kafka Manager</span>
|
||||
</div>
|
||||
<div className="mid-content">
|
||||
<span className={active === 'user' ? 'k-active' : ''}><a href="/">集群监控</a></span>
|
||||
{role ? <span className={active === 'admin' ? 'k-active' : ''}><a href="/admin">集群管控</a></span> : ''}
|
||||
</div>
|
||||
<div className="right-content">
|
||||
<img className="kafka-avatar-icon" src={role === 2? adminIcon : role === 1 ? devIcon: userIcon } alt="" />
|
||||
<Dropdown overlay={menu}>
|
||||
<span className="kafka-header-text">
|
||||
{users.mapRole(role)} : {username}</span>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
79
console/src/container/left-menu/constant.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
export const userMenu = [{
|
||||
href: '/',
|
||||
i: 'k-icon-iconfontzhizuobiaozhun023110',
|
||||
title: 'Topic列表',
|
||||
}, {
|
||||
href: '/user/my_order',
|
||||
i: 'k-icon-order1',
|
||||
title: '工单列表',
|
||||
}, {
|
||||
href: '/user/alarm',
|
||||
i: 'k-icon-gaojing',
|
||||
title: '告警配置',
|
||||
}, {
|
||||
href: '/user/modify_user',
|
||||
i: 'k-icon-yonghuguanli',
|
||||
title: '密码修改',
|
||||
}];
|
||||
|
||||
export const adminMenu = [{
|
||||
href: '/admin',
|
||||
i: 'k-icon-jiqun',
|
||||
title: '集群列表',
|
||||
}, {
|
||||
href: '/admin/order',
|
||||
i: 'k-icon-order1',
|
||||
title: '资源审批',
|
||||
},
|
||||
// }, {
|
||||
// href: '/admin/task',
|
||||
// i: 'k-icon-renwuliebiao',
|
||||
// title: '任务列表',
|
||||
// }, {
|
||||
// {
|
||||
// href: '/admin/alarm',
|
||||
// i: 'k-icon-gaojing',
|
||||
// title: '告警配置',
|
||||
// }, {
|
||||
{
|
||||
href: '/admin/user_manage',
|
||||
i: 'k-icon-yonghuguanli',
|
||||
title: '用户管理',
|
||||
},
|
||||
// }, {
|
||||
// href: '/admin/auto_approval',
|
||||
// i: 'k-icon-shenpi1',
|
||||
// title: '自动审批管理',
|
||||
// }, {
|
||||
{
|
||||
href: '/admin/operation',
|
||||
i: 'k-icon-xiaofeikecheng',
|
||||
title: '任务管理',
|
||||
// }, {
|
||||
// href: '/admin/modify_user',
|
||||
// i: 'k-icon-jiaoseshouquan',
|
||||
// title: '密码修改',
|
||||
}];
|
||||
|
||||
export interface IMenuItem {
|
||||
href: string;
|
||||
i: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const userMap = new Map<string, IMenuItem>();
|
||||
userMenu.forEach(m => {
|
||||
userMap.set(m.href, m);
|
||||
});
|
||||
|
||||
export const adminMap = new Map<string, IMenuItem>();
|
||||
adminMenu.forEach(m => {
|
||||
adminMap.set(m.href, m);
|
||||
});
|
||||
|
||||
export const getActiveMenu = (mode: 'admin' | 'user', href: string) => {
|
||||
const map = mode === 'admin' ? adminMap : userMap;
|
||||
const defaultMenu = mode === 'admin' ? '/admin' : '/';
|
||||
const menuItem = map.get(href);
|
||||
return menuItem && menuItem.href || defaultMenu;
|
||||
};
|
||||
77
console/src/container/left-menu/index.less
Normal file
@@ -0,0 +1,77 @@
|
||||
.left-menu {
|
||||
width: 64px;
|
||||
background: rgba(25, 24, 24, 1);
|
||||
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
transition: all 150ms ease-in-out;
|
||||
&.k-open {
|
||||
width: 156px;
|
||||
.k-float-op {
|
||||
width: 156px;
|
||||
}
|
||||
}
|
||||
.k-float-op {
|
||||
color: #878380;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
bottom: 0;
|
||||
width: 64px;
|
||||
height: 28px;
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
cursor: pointer;
|
||||
transition: all 150ms ease-in-out;
|
||||
z-index: 10;
|
||||
i {
|
||||
vertical-align: -6px;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
margin-top: 40px;
|
||||
li {
|
||||
padding-left: 20px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
font-size: 0;
|
||||
i,
|
||||
span {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
padding-left: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
&.active {
|
||||
i,
|
||||
span {
|
||||
color: #f38031;
|
||||
}
|
||||
}
|
||||
}
|
||||
// .active {
|
||||
// color: #f38031;
|
||||
|
||||
&:not(.active) {
|
||||
i,
|
||||
span {
|
||||
color: #878380;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
i,
|
||||
span {
|
||||
color: #f38031;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
console/src/container/left-menu/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import './index.less';
|
||||
import { Tooltip, Icon } from 'component/antd';
|
||||
import { adminMenu, userMenu } from './constant';
|
||||
import { BrowserRouter as Router, NavLink } from 'react-router-dom';
|
||||
|
||||
interface ILeftMenuProps {
|
||||
page: string;
|
||||
mode?: 'admin' | 'user';
|
||||
}
|
||||
|
||||
export class LeftMenu extends React.Component<ILeftMenuProps> {
|
||||
public state = {
|
||||
status: 'k-open',
|
||||
};
|
||||
|
||||
public open = () => {
|
||||
const { status } = this.state;
|
||||
const newStatus = !status ? 'k-open' : '';
|
||||
this.setState({
|
||||
status: newStatus,
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { status } = this.state;
|
||||
const { page, mode } = this.props;
|
||||
const menu = mode === 'admin' ? adminMenu : userMenu;
|
||||
return (
|
||||
<div className={`left-menu ${status}`}>
|
||||
<ul>
|
||||
{
|
||||
menu.map((m, i) => {
|
||||
const cnt = (
|
||||
<li key={m.i}>
|
||||
<NavLink exact={true} to={m.href} activeClassName="active">
|
||||
<i className={m.i} />
|
||||
{status ? <span>{m.title}</span> : null}
|
||||
</NavLink>
|
||||
</li>
|
||||
);
|
||||
|
||||
if (!status) {
|
||||
return <Tooltip placement="right" title={m.title} key={m.i} >{cnt}</Tooltip>;
|
||||
}
|
||||
return cnt;
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
<div className="k-float-op" onClick={this.open}>
|
||||
<Icon type={status ? 'double-left' : 'double-right'} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
141
console/src/container/modal/admin-expand.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import * as React from 'react';
|
||||
import { Modal, Form, Row, Input, Select, notification } from 'component/antd';
|
||||
import { modal } from 'store/modal';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { operation } from 'store/operation';
|
||||
import { topic, IAdminExpand } from 'store/topic';
|
||||
import { observer } from 'mobx-react';
|
||||
import { broker } from 'store/broker';
|
||||
import { topicDilatation } from 'lib/api';
|
||||
import urlQuery from 'store/url-query';
|
||||
import { IValueLabel } from 'types/base-type';
|
||||
|
||||
const topicFormItemLayout = {
|
||||
labelCol: {
|
||||
span: 7,
|
||||
},
|
||||
wrapperCol: {
|
||||
span: 11,
|
||||
},
|
||||
};
|
||||
@observer
|
||||
class Topic extends React.Component<any> {
|
||||
|
||||
public handleSubmit = () => {
|
||||
this.props.form.validateFields((err: Error, values: any) => {
|
||||
if (err) return;
|
||||
values.partitionNum = +values.partitionNum;
|
||||
topicDilatation(values).then(() => {
|
||||
topic.getAdminTopics(urlQuery.clusterId);
|
||||
notification.success({ message: '扩容成功' });
|
||||
modal.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const { clusterId, topicName } = modal.topicDetail;
|
||||
operation.initRegionOptions(clusterId);
|
||||
broker.initBrokerOptions(clusterId);
|
||||
cluster.getClusters();
|
||||
topic.getTopicMetaData(clusterId, topicName);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
const initialData = topic.topicDetail || {} as IAdminExpand;
|
||||
return (
|
||||
<Modal
|
||||
title="扩分区"
|
||||
style={{ top: 70 }}
|
||||
visible={true}
|
||||
onCancel={modal.close}
|
||||
maskClosable={false}
|
||||
width="680px"
|
||||
destroyOnClose={true}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
onOk={this.handleSubmit}
|
||||
>
|
||||
<Form {...topicFormItemLayout}>
|
||||
<Row>
|
||||
<Form.Item label="集群名称">
|
||||
{getFieldDecorator('clusterId', {
|
||||
rules: [{ required: true, message: '请选择集群' }],
|
||||
initialValue: +initialData.clusterId || +cluster.data[1].clusterId,
|
||||
})(
|
||||
<Select disabled={true}>
|
||||
{
|
||||
cluster.data.slice(1).map(c => {
|
||||
return <Select.Option value={c.clusterId} key={c.clusterId}>{c.clusterName}</Select.Option>;
|
||||
})
|
||||
}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item label="Topic名称">
|
||||
{getFieldDecorator('topicName', {
|
||||
rules: [{ required: true, message: '请选择Topic' }],
|
||||
initialValue: initialData.topicName,
|
||||
})(
|
||||
<Input disabled={true} />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Form.Item label="所处Broker列表">
|
||||
<Input value={initialData.brokerIdList && initialData.brokerIdList.join(',')} disabled={true} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="已有分区数"
|
||||
>
|
||||
<Input value={initialData.partitionNum} disabled={true} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="副本数"
|
||||
>
|
||||
<Input value={initialData.replicaNum} disabled={true} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="新扩Broker列表"
|
||||
>
|
||||
{getFieldDecorator('brokerIdList', {
|
||||
initialValue: initialData.brokerIdList || [],
|
||||
rules: [{
|
||||
required: true,
|
||||
validator: (_: any, value: string, callback: (wrong?: string) => void) => {
|
||||
if (initialData.replicaNum > value.length) {
|
||||
callback('Broker数需要大于或等于副本数');
|
||||
}
|
||||
callback();
|
||||
},
|
||||
}],
|
||||
})(
|
||||
<Select mode="multiple" optionLabelProp="title" placeholder="请选择broker">
|
||||
{
|
||||
broker.BrokerOptions.map((t: IValueLabel) => {
|
||||
return <Select.Option value={t.value} title={t.value + ''} key={t.value} >
|
||||
<span aria-label={t.value}> {t.label} </span>
|
||||
</Select.Option>;
|
||||
})
|
||||
}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="新增分区数"
|
||||
>
|
||||
{getFieldDecorator('partitionNum', {
|
||||
rules: [{ required: true, message: '请输入新增分区数' }],
|
||||
})(
|
||||
<Input placeholder="请输入新增分区数" />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create({ name: 'topic' })(Topic);
|
||||
303
console/src/container/modal/alarm-config.tsx
Normal file
@@ -0,0 +1,303 @@
|
||||
import * as React from 'react';
|
||||
import { Modal, Form, Row, Input, Select, notification, Col, Radio } from 'component/antd';
|
||||
import { modal } from 'store/modal';
|
||||
import { alarm } from 'store/alarm';
|
||||
import { observer } from 'mobx-react';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { addAlarm, modifyAlarm } from 'lib/api';
|
||||
import { topic } from 'store/topic';
|
||||
import { broker } from 'store/broker';
|
||||
import { getRandomPassword, getCookie } from 'lib/utils';
|
||||
import { IAlarmBase } from 'types/base-type';
|
||||
|
||||
const Option = Select.Option;
|
||||
|
||||
const topicFormItemLayout = {
|
||||
labelCol: {
|
||||
span: 6,
|
||||
},
|
||||
wrapperCol: {
|
||||
span: 14,
|
||||
},
|
||||
};
|
||||
|
||||
@observer
|
||||
class Alarm extends React.Component<any> {
|
||||
public data: any = null;
|
||||
public state = {
|
||||
loading: false,
|
||||
type: 'Lag',
|
||||
};
|
||||
|
||||
public handleSubmit = () => {
|
||||
this.props.form.validateFields((err: Error, values: any) => {
|
||||
if (err) return;
|
||||
this.setState({ loading: true });
|
||||
const { metric, opt, threshold, duration, alarmName, principalList: principal, actionTag, status } = values;
|
||||
const principalList = typeof (principal) === 'object' ? principal : principal.split(',');
|
||||
const strategyActionList = [{ actionTag, actionWay: 'KAFKA' }];
|
||||
const strategyFilterList = Array.from(['topicName', 'consumerGroup', 'brokerId', 'clusterId'], (item) => {
|
||||
if ((this.state.type === 'Lag' || !+getCookie('role')) && item === 'brokerId') return;
|
||||
if (this.state.type !== 'Lag' && item === 'consumerGroup' || JSON.stringify(values[item]) === '[]') return;
|
||||
return {
|
||||
key: item,
|
||||
value: values[item],
|
||||
};
|
||||
}).filter(i => i);
|
||||
const params: IAlarmBase = {
|
||||
status, strategyActionList, strategyFilterList, alarmName, principalList,
|
||||
strategyExpressionList: [{ metric, opt, threshold: +threshold, duration: +duration }],
|
||||
};
|
||||
const notiMessage = alarm.curData ? { message: '修改成功' } : { message: '添加告警成功' };
|
||||
const fn = alarm.curData ? modifyAlarm : addAlarm;
|
||||
fn(alarm.curData ? Object.assign({ id: alarm.curData.id }, params) : params).then(() => {
|
||||
notification.success(notiMessage);
|
||||
alarm.getAlarm();
|
||||
modal.close();
|
||||
}, (err) => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public initSelection = (value: number) => {
|
||||
topic.getTopicList(value);
|
||||
broker.initBrokerOptions(value);
|
||||
}
|
||||
|
||||
public componentDidMount = () => {
|
||||
cluster.getClusters();
|
||||
if (this.isModify()) {
|
||||
this.setState({ type: alarm.curData.strategyExpressionList[0].metric }, () => {
|
||||
this.data = this.getFilterList();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onChange = (type: any) => {
|
||||
this.setState({ type });
|
||||
}
|
||||
|
||||
public filterSelection = (input: string, option: any) => {
|
||||
return option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
}
|
||||
|
||||
public getFilterList = () => {
|
||||
const filterList = new Map();
|
||||
alarm.curData.strategyFilterList.forEach(item => {
|
||||
filterList.set(item.key, item.value);
|
||||
});
|
||||
this.initSelection(+filterList.get('clusterId'));
|
||||
return filterList;
|
||||
}
|
||||
|
||||
public getActionTag = () => {
|
||||
this.props.form.setFieldsValue({
|
||||
actionTag: 'KAFKA_' + getRandomPassword(),
|
||||
});
|
||||
}
|
||||
|
||||
public isModify = () => !!alarm.curData;
|
||||
|
||||
public render() {
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
const { loading, type } = this.state;
|
||||
const isModify = this.isModify();
|
||||
const initialData = alarm.curData;
|
||||
const { data } = this;
|
||||
const role = +getCookie('role');
|
||||
return (
|
||||
<Modal
|
||||
title="告警配置"
|
||||
style={{ top: 30 }}
|
||||
visible={true}
|
||||
onCancel={modal.close}
|
||||
maskClosable={false}
|
||||
width={700}
|
||||
destroyOnClose={true}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
onOk={this.handleSubmit}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form {...topicFormItemLayout} >
|
||||
<Form.Item label="告警名称">
|
||||
{getFieldDecorator('alarmName', {
|
||||
rules: [{ required: true, message: '告警规则名称' }],
|
||||
initialValue: isModify ? initialData.alarmName : '',
|
||||
})(
|
||||
<Input placeholder="请输入告警配置名称" />,
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item label="负责人">
|
||||
{getFieldDecorator('principalList', {
|
||||
rules: [{ required: true, message: '请输入负责人' }],
|
||||
initialValue: isModify ? initialData.principalList : getCookie('username'),
|
||||
})(
|
||||
<Input placeholder="多个负责人请用逗号隔开" />,
|
||||
)}
|
||||
</Form.Item>
|
||||
<Row>
|
||||
<Col span={6}>
|
||||
<p style={{ textAlign: 'right', color: 'rgba(0,0,0,.85)', paddingTop: '8px' }}>告警规则: </p>
|
||||
</Col>
|
||||
<Col span={14} className="ruleArea">
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item wrapperCol={{ span: 22 }}>
|
||||
{getFieldDecorator('metric', {
|
||||
rules: [{ required: true, message: '请选择 Metric' }],
|
||||
initialValue: isModify ? initialData.strategyExpressionList[0].metric : '',
|
||||
})(
|
||||
<Select placeholder="请选择 Metric" onChange={this.onChange}>
|
||||
{
|
||||
alarm.alarmConstant.metricTypeList.map((ele, index) => {
|
||||
return <Option key={index} value={Object.keys(ele)[0]}> {Object.keys(ele)[0]}</Option>;
|
||||
})
|
||||
}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item wrapperCol={{ span: 22 }}>
|
||||
{getFieldDecorator('opt', {
|
||||
rules: [{ required: true, message: '请选择 Condition' }],
|
||||
initialValue: isModify ? initialData.strategyExpressionList[0].opt : '',
|
||||
})(
|
||||
<Select placeholder="请选择 Condition">
|
||||
{
|
||||
alarm.alarmConstant.conditionTypeList.map((ele, index) => {
|
||||
return <Option key={index} value={Object.keys(ele)[0]}> {Object.keys(ele)[0]} </Option>;
|
||||
})
|
||||
}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item wrapperCol={{ span: 22 }}>
|
||||
{getFieldDecorator('threshold', {
|
||||
rules: [{ required: true, message: '请输入 value' }],
|
||||
initialValue: isModify ? initialData.strategyExpressionList[0].threshold : '',
|
||||
})(
|
||||
<Input addonBefore="metricValue" />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item wrapperCol={{ span: 22 }}>
|
||||
{getFieldDecorator('duration', {
|
||||
rules: [{ required: true, message: '请输入持续时间' }],
|
||||
initialValue: isModify ? initialData.strategyExpressionList[0].duration : '',
|
||||
})(
|
||||
<Input addonBefore="duration" />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={6}>
|
||||
<p style={{ textAlign: 'right', color: 'rgba(0,0,0,.85)', paddingTop: '8px' }}>过滤规则: </p>
|
||||
</Col>
|
||||
<Col span={14} className="ruleArea">
|
||||
<span className="label">集群名称</span>
|
||||
<Form.Item wrapperCol={{ span: 25 }} className="t-input">
|
||||
{getFieldDecorator('clusterId', {
|
||||
rules: [{ required: true, message: '请选择集群' }],
|
||||
initialValue: data && +data.get('clusterId') || '',
|
||||
})(
|
||||
<Select onChange={this.initSelection} placeholder="请选择集群" >
|
||||
{
|
||||
cluster.data.slice(1).map(c => {
|
||||
return <Select.Option value={c.clusterId} key={c.clusterId}>{c.clusterName}</Select.Option>;
|
||||
})
|
||||
}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
<Row>
|
||||
<span className="label">Topic名称</span>
|
||||
<Form.Item wrapperCol={{ span: 25 }} className="t-input">
|
||||
{getFieldDecorator('topicName', {
|
||||
rules: [{ required: type === 'Lag' || !role, message: '请选择Topic' }],
|
||||
initialValue: data && data.get('topicName') || '',
|
||||
})(
|
||||
<Select
|
||||
placeholder="请选择Topic"
|
||||
allowClear={true}
|
||||
filterOption={this.filterSelection}
|
||||
showSearch={true}
|
||||
>
|
||||
{
|
||||
topic.topicNameList.map(t => {
|
||||
return <Select.Option value={t} key={t}>{t}</Select.Option>;
|
||||
})
|
||||
}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
{type !== 'Lag' ? role ? (
|
||||
<Row>
|
||||
<span className="label">BrokeId</span>
|
||||
<Form.Item wrapperCol={{ span: 25 }} className="t-input">
|
||||
{getFieldDecorator('brokerId', {
|
||||
initialValue: data && data.get('brokerId') || [],
|
||||
})(
|
||||
<Select optionLabelProp="title" placeholder="请选择Broker" allowClear={true}>
|
||||
{
|
||||
broker.BrokerOptions.map(t => {
|
||||
return <Select.Option value={t.value} title={t.value + ''} key={t.value} >
|
||||
<span aria-label={t.value}> {t.label} </span>
|
||||
</Select.Option>;
|
||||
})
|
||||
}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
) : null : (
|
||||
<>
|
||||
<span className="label">消费组</span>
|
||||
<Form.Item wrapperCol={{ span: 25 }} className="t-input">
|
||||
{getFieldDecorator('consumerGroup', {
|
||||
rules: [{ required: type === 'Lag', message: '请输入消费组' }],
|
||||
initialValue: data && data.get('consumerGroup') || '',
|
||||
})(
|
||||
<Input placeholder="请输入消费组" />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item label="TagName">
|
||||
{getFieldDecorator('actionTag', {
|
||||
rules: [{ required: true, message: '请输入tag' }],
|
||||
initialValue: isModify ? initialData.strategyActionList[0].actionTag : '',
|
||||
})(
|
||||
<Input placeholder="请输入tag" addonAfter={<span style={{ cursor: 'pointer' }} onClick={this.getActionTag}>自动生成</span>} />,
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item label="开启告警">
|
||||
{getFieldDecorator('status', {
|
||||
initialValue: isModify ? initialData.status : 1,
|
||||
})(
|
||||
<Radio.Group>
|
||||
<Radio value={1}>是</Radio>
|
||||
<Radio value={0}>否</Radio>
|
||||
</Radio.Group>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal >
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create({ name: 'alarm' })(Alarm);
|
||||
24
console/src/container/modal/cluster-network.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as React from 'react';
|
||||
import { Modal } from 'component/antd';
|
||||
import { modal } from 'store/modal';
|
||||
import { NetWorkFlow } from 'container/topic-detail/com';
|
||||
|
||||
export class ClusterDetail extends React.Component<any> {
|
||||
public render() {
|
||||
return (
|
||||
<Modal
|
||||
title="集群流量"
|
||||
style={{ top: 70 }}
|
||||
visible={true}
|
||||
onCancel={modal.close}
|
||||
maskClosable={false}
|
||||
width={1000}
|
||||
destroyOnClose={true}
|
||||
>
|
||||
<div className="k-row">
|
||||
<NetWorkFlow clusterId={modal.currentCluster.clusterId} />
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
173
console/src/container/modal/cluster.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import * as React from 'react';
|
||||
import { Modal, Form, Row, Input, Select, Radio, message } from 'component/antd';
|
||||
import { modal } from 'store/modal';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { INewCluster } from 'types/base-type';
|
||||
import { newCluster, modifyCluster } from 'lib/api';
|
||||
|
||||
const Option = Select.Option;
|
||||
|
||||
const topicFormItemLayout = {
|
||||
labelCol: {
|
||||
span: 8,
|
||||
},
|
||||
wrapperCol: {
|
||||
span: 12,
|
||||
},
|
||||
};
|
||||
|
||||
class Cluster extends React.Component<any> {
|
||||
public handleSubmit = (e: React.MouseEvent<any, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
const { clusterId } = modal.currentCluster;
|
||||
this.props.form.validateFieldsAndScroll((err: any, values: INewCluster) => {
|
||||
if (err) return;
|
||||
const commonFn = (fn: any) => {
|
||||
fn.then(() => {
|
||||
message.success(this.getTips());
|
||||
modal.close();
|
||||
cluster.getClusters();
|
||||
});
|
||||
};
|
||||
clusterId ?
|
||||
commonFn(modifyCluster(Object.assign(values, {clusterId}))) : commonFn(newCluster(values));
|
||||
});
|
||||
}
|
||||
|
||||
public getTips() {
|
||||
if (modal.currentCluster.clusterId) return '修改成功';
|
||||
return '添加成功';
|
||||
}
|
||||
|
||||
public getTitle() {
|
||||
if (modal.currentCluster.clusterId) return '修改集群';
|
||||
return '添加集群';
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
return (
|
||||
<Modal
|
||||
title={this.getTitle()}
|
||||
style={{ top: 70 }}
|
||||
visible={true}
|
||||
onCancel={modal.close}
|
||||
maskClosable={false}
|
||||
width={700}
|
||||
destroyOnClose={true}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
onOk={this.handleSubmit}
|
||||
>
|
||||
<Form {...topicFormItemLayout} >
|
||||
<Row>
|
||||
<Form.Item
|
||||
label="集群名称"
|
||||
>
|
||||
{getFieldDecorator('clusterName', {
|
||||
rules: [{ required: true, message: '请输入集群名称' }],
|
||||
initialValue: modal.currentCluster.clusterName,
|
||||
})(
|
||||
<Input
|
||||
placeholder="请输入集群名称"
|
||||
/>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item
|
||||
label="zookeeper地址"
|
||||
>
|
||||
{getFieldDecorator('zookeeper', {
|
||||
rules: [{ required: true, message: '请输入 zookeeper 地址' }],
|
||||
initialValue: modal.currentCluster.zookeeper,
|
||||
})(
|
||||
<Input.TextArea
|
||||
placeholder="请输入 zookeeper 地址"
|
||||
/>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item
|
||||
label="kafka版本"
|
||||
>
|
||||
{getFieldDecorator('kafkaVersion', {
|
||||
rules: [{ required: true, message: '请选择 kafka 版本' }],
|
||||
initialValue: modal.currentCluster.kafkaVersion,
|
||||
})(
|
||||
<Select placeholder="请选择 kafka 版本">
|
||||
{cluster.kafkaVersions.map((v) => {
|
||||
return <Option key={v} value={v}>{v}</Option>;
|
||||
})}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item
|
||||
label="集群访问地址"
|
||||
>
|
||||
{getFieldDecorator('bootstrapServers', {
|
||||
rules: [{ required: true, message: '请输入集群访问地址' }],
|
||||
initialValue: modal.currentCluster.bootstrapServers,
|
||||
})(
|
||||
<Input.TextArea placeholder="请输入集群访问地址" />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item
|
||||
label="SASL JAAS配置"
|
||||
>
|
||||
{getFieldDecorator('saslJaasConfig', {
|
||||
initialValue: modal.currentCluster.saslJaasConfig,
|
||||
})(
|
||||
<Input placeholder="请输入SASL JAAS配置" />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item
|
||||
label="SASL机制"
|
||||
>
|
||||
{getFieldDecorator('saslMechanism', {
|
||||
initialValue: modal.currentCluster.saslMechanism,
|
||||
})(
|
||||
<Input placeholder="请输入SASL机制" />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item
|
||||
label="安全协议"
|
||||
>
|
||||
{getFieldDecorator('securityProtocol', {
|
||||
initialValue: modal.currentCluster.securityProtocol,
|
||||
})(
|
||||
<Input placeholder="请输入安全协议" />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item
|
||||
label="是否开启告警"
|
||||
>
|
||||
{getFieldDecorator('alarmFlag', {
|
||||
rules: [{ required: true, message: '请选择是否开启告警' }],
|
||||
initialValue: modal.currentCluster.alarmFlag === undefined ? 1 : modal.currentCluster.alarmFlag,
|
||||
})(
|
||||
<Radio.Group>
|
||||
<Radio value={1}>是</Radio>
|
||||
<Radio value={0}>否</Radio>
|
||||
</Radio.Group>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create({ name: 'cluster' })(Cluster);
|
||||
42
console/src/container/modal/cosumer-topic.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import * as React from 'react';
|
||||
import { modal } from 'store/modal';
|
||||
import { consume } from 'store/consume';
|
||||
import Modal from 'antd/es/modal';
|
||||
import { observer } from 'mobx-react';
|
||||
import Table from 'antd/es/table';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'Topic 列表',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
},
|
||||
];
|
||||
|
||||
@observer
|
||||
export default class ConsumerTopic extends React.Component {
|
||||
public componentDidMount() {
|
||||
const { clusterId, consumerGroup, location } = modal.consumberGroup;
|
||||
consume.getConsumerTopic(clusterId, consumerGroup, location.toLowerCase());
|
||||
}
|
||||
public render() {
|
||||
return (
|
||||
<Modal
|
||||
title="消费的Topic列表"
|
||||
style={{ top: 200 }}
|
||||
visible={true}
|
||||
onCancel={modal.close}
|
||||
maskClosable={false}
|
||||
width={500}
|
||||
footer={null}
|
||||
>
|
||||
<Table
|
||||
rowKey="topicName"
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
dataSource={consume.consumerTopic}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
86
console/src/container/modal/index.less
Normal file
@@ -0,0 +1,86 @@
|
||||
.ml-5 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.order-detail {
|
||||
.ant-collapse-header {
|
||||
font-size: 15px;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
.b-container {
|
||||
text-align: right;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.2);
|
||||
// margin-top: 20px;
|
||||
.ant-col-14 {
|
||||
width: 100%;
|
||||
button {
|
||||
margin: 20px 10px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.u-password {
|
||||
width: 100%;
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.t-expand {
|
||||
.ant-input,
|
||||
.ant-select {
|
||||
width: 87%;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-collapse > .ant-collapse-item {
|
||||
border: none;
|
||||
}
|
||||
.ruleArea {
|
||||
border: 1px solid #eaeefb;
|
||||
border-radius: 4px;
|
||||
transition: 0.2s;
|
||||
width: 380px;
|
||||
padding: 15px 2px 0 17px;
|
||||
margin-bottom: 17px;
|
||||
&:hover {
|
||||
box-shadow: 0px 0px 2px 0px #afafaf;
|
||||
}
|
||||
.label {
|
||||
display: inline-block;
|
||||
width: 72px;
|
||||
margin-top: 10px;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.t-input {
|
||||
display: inline-block;
|
||||
width: 72%;
|
||||
}
|
||||
|
||||
.region_style {
|
||||
width: 100%;
|
||||
li {
|
||||
float: left;
|
||||
padding: 8px;
|
||||
border-radius: 9px;
|
||||
margin: 0px 5px 9px 0px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
&.success {
|
||||
background-color: rgba(47, 194, 91, 0.2);
|
||||
color: #2fc25b;
|
||||
}
|
||||
&.fail {
|
||||
background: rgba(255, 241, 240, 1);
|
||||
color: #f5222d;
|
||||
}
|
||||
&.pending {
|
||||
background: linear-gradient(
|
||||
to top right,
|
||||
rgba(47, 194, 91, 0.2) 60%,
|
||||
#fff 40%
|
||||
);
|
||||
color: #2fc25b;
|
||||
}
|
||||
}
|
||||
}
|
||||
47
console/src/container/modal/index.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { modal } from 'store/modal';
|
||||
import TopicNew from './topic-new';
|
||||
import TopicExpand from './topic-expand';
|
||||
import Alarm from './alarm-config';
|
||||
import ClusterNew from './cluster';
|
||||
import LeaderRebalance from './leader-rebalance';
|
||||
import Task from './task-new';
|
||||
import OrderApprove from './order-approve';
|
||||
import PartitionApprove from './partition-approve';
|
||||
import NewUser from './new-user';
|
||||
import Region from './region';
|
||||
import TopicCreate from './topic-create';
|
||||
import AdminExpand from './admin-expand';
|
||||
import ConsumerTopic from './cosumer-topic';
|
||||
|
||||
import './index.less';
|
||||
|
||||
@observer
|
||||
export default class AllModalInOne extends React.Component {
|
||||
public render() {
|
||||
if (!modal.id) return null;
|
||||
return (
|
||||
<>
|
||||
{modal.id === 'showNewTopic' ? <TopicNew /> : null}
|
||||
{modal.id === 'showNewCluster' ? <ClusterNew /> : null}
|
||||
{modal.id === 'showModifyCluster' ? <ClusterNew /> : null}
|
||||
{modal.id === 'showAdimTopic' ? <TopicCreate /> : null}
|
||||
{modal.id === 'showExpandTopic' ? <TopicExpand /> : null}
|
||||
{modal.id === 'showExpandAdmin' ? <AdminExpand /> : null}
|
||||
{modal.id === 'showAlarm' ? <Alarm /> : null}
|
||||
{modal.id === 'showAlarmModify' ? <Alarm /> : null}
|
||||
{modal.id === 'showRegion' ? <Region /> : null}
|
||||
{modal.id === 'showLeaderRebalance' ? <LeaderRebalance /> : null}
|
||||
{modal.id === 'showTask' ? <Task /> : null}
|
||||
{modal.id === 'showTaskDetail' ? <Task /> : null}
|
||||
{modal.id === 'showOrderApprove' ? <OrderApprove /> : null}
|
||||
{modal.id === 'showOrderDetail' ? <OrderApprove /> : null}
|
||||
{modal.id === 'showPartitionDetail' ? <PartitionApprove /> : null}
|
||||
{modal.id === 'showPartition' ? <PartitionApprove /> : null}
|
||||
{modal.id === 'showNewUser' ? <NewUser /> : null}
|
||||
{modal.id === 'showConsumerTopic' ? <ConsumerTopic /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
107
console/src/container/modal/leader-rebalance.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import * as React from 'react';
|
||||
import { Modal, Form, Input, Button, Table } from 'component/antd';
|
||||
import { modal } from 'store/modal';
|
||||
import Url from 'lib/url-parser';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { addRebalance } from 'lib/api';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
const topicFormItemLayout = {
|
||||
labelCol: {
|
||||
span: 5,
|
||||
},
|
||||
wrapperCol: {
|
||||
span: 16,
|
||||
},
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '集群',
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
},
|
||||
{
|
||||
title: 'Broker ID',
|
||||
dataIndex: 'brokerId',
|
||||
key: 'brokerId',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
},
|
||||
];
|
||||
|
||||
@observer
|
||||
class LeaderRebalance extends React.Component<any> {
|
||||
public clusterName: string;
|
||||
public clusterId: number;
|
||||
public brokerId: number;
|
||||
|
||||
public state = {
|
||||
loading: false,
|
||||
};
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterName = decodeURI(atob(url.search.clusterName));
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
}
|
||||
|
||||
public handleSubmit = (e: React.MouseEvent<any, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
this.setState({ loading: true });
|
||||
this.props.form.validateFieldsAndScroll((err: any, values: any) => {
|
||||
this.brokerId = Number(values.brokerId);
|
||||
addRebalance({ brokerId: this.brokerId, clusterId: this.clusterId, dimension: 0 }).then(() => {
|
||||
cluster.getRebalance(this.clusterId).then(() => {
|
||||
if (cluster.leaderStatus && cluster.leaderStatus !== 'RUNNING') {
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
const { brokerId, clusterName } = this;
|
||||
return (
|
||||
<Modal
|
||||
title="Leader Rebalance"
|
||||
style={{ top: 70 }}
|
||||
visible={true}
|
||||
footer={null}
|
||||
onCancel={modal.close}
|
||||
width={500}
|
||||
destroyOnClose={true}
|
||||
>
|
||||
<Form onSubmit={this.handleSubmit} {...topicFormItemLayout}>
|
||||
<Form.Item label="集群">
|
||||
<Input value={this.clusterName} disabled={true} />
|
||||
</Form.Item>
|
||||
<Form.Item label="brokerId">
|
||||
{getFieldDecorator('brokerId', {
|
||||
rules: [{ required: true, message: 'brokerId 不能为空' }],
|
||||
})(<Input placeholder="请输入 brokerId" />)}
|
||||
</Form.Item>
|
||||
<Form.Item label="">
|
||||
<Button style={{ left: 380 }} type="primary" htmlType="submit" loading={this.state.loading}>执行
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
{
|
||||
cluster.leaderStatus ? <Table
|
||||
rowKey="clusterName"
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
dataSource={[{ clusterName, brokerId, status: cluster.leaderStatus }]}
|
||||
/> : ''
|
||||
}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create({ name: 'LeaderRebalance' })(LeaderRebalance);
|
||||
110
console/src/container/modal/new-user.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import * as React from 'react';
|
||||
import { Modal, Form, Row, Input, notification, Select } from 'component/antd';
|
||||
import { modal } from 'store/modal';
|
||||
import { users } from 'store/users';
|
||||
import { addUser, modifyUser } from 'lib/api';
|
||||
import { getRandomPassword } from 'lib/utils';
|
||||
import { IUserDetail } from 'store/users';
|
||||
|
||||
import './index.less';
|
||||
|
||||
const topicFormItemLayout = {
|
||||
labelCol: {
|
||||
span: 7,
|
||||
},
|
||||
wrapperCol: {
|
||||
span: 11,
|
||||
},
|
||||
};
|
||||
|
||||
class NewUser extends React.Component<any> {
|
||||
|
||||
public handleSubmit = () => {
|
||||
this.props.form.validateFields((err: Error, values: any) => {
|
||||
if (err) return;
|
||||
this.getIsModify() ?
|
||||
modifyUser(values).then(() => {
|
||||
notification.success({ message: '修改成功' });
|
||||
modal.close();
|
||||
users.getUsers();
|
||||
}) :
|
||||
addUser(values).then(() => {
|
||||
notification.success({ message: '创建用户成功' });
|
||||
modal.close();
|
||||
users.getUsers();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public getPassword = () => {
|
||||
this.props.form.setFieldsValue({
|
||||
password: getRandomPassword(6),
|
||||
});
|
||||
}
|
||||
|
||||
public getIsModify = () => !!modal.userDetail;
|
||||
|
||||
public render() {
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
const initailData = modal.userDetail || {} as IUserDetail;
|
||||
const isModify = this.getIsModify();
|
||||
return (
|
||||
<Modal
|
||||
title={isModify ? '修改用户信息' : '添加用户'}
|
||||
style={{ top: 70 }}
|
||||
visible={true}
|
||||
onCancel={modal.close}
|
||||
maskClosable={false}
|
||||
width="680px"
|
||||
destroyOnClose={true}
|
||||
okText="提交"
|
||||
cancelText="取消"
|
||||
onOk={this.handleSubmit}
|
||||
>
|
||||
<Form {...topicFormItemLayout} >
|
||||
<Row>
|
||||
<Form.Item label="用户名" >
|
||||
{getFieldDecorator('username', {
|
||||
rules: [{ required: true, message: '请输入用户名' }],
|
||||
initialValue: initailData.username,
|
||||
})(
|
||||
<Input placeholder="请输入用户名" />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item label="密码">
|
||||
{getFieldDecorator('password', {
|
||||
rules: [
|
||||
{ required: !isModify, message: '请输入密码' },
|
||||
{ pattern: /^[a-zA-Z0-9]{6,10}$/, message: '请输入6-10位密码' }],
|
||||
nitialValue: initailData.password,
|
||||
})(
|
||||
<Input
|
||||
addonAfter={<span style={{ cursor: 'pointer' }} onClick={this.getPassword}>随机生成密码</span>}
|
||||
placeholder="请输入密码"
|
||||
/>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item label="角色">
|
||||
{getFieldDecorator('role', {
|
||||
rules: [{ required: true, message: '请选择角色' }],
|
||||
initialValue: initailData.role,
|
||||
})(
|
||||
<Select>
|
||||
{users.roleMap.map((r, k) => {
|
||||
return <Select.Option key={k} value={k}>{r}</Select.Option>;
|
||||
})}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create({ name: 'newUser' })(NewUser);
|
||||
250
console/src/container/modal/order-approve.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
import * as React from 'react';
|
||||
import { Modal, Form, Row, Col, Input, Select, Button, notification, Collapse, Switch } from 'component/antd';
|
||||
import { modal } from 'store/modal';
|
||||
import { addTopicApprove } from 'lib/api';
|
||||
import { region } from 'store/region';
|
||||
import './index.less';
|
||||
import { broker } from 'store/broker';
|
||||
import moment from 'moment';
|
||||
import { observer } from 'mobx-react';
|
||||
import { order } from 'store/order';
|
||||
import { IValueLabel } from 'types/base-type';
|
||||
|
||||
const topicFormItemLayout = {
|
||||
labelCol: {
|
||||
span: 7,
|
||||
},
|
||||
wrapperCol: {
|
||||
span: 16,
|
||||
},
|
||||
};
|
||||
|
||||
@observer
|
||||
class OrderApprove extends React.Component<any> {
|
||||
public state = {
|
||||
broker: false,
|
||||
orderStatus: 1,
|
||||
};
|
||||
|
||||
public handleSubmit = (e: React.MouseEvent<any, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
if (modal.id === 'showOrderDetail') { modal.close(); return false; }
|
||||
this.props.form.validateFields((err: Error, values: any) => {
|
||||
if (err) return;
|
||||
const { orderStatus } = this.state;
|
||||
const { orderId } = modal.orderDetail;
|
||||
const params = Object.assign(values, { orderStatus, orderId });
|
||||
addTopicApprove(params).then(() => {
|
||||
notification.success({ message: '审批成功' });
|
||||
order.getAdminOrder();
|
||||
modal.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public handleReject = (e: React.MouseEvent<any, MouseEvent>) => {
|
||||
const event = e.currentTarget;
|
||||
this.setState({orderStatus: 2}, () => {
|
||||
event.nextElementSibling.click();
|
||||
});
|
||||
}
|
||||
|
||||
public changeBroker = (broker: boolean) => {
|
||||
this.setState({ broker });
|
||||
}
|
||||
|
||||
public componentDidMount = () => {
|
||||
region.getRegions(modal.orderDetail.clusterId);
|
||||
broker.initBrokerOptions(modal.orderDetail.clusterId);
|
||||
}
|
||||
|
||||
public renderFooter = () => {
|
||||
return (
|
||||
<Form.Item>
|
||||
{
|
||||
modal.id !== 'showOrderDetail' ?
|
||||
<>
|
||||
<Button type="primary" onClick={this.handleReject}>驳回</Button>
|
||||
<Button type="primary" onClick={this.handleSubmit}>同意</Button>
|
||||
</>
|
||||
: null
|
||||
}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { orderDetail } = modal;
|
||||
const disabled = modal.id === 'showOrderDetail';
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
const { orderStatus } = this.state;
|
||||
return (
|
||||
<Modal
|
||||
title={disabled ? 'Topic申请工单详情' : 'Topic申请工单审批'}
|
||||
style={{ top: 20 }}
|
||||
visible={true}
|
||||
maskClosable={false}
|
||||
width="1000px"
|
||||
destroyOnClose={true}
|
||||
footer={this.renderFooter()}
|
||||
onCancel={modal.close}
|
||||
>
|
||||
<Collapse className="order-detail" bordered={false} destroyInactivePanel={true} defaultActiveKey={['1', '2']}>
|
||||
<Collapse.Panel header="工单信息" key="1">
|
||||
<Form {...topicFormItemLayout}>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item label="工单ID" >
|
||||
<Input value={orderDetail.orderId} disabled={true} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="集群名称" >
|
||||
<Input value={orderDetail.clusterName} disabled={true} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item label="Topic 名称" >
|
||||
<Input value={orderDetail.topicName} disabled={true} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="流量上限" >
|
||||
<Input value={orderDetail.peakBytesIn} disabled={true} addonAfter="MB/s" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item label="申请人" >
|
||||
<Input value={orderDetail.applicant} disabled={true} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="负责人" >
|
||||
<Input value={orderDetail.principals} disabled={true} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item label="工单处理状态" >
|
||||
<Input value={orderDetail.statusStr} disabled={true} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="工单申请时间" >
|
||||
<Input value={moment(orderDetail.gmtCreate).format('YYYY-MM-DD HH:mm:ss')} disabled={true} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item label="业务意义" >
|
||||
<Input.TextArea value={orderDetail.description} disabled={true} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Collapse.Panel>
|
||||
<Collapse.Panel header="Topic配置" key="2">
|
||||
<Form {...topicFormItemLayout} onSubmit={this.handleSubmit}>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item label="Broker类型">
|
||||
<Switch
|
||||
disabled={disabled}
|
||||
onChange={this.changeBroker}
|
||||
checkedChildren="region"
|
||||
unCheckedChildren="broker"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
{
|
||||
this.state.broker ?
|
||||
<Form.Item label="region">
|
||||
{getFieldDecorator('regionIdList', {
|
||||
rules: [{ required: orderStatus === 1, message: '不能为空' }],
|
||||
})(
|
||||
<Select disabled={disabled} mode="multiple">
|
||||
{
|
||||
region.data.map(r => {
|
||||
return <Select.Option key={r.regionId} value={r.regionId}>
|
||||
{r.regionName}
|
||||
</Select.Option>;
|
||||
})
|
||||
}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
:
|
||||
<Form.Item label="BrokerID">
|
||||
{getFieldDecorator('brokerIdList', {
|
||||
rules: [{ required: orderStatus === 1, message: '不能为空' }],
|
||||
initialValue: (orderDetail.brokers && orderDetail.brokers.split(',')) ||
|
||||
(orderDetail.regions && orderDetail.regions.split(',')) || [],
|
||||
})(
|
||||
<Select mode="multiple" optionLabelProp="title" disabled={disabled}>
|
||||
{
|
||||
broker.BrokerOptions.map((t: IValueLabel) => {
|
||||
return <Select.Option value={t.value} title={t.value + ''} key={t.value} >
|
||||
<span aria-label={t.value}> {t.label} </span>
|
||||
</Select.Option>;
|
||||
})
|
||||
}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
}
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="副本数" >
|
||||
{getFieldDecorator('replicaNum', {
|
||||
rules: [{ required: orderStatus === 1, message: '不能为空' }],
|
||||
initialValue: orderDetail.replicaNum || '',
|
||||
})(<Input placeholder="请输入副本数" disabled={disabled} />)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item label="数据保留时间" >
|
||||
{getFieldDecorator('retentionTime', {
|
||||
rules: [{ required: orderStatus === 1, message: '不能为空' }],
|
||||
initialValue: orderDetail && orderDetail.retentionTime / 3600000 || '',
|
||||
})(<Input addonAfter="小时" placeholder="请输入数据保留时间" disabled={disabled} />)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="分区数" >
|
||||
{getFieldDecorator('partitionNum', {
|
||||
rules: [{ required: orderStatus === 1, message: '不能为空' }],
|
||||
initialValue: orderDetail.partitionNum,
|
||||
})(<Input placeholder="请输入partition数" disabled={disabled} />)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item label="审批意见" >
|
||||
{getFieldDecorator('approvalOpinions', {
|
||||
rules: [{ required: orderStatus === 2, message: '审批意见不能为空' }],
|
||||
initialValue: orderDetail.approvalOpinions || undefined,
|
||||
})(<Input.TextArea style={{ height: 80 }} disabled={disabled} />)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create({ name: 'orderApprove' })(OrderApprove);
|
||||
209
console/src/container/modal/partition-approve.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import * as React from 'react';
|
||||
import { Modal, Form, Row, Col, Input, Select, Button, notification, Switch } from 'component/antd';
|
||||
import { modal } from 'store/modal';
|
||||
import { addAdminPartition } from 'lib/api';
|
||||
import { broker } from 'store/broker';
|
||||
import { observer } from 'mobx-react';
|
||||
import moment from 'moment';
|
||||
import { getCookie } from 'lib/utils';
|
||||
import { order } from 'store/order';
|
||||
import { IValueLabel } from 'types/base-type';
|
||||
import './index.less';
|
||||
|
||||
const topicFormItemLayout = {
|
||||
labelCol: {
|
||||
span: 8,
|
||||
},
|
||||
wrapperCol: {
|
||||
span: 14,
|
||||
},
|
||||
};
|
||||
|
||||
@observer
|
||||
class PartitionApprove extends React.Component<any> {
|
||||
public state = {
|
||||
orderStatus: 1,
|
||||
};
|
||||
public componentDidMount() {
|
||||
const { clusterId } = modal.orderDetail;
|
||||
broker.initBrokerOptions(clusterId);
|
||||
}
|
||||
|
||||
public handleSubmit = (e: React.MouseEvent<any, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
if (modal.id === 'showPartitionDetail') { modal.close(); return false; }
|
||||
this.props.form.validateFields((err: Error, values: any) => {
|
||||
if (err) return;
|
||||
const { orderStatus } = this.state;
|
||||
const { orderId } = modal.orderDetail;
|
||||
const { partitionNum: num } = values;
|
||||
values.partitionNum = +num;
|
||||
const params = Object.assign(values, { orderStatus, orderId });
|
||||
addAdminPartition(params).then(() => {
|
||||
notification.success({ message: '审批成功' });
|
||||
order.getAdminOrder();
|
||||
modal.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public handleReject = (e: React.MouseEvent<any, MouseEvent>) => {
|
||||
const event = e.currentTarget;
|
||||
this.setState({ orderStatus: 2 }, () => {
|
||||
event.nextElementSibling.click();
|
||||
});
|
||||
}
|
||||
|
||||
public renderFooter = () => {
|
||||
return (
|
||||
<Form.Item>
|
||||
{
|
||||
modal.id !== 'showPartitionDetail' ?
|
||||
<>
|
||||
<Button type="primary" onClick={this.handleReject}>驳回</Button>
|
||||
<Button type="primary" onClick={this.handleSubmit}>通过</Button>
|
||||
</>
|
||||
: null
|
||||
}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { orderDetail } = modal;
|
||||
const disabled = modal.id === 'showPartitionDetail';
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
const { orderStatus } = this.state;
|
||||
return (
|
||||
<Modal
|
||||
title={disabled ? 'Topic扩容申请-工单详情' : 'Topic扩容工单审批'}
|
||||
style={{ top: 20 }}
|
||||
visible={true}
|
||||
onCancel={modal.close}
|
||||
maskClosable={false}
|
||||
width="900px"
|
||||
footer={this.renderFooter()}
|
||||
destroyOnClose={true}
|
||||
>
|
||||
<Form {...topicFormItemLayout}>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item label="工单ID" >
|
||||
<Input value={orderDetail.orderId} disabled={true} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="集群名称" >
|
||||
<Input value={orderDetail.clusterName} disabled={true} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item label="Topic名称" >
|
||||
<Input value={orderDetail.topicName} disabled={true} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="现有分区" >
|
||||
<Input value={orderDetail.partitionNum} disabled={true} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item label="预估峰值(MB/s)" >
|
||||
<Input value={orderDetail.predictBytesIn} disabled={true} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="申请人" >
|
||||
<Input value={orderDetail.applicant} disabled={true} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item label="实际峰值(MB/s)" >
|
||||
<Input value={orderDetail.realBytesIn} disabled={true} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="审批人" >
|
||||
<Input value={getCookie('username')} disabled={true} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item label="申请时间" >
|
||||
<Input value={moment(orderDetail.gmtCreate).format('YYYY-MM-DD HH:mm:ss')} disabled={true} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="备注信息" >
|
||||
<Input value={orderDetail.description} disabled={true} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item label="Region列表" >
|
||||
<Input value={orderDetail.regionNameList} disabled={true} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="RegionBroker列表" >
|
||||
<Input value={orderDetail.regionBrokerIdList} disabled={true} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item label="当前Broker列表" >
|
||||
<Input value={orderDetail.brokerIdList} disabled={true} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="新增分区数" >
|
||||
{getFieldDecorator('partitionNum', {
|
||||
rules: [{ required: orderStatus === 1, message: '不能为空' }],
|
||||
})(<Input placeholder="请输入partition数" disabled={disabled} />)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item label="当前Broker列表" labelCol={{ span: 4 }} wrapperCol={{ span: 7 }}>
|
||||
{getFieldDecorator('brokerIdList', {
|
||||
rules: [{ required: orderStatus === 1, message: '不能为空' }],
|
||||
initialValue: orderDetail.brokerIdList || [],
|
||||
})(
|
||||
<Select mode="multiple" optionLabelProp="title" disabled={disabled} >
|
||||
{
|
||||
broker.BrokerOptions.map((t: IValueLabel) => {
|
||||
return <Select.Option value={t.value} title={t.value + ''} key={t.value} >
|
||||
<span aria-label={t.value}> {t.label} </span>
|
||||
</Select.Option>;
|
||||
})
|
||||
}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row style={{ paddingTop: '20px', borderTop: '1px dashed rgba(0,0,0,.1)' }}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="审批意见">
|
||||
{getFieldDecorator('approvalOpinions', {
|
||||
rules: [{ required: orderStatus === 2, message: '审批意见不能为空' }],
|
||||
initialValue: orderDetail.approvalOpinions || undefined,
|
||||
})(<Input.TextArea style={{ height: 50 }} disabled={disabled} />)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create({ name: 'partitionApprove' })(PartitionApprove);
|
||||
171
console/src/container/modal/region.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import { Modal, Form, Row, Select, Input, message } from 'component/antd';
|
||||
import { IRegionData, statusMap, region } from 'store/region';
|
||||
import { cluster } from 'store/cluster';
|
||||
import urlQuery from 'store/url-query';
|
||||
import { observer } from 'mobx-react';
|
||||
import { modal } from 'store';
|
||||
import React from 'react';
|
||||
import { addRegion, modifyRegion } from 'lib/api';
|
||||
import { broker } from 'store/broker';
|
||||
import { IValueLabel } from 'types/base-type';
|
||||
|
||||
const regionFormItemLayout = {
|
||||
labelCol: {
|
||||
span: 7,
|
||||
},
|
||||
wrapperCol: {
|
||||
span: 11,
|
||||
},
|
||||
};
|
||||
|
||||
@observer
|
||||
export class Region extends React.Component<any> {
|
||||
public componentDidMount() {
|
||||
cluster.getClusters();
|
||||
broker.initBrokerOptions(urlQuery.clusterId);
|
||||
}
|
||||
|
||||
public handleSubmit = (e: React.MouseEvent<any, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
this.props.form.validateFieldsAndScroll((err: any, values: any) => {
|
||||
if (err) return;
|
||||
const commonFn = (fn: any) => {
|
||||
fn.then(() => {
|
||||
message.success(this.getTips());
|
||||
region.getRegions(urlQuery.clusterId);
|
||||
modal.close();
|
||||
});
|
||||
};
|
||||
modal.regionData ?
|
||||
commonFn(modifyRegion(Object.assign(values, { regionId: modal.regionData.regionId }))) : commonFn(addRegion(values));
|
||||
});
|
||||
}
|
||||
|
||||
public getTips() {
|
||||
if (modal.regionData) return '修改成功';
|
||||
return '添加成功';
|
||||
}
|
||||
|
||||
public getTitle() {
|
||||
if (modal.regionData) return '修改Region';
|
||||
return '添加Region';
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
let title = '新增Region';
|
||||
if (modal.regionData) title = '更新Region';
|
||||
const regionData = modal.regionData || {} as IRegionData;
|
||||
return (
|
||||
<Modal
|
||||
title={title}
|
||||
style={{ top: 70 }}
|
||||
visible={true}
|
||||
onCancel={modal.close}
|
||||
maskClosable={false}
|
||||
width={700}
|
||||
destroyOnClose={true}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
onOk={this.handleSubmit}
|
||||
// confirmLoading={loading}
|
||||
>
|
||||
<Form {...regionFormItemLayout} >
|
||||
<Row>
|
||||
<Form.Item
|
||||
label="Region名称"
|
||||
>
|
||||
{getFieldDecorator('regionName', {
|
||||
rules: [{ required: true, message: '请输入Region名称' }],
|
||||
initialValue: regionData.regionName,
|
||||
})(
|
||||
<Input placeholder="请输入Region名称" />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item
|
||||
label="集群"
|
||||
>
|
||||
{getFieldDecorator('clusterId', {
|
||||
rules: [{ required: true, message: '请选择集群' }],
|
||||
initialValue: urlQuery.clusterId,
|
||||
})(
|
||||
<Select disabled={true}>
|
||||
{
|
||||
cluster.data.slice(1).map(c => {
|
||||
return <Select.Option value={c.clusterId} key={c.clusterId}>{c.clusterName}</Select.Option>;
|
||||
})
|
||||
}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item
|
||||
label="BrokerList"
|
||||
>
|
||||
{getFieldDecorator('brokerIdList', {
|
||||
rules: [{ required: true, message: '请输入brokerList' }],
|
||||
initialValue: regionData.brokerIdList,
|
||||
})(
|
||||
<Select mode="multiple" optionLabelProp="title">
|
||||
{
|
||||
broker.BrokerOptions.map((t: IValueLabel) => {
|
||||
return <Select.Option value={t.value} title={t.value + ''} key={t.value} >
|
||||
<span aria-label={t.value}> {t.label} </span>
|
||||
</Select.Option>;
|
||||
})
|
||||
}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item
|
||||
label="状态"
|
||||
>
|
||||
{getFieldDecorator('status', {
|
||||
initialValue: regionData.status || 0,
|
||||
})(
|
||||
<Select>
|
||||
{statusMap.map((ele, index) => {
|
||||
return <Select.Option key={index} value={index - 1}>{ele}</Select.Option>;
|
||||
})}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item
|
||||
label="等级"
|
||||
>
|
||||
{getFieldDecorator('level', {
|
||||
initialValue: regionData.level || 0,
|
||||
})(
|
||||
<Select>
|
||||
<Select.Option value={0}>普通</Select.Option>
|
||||
<Select.Option value={1}>重要</Select.Option>
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item
|
||||
label="备注"
|
||||
>
|
||||
{getFieldDecorator('description', {
|
||||
rules: [{ required: false }],
|
||||
initialValue: regionData.description,
|
||||
})(
|
||||
<Input.TextArea />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create({ name: 'Region' })(Region);
|
||||
78
console/src/container/modal/reset-offset.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import * as React from 'react';
|
||||
import { Modal, Form, Row, Input, Select, DatePicker, Col, Button } from 'component/antd';
|
||||
import { modal } from 'store/modal';
|
||||
|
||||
const Option = Select.Option;
|
||||
|
||||
const topicFormItemLayout = {
|
||||
labelCol: {
|
||||
span: 8,
|
||||
},
|
||||
wrapperCol: {
|
||||
span: 16,
|
||||
},
|
||||
};
|
||||
|
||||
class ResetOffset extends React.Component<any> {
|
||||
public handleSubmit = () => {
|
||||
debugger
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
return (
|
||||
<Modal
|
||||
title="重置"
|
||||
style={{ top: 70 }}
|
||||
visible={true}
|
||||
onCancel={modal.close}
|
||||
maskClosable={false}
|
||||
width={800}
|
||||
destroyOnClose={true}
|
||||
>
|
||||
<Form {...topicFormItemLayout} >
|
||||
<Row>
|
||||
<Col span={20}>
|
||||
<Form.Item
|
||||
label="重置到指定时间"
|
||||
>
|
||||
<DatePicker showTime={true} style={{ width: '100%' }}/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{/* <Col span={3} offset={1}>
|
||||
<Form.Item>
|
||||
<Button type="primary" size="small">确定重置</Button>
|
||||
</Form.Item>
|
||||
</Col> */}
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={20}>
|
||||
<Form.Item
|
||||
label="重置指定分区"
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Row>
|
||||
<Col span={8}><Select placeholder="请选择分区 id"/></Col>
|
||||
<Col span={16}><Input placeholder="分区 offset"/></Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={8}><Select placeholder="请选择分区 id"/></Col>
|
||||
<Col span={16}><Input placeholder="分区 offset"/></Col>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
wrapperCol={{
|
||||
offset: 8,
|
||||
}}
|
||||
>
|
||||
<Button size="small" style={{ width: '100%' }}>新增</Button>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create({ name: 'topic' })(ResetOffset);
|
||||
200
console/src/container/modal/task-new.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import * as React from 'react';
|
||||
import { Modal, Form, Row, Input, Select, notification, InputNumber, Switch, Icon, Tooltip, Col } from 'component/antd';
|
||||
import { modal } from 'store/modal';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { executeTask, modifyTask } from 'lib/api';
|
||||
import { operation, ITask } from 'store/operation';
|
||||
import { observer } from 'mobx-react';
|
||||
import { IValueLabel } from 'types/base-type';
|
||||
import { topic } from 'store/topic';
|
||||
import { broker } from 'store/broker';
|
||||
|
||||
const topicFormItemLayout = {
|
||||
labelCol: {
|
||||
span: 7,
|
||||
},
|
||||
wrapperCol: {
|
||||
span: 11,
|
||||
},
|
||||
};
|
||||
|
||||
@observer
|
||||
class Task extends React.Component<any> {
|
||||
public handleSubmit = () => {
|
||||
this.props.form.validateFields((err: Error, values: any) => {
|
||||
if (err) return;
|
||||
const { throttle, partitionIdList } = values;
|
||||
values.throttle = throttle * 1024 * 1024;
|
||||
values.partitionIdList = typeof partitionIdList === 'string' ? values.partitionIdList.split(',') : [];
|
||||
if (this.getDisabled()) {
|
||||
const { taskId } = operation.taskDetail;
|
||||
modifyTask({ throttle, taskId, action: 'modify' }).then(() => {
|
||||
notification.success({ message: '修改成功' });
|
||||
operation.getTask();
|
||||
modal.close();
|
||||
});
|
||||
} else {
|
||||
executeTask(values).then(() => {
|
||||
notification.success({ message: '迁移任务创建成功' });
|
||||
operation.getTask();
|
||||
modal.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getDisabled = () => !!operation.taskDetail;
|
||||
|
||||
public initSelection = (value: number) => {
|
||||
topic.getTopicList(value);
|
||||
broker.initBrokerOptions(value);
|
||||
}
|
||||
|
||||
public filterSelection = (input: string, option: any) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
|
||||
public test = (): any => {
|
||||
if (modal.id === 'showTaskDetail') return null;
|
||||
}
|
||||
public render() {
|
||||
const disabled = this.getDisabled();
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
const initialData = operation.taskDetail || {} as ITask;
|
||||
const isModify = modal.id === 'showTaskDetail';
|
||||
return (
|
||||
<Modal
|
||||
title="Topic 迁移"
|
||||
style={{ top: 70 }}
|
||||
visible={true}
|
||||
onCancel={modal.close}
|
||||
maskClosable={false}
|
||||
width="680px"
|
||||
destroyOnClose={true}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
onOk={this.handleSubmit}
|
||||
footer={this.test()}
|
||||
>
|
||||
<Form {...topicFormItemLayout} >
|
||||
<Row>
|
||||
<Form.Item label="集群名称">
|
||||
{getFieldDecorator('clusterId', {
|
||||
rules: [{ required: true, message: '请选择集群' }],
|
||||
initialValue: initialData.clusterId,
|
||||
})(
|
||||
<Select onChange={this.initSelection} disabled={disabled} placeholder="请选择集群">
|
||||
{
|
||||
cluster.data.slice(1).map(c => {
|
||||
return <Select.Option value={c.clusterId} key={c.clusterId}>{c.clusterName}</Select.Option>;
|
||||
})
|
||||
}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item label="Topic名称">
|
||||
{getFieldDecorator('topicName', {
|
||||
rules: [{ required: true, message: '请选择Topic' }],
|
||||
initialValue: initialData.topicName,
|
||||
})(
|
||||
<Select filterOption={this.filterSelection} showSearch={true} disabled={disabled} placeholder="请选择Topic">
|
||||
{
|
||||
topic.topicNameList.map(t => {
|
||||
return <Select.Option value={t} key={t}>{t}</Select.Option>;
|
||||
})
|
||||
}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item label="分区ID列表">
|
||||
{getFieldDecorator('partitionIdList', {
|
||||
initialValue: initialData.partitionIdList || [],
|
||||
})(
|
||||
<Input disabled={isModify} />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item
|
||||
label={<span>
|
||||
流量上限
|
||||
<Tooltip title="仅需填写迁移流量、副本同步流量自动涵盖">
|
||||
<Icon type="question-circle-o" />
|
||||
</Tooltip>
|
||||
</span>}
|
||||
>
|
||||
{getFieldDecorator('throttle', {
|
||||
rules: [{ required: true, message: '请输入限流值' }],
|
||||
initialValue: +(initialData.throttle / (1024 * 1024)).toFixed(2) || 1,
|
||||
})(
|
||||
<InputNumber min={0} disabled={isModify} />,
|
||||
)}
|
||||
<span className="ml-5">MB/s</span>
|
||||
</Form.Item>
|
||||
</Row>
|
||||
{
|
||||
isModify ? (
|
||||
<Row>
|
||||
<Col span={7} style={{ textAlign: 'right', color: 'rgba(0, 0, 0, 0.85)', margin: '5px 5px 0 0' }}>
|
||||
分区列表
|
||||
<Tooltip title={<span>无色:待迁移<br />半绿:迁移中<br />全绿:迁移成功<br />红色:迁移失败</span>}>
|
||||
<Icon type="question-circle-o" />
|
||||
</Tooltip>:
|
||||
</Col>
|
||||
<Col span={14}>
|
||||
<ul className="region_style">
|
||||
{initialData.regionList && initialData.regionList.map((i, index) => {
|
||||
return <li
|
||||
key={index}
|
||||
className={!initialData.migrationStatus[i[0]] ? '' :
|
||||
initialData.migrationStatus[i[0]] === 2 ? 'success' :
|
||||
initialData.migrationStatus[i[0]] === 3 ? 'fail' : 'pending'}
|
||||
>分区{i[0]}: <span>{i[1].join(',')}</span>
|
||||
</li>;
|
||||
})}
|
||||
</ul>
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
!disabled ? (
|
||||
<>
|
||||
<Row>
|
||||
<Form.Item label="目标Broker列表">
|
||||
{getFieldDecorator('brokerIdList', {
|
||||
rules: [{ required: true, message: '请输入broker' }],
|
||||
})(
|
||||
<Select mode="multiple" optionLabelProp="title" placeholder="请选择broker">
|
||||
{
|
||||
broker.BrokerOptions.map((t: IValueLabel) => {
|
||||
return <Select.Option value={t.value} title={t.value + ''} key={t.value} >
|
||||
<span aria-label={t.value}> {t.label} </span>
|
||||
</Select.Option>;
|
||||
})
|
||||
}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item label="说明">
|
||||
{getFieldDecorator('description', {
|
||||
rules: [{ required: true, message: '请输入迁移说明' }],
|
||||
})(
|
||||
<Input.TextArea style={{ height: 100 }} placeholder="请输入迁移说明" />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create({ name: 'task' })(Task);
|
||||
220
console/src/container/modal/topic-create.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
import * as React from 'react';
|
||||
import { Modal, Form, Row, Input, Select, InputNumber, notification, Switch } from 'component/antd';
|
||||
import { modal } from 'store/modal';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { adminCreateTopic, modifyTopic } from 'lib/api';
|
||||
import { ITopic, IValueLabel } from 'types/base-type';
|
||||
import { operation } from 'store/operation';
|
||||
import { broker } from 'store/broker';
|
||||
import urlQuery from 'store/url-query';
|
||||
import { observer } from 'mobx-react';
|
||||
import { topic } from 'store/topic';
|
||||
import { getCookie } from 'lib/utils';
|
||||
|
||||
const topicFormItemLayout = {
|
||||
labelCol: {
|
||||
span: 7,
|
||||
},
|
||||
wrapperCol: {
|
||||
span: 11,
|
||||
},
|
||||
};
|
||||
|
||||
const bussDesc = `概要描述Topic的数据源, Topic数据的生产者/消费者, Topic的申请原因及备注信息等。`;
|
||||
|
||||
@observer
|
||||
class Topic extends React.Component<any> {
|
||||
public state = {
|
||||
loading: false,
|
||||
targetType: false,
|
||||
};
|
||||
|
||||
public getDisabled = () => !!modal.topicData;
|
||||
|
||||
public handleTarget = (targetType: boolean) => {
|
||||
this.setState({ targetType });
|
||||
}
|
||||
|
||||
public handleSubmit = () => {
|
||||
this.props.form.validateFields((err: Error, values: any) => {
|
||||
if (err) return;
|
||||
const { principalList, retentionTime } = values;
|
||||
values.principalList = typeof principalList === 'string' ? principalList.split(',') : values.principalList;
|
||||
values.retentionTime = retentionTime * 3600000;
|
||||
this.setState({ loading: true });
|
||||
const fn = this.getDisabled() ? modifyTopic : adminCreateTopic;
|
||||
fn(values).then(data => {
|
||||
topic.getAdminTopics(urlQuery.clusterId);
|
||||
notification.success(this.getDisabled() ? { message: '修改Topic成功' } : { message: 'Topic创建成功' });
|
||||
modal.close();
|
||||
}, (err) => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
});
|
||||
}
|
||||
public componentDidMount() {
|
||||
operation.initRegionOptions(urlQuery.clusterId);
|
||||
broker.initBrokerOptions(urlQuery.clusterId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
const { loading } = this.state;
|
||||
const initialData = modal.topicData || {} as ITopic;
|
||||
const disabled = this.getDisabled();
|
||||
return (
|
||||
<Modal
|
||||
title={disabled ? '修改Topic' : 'Topic创建'}
|
||||
style={{ top: 70 }}
|
||||
visible={true}
|
||||
onCancel={modal.close}
|
||||
maskClosable={false}
|
||||
width="680px"
|
||||
destroyOnClose={true}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
onOk={this.handleSubmit}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form {...topicFormItemLayout}>
|
||||
<Row>
|
||||
<Form.Item label="集群名称">
|
||||
{getFieldDecorator('clusterId', {
|
||||
rules: [{ required: true, message: '请选择集群' }],
|
||||
initialValue: +urlQuery.clusterId,
|
||||
})(
|
||||
<Select disabled={true} onChange={operation.initRegionOptions}>
|
||||
{
|
||||
cluster.data.slice(1).map(c => {
|
||||
return <Select.Option value={c.clusterId} key={c.clusterId}>{c.clusterName}</Select.Option>;
|
||||
})
|
||||
}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item label="Topic名称">
|
||||
{getFieldDecorator('topicName', {
|
||||
rules: [{ required: true, message: '请输入Topic 名称' },
|
||||
{ pattern: /^[a-zA-Z0-9_-]{1,64}$/, message: '格式不正确' }],
|
||||
initialValue: initialData.topicName,
|
||||
})(
|
||||
<Input placeholder="支持字母、数字、下划线、中划线,6-64个字符" disabled={disabled} />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item label="负责人">
|
||||
{getFieldDecorator('principalList', {
|
||||
rules: [{ required: true, message: '请输入负责人' }],
|
||||
initialValue: disabled ? initialData.principalList || ' ' : getCookie('username'),
|
||||
})(
|
||||
<Input placeholder="多个负责人请用逗号隔开" />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item label="保存时间">
|
||||
{getFieldDecorator('retentionTime', {
|
||||
rules: [{ required: true, message: '请输入保存时间' }],
|
||||
initialValue: disabled ? (initialData.retentionTime / 3600000).toFixed(0) : 24,
|
||||
})(
|
||||
<InputNumber min={0} />,
|
||||
)}
|
||||
<span className="ml-5">小时</span>
|
||||
</Form.Item>
|
||||
</Row>
|
||||
{!disabled ?
|
||||
<>
|
||||
<Row>
|
||||
<Form.Item label="Broker类型">
|
||||
<Switch
|
||||
checkedChildren="region"
|
||||
unCheckedChildren="broker"
|
||||
onChange={this.handleTarget}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
{
|
||||
this.state.targetType ? (
|
||||
<Form.Item label="Target Region">
|
||||
{getFieldDecorator('regionIdList', {
|
||||
rules: [{ required: true, message: '请输入选择' }],
|
||||
})(
|
||||
<Select mode="multiple">
|
||||
{
|
||||
operation.RegionOptions.map(t => {
|
||||
return <Select.Option value={t.value} key={t.value}>{t.label}</Select.Option>;
|
||||
})
|
||||
}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
) : (
|
||||
<Form.Item label="目标BrokerID">
|
||||
{getFieldDecorator('brokerIdList', {
|
||||
rules: [{ required: true, message: '请输入broker' }],
|
||||
})(
|
||||
<Select mode="multiple" optionLabelProp="title">
|
||||
{
|
||||
broker.BrokerOptions.map((t: IValueLabel) => {
|
||||
return <Select.Option value={t.value} title={t.value + ''} key={t.value} >
|
||||
<span aria-label={t.value}> {t.label} </span>
|
||||
</Select.Option>;
|
||||
})
|
||||
}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
</Row>
|
||||
</> : null
|
||||
}
|
||||
<Row>
|
||||
<Form.Item label="分区数">
|
||||
{getFieldDecorator('partitionNum', {
|
||||
rules: [{ required: true, message: '请输入partition数' }],
|
||||
initialValue: topic.topicDetail && topic.topicDetail.partitionNum,
|
||||
})(
|
||||
<Input placeholder="请输入partition数" disabled={disabled} />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item label="副本数">
|
||||
{getFieldDecorator('replicaNum', {
|
||||
rules: [{ required: true, message: '请输入副本数' }],
|
||||
initialValue: topic.topicDetail && topic.topicDetail.replicaNum,
|
||||
})(
|
||||
<Input placeholder="请输入副本数" disabled={disabled} />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item label="自定义属性" >
|
||||
{getFieldDecorator('properties', {
|
||||
initialValue: initialData.properties,
|
||||
})(
|
||||
<Input.TextArea placeholder="请严格按照JSON格式来填写:{'key':'value'}" />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item label="业务说明" >
|
||||
{getFieldDecorator('description', {
|
||||
initialValue: initialData.description || disabled && ' ',
|
||||
})(
|
||||
<Input.TextArea placeholder={bussDesc} style={{ height: 100 }} />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create({ name: 'topic' })(Topic);
|
||||
138
console/src/container/modal/topic-expand.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import * as React from 'react';
|
||||
import { Modal, Form, Row, Input, Select, notification } from 'component/antd';
|
||||
import { modal } from 'store/modal';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { addPartitionApprove } from 'lib/api';
|
||||
import { IBaseOrder } from 'types/base-type';
|
||||
import { topic } from 'store/topic';
|
||||
import { observer } from 'mobx-react';
|
||||
import { order } from 'store/order';
|
||||
|
||||
const topicFormItemLayout = {
|
||||
labelCol: {
|
||||
span: 7,
|
||||
},
|
||||
wrapperCol: {
|
||||
span: 11,
|
||||
},
|
||||
};
|
||||
@observer
|
||||
class Topic extends React.Component<any> {
|
||||
|
||||
public handleSubmit = () => {
|
||||
if (!!modal.topicDetail) modal.close();
|
||||
this.props.form.validateFields((err: Error, values: any) => {
|
||||
if (err) return;
|
||||
if (modal.topicDetail) values.clusterId = modal.topicDetail.clusterId || values.clusterId;
|
||||
addPartitionApprove(values).then(() => {
|
||||
notification.success({ message: '申请成功' });
|
||||
order.getOrder();
|
||||
modal.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public filterSelection = (input: string, option: any) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
|
||||
public initSelection = (value: number) => {
|
||||
topic.getTopicList(value);
|
||||
cluster.getClusters();
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const value = !!modal.topicDetail ? modal.topicDetail.clusterId : cluster.data[1].clusterId;
|
||||
this.initSelection(value);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
const initialData = modal.topicDetail || {} as IBaseOrder;
|
||||
const disabled = !!modal.topicDetail;
|
||||
const isDetail = location.pathname.includes('topic_detai');
|
||||
return (
|
||||
<Modal
|
||||
title={isDetail ? '扩容申请' : disabled ? 'Topic 扩容申请详情' : 'Topic 扩容申请'}
|
||||
style={{ top: 70 }}
|
||||
visible={true}
|
||||
onCancel={modal.close}
|
||||
maskClosable={false}
|
||||
width="680px"
|
||||
destroyOnClose={true}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
onOk={this.handleSubmit}
|
||||
>
|
||||
<Form {...topicFormItemLayout} className="t-expand" >
|
||||
<Row>
|
||||
<Form.Item label="集群名称">
|
||||
{getFieldDecorator('clusterId', {
|
||||
rules: [{ required: true, message: '请选择集群' }],
|
||||
initialValue: +initialData.clusterId || +cluster.data[1].clusterId,
|
||||
})(
|
||||
<Select onChange={this.initSelection} disabled={disabled}>
|
||||
{
|
||||
cluster.data.slice(1).map(c => {
|
||||
return <Select.Option value={c.clusterId} key={c.clusterId}>{c.clusterName}</Select.Option>;
|
||||
})
|
||||
}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item label="Topic名称">
|
||||
{getFieldDecorator('topicName', {
|
||||
rules: [{ required: true, message: '请选择Topic' }],
|
||||
initialValue: initialData.topicName,
|
||||
})(
|
||||
<Select disabled={disabled} filterOption={this.filterSelection} showSearch={true}>
|
||||
{
|
||||
topic.topicNameList.map(t => {
|
||||
return <Select.Option value={t} key={t}>{t}</Select.Option>;
|
||||
})
|
||||
}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Form.Item
|
||||
label="预估峰值流量"
|
||||
>
|
||||
{getFieldDecorator('predictBytesIn', {
|
||||
rules: [{ required: true, message: '请输入预估峰值流量' }],
|
||||
initialValue: initialData.predictBytesIn,
|
||||
})(
|
||||
<Input disabled={!isDetail && disabled} />,
|
||||
)}
|
||||
<span className="ml-5">MB/s</span>
|
||||
</Form.Item>
|
||||
{
|
||||
!isDetail && disabled ? (
|
||||
<>
|
||||
<Form.Item label="审批人">
|
||||
<Input disabled={disabled} value={initialData.approver} />
|
||||
</Form.Item>
|
||||
<Form.Item label="审批人意见">
|
||||
<Input disabled={disabled} value={initialData.approvalOpinions} />
|
||||
</Form.Item>
|
||||
</>) : null
|
||||
}
|
||||
<Row>
|
||||
<Form.Item
|
||||
label="备注说明"
|
||||
>
|
||||
{getFieldDecorator('description', {
|
||||
rules: [{ required: true, message: '请输入申请原因' }],
|
||||
initialValue: initialData.description,
|
||||
})(
|
||||
<Input.TextArea disabled={!isDetail && disabled} style={{ height: 100 }} />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create({ name: 'topic' })(Topic);
|
||||
142
console/src/container/modal/topic-new.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import * as React from 'react';
|
||||
import { Modal, Form, Row, Input, Select, InputNumber, notification } from 'component/antd';
|
||||
import { modal } from 'store/modal';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { createTopic } from 'lib/api';
|
||||
import { ITopic } from 'types/base-type';
|
||||
import { operation } from 'store/operation';
|
||||
import urlQuery from 'store/url-query';
|
||||
import { getCookie } from 'lib/utils';
|
||||
|
||||
const topicFormItemLayout = {
|
||||
labelCol: {
|
||||
span: 7,
|
||||
},
|
||||
wrapperCol: {
|
||||
span: 11,
|
||||
},
|
||||
};
|
||||
|
||||
const bussDesc = `概要描述Topic的数据源, Topic数据的生产者/消费者, Topic的申请原因及备注信息等。`;
|
||||
|
||||
class Topic extends React.Component<any> {
|
||||
public state = {
|
||||
loading: false,
|
||||
};
|
||||
|
||||
public getDisabled = () => !!modal.topicData;
|
||||
|
||||
public handleSubmit = () => {
|
||||
if (this.getDisabled()) return modal.close();
|
||||
this.props.form.validateFields((err: Error, values: any) => {
|
||||
if (err) return;
|
||||
const { principalList, retentionTime } = values;
|
||||
values.principalList = principalList.split(',');
|
||||
values.retentionTime = retentionTime;
|
||||
this.setState({ loading: true });
|
||||
createTopic(values).then(data => {
|
||||
notification.success({ message: '申请Topic成功' });
|
||||
setTimeout(() => location.assign('/user/my_order'), 500);
|
||||
modal.close();
|
||||
}, (err) => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
const disabled = this.getDisabled();
|
||||
const { loading } = this.state;
|
||||
const initialData = modal.topicData || {} as ITopic;
|
||||
return (
|
||||
<Modal
|
||||
title={disabled ? 'Topic申请详情' : 'Topic申请'}
|
||||
style={{ top: 70 }}
|
||||
visible={true}
|
||||
onCancel={modal.close.bind(null, null)}
|
||||
maskClosable={false}
|
||||
width="680px"
|
||||
destroyOnClose={true}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
onOk={this.handleSubmit}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form {...topicFormItemLayout}>
|
||||
<Row>
|
||||
<Form.Item label="集群名称">
|
||||
{getFieldDecorator('clusterId', {
|
||||
rules: [{ required: true, message: '请选择集群' }],
|
||||
initialValue: +urlQuery.clusterId || initialData.clusterId || (cluster.data[1] && cluster.data[1].clusterId),
|
||||
})(
|
||||
<Select disabled={disabled} onChange={operation.initRegionOptions}>
|
||||
{
|
||||
cluster.data.slice(1).map(c => {
|
||||
return <Select.Option value={c.clusterId} key={c.clusterId}>{c.clusterName}</Select.Option>;
|
||||
})
|
||||
}
|
||||
</Select>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item label="Topic名称">
|
||||
{getFieldDecorator('topicName', {
|
||||
rules: [{ required: true, message: '请输入Topic 名称' },
|
||||
{ pattern: /^[a-zA-Z0-9_-]{1,64}$/, message: '格式不正确' }],
|
||||
initialValue: initialData.topicName,
|
||||
})(
|
||||
<Input placeholder="支持字母、数字、下划线、中划线,6-64个字符" disabled={disabled} />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item label="负责人">
|
||||
{getFieldDecorator('principalList', {
|
||||
rules: [{ required: true, message: '请输入负责人' }],
|
||||
initialValue: disabled ? initialData.principals || ' ' : getCookie('username'),
|
||||
})(
|
||||
<Input disabled={disabled} placeholder="多个负责人请用逗号隔开" />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item label="保存时间">
|
||||
{getFieldDecorator('retentionTime', {
|
||||
rules: [{ required: true, message: '请输入保存时间' }],
|
||||
initialValue: (initialData.retentionTime / 3600000) || 24,
|
||||
})(
|
||||
<InputNumber disabled={disabled} min={0} />,
|
||||
)}
|
||||
<span className="ml-5">小时</span>
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item label="流量上限">
|
||||
{getFieldDecorator('peakBytesIn', {
|
||||
rules: [{ required: true, message: '请输入限流值' }],
|
||||
initialValue: initialData.peakBytesIn || 1,
|
||||
})(
|
||||
<InputNumber min={0} disabled={disabled} />,
|
||||
)}
|
||||
<span className="ml-5">MB/s</span>
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item label="业务说明" >
|
||||
{getFieldDecorator('description', {
|
||||
rules: [{ required: true, message: '请输入业务说明' }],
|
||||
initialValue: initialData.description || '',
|
||||
})(
|
||||
<Input.TextArea disabled={disabled} placeholder={bussDesc} style={{ height: 100 }} />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create({ name: 'topic' })(Topic);
|
||||
45
console/src/container/modify-user/index.less
Normal file
@@ -0,0 +1,45 @@
|
||||
.fw-container {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
margin: auto;
|
||||
height: 425px;
|
||||
width: 415px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
background: #fff;
|
||||
transition: all 500ms ease-in-out;
|
||||
&:hover {
|
||||
box-shadow: 0px 0px 14px 0px #afafaf;
|
||||
}
|
||||
.ant-form-item-control {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.ant-input {
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
border-radius: 0;
|
||||
}
|
||||
.style-button {
|
||||
text-align: right;
|
||||
.ant-btn {
|
||||
width: 50%;
|
||||
height: 40px;
|
||||
border-radius: 0px;
|
||||
border-right: 1px solid #fff;
|
||||
}
|
||||
.b-item {
|
||||
text-align: right;
|
||||
.ant-col-10 {
|
||||
width: 55%;
|
||||
button {
|
||||
margin: 20px 10px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
83
console/src/container/modify-user/index.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Form, Row, Input, notification, Button } from 'component/antd';
|
||||
import { modifyUser } from 'lib/api';
|
||||
import { getCookie } from 'lib/utils';
|
||||
|
||||
import './index.less';
|
||||
|
||||
class ModifyUser extends React.Component<any> {
|
||||
public handleSubmit = (e: React.MouseEvent<any, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
this.props.form.validateFields((err: Error, values: any) => {
|
||||
const username = getCookie('username');
|
||||
const role = getCookie('role');
|
||||
const { oldPassword, password } = values;
|
||||
if (err) return;
|
||||
modifyUser({ role, username, oldPassword, password }).then(() => {
|
||||
notification.success({ message: '修改成功' });
|
||||
this.handleReset();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public comparePassword = (rule: any, value: any, callback: any) => {
|
||||
const { form } = this.props;
|
||||
if (value && value !== form.getFieldValue('password')) {
|
||||
callback('两次密码不相同');
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
public handleReset = () => {
|
||||
this.props.form.resetFields();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
return (
|
||||
<div className="fw-container">
|
||||
<Form onSubmit={this.handleSubmit} style={{ width: '80%' }} >
|
||||
<Row>
|
||||
<Form.Item >
|
||||
{getFieldDecorator('oldPassword', {
|
||||
rules: [{ required: true, message: '请输入原密码' }],
|
||||
})(
|
||||
<Input placeholder="请输入原密码" />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item>
|
||||
{getFieldDecorator('password', {
|
||||
rules: [{ required: true, message: '请输入新密码' }],
|
||||
})(
|
||||
<Input.Password placeholder="请输入新密码" />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item>
|
||||
{getFieldDecorator('confirm password', {
|
||||
rules: [{ required: true, message: '请重新输入新密码' }, {
|
||||
validator: this.comparePassword,
|
||||
}],
|
||||
})(
|
||||
<Input.Password placeholder="请重新输入新密码" />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
<Row>
|
||||
<Form.Item className="style-button">
|
||||
<Button type="primary" htmlType="submit">确认</Button>
|
||||
<Button type="primary" onClick={this.handleReset}>取消</Button>
|
||||
</Form.Item>
|
||||
</Row>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create({ name: 'modifyUser' })(ModifyUser);
|
||||
205
console/src/container/my-order/index.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Table, Tabs, Select, Input, notification, Modal } from 'component/antd';
|
||||
import { PaginationConfig } from 'antd/es/table/interface';
|
||||
import { modal } from 'store';
|
||||
import { observer } from 'mobx-react';
|
||||
import { order, tableStatusFilter } from 'store/order';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { recallPartition, recallOrder } from 'lib/api';
|
||||
import moment from 'moment';
|
||||
import { handleTabKey, tableFilter } from 'lib/utils';
|
||||
import { SearchAndFilter } from 'container/cluster-topic';
|
||||
import { IBaseOrder } from 'types/base-type';
|
||||
|
||||
const TabPane = Tabs.TabPane;
|
||||
const Search = Input.Search;
|
||||
const Option = Select.Option;
|
||||
|
||||
const handleRecallOrder = (record: IBaseOrder) => {
|
||||
const flag = +location.hash.substr(1);
|
||||
Modal.confirm({
|
||||
title: `确认撤回 Topic: ${record.topicName}${flag ? ' 扩容的' : ''}申请 ?`,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
if (flag) {
|
||||
recallPartition(record.orderId).then(() => {
|
||||
notification.success({ message: '撤回成功' });
|
||||
order.getOrder();
|
||||
});
|
||||
} else {
|
||||
recallOrder(record.orderId).then(() => {
|
||||
notification.success({ message: '撤回成功' });
|
||||
order.getOrder();
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const pagination: PaginationConfig = {
|
||||
position: 'bottom',
|
||||
showQuickJumper: true,
|
||||
pageSize: 10,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
};
|
||||
|
||||
@observer
|
||||
export class MyOrder extends SearchAndFilter {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
filterStatusVisible: false,
|
||||
filterClusterVisible: false,
|
||||
filterSVisible: false,
|
||||
filterCVisible: false,
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
order.getOrder();
|
||||
cluster.getClustersBasic();
|
||||
}
|
||||
|
||||
public renderColumns = (data: IBaseOrder[], type: boolean) => {
|
||||
const cluster = Object.assign({
|
||||
title: '集群名称',
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
filters: tableFilter<IBaseOrder>(data, 'clusterName'),
|
||||
onFilter: (value: string, record: IBaseOrder) => record.clusterName.indexOf(value) === 0,
|
||||
}, this.renderColumnsFilter(type ? 'filterClusterVisible' : 'filterCVisible'));
|
||||
|
||||
const status = Object.assign({
|
||||
title: '工单状态',
|
||||
dataIndex: 'statusStr',
|
||||
key: 'statusStr',
|
||||
filters: tableStatusFilter,
|
||||
onFilter: (value: string, record: IBaseOrder) => record.statusStr.indexOf(value) === 0,
|
||||
render: (t: string) => <span className={t === '通过' ? 'success' : t === '拒绝' ? 'fail' : ''}>{t}</span>,
|
||||
}, this.renderColumnsFilter(type ? 'filterStatusVisible' : 'filterSVisible'));
|
||||
|
||||
return [
|
||||
{
|
||||
title: '工单 ID',
|
||||
dataIndex: 'orderId',
|
||||
key: 'orderId',
|
||||
sorter: (a: IBaseOrder, b: IBaseOrder) => a.orderId - b.orderId,
|
||||
},
|
||||
cluster,
|
||||
{
|
||||
title: 'Topic 名称',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
sorter: (a: IBaseOrder, b: IBaseOrder) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
},
|
||||
{
|
||||
title: 'Topic申请人',
|
||||
dataIndex: 'applicant',
|
||||
key: 'applicant',
|
||||
sorter: (a: IBaseOrder, b: IBaseOrder) => a.principals.charCodeAt(0) - b.principals.charCodeAt(0),
|
||||
},
|
||||
{
|
||||
title: '申请时间',
|
||||
dataIndex: 'gmtCreate',
|
||||
key: 'gmtCreate',
|
||||
sorter: (a: IBaseOrder, b: IBaseOrder) => a.gmtCreate - b.gmtCreate,
|
||||
render: (t: number) => moment(t).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
status,
|
||||
{
|
||||
title: '审批人',
|
||||
dataIndex: 'approver',
|
||||
key: 'approver',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
key: 'operation',
|
||||
render: (text: string, r: IBaseOrder) => {
|
||||
return (
|
||||
<span className="table-operation">
|
||||
<a
|
||||
onClick={+location.hash.substr(1) ? modal.showExpandTopic.bind(null, r) : modal.showNewTopic.bind(null, r)}
|
||||
>详情
|
||||
</a>
|
||||
{r.orderStatus === 0 ? <a onClick={handleRecallOrder.bind(null, r)}>撤回</a> : null}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public renderTopic() {
|
||||
return (
|
||||
<Table
|
||||
columns={this.renderColumns(order.topicOrder, true)}
|
||||
dataSource={this.getData(order.topicOrder)}
|
||||
pagination={pagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public renderPartition() {
|
||||
return (
|
||||
<Table
|
||||
columns={this.renderColumns(order.partitionOrder, false)}
|
||||
dataSource={this.getData(order.partitionOrder)}
|
||||
pagination={pagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public getData<T extends IBaseOrder>(origin: T[]) {
|
||||
let data: T[] = [];
|
||||
origin.forEach((d) => {
|
||||
if (cluster.active === -1 || d.clusterId === cluster.active) {
|
||||
return data.push(d);
|
||||
}
|
||||
});
|
||||
const { searchKey } = this.state;
|
||||
|
||||
if (searchKey) {
|
||||
data = data.filter((d) => d.topicName.includes(searchKey));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const activeKey = location.hash.substr(1);
|
||||
return (
|
||||
<>
|
||||
<ul className="table-operation-bar">
|
||||
<li
|
||||
className="new-topic"
|
||||
onClick={+activeKey ? modal.showExpandTopic.bind(null, null) : modal.showNewTopic.bind(null, null)}
|
||||
>
|
||||
<i className="k-icon-xinjian didi-theme" />{+activeKey ? '扩容申请' : 'Topic申请'}
|
||||
</li>
|
||||
<li>
|
||||
<Select value={cluster.active} onChange={cluster.changeCluster}>
|
||||
{cluster.data.map((d) => <Option value={d.clusterId} key={d.clusterId}>{d.clusterName}</Option>)}
|
||||
</Select>
|
||||
</li>
|
||||
<li><Search placeholder="请输入Topic名称" onChange={this.onSearch} /></li>
|
||||
</ul>
|
||||
<Tabs activeKey={activeKey || '0'} type="card" onChange={handleTabKey}>
|
||||
<TabPane tab="Topic 申请" key="0">
|
||||
{this.renderTopic()}
|
||||
</TabPane>
|
||||
<TabPane tab="Topic 扩容" key="1">
|
||||
{this.renderPartition()}
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private onSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const searchKey = e.target.value.trim();
|
||||
this.setState({
|
||||
searchKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
198
console/src/container/topic-detail/com.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import * as React from 'react';
|
||||
import { Table, DatePicker, Select, Button, PaginationConfig, notification, Spin, Tooltip } from 'component/antd';
|
||||
import echarts from 'echarts/lib/echarts';
|
||||
import { cluster } from 'store/cluster';
|
||||
|
||||
// 引入柱状图
|
||||
import 'echarts/lib/chart/line';
|
||||
// 引入提示框和标题组件
|
||||
import 'echarts/lib/component/tooltip';
|
||||
import 'echarts/lib/component/title';
|
||||
import 'echarts/lib/component/legend';
|
||||
import { observer } from 'mobx-react';
|
||||
import { topic, IConsumeInfo, ITopicStatusInfo } from 'store/topic';
|
||||
import { StatusGraghCom } from 'component/flow-table';
|
||||
import { IOptionType } from 'types/base-type';
|
||||
import moment from 'moment';
|
||||
|
||||
export const Base = observer(() => {
|
||||
if (!topic.baseInfo) return null;
|
||||
return (
|
||||
<ul className="base-detail">
|
||||
<li className="special"><div><span>负责人:</span>
|
||||
<Tooltip placement="top" title={topic.baseInfo.principals}>
|
||||
<span className="principal">{topic.baseInfo.principals}</span>
|
||||
</Tooltip>
|
||||
</div></li>
|
||||
<li><span>分区数: </span>{topic.baseInfo.partitionNum} 个</li>
|
||||
<li><span>储存时间: </span>{(topic.baseInfo.retentionTime / 3600000).toFixed(0)} 小时</li>
|
||||
<li><span>副本数: </span>{topic.baseInfo.replicaNum} 个</li>
|
||||
<li><span>创建时间:</span>{moment(topic.baseInfo.createTime).format('YYYY-MM-DD HH:mm:ss')}</li>
|
||||
<li><span>Broker数:</span>{topic.baseInfo.brokerNum} 个</li>
|
||||
<li><span>修改时间:</span>{moment(topic.baseInfo.modifyTime).format('YYYY-MM-DD HH:mm:ss')}</li>
|
||||
<li className="special"><div><span>Region:</span>{topic.baseInfo.regionNames}</div></li>
|
||||
<li className="special"><div><span>描述信息:</span>
|
||||
<Tooltip placement="top" title={topic.baseInfo.description}>
|
||||
<span className="principal">{topic.baseInfo.description}</span>
|
||||
</Tooltip>
|
||||
</div></li>
|
||||
</ul>
|
||||
);
|
||||
});
|
||||
|
||||
const pagination: PaginationConfig = {
|
||||
position: 'bottom',
|
||||
showQuickJumper: true,
|
||||
pageSize: 6,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
};
|
||||
|
||||
interface IGroupProps {
|
||||
data: IConsumeInfo[];
|
||||
pagination?: PaginationConfig;
|
||||
}
|
||||
|
||||
// export const Group = (props: IGroupProps) => {
|
||||
// return (
|
||||
// <div className="group-detail">
|
||||
// <Table columns={groupColumns} dataSource={props.data} pagination={props.pagination || pagination}/>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
export class Group extends React.Component<IGroupProps> {
|
||||
public columns = [{
|
||||
title: '消费组名称',
|
||||
dataIndex: 'consumerGroup',
|
||||
key: 'consumerGroup',
|
||||
width: '80%',
|
||||
render: (t: string, r: IConsumeInfo) => this.renderOp(r),
|
||||
}, {
|
||||
title: 'location',
|
||||
dataIndex: 'location',
|
||||
key: 'location',
|
||||
width: '20%',
|
||||
render: (t: string) => t.toLowerCase(),
|
||||
},
|
||||
];
|
||||
|
||||
public renderOp = (record: IConsumeInfo) => {
|
||||
return (
|
||||
<span className="table-operation">
|
||||
<a
|
||||
// tslint:disable-next-line:max-line-length
|
||||
href={`/user/consumer?topic=${topic.currentTopicName}&clusterId=${topic.currentClusterId}&group=${record.consumerGroup}&location=${record.location.toLowerCase()}#2`}
|
||||
>
|
||||
{record.consumerGroup}
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="group-detail">
|
||||
<Table
|
||||
columns={this.columns}
|
||||
dataSource={this.props.data}
|
||||
pagination={this.props.pagination || pagination}
|
||||
rowKey="consumerGroup"
|
||||
scroll={{ y: 400 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export class StatusGragh extends StatusGraghCom<ITopicStatusInfo> {
|
||||
public getData = () => {
|
||||
return topic.statusInfo;
|
||||
}
|
||||
}
|
||||
@observer
|
||||
export class NetWorkFlow extends React.Component<any> {
|
||||
public id: HTMLDivElement = null;
|
||||
public chart: echarts.ECharts;
|
||||
|
||||
public state = {
|
||||
loading: true,
|
||||
data: false,
|
||||
type: 'normal',
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
this.chart = echarts.init(this.id);
|
||||
cluster.initTime();
|
||||
this.handleSearch();
|
||||
}
|
||||
|
||||
public handleApi = () => {
|
||||
const { topicName, brokerId, clusterId } = this.props;
|
||||
if (topicName) {
|
||||
this.setState({ type: 'topic' });
|
||||
return cluster.getMetriceInfo(clusterId, topicName);
|
||||
}
|
||||
if (brokerId !== undefined) return cluster.getBrokerMetrics(clusterId, brokerId);
|
||||
return cluster.getClusterMetricsHistory(clusterId);
|
||||
}
|
||||
|
||||
public handleSearch = () => {
|
||||
const { startTime, endTime } = cluster;
|
||||
if (startTime >= endTime) {
|
||||
notification.error({ message: '开始时间不能大于或等于结束时间' });
|
||||
return false;
|
||||
}
|
||||
this.setState({ loading: true });
|
||||
this.handleApi().then(data => {
|
||||
this.setState({ loading: false });
|
||||
this.setState({ data: !data.xAxis.data.length });
|
||||
this.chart.setOption(data as any, true);
|
||||
});
|
||||
}
|
||||
public handleChange = (value: IOptionType) => {
|
||||
this.chart.setOption(cluster.changeType(value) as any, true);
|
||||
}
|
||||
|
||||
public handleStartTimeChange(value: moment.Moment) {
|
||||
cluster.changeStartTime(value);
|
||||
}
|
||||
|
||||
public handleEndTimeChange(value: moment.Moment) {
|
||||
cluster.changeEndTime(value);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<div className="status-graph">
|
||||
<ul className="k-toolbar topic-line-tool">
|
||||
<li>
|
||||
<span className="label">开始时间</span>
|
||||
<DatePicker showTime={true} value={cluster.startTime} onChange={this.handleStartTimeChange} />
|
||||
</li>
|
||||
<li>
|
||||
<span className="label" >结束时间</span>
|
||||
<DatePicker showTime={true} value={cluster.endTime} onChange={this.handleEndTimeChange} />
|
||||
</li>
|
||||
<li>
|
||||
<span className="label">类型</span>
|
||||
<Select defaultValue={cluster.type} style={{ width: '160px' }} onChange={this.handleChange}>
|
||||
<Select.Option value="byteIn/byteOut">Bytes In/Bytes Out</Select.Option>
|
||||
<Select.Option value="byteRejected">Bytes Rejected</Select.Option>
|
||||
{this.state.type === 'topic' ? <Select.Option value="messageIn/totalProduceRequests">Message In/TotalProduceRequests</Select.Option> :
|
||||
<Select.Option value="messageIn">Message In</Select.Option>}
|
||||
</Select>
|
||||
</li>
|
||||
<li><Button type="primary" size="small" onClick={this.handleSearch}>查询</Button></li>
|
||||
{/* <li><Button type="primary" size="small">配额信息</Button></li> */}
|
||||
</ul>
|
||||
</div>
|
||||
<Spin spinning={this.state.loading} >
|
||||
{this.state.data ? <div className="nothing-style">暂无数据</div> : null}
|
||||
<div style={{ height: 400 }} ref={(id) => this.id = id} />
|
||||
</Spin>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
173
console/src/container/topic-detail/index.less
Normal file
@@ -0,0 +1,173 @@
|
||||
p.k-title {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
font-family: PingFangSC-Medium;
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
padding-left: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
.right-flow {
|
||||
.k-abs {
|
||||
right: 24px;
|
||||
cursor: pointer;
|
||||
& > i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.status-graph {
|
||||
position: relative;
|
||||
height: 48px;
|
||||
width: 100%;
|
||||
background: rgba(250, 250, 250, 1);
|
||||
}
|
||||
.topic-line-tool {
|
||||
font-size: 14px;
|
||||
font-family: PingFangSC-Regular;
|
||||
font-weight: 400;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
background: rgba(250, 250, 250);
|
||||
li {
|
||||
margin-right: 20px;
|
||||
& > span.label {
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.base-detail {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
li {
|
||||
float: left;
|
||||
color: rgba(3, 2, 2, 0.85);
|
||||
background: #fff;
|
||||
line-height: 52px;
|
||||
font-size: 14px;
|
||||
font-family: PingFangSC-Regular;
|
||||
white-space: nowrap;
|
||||
&:nth-child(2n + 1) {
|
||||
width: 65%;
|
||||
span:first-child {
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
&:nth-child(2n) {
|
||||
width: 35%;
|
||||
padding-left: 13px;
|
||||
}
|
||||
&.special {
|
||||
width: 100%;
|
||||
padding-left: 13px;
|
||||
height: 52px;
|
||||
span:first-child {
|
||||
width: 20%;
|
||||
text-align: left;
|
||||
}
|
||||
.principal {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 70%;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
span:first-child {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
font-family: PingFangSC-Regular;
|
||||
font-weight: 400;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
width: 54%;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.k-row {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
.k-abs {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
}
|
||||
.k-tab {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
padding: 0px 24px;
|
||||
font-size: 14px;
|
||||
font-family: PingFangSC-Medium;
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.k-top-row {
|
||||
position: relative;
|
||||
& + .k-top-row {
|
||||
padding-left: 8px;
|
||||
}
|
||||
.group-search {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 24px;
|
||||
width: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
.k-toolbar {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
li {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav {
|
||||
color: #3f3f3fc9;
|
||||
p {
|
||||
display: inline-block;
|
||||
font-size: 20px;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
.t-button {
|
||||
font-size: 12px;
|
||||
button {
|
||||
border: 1px solid #f38031;
|
||||
color: #f38031;
|
||||
margin-left: 10px;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
.group-detail {
|
||||
.ant-table-body {
|
||||
height: 270px;
|
||||
}
|
||||
.ant-table-placeholder {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.nothing-style {
|
||||
position: absolute;
|
||||
font-size: 18px;
|
||||
color: #999;
|
||||
top: 175px;
|
||||
left: 520px;
|
||||
}
|
||||
302
console/src/container/topic-detail/index.tsx
Normal file
@@ -0,0 +1,302 @@
|
||||
import * as React from 'react';
|
||||
import './index.less';
|
||||
import { NetWorkFlow, StatusGragh, Group, Base } from './com';
|
||||
import { Table, Tabs, Button, PaginationConfig } from 'component/antd';
|
||||
import Url from 'lib/url-parser';
|
||||
import { topic, ITopicPartition, ITopicBroker } from 'store/topic';
|
||||
import { observer } from 'mobx-react';
|
||||
import { modal } from 'store';
|
||||
import { drawer } from 'store/drawer';
|
||||
import { handleTabKey } from 'lib/utils';
|
||||
import { getCookie } from 'lib/utils';
|
||||
import { SearchAndFilter } from 'container/cluster-topic';
|
||||
import { broker } from 'store/broker';
|
||||
|
||||
const pagination: PaginationConfig = {
|
||||
position: 'bottom',
|
||||
showQuickJumper: true,
|
||||
pageSize: 10,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
};
|
||||
|
||||
@observer
|
||||
export class TopicDetail extends SearchAndFilter {
|
||||
public clusterId: number;
|
||||
public topicName: string;
|
||||
public role: string;
|
||||
|
||||
public brokerColumns = [{
|
||||
title: 'BrokerID',
|
||||
key: 'brokerId',
|
||||
dataIndex: 'brokerId',
|
||||
sorter: (a: ITopicBroker, b: ITopicBroker) => a.brokerId - b.brokerId,
|
||||
render: (t: string) => {
|
||||
return (
|
||||
<a href={`/admin/broker_detail?clusterId=${this.clusterId}&brokerId=${t}`}>
|
||||
{t}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
title: 'Host',
|
||||
key: 'host',
|
||||
dataIndex: 'host',
|
||||
}, {
|
||||
title: 'Leader个数',
|
||||
key: 'leaderPartitionIdListLength',
|
||||
dataIndex: 'leaderPartitionIdList',
|
||||
render: (t: []) => {
|
||||
return t.length;
|
||||
},
|
||||
}, {
|
||||
title: '分区LeaderID',
|
||||
key: 'leaderPartitionIdList',
|
||||
dataIndex: 'leaderPartitionIdList',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 180,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
render: (t: []) => {
|
||||
return t.map(i => <span key={i} className="p-params">{i}</span>);
|
||||
},
|
||||
}, {
|
||||
title: '分区个数',
|
||||
key: 'partitionNum',
|
||||
dataIndex: 'partitionNum',
|
||||
}, {
|
||||
title: '分区ID',
|
||||
key: 'partitionIdList',
|
||||
dataIndex: 'partitionIdList',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 180,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
render: (t: []) => {
|
||||
return t.map(i => <span key={i} className="p-params">{i}</span>);
|
||||
},
|
||||
}, {
|
||||
title: '操作',
|
||||
render: (record: ITopicBroker) => {
|
||||
return (<a onClick={broker.handleOpen.bind(broker, record.brokerId)}>查看详情</a>);
|
||||
},
|
||||
}];
|
||||
|
||||
public state = {
|
||||
searchKey: '',
|
||||
partitionKey: '',
|
||||
brokerKey: '',
|
||||
consumerKey: '',
|
||||
filterVisible: false,
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
this.topicName = url.search.topic;
|
||||
this.role = getCookie('role');
|
||||
}
|
||||
|
||||
public renderColumns = () => {
|
||||
const underReplicated = Object.assign({
|
||||
title: '已同步',
|
||||
key: 'underReplicated',
|
||||
dataIndex: 'underReplicated',
|
||||
filters: [{ text: '是', value: '0' }, { text: '否', value: '1' }],
|
||||
onFilter: (value: string, record: ITopicPartition) => +record.underReplicated === +value,
|
||||
render: (t: any) => <span className={t ? 'fail' : 'success'}>{t ? '否' : '是'}</span>,
|
||||
}, this.renderColumnsFilter('filterVisible'));
|
||||
|
||||
return [{
|
||||
title: '分区号',
|
||||
key: 'partitionId',
|
||||
dataIndex: 'partitionId',
|
||||
sorter: (a: ITopicPartition, b: ITopicPartition) => a.partitionId - b.partitionId,
|
||||
}, {
|
||||
title: '偏移量',
|
||||
key: 'offset',
|
||||
dataIndex: 'offset',
|
||||
}, {
|
||||
title: 'LeaderBrokerID',
|
||||
key: 'leaderBrokerId',
|
||||
dataIndex: 'leaderBrokerId',
|
||||
}, {
|
||||
title: '副本BrokerID',
|
||||
key: 'replicaBrokerIdList',
|
||||
dataIndex: 'replicaBrokerIdList',
|
||||
render: (t: []) => {
|
||||
return t.map(i => <span key={i} className="p-params">{i}</span>);
|
||||
},
|
||||
}, {
|
||||
title: 'ISR',
|
||||
key: 'isrBrokerIdList',
|
||||
dataIndex: 'isrBrokerIdList',
|
||||
render: (t: []) => {
|
||||
return t.map(i => <span key={i} className="p-params">{i}</span>);
|
||||
},
|
||||
}, {
|
||||
title: '首选Leader副本',
|
||||
key: 'preferredBrokerId',
|
||||
dataIndex: 'preferredBrokerId',
|
||||
},
|
||||
underReplicated];
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const { topicName, clusterId } = this;
|
||||
topic.getTopicBasicInfo(topicName, clusterId);
|
||||
topic.getTopicStatusInfo(topicName, clusterId);
|
||||
topic.getTopicConsumeInfo(clusterId, topicName);
|
||||
topic.getTopicBroker(clusterId, topicName);
|
||||
topic.getTopicPartition(clusterId, topicName);
|
||||
}
|
||||
|
||||
public updateStatus = () => {
|
||||
topic.getTopicStatusInfo(this.topicName, this.clusterId);
|
||||
}
|
||||
|
||||
public getMoreDetail = (record: ITopicBroker) => {
|
||||
return (
|
||||
<div className="p-description">
|
||||
<p><span>BrokerID: </span>{record.brokerId}</p>
|
||||
<p><span>Host:</span>{record.host}</p>
|
||||
<p><span>LeaderID: </span>{record.leaderPartitionIdList.join(',')}(共{record.leaderPartitionIdList.length}个)</p>
|
||||
<p><span>分区ID:</span>{record.partitionIdList.join(',')}(共{record.partitionIdList.length}个)</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public renderMore() {
|
||||
const data = this.state.brokerKey ?
|
||||
topic.topicBrokers.filter((d) => d.host.includes(this.state.brokerKey)) : topic.topicBrokers;
|
||||
return (
|
||||
<>
|
||||
<div className="k-row mb-24">
|
||||
<ul className="k-tab">
|
||||
<li>Broker信息</li>
|
||||
{this.renderSearch('请输入Host', 'brokerKey')}
|
||||
</ul>
|
||||
<div style={this.state.brokerKey ? { minHeight: 370 } : null}>
|
||||
<Table
|
||||
columns={this.brokerColumns}
|
||||
dataSource={data}
|
||||
rowKey="brokerId"
|
||||
pagination={pagination}
|
||||
expandedRowRender={this.getMoreDetail}
|
||||
expandIconAsCell={false}
|
||||
expandIconColumnIndex={-1}
|
||||
expandedRowKeys={broker.openKeys}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public renderOperation() {
|
||||
const { topicName, clusterId } = this;
|
||||
return (
|
||||
<div className="t-button">
|
||||
{location.hash.substr(1) === '2' ? <Button onClick={drawer.showResetOffset}>重置offset</Button> :
|
||||
<>
|
||||
<Button onClick={modal.showExpandTopic.bind(null, { topicName, clusterId })}>扩容申请</Button>
|
||||
<Button onClick={drawer.showTopicSample.bind(null, { topicName, clusterId })}>采样</Button>
|
||||
</>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public renderFlow() {
|
||||
return (
|
||||
<>
|
||||
<div className="k-row mb-24">
|
||||
<p className="k-title">历史流量</p>
|
||||
<NetWorkFlow clusterId={this.clusterId} topicName={this.topicName} />
|
||||
</div>
|
||||
<div className="k-row right-flow mb-24">
|
||||
<p className="k-title">实时流量</p>
|
||||
<span className="k-abs" onClick={this.updateStatus}>
|
||||
<i className="k-icon-shuaxin didi-theme" />刷新
|
||||
</span>
|
||||
<StatusGragh />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public renderMessage() {
|
||||
const data = this.state.partitionKey ?
|
||||
topic.topicPartitions.filter((d) => d.partitionId + '' === this.state.partitionKey) : topic.topicPartitions;
|
||||
const consumerData = this.state.consumerKey ?
|
||||
topic.consumeInfo.filter((d) => d.consumerGroup.includes(this.state.consumerKey)) : topic.consumeInfo;
|
||||
return (
|
||||
<>
|
||||
<div className="k-row mb-24">
|
||||
<div className="k-top-row" style={{ width: '42%', float: 'left' }}>
|
||||
<p className="k-title">基本信息</p>
|
||||
<Base />
|
||||
</div>
|
||||
<div className="k-top-row" style={{ width: '58%', float: 'right' }}>
|
||||
<ul className="k-tab">
|
||||
<li>消费组信息</li>
|
||||
{this.renderSearch('请输入消费组名称', 'consumerKey')}
|
||||
</ul>
|
||||
<Group data={consumerData} />
|
||||
</div>
|
||||
</div>
|
||||
{+this.role ? this.renderMore() : null}
|
||||
<div className="k-row" >
|
||||
<ul className="k-tab">
|
||||
<li>分区信息</li>
|
||||
{this.renderSearch('请输入分区号', 'partitionKey')}
|
||||
</ul>
|
||||
<div style={this.state.partitionKey ? { minHeight: 700 } : null}>
|
||||
<Table
|
||||
columns={this.renderColumns()}
|
||||
table-Layout="fixed"
|
||||
dataSource={data}
|
||||
rowKey="partitionId"
|
||||
pagination={pagination}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public renderTab() { };
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<div className="nav">
|
||||
<p>{this.topicName}</p>
|
||||
</div>
|
||||
<Tabs
|
||||
activeKey={location.hash.substr(1) || '1'}
|
||||
type="card"
|
||||
onChange={handleTabKey}
|
||||
tabBarExtraContent={this.renderOperation()}
|
||||
>
|
||||
<Tabs.TabPane tab="Topic 信息" key="1">
|
||||
{this.renderMessage()}
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Topic 流量" key="0">
|
||||
{this.renderFlow()}
|
||||
</Tabs.TabPane>
|
||||
{this.renderTab()}
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
73
console/src/container/user-home/index.less
Normal file
@@ -0,0 +1,73 @@
|
||||
.content-container {
|
||||
position: relative;
|
||||
.table-operation {
|
||||
a {
|
||||
color: #f38031;
|
||||
}
|
||||
a + a {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.table-operation-bar {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
z-index: 100;
|
||||
li {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
.ant-select {
|
||||
width: 150px;
|
||||
}
|
||||
.ant-input-search {
|
||||
width: 200px;
|
||||
}
|
||||
&.new-topic {
|
||||
margin-right: 30px;
|
||||
cursor: pointer;
|
||||
& > i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.k-collect {
|
||||
width: 300px;
|
||||
position: relative;
|
||||
margin-bottom: 12px;
|
||||
.ant-alert-close-icon {
|
||||
top: 7px;
|
||||
}
|
||||
.k-coll-btn {
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
right: 15px;
|
||||
}
|
||||
}
|
||||
.p-params {
|
||||
display: inline-block;
|
||||
padding: 0px 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(217, 217, 217, 1);
|
||||
margin: 0px 8px 8px 0px;
|
||||
&-unFinished {
|
||||
background: rgba(245, 34, 45, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon:hover {
|
||||
position: relative;
|
||||
.ant-checkbox-disabled {
|
||||
&::after {
|
||||
content: '已收藏';
|
||||
color: #fff;
|
||||
background-color: rgba(0, 0, 0, 0.65);
|
||||
position: absolute;
|
||||
padding: 8px 0px;
|
||||
width: 60px;
|
||||
border-radius: 4px;
|
||||
top: -30px;
|
||||
left: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
286
console/src/container/user-home/index.tsx
Normal file
@@ -0,0 +1,286 @@
|
||||
import * as React from 'react';
|
||||
import './index.less';
|
||||
|
||||
import { Table, Tabs, Alert, notification, PaginationConfig, Modal, Tooltip } from 'component/antd';
|
||||
import { modal } from 'store';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { observer } from 'mobx-react';
|
||||
import { topic } from 'store/topic';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { collect, uncollect } from 'lib/api';
|
||||
import { SearchAndFilter } from 'container/cluster-topic';
|
||||
import moment from 'moment';
|
||||
import { handleTabKey, tableFilter } from 'lib/utils';
|
||||
import { ITopic } from 'types/base-type';
|
||||
|
||||
const TabPane = Tabs.TabPane;
|
||||
|
||||
const pagination: PaginationConfig = {
|
||||
position: 'bottom',
|
||||
showQuickJumper: true,
|
||||
pageSize: 10,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
};
|
||||
|
||||
@observer
|
||||
export class UserHome extends SearchAndFilter {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
filterCollVisible: false,
|
||||
filterUnCollVisible: false,
|
||||
filterFavorite: false,
|
||||
};
|
||||
|
||||
public collRef: HTMLDivElement = null;
|
||||
public uncollRef: HTMLDivElement = null;
|
||||
|
||||
public rowSelection = {
|
||||
onChange: (selectedRowKeys: string[], selectedRows: ITopic[]) => {
|
||||
const num = selectedRows.length;
|
||||
ReactDOM.render(
|
||||
selectedRows.length ? (
|
||||
<>
|
||||
<Alert
|
||||
type="warning"
|
||||
message={`已选择 ${num} 项 `}
|
||||
showIcon={true}
|
||||
closable={false}
|
||||
/>
|
||||
<a className="k-coll-btn didi-theme" onClick={this.collect.bind(this, selectedRows)}>收藏</a>
|
||||
</>) : null,
|
||||
this.collRef,
|
||||
);
|
||||
},
|
||||
getCheckboxProps: (record: any) => ({ disabled: record.favorite, className: 'icon' }),
|
||||
};
|
||||
|
||||
public unrowSelection = {
|
||||
onChange: (selectedRowKeys: string[], selectedRows: ITopic[]) => {
|
||||
const num = selectedRows.length;
|
||||
ReactDOM.render(
|
||||
selectedRows.length ? (
|
||||
<>
|
||||
<Alert
|
||||
type="warning"
|
||||
message={`已选择 ${num} 项 `}
|
||||
showIcon={true}
|
||||
closable={false}
|
||||
/>
|
||||
<a className="k-coll-btn didi-theme" onClick={this.uncollect.bind(this, selectedRows)}>取消收藏</a>
|
||||
</>) : null,
|
||||
this.uncollRef,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
public renderColumns = (data: ITopic[], type: boolean) => {
|
||||
const cluster = Object.assign({
|
||||
title: '集群名称',
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
width: '12%',
|
||||
filters: tableFilter<ITopic>(data, 'clusterName'),
|
||||
onFilter: (value: string, record: ITopic) => record.clusterName.indexOf(value) === 0,
|
||||
}, this.renderColumnsFilter(type ? 'filterCollVisible' : 'filterUnCollVisible'));
|
||||
|
||||
const favorite = Object.assign({
|
||||
title: '状态',
|
||||
dataIndex: 'favorite',
|
||||
key: 'favorite',
|
||||
filters: [{ text: '已收藏', value: 'true' }, { text: '未收藏', value: 'false' }],
|
||||
onFilter: (value: string, record: ITopic) => record.favorite + '' === value,
|
||||
render: (t: boolean) => t ? '已收藏' : '未收藏',
|
||||
}, this.renderColumnsFilter('filterFavorite'));
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'Topic 名称',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
width: 250,
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 250,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
sorter: (a: ITopic, b: ITopic) => a.topicName ? a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0) : null,
|
||||
render: (t: string, r: ITopic) => {
|
||||
return (
|
||||
<Tooltip placement="top" title={r.topicName} >
|
||||
<a href={`/user/topic_detail?clusterId=${r.clusterId}&topic=${r.topicName}`}>{t}</a>
|
||||
</Tooltip>);
|
||||
},
|
||||
},
|
||||
cluster,
|
||||
{
|
||||
title: '分区数',
|
||||
dataIndex: 'partitionNum',
|
||||
key: 'partitionNum',
|
||||
width: 120,
|
||||
sorter: (a: ITopic, b: ITopic) => b.partitionNum - a.partitionNum,
|
||||
},
|
||||
{
|
||||
title: '流入(KB/s)',
|
||||
dataIndex: 'byteIn',
|
||||
key: 'byteIn',
|
||||
width: 120,
|
||||
sorter: (a: ITopic, b: ITopic) => b.byteIn - a.byteIn,
|
||||
render: (t: number) => (t / 1024).toFixed(2),
|
||||
},
|
||||
{
|
||||
title: '流入(QPS)',
|
||||
dataIndex: 'produceRequest',
|
||||
key: 'produceRequest',
|
||||
width: 120,
|
||||
sorter: (a: ITopic, b: ITopic) => b.produceRequest - a.produceRequest,
|
||||
render: (t: number) => t.toFixed(2),
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
dataIndex: 'principals',
|
||||
key: 'principals',
|
||||
width: 120,
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 100,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
render: (t: string) => <Tooltip placement="topLeft" title={t} >{t}</Tooltip>,
|
||||
sorter: (a: ITopic, b: ITopic) =>
|
||||
a.principals && b.principals ? a.principals.charCodeAt(0) - b.principals.charCodeAt(0) : (-1),
|
||||
},
|
||||
{
|
||||
title: '修改时间',
|
||||
dataIndex: 'updateTime',
|
||||
key: 'updateTime',
|
||||
width: '15%',
|
||||
sorter: (a: ITopic, b: ITopic) => a.updateTime - b.updateTime,
|
||||
render: (t: number) => moment(t).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
favorite,
|
||||
];
|
||||
if (!type) return columns.splice(0, columns.length - 1);
|
||||
return columns;
|
||||
}
|
||||
|
||||
public renderCollection(favData: ITopic[]) {
|
||||
return (
|
||||
<Table
|
||||
rowKey="key"
|
||||
rowSelection={this.unrowSelection}
|
||||
dataSource={favData}
|
||||
columns={this.renderColumns(favData, false)}
|
||||
pagination={pagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public renderList(data: ITopic[]) {
|
||||
return (
|
||||
<Table
|
||||
rowKey="key"
|
||||
rowSelection={this.rowSelection}
|
||||
columns={this.renderColumns(data, true)}
|
||||
dataSource={data}
|
||||
pagination={pagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
if (cluster.data.length === 0) {
|
||||
cluster.getClustersBasic();
|
||||
}
|
||||
|
||||
if (topic.data.length === 0) {
|
||||
topic.getTopics();
|
||||
}
|
||||
}
|
||||
|
||||
public collect = (selectedRowKeys: ITopic[]) => {
|
||||
collect(selectedRowKeys.map(s => ({ clusterId: s.clusterId, topicName: s.topicName } as ITopic))).then(() => {
|
||||
ReactDOM.unmountComponentAtNode(this.collRef);
|
||||
topic.getTopics();
|
||||
notification.success({ message: '收藏成功' });
|
||||
});
|
||||
}
|
||||
|
||||
public uncollect = (selectedRowKeys: ITopic[]) => {
|
||||
Modal.confirm({
|
||||
title: `确认取消收藏?`,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
uncollect(selectedRowKeys.map(s => ({ clusterId: s.clusterId, topicName: s.topicName } as ITopic))).then(() => {
|
||||
ReactDOM.unmountComponentAtNode(this.uncollRef);
|
||||
notification.success({ message: '取消收藏成功' });
|
||||
topic.getTopics();
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public getData<T extends ITopic>(origin: T[]) {
|
||||
let data: T[] = [];
|
||||
origin.forEach((d) => {
|
||||
if (cluster.active === -1 || d.clusterId === cluster.active) {
|
||||
return data.push(d);
|
||||
}
|
||||
});
|
||||
const { searchKey } = this.state;
|
||||
|
||||
if (searchKey) {
|
||||
data = origin.filter((d) => d.topicName.includes(searchKey) ||
|
||||
(d.principals && d.principals.includes(searchKey)));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public renderTable() {
|
||||
return (
|
||||
<Tabs activeKey={location.hash.substr(1) || '1'} type="card" onChange={handleTabKey}>
|
||||
<TabPane tab="Topic收藏" key="1">
|
||||
<div className="k-collect" ref={(id) => this.uncollRef = id} />
|
||||
{this.renderCollection(this.getData(topic.favData))}
|
||||
</TabPane>
|
||||
<TabPane tab="Topic列表" key="2">
|
||||
<div className="k-collect" ref={(id) => this.collRef = id} />
|
||||
{this.renderList(this.getData(topic.data))}
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
public renderClusterTopic() {
|
||||
return (
|
||||
<>
|
||||
{this.renderCluster()}
|
||||
{this.renderSearch('请输入Topic名称或者负责人')}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const isAdmin = location.pathname.includes('admin');
|
||||
return (
|
||||
<>
|
||||
<ul className="table-operation-bar">
|
||||
<li className="new-topic" onClick={isAdmin ? modal.showAdimTopic.bind(null, null) : modal.showNewTopic.bind(null, null)}>
|
||||
<i className="k-icon-xinjian didi-theme" />{`Topic${isAdmin ? '创建' : '申请'}`}
|
||||
</li>
|
||||
{this.renderClusterTopic()}
|
||||
</ul>
|
||||
{this.renderTable()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
379
console/src/lib/api.ts
Normal file
@@ -0,0 +1,379 @@
|
||||
import fetch from './fetch';
|
||||
import { INewCluster, ITopic, IAlarmBase, IDilation, ITaskBase, IRebalance, IOrderTopic, IUser, ISample, IDeleteTopic, IOffset } from 'types/base-type';
|
||||
import { IRegionData } from 'store/region';
|
||||
|
||||
export const getClusters = (cluster?: number) => {
|
||||
return fetch(`/clusters${cluster ? '/' + cluster : ''}`);
|
||||
};
|
||||
|
||||
export const getTopic = (clusterId?: number, favorite?: boolean) => {
|
||||
const query = !clusterId && favorite === undefined ? '/topics' :
|
||||
(clusterId ? `/topics?clusterId=${clusterId}` : `/topics?favorite=${favorite}`);
|
||||
return fetch(query);
|
||||
};
|
||||
|
||||
export const collect = (topicFavoriteList: ITopic[]) => {
|
||||
return fetch('/topics/favorite', {
|
||||
body: {
|
||||
topicFavoriteList,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const uncollect = (topicFavoriteList: ITopic[]) => {
|
||||
return fetch('/topics/favorite', {
|
||||
method: 'DELETE',
|
||||
body: {
|
||||
topicFavoriteList,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getTopicBasicInfo = (topicName: string, clusterId: number) => {
|
||||
return fetch(`/${clusterId}/topics/${topicName}/basic-info`);
|
||||
};
|
||||
|
||||
export const getTopicConsumeInfo = (clusterId: number, topicName: string) => {
|
||||
return fetch(`/${clusterId}/topics/${topicName}/consumer-groups`);
|
||||
};
|
||||
|
||||
export const getConsumeInfo = (clusterId: number) => {
|
||||
return fetch(`/${clusterId}/consumers/consumer-groups`);
|
||||
};
|
||||
|
||||
export const getTopicStatusInfo = (topicName: string, clusterId: number) => {
|
||||
return fetch(`/${clusterId}/topics/${topicName}/metrics`);
|
||||
};
|
||||
|
||||
export const getGroupInfo = (topicName: string, clusterId: number, group: string, location: string) => {
|
||||
return fetch(`/${clusterId}/consumers/${group}/topics/${topicName}/consume-detail?location=${location}`);
|
||||
};
|
||||
|
||||
export const getTopicOrder = () => {
|
||||
return fetch('/orders/topic');
|
||||
};
|
||||
|
||||
export const getPartitionOrder = () => {
|
||||
return fetch('/orders/partition');
|
||||
};
|
||||
|
||||
export const getAlarm = () => {
|
||||
return fetch(`/alarms/alarm-rules`);
|
||||
};
|
||||
|
||||
export const createTopic = (params: ITopic) => {
|
||||
return fetch('/orders/topic', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const getRegions = (clusterId: number) => {
|
||||
return fetch(`/admin/${clusterId}/regions`);
|
||||
};
|
||||
|
||||
export const delRegion = (regionId: number) => {
|
||||
return fetch(`/admin/regions/${regionId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
};
|
||||
|
||||
export const addRegion = (params: IRegionData) => {
|
||||
return fetch(`/admin/regions/region`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const modifyRegion = (params: IRegionData) => {
|
||||
return fetch(`/admin/regions/region`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const topicDilatation = (params: IDilation) => {
|
||||
return fetch('/admin/utils/topic/dilatation', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const addAlarm = (params: IAlarmBase) => {
|
||||
return fetch('/alarms/alarm-rule', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const modifyAlarm = (params: IAlarmBase) => {
|
||||
return fetch('/alarms/alarm-rule', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteAlarm = (alarmRuleId: number) => {
|
||||
return fetch(`/alarms/alarm-rule?alarmRuleId=${alarmRuleId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
};
|
||||
|
||||
export const recallOrder = (orderId: number) => {
|
||||
return fetch(`/orders/topic?orderId=${orderId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
};
|
||||
|
||||
export const getBrokerBaseInfo = (clusterId: number, brokerId: number) => {
|
||||
return fetch(`/${clusterId}/brokers/${brokerId}/basic-info`);
|
||||
};
|
||||
|
||||
export const getBrokerList = (clusterId: number) => {
|
||||
return fetch(`/${clusterId}/brokers/overview`);
|
||||
};
|
||||
|
||||
export const getTopicBroker = (clusterId: number, topicName: string) => {
|
||||
return fetch(`/${clusterId}/topics/${topicName}/brokers`);
|
||||
};
|
||||
|
||||
export const getTopicPartition = (clusterId: number, topicName: string) => {
|
||||
return fetch(`/${clusterId}/topics/${topicName}/partitions`);
|
||||
};
|
||||
|
||||
export const getBrokerNetwork = (clusterId: number) => {
|
||||
return fetch(`/clusters/${clusterId}/metrics`);
|
||||
};
|
||||
|
||||
export const getOneBrokerNetwork = (clusterId: number, brokerId: number) => {
|
||||
return fetch(`/${clusterId}/brokers/${brokerId}/metrics`);
|
||||
};
|
||||
|
||||
export const getBrokerPartition = (clusterId: number) => {
|
||||
return fetch(`/${clusterId}/brokers/overall`);
|
||||
};
|
||||
|
||||
export const getBrokerTopic = (clusterId: number, brokerId: number) => {
|
||||
return fetch(`/${clusterId}/brokers/${brokerId}/topics`);
|
||||
};
|
||||
|
||||
export const getController = (clusterId: number) => {
|
||||
return fetch(`/clusters/${clusterId}/controller-history`);
|
||||
};
|
||||
|
||||
export const getConsumeGroup = (clusterId: number, consumerGroup: string, location: string) => {
|
||||
return fetch(`/${clusterId}/consumer/${consumerGroup}/topics?location=${location}`);
|
||||
};
|
||||
|
||||
export const newCluster = (cluster: INewCluster) => {
|
||||
return fetch(`/clusters`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(cluster),
|
||||
});
|
||||
};
|
||||
|
||||
export const modifyCluster = (cluster: INewCluster) => {
|
||||
return fetch(`/clusters`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(cluster),
|
||||
});
|
||||
};
|
||||
|
||||
export const getKafkaVersion = () => {
|
||||
return fetch(`/clusters/kafka-version`);
|
||||
};
|
||||
|
||||
export const getClusterMetricsHistory = (clusterId: number, startTime: string, endTime: string) => {
|
||||
return fetch(`/clusters/${clusterId}/metrics-history?startTime=${startTime}&endTime=${endTime}`);
|
||||
};
|
||||
|
||||
export const getPartitions = (clusterId: number, brokerId: number) => {
|
||||
return fetch(`/${clusterId}/brokers/${brokerId}/partitions`);
|
||||
};
|
||||
|
||||
export const getBrokerKeyMetrics = (clusterId: number, brokerId: number, startTime: string, endTime: string) => {
|
||||
return fetch(`/${clusterId}/brokers/${brokerId}/key-metrics?startTime=${startTime}&endTime=${endTime}`);
|
||||
};
|
||||
|
||||
export const getTask = (value?: number) => {
|
||||
return fetch(`/admin/migration/tasks${value ? '/' + value : ''}`);
|
||||
};
|
||||
|
||||
export const executeTask = (params: ITaskBase) => {
|
||||
return fetch('/admin/migration/tasks', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const modifyTask = (params: ITaskBase) => {
|
||||
return fetch('/admin/migration/tasks', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const getAdminTopicOrder = () => {
|
||||
return fetch('/admin/orders/topic');
|
||||
};
|
||||
|
||||
export const getAdminPartitionOrder = (orderId?: number) => {
|
||||
return fetch(`/admin/orders/partition${orderId ? '?orderId=' + orderId : ''}`);
|
||||
};
|
||||
|
||||
export const getRebalanceStatus = (clusterId: number) => {
|
||||
return fetch(`/admin/utils/rebalance/clusters/${clusterId}/status`);
|
||||
};
|
||||
|
||||
export const addRebalance = (params: IRebalance) => {
|
||||
return fetch('/admin/utils/rebalance', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const getBrokerTopicAnalyzer = (clusterId: number, brokerId: number) => {
|
||||
return fetch(`/${clusterId}/brokers/${brokerId}/analysis`);
|
||||
};
|
||||
|
||||
export const addTopicApprove = (params: IOrderTopic) => {
|
||||
return fetch('/admin/orders/topic', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const addAdminPartition = (params: IOrderTopic) => {
|
||||
return fetch('/admin/orders/partition', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const addPartitionApprove = (params: IOrderTopic) => {
|
||||
return fetch('/orders/partition', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const getUsers = () => {
|
||||
return fetch('/admin/accounts');
|
||||
};
|
||||
|
||||
export const addUser = (params: IUser) => {
|
||||
return fetch('/admin/accounts/account', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteUser = (username: string) => {
|
||||
return fetch(`/admin/accounts/account?username=${username}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
};
|
||||
|
||||
export const modifyUser = (params: IUser) => {
|
||||
return fetch('/admin/accounts/account', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const getLogin = () => {
|
||||
return fetch('/login/loginPage');
|
||||
};
|
||||
|
||||
export const userLogin = (params: IUser) => {
|
||||
return fetch('/login/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const userLogoff = (value: string) => {
|
||||
return fetch(`/login/logoff?username=${value}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
};
|
||||
|
||||
export const addSample = (params: ISample) => {
|
||||
const {clusterId, topicName, ...rest} = params;
|
||||
return fetch(`/${clusterId}/topics/${topicName}/sample`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(rest),
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteTopic = (params: IDeleteTopic) => {
|
||||
return fetch(`/admin/utils/topic`, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const modifyTopic = (params: ITopic) => {
|
||||
return fetch('/admin/utils/topic/config', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const resetOffset = (params: IOffset) => {
|
||||
return fetch('/consumers/offsets', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const getClustersBasic = () => {
|
||||
return fetch('/clusters/basic-info');
|
||||
};
|
||||
|
||||
export const getAlarmConstant = () => {
|
||||
return fetch('/alarms/alarm/constant');
|
||||
};
|
||||
|
||||
export const getTopicNameById = (clusterId: number) => {
|
||||
return fetch(`/${clusterId}/topics/topic-names`);
|
||||
};
|
||||
|
||||
export const deleteBroker = (clusterId: number, brokerId: number) => {
|
||||
return fetch(`/${clusterId}/brokers/${brokerId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
};
|
||||
|
||||
export const recallPartition = (orderId: number) => {
|
||||
return fetch(`/orders/partition?orderId=${orderId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
};
|
||||
|
||||
export const getBrokerNameList = (clusterId: number) => {
|
||||
return fetch(`/${clusterId}/brokers/broker-metadata`);
|
||||
};
|
||||
|
||||
export const adminCreateTopic = (params: ITopic) => {
|
||||
return fetch('/admin/utils/topic', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const getAdminTopicDetail = (clusterId: number, topicName: string) => {
|
||||
return fetch(`/admin/utils/${clusterId}/topics/${topicName}/detail`);
|
||||
};
|
||||
|
||||
export const getTopicMetriceInfo = (clusterId: number, topicName: string, startTime: string, endTime: string) => {
|
||||
return fetch(`/${clusterId}/topics/${topicName}/metrics-history?startTime=${startTime}&endTime=${endTime}`);
|
||||
};
|
||||
|
||||
export const getBrokerMetrics = (clusterId: number, brokerId: number, startTime: string, endTime: string) => {
|
||||
return fetch(`/${clusterId}/brokers/${brokerId}/metrics-history?startTime=${startTime}&endTime=${endTime}`);
|
||||
};
|
||||
|
||||
export const getTopicMetaData = (clusterId: number, topicName: string) => {
|
||||
return fetch(`/${clusterId}/topics/${topicName}/metadata`);
|
||||
};
|
||||
141
console/src/lib/charts-config.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { ISeriesOption, IOptionType, IClusterMetrics, IBrokerMetrics, IValueLabel } from 'types/base-type';
|
||||
import moment = require('moment');
|
||||
|
||||
export const getClusterMetricOption = (type: IOptionType, data: IClusterMetrics[]) => {
|
||||
let name;
|
||||
let series: ISeriesOption[];
|
||||
const date = data.map(i => moment(i.gmtCreate).format('YYYY-MM-DD HH:mm:ss'));
|
||||
const legend = type === 'byteIn/byteOut' ? ['bytesInPerSec', 'bytesOutPerSec'] :
|
||||
type === 'messageIn/totalProduceRequests' ? ['messagesInPerSec', 'totalProduceRequestsPerSec'] : [type];
|
||||
series = Array.from(legend, (item: IOptionType) => ({
|
||||
name: item,
|
||||
type: 'line',
|
||||
symbol: 'circle',
|
||||
data: data.map(i => {
|
||||
let seriesType = item as keyof IClusterMetrics;
|
||||
if (type !== 'byteIn/byteOut' && type !== 'messageIn/totalProduceRequests') {
|
||||
seriesType = item === 'byteRejected' ? 'bytesRejectedPerSec' : 'messagesInPerSec';
|
||||
}
|
||||
return Number(i[seriesType]);
|
||||
}),
|
||||
}));
|
||||
switch (type) {
|
||||
case 'byteRejected':
|
||||
name = 'B/s';
|
||||
break;
|
||||
case 'messageIn/totalProduceRequests':
|
||||
name = 'QPS';
|
||||
break;
|
||||
case 'messageIn':
|
||||
name = '条';
|
||||
break;
|
||||
default:
|
||||
if (series.map(i => isMB(i.data)).some(i => i === true)) {
|
||||
name = 'MB/s';
|
||||
series.map(i => {i.data = i.data.map(ele => (ele / (1024 * 1024)).toFixed(2)) as []; });
|
||||
} else {
|
||||
name = 'KB/s';
|
||||
series.map(i => {i.data = i.data.map(ele => (ele / 1024).toFixed(2)) as []; });
|
||||
}
|
||||
}
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
padding: 10,
|
||||
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||
borderColor: '#333',
|
||||
textStyle: {
|
||||
color: '#f3f3f3',
|
||||
fontSize: '12px',
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
boundaryGap: false,
|
||||
data: date,
|
||||
},
|
||||
legend: {
|
||||
data: legend,
|
||||
right: '1%',
|
||||
top: '10px',
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name,
|
||||
nameLocation: 'end',
|
||||
nameGap: 10,
|
||||
},
|
||||
grid: {
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
bottom: '3%',
|
||||
top: '40px',
|
||||
containLabel: true,
|
||||
},
|
||||
series,
|
||||
};
|
||||
};
|
||||
|
||||
export const getBrokerMetricOption = (charts: IValueLabel[], data: any) => {
|
||||
const legend: string[] = [];
|
||||
const seriesData = new Map();
|
||||
let date: string[] = [];
|
||||
const valueData = Object.keys(data).map(item => {
|
||||
legend.push(item);
|
||||
return data[item];
|
||||
});
|
||||
|
||||
if (valueData.length) {
|
||||
date = valueData[0].map((i: IBrokerMetrics) => moment(i.gmtCreate).format('HH:mm:ss'));
|
||||
}
|
||||
charts.forEach(item => {
|
||||
if (!item.value) return false;
|
||||
seriesData.set(item.value, legend.map((ele, index) => {
|
||||
return {
|
||||
name: ele,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
data: valueData[index].map((i: IBrokerMetrics) => i[item.value as keyof IBrokerMetrics]),
|
||||
};
|
||||
}));
|
||||
});
|
||||
return charts.map(i => {
|
||||
if (!i.value) return null;
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
padding: 10,
|
||||
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||
borderColor: '#333',
|
||||
textStyle: {
|
||||
color: '#f3f3f3',
|
||||
fontSize: '12px',
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
boundaryGap: false,
|
||||
data: date,
|
||||
},
|
||||
legend: {
|
||||
data: legend,
|
||||
right: '1%',
|
||||
top: '10px',
|
||||
},
|
||||
yAxis: {},
|
||||
grid: {
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
bottom: '3%',
|
||||
top: '40px',
|
||||
containLabel: true,
|
||||
},
|
||||
series: seriesData.get(i.value),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function isMB(arr: number[]) {
|
||||
const filterData = arr.filter(i => i !== 0);
|
||||
if (filterData.length) return filterData.reduce((cur, pre) => cur + pre) / filterData.length >= 100000;
|
||||
return false;
|
||||
}
|
||||
86
console/src/lib/fetch.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { notification } from 'component/antd';
|
||||
|
||||
const window = self.window;
|
||||
|
||||
export interface IRes {
|
||||
code: number;
|
||||
message: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
const checkStatus = (res: Response) => {
|
||||
if (res.status === 200 && res.redirected) {
|
||||
let url = res.url;
|
||||
|
||||
if (!/^http(s)?:\/\//.test(url)) {
|
||||
url = `${window.location.protocol}//${url}`;
|
||||
}
|
||||
|
||||
if (url) {
|
||||
window.location.href = `${url}?jumpto=${encodeURIComponent(window.location.href)}`;
|
||||
return null;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
const filter = (init: IInit) => (res: IRes) => {
|
||||
if (res.code === 401) {
|
||||
let url = res.data;
|
||||
|
||||
if (!/^http(s)?:\/\//.test(url)) {
|
||||
url = `${window.location.protocol}//${url}`;
|
||||
}
|
||||
|
||||
window.location.href = `${url}?jumpto=${encodeURIComponent(window.location.href)}`;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (res.code !== 0) {
|
||||
if (!init.errorNoTips) {
|
||||
notification.error({
|
||||
message: '错误',
|
||||
description: res.message || '服务器错误,请重试!',
|
||||
});
|
||||
}
|
||||
throw res;
|
||||
}
|
||||
|
||||
return res.data;
|
||||
};
|
||||
|
||||
const preFix = '/api/v1';
|
||||
|
||||
interface IInit extends RequestInit {
|
||||
errorNoTips?: boolean;
|
||||
body?: BodyInit | null | any;
|
||||
}
|
||||
|
||||
const csrfTokenMethod = ['POST', 'PUT', 'DELETE'];
|
||||
|
||||
export default function fetch(url: string, init?: IInit) {
|
||||
if (!init) init = {};
|
||||
|
||||
if (!init.credentials) init.credentials = 'include';
|
||||
if (init.body && typeof init.body === 'object') init.body = JSON.stringify(init.body);
|
||||
if (init.body && !init.method) init.method = 'POST';
|
||||
if (init.method) init.method = init.method.toUpperCase();
|
||||
|
||||
if (csrfTokenMethod.includes(init.method)) {
|
||||
init.headers = Object.assign({}, init.headers || {
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
}
|
||||
|
||||
let realUrl = url;
|
||||
if (!/^http(s)?:\/\//.test(url)) {
|
||||
realUrl = `${preFix}${url}`;
|
||||
}
|
||||
return window
|
||||
.fetch(realUrl, init)
|
||||
.then(res => checkStatus(res))
|
||||
.then((res) => res.json())
|
||||
.then(filter(init));
|
||||
}
|
||||
26
console/src/lib/url-parser.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
interface IMap {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const Url = {
|
||||
hash: {} as IMap,
|
||||
search: {} as IMap,
|
||||
} as {
|
||||
hash: IMap;
|
||||
search: IMap;
|
||||
[key: string]: IMap;
|
||||
};
|
||||
|
||||
window.location.hash.slice(1).split('&').map(str => {
|
||||
const kv = str.split('=');
|
||||
Url.hash[kv[0]] = kv[1];
|
||||
});
|
||||
|
||||
window.location.search.slice(1).split('&').map(str => {
|
||||
const kv = str.split('=');
|
||||
Url.search[kv[0]] = kv[1];
|
||||
});
|
||||
|
||||
return Url;
|
||||
};
|
||||
65
console/src/lib/utils.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { IFiler } from 'types/base-type';
|
||||
|
||||
export interface IMap {
|
||||
[index: string]: string;
|
||||
}
|
||||
interface ICookie {
|
||||
key: string;
|
||||
value?: string;
|
||||
time?: number;
|
||||
}
|
||||
export const getCookie = (key: string): string => {
|
||||
const map: IMap = {};
|
||||
document.cookie.split(';').map((kv) => {
|
||||
const d = kv.trim().split('=');
|
||||
map[d[0]] = d[1];
|
||||
return null;
|
||||
});
|
||||
return map[key];
|
||||
};
|
||||
|
||||
export const uuid = (): string => {
|
||||
return 'c' + `${Math.random()}`.slice(2);
|
||||
};
|
||||
|
||||
export const getRandomPassword = (len?: number) => {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
if (len) {
|
||||
let res = '';
|
||||
for (let i = 0; i < len; i++) {
|
||||
const id = Math.ceil(Math.random() * 62);
|
||||
res += chars[id];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
return Math.ceil(Math.random() * 100000);
|
||||
};
|
||||
|
||||
export const setCookie = (cData: ICookie[]) => {
|
||||
const date = new Date();
|
||||
cData.forEach(ele => {
|
||||
date.setTime(date.getTime() + (ele.time * 24 * 60 * 60 * 1000));
|
||||
const expires = 'expires=' + date.toUTCString();
|
||||
document.cookie = ele.key + '=' + ele.value + '; ' + expires + '; path=/';
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteCookie = (cData: string[]) => {
|
||||
setCookie(cData.map(i => ({key: i, value: '', time: -1})));
|
||||
};
|
||||
|
||||
export const handleTabKey = (key: string) => {
|
||||
location.hash = key;
|
||||
};
|
||||
|
||||
export const tableFilter = <T>(data: T[], name: keyof T): IFiler[] => {
|
||||
if (!data) return [];
|
||||
const obj: any = {};
|
||||
return data.reduce((cur, pre) => {
|
||||
if (!obj[pre[name]]) {
|
||||
obj[pre[name]] = true;
|
||||
cur.push({ text: pre[name], value: pre[name] });
|
||||
}
|
||||
return cur;
|
||||
}, []);
|
||||
};
|
||||
12
console/src/routers/index.htm
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>KafkaManager</title>
|
||||
<link rel="stylesheet" href="//at.alicdn.com/t/font_1251424_q66z80q0hio.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="modal"></div>
|
||||
</body>
|
||||
</html>
|
||||
17
console/src/routers/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { Provider } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import * as store from 'store';
|
||||
import Router from './router';
|
||||
|
||||
const renderApp = () => {
|
||||
ReactDOM.render(
|
||||
<Provider>
|
||||
<Router />
|
||||
</Provider>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
};
|
||||
|
||||
renderApp();
|
||||
71
console/src/routers/page/admin/index.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import 'component/antd';
|
||||
import { Header } from 'container/header';
|
||||
import { LeftMenu } from 'container/left-menu';
|
||||
import AllModalInOne from 'container/modal';
|
||||
import { TopicDetail } from 'container/topic-detail';
|
||||
import { BrokerDetail } from 'container/broker-detail';
|
||||
import { BrokerInfo } from 'container/broker-info';
|
||||
import { AdminHome } from 'container/admin-home';
|
||||
import AdminTopic from 'container/admin-topic';
|
||||
import { BrowserRouter as Router, Route } from 'react-router-dom';
|
||||
import { AdminOrder } from 'container/admin-order';
|
||||
import urlParser from 'lib/url-parser';
|
||||
import urlQuery from 'store/url-query';
|
||||
// import { AdminAlarm } from 'container/admin-alarm';
|
||||
import { AdminRegion } from 'container/admin-region';
|
||||
import { AdminController } from 'container/admin-controller';
|
||||
import { AdminConsume } from 'container/admin-consume';
|
||||
// import { ConsumerDetail } from 'container/admin-consume/detail';
|
||||
import { AdminOperation } from 'container/admin-operation';
|
||||
import { UserManage } from 'container/admin-usermanage';
|
||||
import ModifyUser from 'container/modify-user';
|
||||
import AllDrawerInOne from 'container/drawer';
|
||||
import { ClusterDetail } from 'container/admin-home/cluster-detail';
|
||||
|
||||
export default class Home extends React.Component<any> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const search = urlParser().search;
|
||||
urlQuery.clusterId = Number(search.clusterId);
|
||||
urlQuery.brokerId = Number(search.brokerId);
|
||||
urlQuery.group = search.group;
|
||||
urlQuery.location = search.location;
|
||||
urlQuery.topicName = search.topic;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { match } = this.props;
|
||||
const page = match.url;
|
||||
return (
|
||||
<>
|
||||
<Header active="admin"/>
|
||||
<Router>
|
||||
<div className="core-container">
|
||||
<LeftMenu page={page} mode="admin" />
|
||||
<div className="content-container">
|
||||
<Route path="/admin" exact={true} component={AdminHome} />
|
||||
<Route path="/admin/broker_detail" exact={true} component={BrokerDetail} />
|
||||
<Route path="/admin/controller" exact={true} component={AdminController} />
|
||||
<Route path="/admin/consumer" exact={true} component={AdminConsume} />
|
||||
{/* <Route path="/admin/consumer_detail" exact={true} component={ConsumerDetail} /> */}
|
||||
<Route path="/admin/broker_info" exact={true} component={BrokerInfo} />
|
||||
<Route path="/admin/region" exact={true} component={AdminRegion} />
|
||||
<Route path="/admin/topic" exact={true} component={AdminTopic} />
|
||||
<Route path="/admin/topic_detail" exact={true} component={TopicDetail} />
|
||||
<Route path="/admin/order" exact={true} component={AdminOrder} />
|
||||
{/* <Route path="/admin/alarm" exact={true} component={AdminAlarm} /> */}
|
||||
<Route path="/admin/operation" exact={true} component={AdminOperation} />
|
||||
<Route path="/admin/user_manage" exact={true} component={UserManage} />
|
||||
<Route path="/admin/modify_user" exact={true} component={ModifyUser} />
|
||||
<Route path="/admin/cluster_detail" exact={true} component={ClusterDetail} />
|
||||
</div>
|
||||
</div>
|
||||
</Router>
|
||||
<AllModalInOne />
|
||||
<AllDrawerInOne />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
92
console/src/routers/page/home/index.less
Normal file
@@ -0,0 +1,92 @@
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
html, body, .router-nav {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: PingFangSC-Regular;
|
||||
}
|
||||
|
||||
[class*=k-icon-] {
|
||||
font-family: kafka-manager;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
vertical-align: baseline;
|
||||
display: inline-block;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-variant: normal normal;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.core-container {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
bottom: 0;
|
||||
top: 64px;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.didi-theme {
|
||||
color: #f38031;
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th, .ant-table-tbody > tr > td {
|
||||
padding: 13px;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.ant-select-dropdown-menu-item-active,
|
||||
.ant-select-dropdown-menu-item:hover {
|
||||
background: #f3f3f3;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
background: rgba(243, 244, 245, 1);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mb-24 {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th .ant-table-filter-icon {
|
||||
right: initial;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #2fc25b;
|
||||
}
|
||||
.fail {
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
43
console/src/routers/page/home/index.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import 'component/antd';
|
||||
import './index.less';
|
||||
import { Header } from 'container/header';
|
||||
import { LeftMenu } from 'container/left-menu';
|
||||
import { UserHome } from 'container/user-home';
|
||||
import { TopicDetail } from 'container/topic-detail';
|
||||
import AllModalInOne from 'container/modal';
|
||||
import AllDrawerInOne from 'container/drawer';
|
||||
import { MyOrder } from 'container/my-order';
|
||||
import { Alarm } from 'container/alarm';
|
||||
import { Consumer } from 'container/consumer';
|
||||
import ModifyUser from 'container/modify-user';
|
||||
import { BrowserRouter as Router, Route } from 'react-router-dom';
|
||||
|
||||
export default class Home extends React.Component<any> {
|
||||
|
||||
public render() {
|
||||
const { match } = this.props;
|
||||
const page = match.url;
|
||||
return (
|
||||
<>
|
||||
<Header active="user"/>
|
||||
<Router>
|
||||
<div className="core-container">
|
||||
<LeftMenu page={page} />
|
||||
<div className="content-container">
|
||||
<Route path="/" exact={true} component={UserHome} />
|
||||
<Route path="/user/topic_detail" exact={true} component={TopicDetail} />
|
||||
<Route path="/user/consumer" exact={true} component={Consumer} />
|
||||
<Route path="/user/my_order" exact={true} component={MyOrder} />
|
||||
<Route path="/user/alarm" exact={true} component={Alarm} />
|
||||
<Route path="/user/modify_user" exact={true} component={ModifyUser} />
|
||||
</div>
|
||||
</div>
|
||||
</Router>
|
||||
<AllModalInOne />
|
||||
<AllDrawerInOne />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
89
console/src/routers/page/login/index.less
Normal file
@@ -0,0 +1,89 @@
|
||||
.l-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-size: 100% 100%;
|
||||
top: -20px;
|
||||
.top-title {
|
||||
width: 320px;
|
||||
height: 57px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.f-container {
|
||||
padding: 20px 30px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0px 0px 6px 2px #999;
|
||||
overflow: hidden;
|
||||
transition: all 500ms ease-in-out;
|
||||
width:540px;
|
||||
height:320px;
|
||||
background:rgba(255,255,255,1);
|
||||
box-shadow:0px 2px 16px 0px rgba(0,0,0,0.1);
|
||||
border-radius:8px;
|
||||
position: relative;
|
||||
|
||||
.item-style {
|
||||
width:400px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.btn-style {
|
||||
width:400px;
|
||||
height:56px;
|
||||
background:rgba(255,129,0,1);
|
||||
border-radius:4px;
|
||||
text-align: center;
|
||||
}
|
||||
.ant-input-affix-wrapper {
|
||||
font-size: 14px;
|
||||
}
|
||||
.ant-input-suffix {
|
||||
top: 40%;
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.title {
|
||||
font-size: 20px;
|
||||
line-height: 35px;
|
||||
margin-bottom: 26px;
|
||||
transition: all 500ms ease-in-out;
|
||||
text-align: center;
|
||||
}
|
||||
.ant-form {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
top: 17%;
|
||||
left: 13%;
|
||||
}
|
||||
.ant-input {
|
||||
height:56px;
|
||||
background:rgba(0,0,0,0.02);
|
||||
font-size: 18px;
|
||||
border-radius:4px;
|
||||
font-family:PingFangSC-Regular,PingFang SC;
|
||||
font-weight:400;
|
||||
color:rgba(153,153,153,1);
|
||||
line-height:25px;
|
||||
border: none;
|
||||
}
|
||||
.b-item {
|
||||
button {
|
||||
background-color: transparent;
|
||||
border-style: none;
|
||||
height: 56px;
|
||||
font-weight: 400;
|
||||
color: rgba(255,255,255,1);
|
||||
line-height: 25px;
|
||||
font-size: 18px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
&.show {
|
||||
.b-item {
|
||||
left: 219px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
73
console/src/routers/page/login/index.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Form, Row, Input, Button, notification, Icon } from 'component/antd';
|
||||
import { userLogin } from 'lib/api';
|
||||
import { setCookie } from 'lib/utils';
|
||||
import './index.less';
|
||||
import bgImgUrl from '../../../assets/image/login-bg.png';
|
||||
import kafkaImgUrl from '../../../assets/image/kafka-manager.png';
|
||||
|
||||
class Login extends React.Component<any> {
|
||||
public state = {
|
||||
show: '',
|
||||
};
|
||||
|
||||
public handleSubmit = (e: React.MouseEvent<any, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
this.props.form.validateFields((err: Error, values: any) => {
|
||||
if (err) return;
|
||||
userLogin(values).then((data) => {
|
||||
setCookie([{ key: 'username', value: data.username, time: 1 }, { key: 'role', value: data.role, time: 1 }]);
|
||||
notification.success({ message: '登录成功' });
|
||||
this.props.history.push('/');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public handleReset = () => {
|
||||
this.props.form.resetFields();
|
||||
}
|
||||
|
||||
public handleKeyPress = (e: any) => {
|
||||
if (e.nativeEvent.keyCode === 13) {
|
||||
this.handleSubmit(e);
|
||||
}
|
||||
}
|
||||
|
||||
public inputOnBlur = () => {
|
||||
this.setState({ show: 'show' });
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
return (
|
||||
<div className="l-container" style={{ background: `url(${bgImgUrl})`, backgroundSize: '100% 100%'}}>
|
||||
<img className="top-title" src={kafkaImgUrl} />
|
||||
<div className="f-container">
|
||||
<Form onSubmit={this.handleSubmit} labelAlign="left" >
|
||||
<Form.Item className="item-style">
|
||||
{getFieldDecorator('username')(
|
||||
<Input
|
||||
placeholder="请输入用户名"
|
||||
onChange={this.inputOnBlur}
|
||||
/>,
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item className="item-style">
|
||||
{getFieldDecorator('password')(
|
||||
<Input.Password
|
||||
placeholder="请输入密码"
|
||||
/>,
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item className="b-item btn-style">
|
||||
<Button htmlType="submit" onKeyPress={this.handleKeyPress}>登录</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create({ name: 'login' })(Login);
|
||||
49
console/src/routers/router.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom';
|
||||
import { hot } from 'react-hot-loader/root';
|
||||
import * as React from 'react';
|
||||
import { notification } from 'component/antd';
|
||||
import { getCookie } from 'lib/utils';
|
||||
|
||||
import Home from './page/home';
|
||||
import Admin from './page/admin';
|
||||
import Login from './page/login';
|
||||
|
||||
class RouterDom extends React.Component {
|
||||
public render() {
|
||||
return (
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route path="/login" component={Login} />
|
||||
|
||||
<RouteGuard path="/admin/:page" component={Admin} />
|
||||
<RouteGuard path="/admin/" exact={true} component={Admin} />
|
||||
<RouteGuard path="/user/:page" component={Home} />
|
||||
<RouteGuard path="/" component={Home} />
|
||||
|
||||
</Switch>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RouteGuard extends React.Component<any> {
|
||||
public isLogin = getCookie('username');
|
||||
public isAdmin = getCookie('role');
|
||||
public render() {
|
||||
const { component: Component, ...rest } = this.props;
|
||||
const renderRoute = (props: any) => {
|
||||
if (!this.isLogin) {
|
||||
return <Redirect to="/login" />;
|
||||
} else if (this.props.path.indexOf('admin') !== -1 && this.isAdmin === '0') {
|
||||
notification.error({ message: '暂无权限,请联系管理员' });
|
||||
window.history.go(-1);
|
||||
} else {
|
||||
return <Component {...props} />;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Route {...rest} render={renderRoute} />
|
||||
);
|
||||
}
|
||||
}
|
||||
export default hot(RouterDom);
|
||||
54
console/src/store/alarm.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { observable, action } from 'mobx';
|
||||
import { getAlarm, getAlarmConstant } from 'lib/api';
|
||||
import { IAlarmBase } from 'types/base-type';
|
||||
|
||||
export interface IAlarm extends IAlarmBase {
|
||||
gmtCreate: number;
|
||||
gmtModify: number;
|
||||
status: number;
|
||||
key?: number;
|
||||
}
|
||||
|
||||
export interface IConstant {
|
||||
conditionTypeList: [];
|
||||
ruleTypeList: [];
|
||||
notifyTypeList: [];
|
||||
metricTypeList: [];
|
||||
}
|
||||
|
||||
class Alarm {
|
||||
@observable
|
||||
public data: IAlarm[] = [];
|
||||
|
||||
@observable
|
||||
public alarmConstant: IConstant = null;
|
||||
|
||||
public curData: IAlarm = null;
|
||||
|
||||
public setCurData(data: IAlarm) {
|
||||
this.curData = data;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setAlarm(data: IAlarm[]) {
|
||||
this.data = data.map((d, i) => {
|
||||
d.key = i;
|
||||
return d;
|
||||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setAlarmConstant(data: IConstant) {
|
||||
this.alarmConstant = data;
|
||||
}
|
||||
|
||||
public getAlarm() {
|
||||
getAlarm().then(this.setAlarm);
|
||||
}
|
||||
|
||||
public getAlarmConstant() {
|
||||
getAlarmConstant().then(this.setAlarmConstant);
|
||||
}
|
||||
}
|
||||
|
||||
export const alarm = new Alarm();
|
||||
255
console/src/store/broker.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import { observable, action } from 'mobx';
|
||||
import { getBrokerBaseInfo, getBrokerList, getBrokerNetwork, getBrokerPartition, getOneBrokerNetwork, getBrokerTopic, getPartitions, getBrokerKeyMetrics, getBrokerTopicAnalyzer, getBrokerNameList } from 'lib/api';
|
||||
import { IFlowInfo } from 'component/flow-table';
|
||||
import { ITopic, IValueLabel } from 'types/base-type';
|
||||
import moment from 'moment';
|
||||
|
||||
export interface IBrokerBaseInfo {
|
||||
host: string;
|
||||
jmxPort: number;
|
||||
leaderCount: number;
|
||||
partitionCount: number;
|
||||
port: number;
|
||||
startTime: number | string;
|
||||
topicNum: number;
|
||||
}
|
||||
|
||||
export interface IBroker {
|
||||
brokerId: number;
|
||||
byteIn: number;
|
||||
byteOut: number;
|
||||
host: string;
|
||||
jmxPort: number;
|
||||
port: number;
|
||||
startTime: number;
|
||||
status: string;
|
||||
regionName: string;
|
||||
}
|
||||
|
||||
export interface IBrokerNetworkInfo extends IFlowInfo {
|
||||
produceRequest: number[];
|
||||
fetchConsumerRequest: number[];
|
||||
}
|
||||
|
||||
export interface IBrokerPartition extends IBroker {
|
||||
leaderCount: number;
|
||||
partitionCount: number;
|
||||
notUnderReplicatedPartitionCount: number;
|
||||
regionName: string;
|
||||
bytesInPerSec: number;
|
||||
}
|
||||
|
||||
export interface IPartitions {
|
||||
followerPartitionIdList: number[];
|
||||
leaderPartitionList: number[];
|
||||
topicName: string;
|
||||
underReplicated: boolean;
|
||||
underReplicatedPartitionsIdList: number[];
|
||||
}
|
||||
|
||||
export interface ITopicStatus {
|
||||
BytesOutPerSec_rate: number;
|
||||
BytesInPerSec_rate: number;
|
||||
}
|
||||
|
||||
export interface IBrokerMetrics {
|
||||
bytesIn?: number;
|
||||
bytesOut?: number;
|
||||
messagesIn?: number;
|
||||
totalFetchRequests?: number;
|
||||
totalProduceRequests?: number;
|
||||
}
|
||||
|
||||
export interface IAnalyzerData extends IBrokerMetrics {
|
||||
topicAnalysisVOList: [];
|
||||
baseTime?: number;
|
||||
brokerId?: number;
|
||||
}
|
||||
|
||||
export type IOverviewKey = 'partitionCount' | 'leaderCount' | 'notUnderReplicatedPartitionCount';
|
||||
|
||||
interface IBrokerOption {
|
||||
host: string;
|
||||
brokerId: string;
|
||||
}
|
||||
|
||||
class Broker {
|
||||
@observable
|
||||
public brokerBaseInfo: IBrokerBaseInfo = {} as IBrokerBaseInfo;
|
||||
|
||||
@observable
|
||||
public list: IBroker[] = [];
|
||||
|
||||
@observable
|
||||
public network: IBrokerNetworkInfo = null;
|
||||
|
||||
@observable
|
||||
public oneNetwork: IBrokerNetworkInfo = null;
|
||||
|
||||
@observable
|
||||
public partitions: IBrokerPartition[] = [];
|
||||
|
||||
@observable
|
||||
public topics: ITopic[] = [];
|
||||
|
||||
@observable
|
||||
public topicPartitionsInfo: [] = [];
|
||||
|
||||
@observable
|
||||
public openKeys: string[] = [];
|
||||
|
||||
@observable
|
||||
public realPartitions: IBrokerPartition[] = [];
|
||||
|
||||
@observable
|
||||
public analyzerData: IAnalyzerData = {topicAnalysisVOList: []};
|
||||
|
||||
@observable
|
||||
public viewType: IOverviewKey = 'partitionCount';
|
||||
|
||||
@observable
|
||||
public regionOption: any = ['all'];
|
||||
|
||||
@observable
|
||||
public endTime = moment();
|
||||
|
||||
@observable
|
||||
public startTime = moment().subtract(1, 'hour');
|
||||
|
||||
@observable
|
||||
public BrokerOptions: IValueLabel[] = [{ value: null, label: '请选择Broker' }];
|
||||
|
||||
@action.bound
|
||||
public setBrokerBaseInfo(data: IBrokerBaseInfo) {
|
||||
data.startTime = moment(data.startTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
this.brokerBaseInfo = data;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setBrokerList(list: IBroker[]) {
|
||||
this.list = list;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setBrokerNetWork(network: IBrokerNetworkInfo) {
|
||||
if (!network) return false;
|
||||
this.network = network;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setBrokerPartition(pars: IBrokerPartition[]) {
|
||||
const res = new Map();
|
||||
this.partitions = pars.map(i => {
|
||||
i.status = i.notUnderReplicatedPartitionCount ? '是' : '否';
|
||||
return i;
|
||||
});
|
||||
this.realPartitions = pars;
|
||||
this.regionOption = pars.filter((a) => !res.has(a.regionName) && res.set(a.regionName, 1));
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setOneBrokerNetwork(network: IBrokerNetworkInfo) {
|
||||
this.oneNetwork = network;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setBrokerTopic(topics: ITopic[]) {
|
||||
this.topics = topics;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setPartitionsInfo(pI: []) {
|
||||
this.topicPartitionsInfo = pI;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public handleOpen(key: string) {
|
||||
if (this.openKeys.includes(key)) {
|
||||
this.openKeys = this.openKeys.filter(k => k !== key);
|
||||
} else {
|
||||
this.openKeys.push(key);
|
||||
this.openKeys = this.openKeys.slice(0);
|
||||
}
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public handleOverview(type: IOverviewKey) {
|
||||
this.viewType = type;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public filterSquare(type: string) {
|
||||
this.realPartitions = this.partitions;
|
||||
if (type !== 'all') {
|
||||
this.realPartitions = this.partitions.filter(i => i.regionName === type);
|
||||
}
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setBrokerTopicAnalyzer(data: IAnalyzerData) {
|
||||
for (const item of Object.keys(data)) {
|
||||
if (item === 'bytesIn' || item === 'bytesOut') data[item] = +(data[item] / (1024 * 1024)).toFixed(2);
|
||||
}
|
||||
this.analyzerData = data;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public changeStartTime(value: moment.Moment) {
|
||||
this.startTime = value;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public changeEndTime(value: moment.Moment) {
|
||||
this.endTime = value;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setBrokerOptions(data: IBrokerOption[]) {
|
||||
this.BrokerOptions = data.map(i => ({
|
||||
label: `BrokerID: ${i.brokerId}, Host: ${i.host}`,
|
||||
value: i.brokerId,
|
||||
}));
|
||||
}
|
||||
|
||||
public getBrokerBaseInfo(clusterId: number, brokerId: number) {
|
||||
getBrokerBaseInfo(clusterId, brokerId).then(this.setBrokerBaseInfo);
|
||||
}
|
||||
|
||||
public getBrokerList(clusterId: number) {
|
||||
getBrokerList(clusterId).then(this.setBrokerList);
|
||||
}
|
||||
|
||||
public getBrokerNetwork(clusterId: number) {
|
||||
getBrokerNetwork(clusterId).then(this.setBrokerNetWork);
|
||||
}
|
||||
|
||||
public getBrokerPartition(clusterId: number) {
|
||||
getBrokerPartition(clusterId).then(this.setBrokerPartition);
|
||||
}
|
||||
|
||||
public getOneBrokerNetwork(clusterId: number, brokerId: number) {
|
||||
getOneBrokerNetwork(clusterId, brokerId).then(this.setOneBrokerNetwork);
|
||||
}
|
||||
|
||||
public getBrokerTopic(clusterId: number, brokerId: number) {
|
||||
getBrokerTopic(clusterId, brokerId).then(this.setBrokerTopic);
|
||||
}
|
||||
|
||||
public getPartitions(clusterId: number, brokerId: number) {
|
||||
getPartitions(clusterId, brokerId).then(this.setPartitionsInfo);
|
||||
}
|
||||
|
||||
public getBrokerKeyMetrics(clusterId: number, brokerId: number, startTime: string, endTime: string) {
|
||||
return getBrokerKeyMetrics(clusterId, brokerId, startTime, endTime);
|
||||
}
|
||||
|
||||
public getBrokerTopicAnalyzer(clusterId: number, brokerId: number) {
|
||||
getBrokerTopicAnalyzer(clusterId, brokerId).then(this.setBrokerTopicAnalyzer);
|
||||
}
|
||||
|
||||
public initBrokerOptions = (clusterId: number) => {
|
||||
getBrokerNameList(clusterId).then(this.setBrokerOptions);
|
||||
}
|
||||
}
|
||||
|
||||
export const broker = new Broker();
|
||||
128
console/src/store/cluster.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { observable, action } from 'mobx';
|
||||
import { getClusters, getKafkaVersion, getClusterMetricsHistory, getRebalanceStatus, getClustersBasic, getTopicMetriceInfo, getBrokerMetrics } from 'lib/api';
|
||||
import { getClusterMetricOption } from 'lib/charts-config';
|
||||
import { IClusterData, IClusterMetrics, IOptionType } from 'types/base-type';
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
class Cluster {
|
||||
@observable
|
||||
public data: IClusterData[] = [];
|
||||
|
||||
@observable
|
||||
public active: number = null;
|
||||
|
||||
@observable
|
||||
public kafkaVersions: string[] = [];
|
||||
|
||||
@observable
|
||||
public startTime: moment.Moment;
|
||||
|
||||
@observable
|
||||
public endTime: moment.Moment;
|
||||
|
||||
@observable
|
||||
public clusterMetrics: IClusterMetrics[] = [];
|
||||
|
||||
@observable
|
||||
public leaderStatus: string = '';
|
||||
|
||||
@observable
|
||||
public type: IOptionType = 'byteIn/byteOut' ;
|
||||
|
||||
@action.bound
|
||||
public setData(data: IClusterData[]) {
|
||||
data.unshift({
|
||||
clusterId: -1,
|
||||
clusterName: '所有集群',
|
||||
} as IClusterData);
|
||||
this.data = data;
|
||||
this.active = (this.data[0] || { clusterId: null }).clusterId;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public changeCluster(data: number) {
|
||||
this.active = data;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setKafkaVersion(data: string[]) {
|
||||
this.kafkaVersions = data;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setChartsOpton(data: IClusterMetrics[]) {
|
||||
this.clusterMetrics = data;
|
||||
return this.changeType(this.type);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public changeType(type: IOptionType) {
|
||||
cluster.type = type;
|
||||
return getClusterMetricOption(type, this.clusterMetrics);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public changeStartTime(value: moment.Moment) {
|
||||
this.startTime = value;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public changeEndTime(value: moment.Moment ) {
|
||||
this.endTime = value;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public initTime() {
|
||||
this.startTime = moment().subtract(1, 'hour');
|
||||
this.endTime = moment();
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setLeaderStatus(type: string) {
|
||||
this.leaderStatus = type;
|
||||
}
|
||||
|
||||
public getClusters() {
|
||||
getClusters().then(this.setData);
|
||||
}
|
||||
|
||||
public getClustersBasic() {
|
||||
getClustersBasic().then(this.setData);
|
||||
}
|
||||
|
||||
public getKafkaVersions() {
|
||||
getKafkaVersion().then(this.setKafkaVersion);
|
||||
}
|
||||
|
||||
public getClusterMetricsHistory(clusterId: number) {
|
||||
return getClusterMetricsHistory(clusterId,
|
||||
this.startTime.format('x'),
|
||||
this.endTime.format('x')).then(this.setChartsOpton);
|
||||
}
|
||||
|
||||
public getMetriceInfo(clusterId: number, topicName: string) {
|
||||
return getTopicMetriceInfo(clusterId, topicName,
|
||||
this.startTime.format('x'),
|
||||
this.endTime.format('x')).then(this.setChartsOpton);
|
||||
}
|
||||
|
||||
public getRebalance(clusterId: number) {
|
||||
return getRebalanceStatus(clusterId).then((type) => {
|
||||
this.setLeaderStatus(type);
|
||||
if (type === 'RUNNING') {
|
||||
setTimeout(() => {
|
||||
this.getRebalance(clusterId);
|
||||
}, 1000 * 2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getBrokerMetrics(clusterId: number, brokerId: number) {
|
||||
return getBrokerMetrics(clusterId, brokerId,
|
||||
this.startTime.format('x'),
|
||||
this.endTime.format('x')).then(this.setChartsOpton);
|
||||
}
|
||||
}
|
||||
|
||||
export const cluster = new Cluster();
|
||||
80
console/src/store/consume.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { observable, action } from 'mobx';
|
||||
import { IConsumeInfo } from './topic';
|
||||
import { getConsumeInfo, resetOffset, getConsumeGroup } from 'lib/api';
|
||||
import { IOffset } from 'types/base-type';
|
||||
|
||||
export interface IConsumeDetail {
|
||||
clientId: string;
|
||||
clusterId: number;
|
||||
consumeOffset: number;
|
||||
consumerGroup: string;
|
||||
lag: number;
|
||||
location: string;
|
||||
partitionId: number;
|
||||
partitionOffset: number;
|
||||
topicName: string;
|
||||
}
|
||||
|
||||
export interface IOffsetlist {
|
||||
offset: number;
|
||||
partitionId: number;
|
||||
}
|
||||
|
||||
class Consume {
|
||||
@observable
|
||||
public data: IConsumeInfo[] = [];
|
||||
|
||||
@observable
|
||||
public consumeGroupDetail: IConsumeDetail[] = [];
|
||||
|
||||
@observable
|
||||
public offsetList: IOffsetlist[] = [{ offset: null, partitionId: null }];
|
||||
|
||||
@observable
|
||||
public consumerTopic: any[] = [];
|
||||
|
||||
@action.bound
|
||||
public setData(data: IConsumeInfo[]) {
|
||||
this.data = data.map(i => {
|
||||
i.location = i.location.toLowerCase();
|
||||
return i;
|
||||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public selectChange(index: number, value: number) {
|
||||
this.offsetList[index].partitionId = value;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public inputChange(index: number, event: any) {
|
||||
this.offsetList[index].offset = event.target.value;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public handleList(index?: number) {
|
||||
index ? this.offsetList.splice(index, 1) : this.offsetList.push({ offset: null, partitionId: null });
|
||||
this.offsetList = this.offsetList.slice(0);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public offsetPartition(params: IOffset, type?: number) {
|
||||
if (type) return resetOffset(Object.assign(params, { offsetList: this.offsetList }));
|
||||
return resetOffset(params);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setConsumerTopic(topic: string[]) {
|
||||
this.consumerTopic = topic.map(i => ({ topicName: i }));
|
||||
}
|
||||
|
||||
public getConsumeInfo(clusterId: number) {
|
||||
getConsumeInfo(clusterId).then(this.setData);
|
||||
}
|
||||
|
||||
public getConsumerTopic( clusterId: number, consumerGroup: string, location: string ) {
|
||||
getConsumeGroup(clusterId, consumerGroup, location).then(this.setConsumerTopic);
|
||||
}
|
||||
}
|
||||
|
||||
export const consume = new Consume();
|
||||
25
console/src/store/controller.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { observable, action } from 'mobx';
|
||||
import { getController } from 'lib/api';
|
||||
|
||||
export interface IController {
|
||||
brokerId: number;
|
||||
controllerTimestamp: number;
|
||||
controllerVersion: number;
|
||||
host: string;
|
||||
}
|
||||
|
||||
class Controller {
|
||||
@observable
|
||||
public data: IController[] = [];
|
||||
|
||||
@action.bound
|
||||
public setData(data: IController[]) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public getController(clusterId: number) {
|
||||
getController(clusterId).then(this.setData);
|
||||
}
|
||||
}
|
||||
|
||||
export const controller = new Controller();
|
||||
31
console/src/store/drawer.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { observable, action } from 'mobx';
|
||||
|
||||
class Drawer {
|
||||
@observable
|
||||
public id: string = null;
|
||||
|
||||
@observable
|
||||
public topicData: any = null;
|
||||
|
||||
@observable
|
||||
public offsetDetail: any = null;
|
||||
|
||||
@action.bound
|
||||
public showResetOffset(r: any) {
|
||||
this.id = 'showResetOffset';
|
||||
this.offsetDetail = r;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public showTopicSample({ clusterId, topicName }: any) {
|
||||
this.id = 'showTopicSample';
|
||||
this.topicData = { clusterId, topicName };
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public close() {
|
||||
this.id = null;
|
||||
}
|
||||
}
|
||||
|
||||
export const drawer = new Drawer();
|
||||
5
console/src/store/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { configure } from 'mobx';
|
||||
configure({ enforceActions: 'observed' });
|
||||
|
||||
export { modal } from './modal';
|
||||
export { drawer } from './drawer';
|
||||
147
console/src/store/modal.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { observable, action } from 'mobx';
|
||||
import { alarm, IAlarm } from './alarm';
|
||||
import { IRegionData } from './region';
|
||||
import { operation, ITask } from './operation';
|
||||
import { IClusterData, IBaseOrder, ITopic } from 'types/base-type';
|
||||
import { getAdminPartitionOrder, getAdminTopicDetail } from 'lib/api';
|
||||
import { IUserDetail } from './users';
|
||||
import { topic, IConsumeInfo } from './topic';
|
||||
|
||||
class Modal {
|
||||
@observable
|
||||
public id: string = null;
|
||||
|
||||
@observable
|
||||
public orderDetail: IBaseOrder = {} as IBaseOrder;
|
||||
|
||||
@observable
|
||||
public topicData: ITopic = null;
|
||||
|
||||
public topicDetail: IBaseOrder = null;
|
||||
public regionData: IRegionData = null;
|
||||
public currentCluster: IClusterData = {} as IClusterData;
|
||||
public userDetail: IUserDetail = null;
|
||||
public consumberGroup: IConsumeInfo = null;
|
||||
|
||||
@action.bound
|
||||
public showNewTopic(r: ITopic) {
|
||||
this.topicData = r;
|
||||
this.id = 'showNewTopic';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public showNewCluster() {
|
||||
this.id = 'showNewCluster';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public showModifyCluster(cluster: IClusterData) {
|
||||
this.id = 'showModifyCluster';
|
||||
this.currentCluster = cluster;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setTopic(data: ITopic) {
|
||||
this.topicData = data;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public showAdimTopic(r: ITopic) {
|
||||
this.id = 'showAdimTopic';
|
||||
this.topicData = r;
|
||||
if (r) {
|
||||
getAdminTopicDetail(r.clusterId, r.topicName).then(this.setTopic);
|
||||
topic.getTopicMetaData(r.clusterId, r.topicName);
|
||||
}
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public showAlarm(r: IAlarm) {
|
||||
alarm.setCurData(r);
|
||||
this.id = 'showAlarm';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public showRegion(r: IRegionData) {
|
||||
this.id = 'showRegion';
|
||||
this.regionData = r;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public showExpandTopic(data: IBaseOrder) {
|
||||
this.topicDetail = data;
|
||||
this.id = 'showExpandTopic';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public showExpandAdmin(data: IBaseOrder) {
|
||||
this.topicDetail = data;
|
||||
this.id = 'showExpandAdmin';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public showResetOffset() {
|
||||
this.id = 'showResetOffset';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public showLeaderRebalance() {
|
||||
this.id = 'showLeaderRebalance';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public showTask(value: ITask, type?: string) {
|
||||
this.id = type === 'detail' ? 'showTaskDetail' : 'showTask';
|
||||
value ? operation.getTaskDetail(value.taskId) : operation.setTaskDetail(value);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public showOrderApprove(value: any, type: string) {
|
||||
this.id = type === 'showOrderApprove' ? 'showOrderApprove' : 'showOrderDetail';
|
||||
this.orderDetail = value;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setDetail(data: any) {
|
||||
if (data[0]) this.orderDetail = data[0];
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public showPartition(value: any, type: string) {
|
||||
this.orderDetail = value;
|
||||
this.id = type === 'showPartition' ? 'showPartition' : 'showPartitionDetail';
|
||||
getAdminPartitionOrder(value.orderId).then(this.setDetail);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public showNewUser(data: IUserDetail) {
|
||||
this.userDetail = data;
|
||||
this.id = 'showNewUser';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public shoeTopicConfig(data: IBaseOrder) {
|
||||
this.topicDetail = data;
|
||||
this.id = 'shoeTopicConfig';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public showAlarmModify(r: IAlarm) {
|
||||
alarm.setCurData(r);
|
||||
this.id = 'showAlarmModify';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public showConsumerTopic(r: IConsumeInfo) {
|
||||
this.consumberGroup = r;
|
||||
this.id = 'showConsumerTopic';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public close() {
|
||||
this.id = null;
|
||||
this.currentCluster = {} as IClusterData;
|
||||
}
|
||||
}
|
||||
|
||||
export const modal = new Modal();
|
||||
66
console/src/store/operation.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { observable, action } from 'mobx';
|
||||
import { getTask, getRegions } from 'lib/api';
|
||||
import { ITaskBase } from 'types/base-type';
|
||||
|
||||
interface IReassign {
|
||||
[propname: string]: number[];
|
||||
}
|
||||
|
||||
interface IMigration {
|
||||
[index: string]: number;
|
||||
}
|
||||
|
||||
export const taskMap = ['待执行', '执行中', '迁移成功', '迁移失败', '已撤销'];
|
||||
|
||||
export interface ITask extends ITaskBase {
|
||||
clusterName: string;
|
||||
gmtCreate: number;
|
||||
operator: string;
|
||||
reassignmentMap?: IReassign;
|
||||
migrationStatus?: IMigration;
|
||||
regionList?: Array<[string, number[]]>;
|
||||
}
|
||||
|
||||
class Operation {
|
||||
@observable
|
||||
public tasks: ITask[] = null;
|
||||
|
||||
@observable
|
||||
public taskDetail: ITask = null;
|
||||
|
||||
@observable
|
||||
public RegionOptions: any[] = ['请选择集群'];
|
||||
|
||||
@action.bound
|
||||
public setTask(data: ITask[]) {
|
||||
this.tasks = data;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setTaskDetail(data: ITask) {
|
||||
if (data) data.regionList = Object.keys(data.reassignmentMap).map(i => [i, data.reassignmentMap[i]]);
|
||||
this.taskDetail = data;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setRegionOptions(data: any) {
|
||||
this.RegionOptions = data.map((i: any) => ({
|
||||
value: i.regionId,
|
||||
label: i.regionName,
|
||||
}));
|
||||
}
|
||||
|
||||
public getTask() {
|
||||
getTask().then(this.setTask);
|
||||
}
|
||||
|
||||
public initRegionOptions = (clusterId: number) => {
|
||||
getRegions(clusterId).then(this.setRegionOptions);
|
||||
}
|
||||
|
||||
public getTaskDetail(value: number) {
|
||||
getTask(value).then(this.setTaskDetail);
|
||||
}
|
||||
}
|
||||
|
||||
export const operation = new Operation();
|
||||