This commit is contained in:
zengqiao
2020-03-19 17:59:34 +08:00
commit 229140f067
407 changed files with 46207 additions and 0 deletions

9326
console/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

45
console/package.json Normal file
View 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
View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

5
console/src/assets/image/images.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View 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,
};

View 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} />
);
}
}

View 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>
</>
);
}
}

View 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;
}
}

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

View 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>
</>
);
}
}

View 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>
</>
);
}
}

View File

@@ -0,0 +1,9 @@
.right-flow {
.k-abs {
right: 24px;
cursor: pointer;
& > i {
margin-right: 5px;
}
}
}

View 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}&#4`}>{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>
</>
);
}
}

View 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>
</>
);
}
}

View 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;
}
}

View 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>
</>
);
}
}

View 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>
</>
);
}
}

View 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);

View 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;
}

View 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"
/>
</>
);
}
}

View 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>
</>
);
}
}

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

View 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>
</>
);
}
}

View 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}
/>
);
}
}

View 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 InMB/ 秒)',
bytesOut: 'Bytes OutMB/ 秒)',
messagesIn: 'Messages In条)',
totalFetchRequests: 'Total Fetch RequestsQPS)',
totalProduceRequests: 'Total Produce RequestsQPS)',
};

View 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;
}
}
}

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

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

View 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} />
</>
);
}
}

View 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>
</>
);
}
}

View 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>
</>
);
}
}

View File

@@ -0,0 +1,5 @@
export const selecOptions = {
partitionCount: '分区数量',
leaderCount: 'leader数量',
notUnderReplicatedPartitionCount: '副本状态',
};

View 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;
}
}

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

View 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),
};
}
}

View 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%;
}
}
}
}

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

View 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;
}
}
}

View 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}
</>
);
}
}

View 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);

View 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);

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

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

View 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;
};

View 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;
}
}
}
}
}

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

View 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);

View 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);

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

View 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);

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

View 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;
}
}
}

View 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}
</>
);
}
}

View 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);

View 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);

View 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);

View 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);

View 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);

View 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);

View 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>
&nbsp;
<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' }}>
&nbsp;
<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);

View 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);

View 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);

View 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);

View 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;
}
}
}
}
}

View 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);

View 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,
});
}
}

View 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>
</>
);
}
}

View 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;
}

View 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>
</>
);
}
}

View 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;
}
}
}

View 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
View 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`);
};

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

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

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

View 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();

View 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 />
</>
);
}
}

View 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;
}

View 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 />
</>
);
}
}

View 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;
}
}
}
}

View 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);

View 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);

View 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
View 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();

View 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();

View 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();

View 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();

View 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();

View 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
View 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();

View 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();

Some files were not shown because too many files have changed in this diff Show More