kafka-manager 2.0

This commit is contained in:
zengqiao
2020-09-28 15:46:34 +08:00
parent 28d985aaf1
commit c6e4b60424
1253 changed files with 82183 additions and 37179 deletions

View File

@@ -0,0 +1,259 @@
import * as React from 'react';
import { Table, notification, Button, Divider, Popconfirm } from 'component/antd';
import { observer } from 'mobx-react';
import { pagination } from 'constants/table';
import { IBrokerData, IEnumsMap, IMetaData } from 'types/base-type';
import { admin } from 'store/admin';
import { tableFilter, transBToMB } from 'lib/utils';
import { SearchAndFilterContainer } from 'container/search-filter';
import { DoughnutChart } from 'component/chart';
import { LeaderRebalanceWrapper } from 'container/modal/admin/leader-rebalance';
import { timeFormat } from 'constants/strategy';
import Url from 'lib/url-parser';
import moment from 'moment';
import './index.less';
@observer
export class ClusterBroker extends SearchAndFilterContainer {
public clusterId: number;
public clusterName: string;
public state = {
searchKey: '',
filterPeakFlowVisible: false,
filterReplicatedVisible: false,
filterRegionVisible: false,
filterStatusVisible: false,
reblanceVisible: false,
};
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
}
public renderBrokerData(clusterBroker: IBrokerData[]) {
let peakFlow = [] as IEnumsMap[];
peakFlow = admin.peakFlowStatusList ? admin.peakFlowStatusList : peakFlow;
const peakFlowStatus = Object.assign({
title: '峰值状态',
dataIndex: 'peakFlowStatus',
key: 'peakFlowStatus',
width: '8%',
filters: peakFlow.map(ele => ({ text: ele.message, value: ele.code + '' })),
onFilter: (value: string, record: IBrokerData) => record.peakFlowStatus === +value,
render: (value: number) => {
let messgae: string;
peakFlow.map(ele => {
if (ele.code === value) {
messgae = ele.message;
}
});
return (
<span>{messgae}</span>
);
},
}, this.renderColumnsFilter('filterPeakFlowVisible'));
const underReplicated = Object.assign({
title: '副本状态',
dataIndex: 'underReplicated',
key: 'underReplicated',
width: '8%',
filters: [{ text: '同步', value: 'false' }, { text: '未同步', value: 'true' }],
onFilter: (value: string, record: IBrokerData) => record.underReplicated === (value === 'true') ? true : false,
render: (t: boolean) => t !== null ? <span className={t ? 'fail' : 'success'}>{t ? '未同步' : '同步'}</span> : '',
}, this.renderColumnsFilter('filterReplicatedVisible'));
const region = Object.assign({
title: 'regionName',
dataIndex: 'regionName',
key: 'regionName',
width: '10%',
filters: tableFilter<any>(clusterBroker, 'regionName'),
onFilter: (value: string, record: IBrokerData) => record.regionName === value,
}, this.renderColumnsFilter('filterRegionVisible'));
const status = Object.assign({
title: '状态',
dataIndex: 'status',
key: 'status',
width: '8%',
filters: [{ text: '未使用', value: '-1' }, { text: '使用中', value: '0' }],
onFilter: (value: string, record: IBrokerData) => record.status === Number(value),
render: (t: number) => <span className={t === 0 ? 'success' : 'fail'}>{t === 0 ? '使用中' : '未使用'}</span>,
}, this.renderColumnsFilter('filterStatusVisible'));
const columns = [
{
title: 'ID',
dataIndex: 'brokerId',
key: 'brokerId',
width: '5%',
sorter: (a: IBrokerData, b: IBrokerData) => b.brokerId - a.brokerId,
render: (text: number, record: IBrokerData) => {
// tslint:disable-next-line:max-line-length
const query = `clusterId=${this.clusterId}&brokerId=${record.brokerId}`;
const judge = record.underReplicated === false && record.status !== 0;
return (
<span className={judge ? 'fail' : ''}>
{
// tslint:disable-next-line:max-line-length
record.status === 0 ? <a href={`${this.urlPrefix}/admin/broker-detail?${query}`}>{text}</a>
: <a style={{ cursor: 'not-allowed', color: '#999' }}>{text}</a>}
</span>);
},
},
{
title: '主机',
dataIndex: 'host',
key: 'host',
width: '10%',
sorter: (a: any, b: any) => a.host.charCodeAt(0) - b.host.charCodeAt(0),
},
{
title: 'Port',
dataIndex: 'port',
key: 'port',
width: '6%',
sorter: (a: IBrokerData, b: IBrokerData) => b.port - a.port,
},
{
title: 'JMX Port',
dataIndex: 'jmxPort',
key: 'jmxPort',
width: '7%',
sorter: (a: IBrokerData, b: IBrokerData) => b.jmxPort - a.jmxPort,
},
{
title: '启动时间',
dataIndex: 'startTime',
key: 'startTime',
width: '10%',
sorter: (a: IBrokerData, b: IBrokerData) => b.startTime - a.startTime,
render: (time: number) => moment(time).format(timeFormat),
},
{
title: 'Bytes InMB/s',
dataIndex: 'byteIn',
key: 'byteIn',
width: '10%',
sorter: (a: IBrokerData, b: IBrokerData) => b.byteIn - a.byteIn,
render: (t: number) => transBToMB(t),
},
{
title: 'Bytes OutMB/s',
dataIndex: 'byteOut',
key: 'byteOut',
width: '10%',
sorter: (a: IBrokerData, b: IBrokerData) => b.byteOut - a.byteOut,
render: (t: number) => transBToMB(t),
},
peakFlowStatus,
underReplicated,
region,
status,
{
title: '操作',
width: '10%',
render: (text: string, record: IBrokerData) => {
// tslint:disable-next-line:max-line-length
const query = `clusterId=${this.clusterId}&brokerId=${record.brokerId}`;
return ( // 0 监控中 可点击详情,不可删除 -1 暂停监控 不可点击详情,可删除
<>
{record.status === 0 ?
<a href={`${this.urlPrefix}/admin/broker-detail?${query}`} className="action-button"></a>
: <a style={{ cursor: 'not-allowed', color: '#999' }}></a>}
<Popconfirm
title="确定删除?"
onConfirm={() => this.deteleTopic(record)}
disabled={record.status === 0}
>
<a style={record.status === 0 ? { cursor: 'not-allowed', color: '#999' } : {}}>
</a>
</Popconfirm>
</>
);
},
},
];
return (
<Table dataSource={clusterBroker} columns={columns} pagination={pagination} />
);
}
public deteleTopic(record: any) {
admin.deteleClusterBrokers(this.clusterId, record.brokerId).then(data => {
notification.success({ message: '删除成功' });
});
}
public reblanceInfo() {
this.setState({ reblanceVisible: true });
}
public handleVisible(val: boolean) {
this.setState({ reblanceVisible: val });
}
public async componentDidMount() {
await admin.getPeakFlowStatus();
admin.getClusterBroker(this.clusterId);
admin.getBrokersMetadata(this.clusterId);
admin.getBrokersStatus(this.clusterId);
}
public getData<T extends IBrokerData>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IBrokerData) =>
(item.brokerId !== undefined && item.brokerId !== null) && (item.brokerId + '').toLowerCase().includes(searchKey as string)
|| (item.host !== undefined && item.host !== null) && item.host.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public render() {
const content = this.props.basicInfo as IMetaData;
if (content) {
this.clusterName = content.clusterName;
}
return (
<>
<div className="diagram">
<div className="diagram-box">
<h2>使</h2>
<Divider className="hotspot-divider" />
{admin.peakValueList.length ? <DoughnutChart
getChartData={() => admin.getPeakFlowChartData(admin.peakValueList, admin.peakValueMap)}
/> : null}
</div>
<div className="diagram-box">
<h2></h2>
<Divider className="hotspot-divider" />
{admin.copyValueList.length ? <DoughnutChart
getChartData={() => admin.getSideStatusChartData(admin.copyValueList)}
/> : null}
</div>
</div>
<div className="leader-seacrh">
<div className="search-top">
{this.renderSearch('', '请输入ID或主机')}
<Button onClick={() => this.reblanceInfo()} type="primary">Leader Rebalance</Button>
</div>
{this.renderBrokerData(this.getData(admin.clusterBroker))}
</div>
{ this.state.reblanceVisible && <LeaderRebalanceWrapper
changeVisible={(val: boolean) => this.handleVisible(val)}
visible={this.state.reblanceVisible}
clusterId={this.clusterId}
clusterName={this.clusterName}
/> }
</>
);
}
}

View File

@@ -0,0 +1,131 @@
import * as React from 'react';
import { Table, Modal, Tooltip } from 'component/antd';
import { observer } from 'mobx-react';
import Url from 'lib/url-parser';
import { IOffset, IXFormWrapper } from 'types/base-type';
import { SearchAndFilterContainer } from 'container/search-filter';
import { pagination } from 'constants/table';
import { admin } from 'store/admin';
import { getConsumerDetails } from 'lib/api';
import './index.less';
@observer
export class ClusterConsumer extends SearchAndFilterContainer {
public clusterId: number;
public consumerDetails = [] as string[];
public state = {
searchKey: '',
detailsVisible: false,
};
public columns = [{
title: '消费组名称',
dataIndex: 'consumerGroup',
key: 'consumerGroup',
width: '70%',
sorter: (a: IOffset, b: IOffset) => a.consumerGroup.charCodeAt(0) - b.consumerGroup.charCodeAt(0),
render: (text: string) => <Tooltip placement="bottomLeft" title={text} >{text}</Tooltip>,
}, {
title: 'Location',
dataIndex: 'location',
key: 'location',
width: '20%',
render: (t: string) => t.toLowerCase(),
}, {
title: '操作',
key: 'operation',
width: '10%',
render: (t: string, item: IOffset) => {
return (<a onClick={() => this.getConsumeDetails(item)}></a>);
},
}];
private xFormModal: IXFormWrapper;
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
}
public getConsumeDetails(record: IOffset) {
getConsumerDetails(this.clusterId, record.consumerGroup, record.location).then((data: string[]) => {
this.consumerDetails = data;
this.setState({ detailsVisible: true });
});
}
public handleDetailsOk() {
this.setState({ detailsVisible: false });
}
public handleDetailsCancel() {
this.setState({ detailsVisible: false });
}
public getData<T extends IOffset>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IOffset) =>
(item.consumerGroup !== undefined && item.consumerGroup !== null) && item.consumerGroup.toLowerCase().includes(searchKey as string)
|| (item.location !== undefined && item.location !== null) && item.location.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public componentDidMount() {
admin.getClusterConsumer(this.clusterId);
}
public render() {
let details: any[];
details = this.consumerDetails ? this.consumerDetails.map((ele, index) => {
return {
key: index,
topicName: ele,
};
}) : [];
const consumptionColumns = [{
title: 'Topic名称',
dataIndex: 'topicName',
key: 'topicName',
}];
return (
<>
<div className="k-row">
<ul className="k-tab">
<li>{this.props.tab}</li>
{this.renderSearch()}
</ul>
<Table
columns={this.columns}
dataSource={this.getData(admin.consumerData)}
pagination={pagination}
rowKey="key"
/>
</div>
<Modal
title="消费的Topic"
visible={this.state.detailsVisible}
onOk={() => this.handleDetailsOk()}
onCancel={() => this.handleDetailsCancel()}
maskClosable={false}
footer={null}
>
<Table
columns={consumptionColumns}
dataSource={details}
pagination={pagination}
rowKey="key"
scroll={{ y: 260 }}
/>
</Modal>
</>
);
}
}

View File

@@ -0,0 +1,97 @@
import * as React from 'react';
import { SearchAndFilterContainer } from 'container/search-filter';
import { Table } from 'component/antd';
import { observer } from 'mobx-react';
import { pagination } from 'constants/table';
import Url from 'lib/url-parser';
import { IController } from 'types/base-type';
import { admin } from 'store/admin';
import './index.less';
import moment from 'moment';
import { timeFormat } from 'constants/strategy';
@observer
export class ClusterController extends SearchAndFilterContainer {
public clusterId: number;
public state = {
searchKey: '',
};
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
}
public getData<T extends IController>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IController) =>
(item.host !== undefined && item.host !== null) && item.host.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public renderController() {
const columns = [
{
title: 'BrokerId',
dataIndex: 'brokerId',
key: 'brokerId',
width: '30%',
sorter: (a: IController, b: IController) => b.brokerId - a.brokerId,
},
{
title: 'BrokerHost',
key: 'host',
dataIndex: 'host',
width: '30%',
render: (r: string, t: IController) => {
return (
<a href={`${this.urlPrefix}/admin/broker-detail?clusterId=${this.clusterId}&brokerId=${t.brokerId}`}>{r}
</a>
);
},
},
{
title: '变更时间',
dataIndex: 'timestamp',
key: 'timestamp',
width: '40%',
sorter: (a: IController, b: IController) => b.timestamp - a.timestamp,
render: (t: number) => moment(t).format(timeFormat),
},
];
return (
<Table
columns={columns}
dataSource={this.getData(admin.controllerHistory)}
pagination={pagination}
rowKey="key"
/>
);
}
public componentDidMount() {
admin.getControllerHistory(this.clusterId);
}
public render() {
return (
<div className="k-row">
<ul className="k-tab">
<li>{this.props.tab}</li>
{this.renderSearch('', '请输入Host')}
</ul>
{this.renderController()}
</div>
);
}
}

View File

@@ -0,0 +1,141 @@
import * as React from 'react';
import { PageHeader, Descriptions, Divider, Tooltip, Icon, Spin } from 'component/antd';
import { ILabelValue, IMetaData, IOptionType, IClusterReal } from 'types/base-type';
import { controlOptionMap, clusterTypeMap } from 'constants/status-map';
import { copyString } from 'lib/utils';
import { observer } from 'mobx-react';
import { admin } from 'store/admin';
import Url from 'lib/url-parser';
import moment from 'moment';
import './index.less';
import { StatusGraghCom } from 'component/flow-table';
import { renderTrafficTable, NetWorkFlow } from 'container/network-flow';
import { timeFormat } from 'constants/strategy';
interface IOverview {
basicInfo: IMetaData;
}
@observer
export class ClusterOverview extends React.Component<IOverview> {
public clusterId: number;
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
}
public clusterContent() {
const content = this.props.basicInfo as IMetaData;
const gmtCreate = moment(content.gmtCreate).format(timeFormat);
const clusterContent = [{
value: content.clusterName,
label: '集群名称',
}, {
value: clusterTypeMap[content.mode],
label: '集群类型',
}, {
value: gmtCreate,
label: '接入时间',
}];
const clusterInfo = [{
value: content.kafkaVersion,
label: 'kafka版本',
}, {
value: content.bootstrapServers,
label: 'Bootstrap Severs',
}, {
value: content.zookeeper,
label: 'Zookeeper',
}];
return (
<>
<div className="chart-title"></div>
<PageHeader className="detail" title="">
<Descriptions size="small" column={3}>
{clusterContent.map((item: ILabelValue, index: number) => (
<Descriptions.Item
key={index}
label={item.label}
>{item.value}
</Descriptions.Item>
))}
{clusterInfo.map((item: ILabelValue, index: number) => (
<Descriptions.Item key={index} label={item.label}>
<Tooltip placement="bottomLeft" title={item.value}>
<span className="overview-bootstrap">
<Icon
onClick={() => copyString(item.value)}
type="copy"
className="didi-theme overview-theme"
/>
<i className="overview-boot">{item.value}</i>
</span>
</Tooltip>
</Descriptions.Item>
))}
</Descriptions>
</PageHeader>
</>
);
}
public updateRealStatus = () => {
admin.getClusterRealTime(this.clusterId);
}
public onSelectChange(e: IOptionType) {
return admin.changeType(e);
}
public getOptionApi = () => {
return admin.getClusterMetrice(this.clusterId);
}
public componentDidMount() {
admin.getClusterRealTime(this.clusterId);
}
public renderHistoryTraffic() {
return (
<NetWorkFlow
key="1"
selectArr={controlOptionMap}
type={admin.type}
selectChange={(value: IOptionType) => this.onSelectChange(value)}
getApi={() => this.getOptionApi()}
/>
);
}
public renderTrafficInfo = () => {
return (
<Spin spinning={admin.realClusterLoading}>
{renderTrafficTable(this.updateRealStatus, StatusGragh)}
</Spin>
);
}
public render() {
return (
<>
<div className="base-info">
{this.clusterContent()}
{this.renderTrafficInfo()}
{this.renderHistoryTraffic()}
</div>
</>
);
}
}
@observer
export class StatusGragh extends StatusGraghCom<IClusterReal> {
public getData = () => {
return admin.clusterRealData;
}
public getLoading = () => {
return admin.realClusterLoading;
}
}

View File

@@ -0,0 +1,217 @@
import * as React from 'react';
import Url from 'lib/url-parser';
import { region } from 'store';
import { admin } from 'store/admin';
import { Table, notification, Tooltip, Popconfirm } from 'antd';
import { pagination, cellStyle } from 'constants/table';
import { observer } from 'mobx-react';
import { IClusterTopics } from 'types/base-type';
import { deleteClusterTopic } from 'lib/api';
import { SearchAndFilterContainer } from 'container/search-filter';
import { users } from 'store/users';
import { urlPrefix } from 'constants/left-menu';
import { transMSecondToHour } from 'lib/utils';
import './index.less';
import moment = require('moment');
import { ExpandPartitionFormWrapper } from 'container/modal/admin/expand-partition';
import { showEditClusterTopic } from 'container/modal/admin';
import { timeFormat } from 'constants/strategy';
@observer
export class ClusterTopic extends SearchAndFilterContainer {
public clusterId: number;
public clusterTopicsFrom: IClusterTopics;
public state = {
searchKey: '',
expandVisible: false,
};
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
}
public getBaseInfo(item: IClusterTopics) {
admin.getTopicsBasicInfo(item.clusterId, item.topicName).then(data => {
showEditClusterTopic(data);
});
}
public handleVisible(val: boolean) {
this.setState({ expandVisible: val });
}
public expandPartition(item: IClusterTopics) {
this.clusterTopicsFrom = item;
this.setState({
expandVisible: true,
});
}
public deleteTopic(item: IClusterTopics) {
const value = [{
clusterId: item.clusterId,
topicName: item.topicName,
unForce: false,
}];
deleteClusterTopic(value).then(data => {
notification.success({ message: '删除成功' });
admin.getClusterTopics(this.clusterId);
});
}
public getData<T extends IClusterTopics>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IClusterTopics) =>
(item.appName !== undefined && item.appName !== null) && item.appName.toLowerCase().includes(searchKey as string)
|| (item.topicName !== undefined && item.topicName !== null) && item.topicName.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public componentDidMount() {
admin.getClusterTopics(this.clusterId);
}
public renderClusterTopicList() {
const clusterColumns = [
{
title: 'Topic名称',
dataIndex: 'topicName',
key: 'topicName',
width: '15%',
sorter: (a: IClusterTopics, b: IClusterTopics) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
render: (text: string, record: IClusterTopics) => {
return (
<Tooltip placement="bottomLeft" title={record.topicName} >
<a
// tslint:disable-next-line:max-line-length
href={`${urlPrefix}/topic/topic-detail?clusterId=${record.clusterId || ''}&topic=${record.topicName || ''}&isPhysicalClusterId=true&region=${region.currentRegion}`}
>
{text}
</a>
</Tooltip>);
},
},
{
title: 'QPS',
dataIndex: 'produceRequest',
key: 'produceRequest',
width: '10%',
sorter: (a: IClusterTopics, b: IClusterTopics) => b.produceRequest - a.produceRequest,
render: (t: number) => t === null ? '' : t.toFixed(2),
},
{
title: 'Bytes In(KB/s)',
dataIndex: 'byteIn',
key: 'byteIn',
width: '15%',
sorter: (a: IClusterTopics, b: IClusterTopics) => b.byteIn - a.byteIn,
render: (t: number) => t === null ? '' : (t / 1024).toFixed(2),
},
{
title: '所属应用',
dataIndex: 'appName',
key: 'appName',
width: '10%',
render: (val: string, record: IClusterTopics) => (
<Tooltip placement="bottomLeft" title={record.appId} >
{val}
</Tooltip>
),
},
{
title: '保存时间(h)',
dataIndex: 'retentionTime',
key: 'retentionTime',
width: '10%',
sorter: (a: IClusterTopics, b: IClusterTopics) => b.retentionTime - a.retentionTime,
render: (time: any) => transMSecondToHour(time),
},
{
title: '更新时间',
dataIndex: 'updateTime',
key: 'updateTime',
render: (t: number) => moment(t).format(timeFormat),
width: '10%',
},
{
title: 'Topic说明',
dataIndex: 'description',
key: 'description',
width: '15%',
onCell: () => ({
style: {
maxWidth: 180,
...cellStyle,
},
}),
},
{
title: '操作',
width: '30%',
render: (value: string, item: IClusterTopics) => (
<>
<a onClick={() => this.getBaseInfo(item)} className="action-button"></a>
<a onClick={() => this.expandPartition(item)} className="action-button"></a>
<Popconfirm
title="确定删除?"
onConfirm={() => this.deleteTopic(item)}
>
<a></a>
</Popconfirm>
</>
),
},
];
if (users.currentUser.role !== 2) {
clusterColumns.splice(-1, 1);
}
return (
<>
<div className="k-row">
<ul className="k-tab">
<li>{this.props.tab}</li>
{this.renderSearch('', '请输入Topic名称应用名称')}
</ul>
<Table
loading={admin.loading}
rowKey="key"
dataSource={this.getData(admin.clusterTopics)}
columns={clusterColumns}
pagination={pagination}
/>
</div>
{this.renderExpandModal()}
</>
);
}
public renderExpandModal() {
let formData = {} as IClusterTopics;
formData = this.clusterTopicsFrom ? this.clusterTopicsFrom : formData;
return (
<>
{this.state.expandVisible && <ExpandPartitionFormWrapper
handleVisible={(val: boolean) => this.handleVisible(val)}
visible={this.state.expandVisible}
formData={formData}
clusterId={this.clusterId}
/>}
</>
);
}
public render() {
return (
admin.clusterTopics ? <> {this.renderClusterTopicList()} </> : null
);
}
}

View File

@@ -0,0 +1,97 @@
import * as React from 'react';
import { SearchAndFilterContainer } from 'container/search-filter';
import { Table, Tooltip } from 'component/antd';
import { observer } from 'mobx-react';
import { pagination } from 'constants/table';
import Url from 'lib/url-parser';
import { IThrottles } from 'types/base-type';
import { admin } from 'store/admin';
import './index.less';
@observer
export class CurrentLimiting extends SearchAndFilterContainer {
public clusterId: number;
public state = {
searchKey: '',
};
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
}
public renderController() {
const clientType = Object.assign({
title: '类型',
dataIndex: 'throttleClientType',
key: 'throttleClientType',
width: '15%',
filters: [{ text: 'fetch', value: 'Fetch' }, { text: 'produce', value: 'Produce' }],
onFilter: (value: string, record: IThrottles) => record.throttleClientType === value,
render: (t: string) => t,
}, this.renderColumnsFilter('filterStatus'));
const columns = [
{
title: 'Topic名称',
key: 'topicName',
dataIndex: 'topicName',
width: '50%',
sorter: (a: IThrottles, b: IThrottles) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
render: (val: string) => <Tooltip placement="bottomLeft" title={val}> {val} </Tooltip>,
},
{
title: '应用ID',
dataIndex: 'appId',
key: 'appId',
width: '15%',
sorter: (a: IThrottles, b: IThrottles) => a.appId.charCodeAt(0) - b.appId.charCodeAt(0),
},
clientType,
{
title: 'Broker',
dataIndex: 'brokerIdList',
key: 'brokerIdList',
width: '20%',
render: (value: number[]) => {
const num = value ? `[${value.join(',')}]` : '';
return(
<span>{num}</span>
);
},
},
];
const { searchKey } = this.state;
if (!admin.clustersThrottles ) return null;
const clustersThrottles = admin.clustersThrottles.filter(d =>
((d.topicName !== undefined && d.topicName !== null) && d.topicName.toLowerCase().includes(searchKey.toLowerCase()))
|| ((d.appId !== undefined && d.appId !== null) && d.appId.toLowerCase().includes(searchKey.toLowerCase())));
return (
<Table
columns={columns}
dataSource={clustersThrottles}
pagination={pagination}
rowKey="key"
/>
);
}
public componentDidMount() {
admin.getClustersThrottles(this.clusterId);
}
public render() {
return (
<div className="k-row">
<ul className="k-tab">
<li>{this.props.tab}</li>
{this.renderSearch()}
</ul>
{this.renderController()}
</div>
);
}
}

View File

@@ -0,0 +1,291 @@
import * as React from 'react';
import { Table, notification, Tooltip, Popconfirm } from 'component/antd';
import { observer } from 'mobx-react';
import { SearchAndFilterContainer } from 'container/search-filter';
import { pagination, cellStyle } from 'constants/table';
import { wrapper } from 'store';
import { admin } from 'store/admin';
import { IXFormWrapper, IBrokersRegions, INewRegions, IMetaData } from 'types/base-type';
import { deleteRegions } from 'lib/api';
import { transBToMB } from 'lib/utils';
import Url from 'lib/url-parser';
import moment from 'moment';
import './index.less';
import { timeFormat } from 'constants/strategy';
@observer
export class ExclusiveCluster extends SearchAndFilterContainer {
public clusterId: number;
public state = {
searchKey: '',
filterStatus: false,
};
private xFormModal: IXFormWrapper;
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
}
public renderColumns = () => {
const status = Object.assign({
title: '状态',
dataIndex: 'status',
key: 'status',
width: '10%',
filters: [{ text: '正常', value: '0' }, { text: '容量已满', value: '1' }],
onFilter: (value: string, record: IBrokersRegions) => record.status === Number(value),
render: (t: number) => <span className={t === 0 ? 'success' : 'fail'}>{t === 0 ? '正常' : '容量已满'}</span>,
}, this.renderColumnsFilter('filterStatus'));
return [
{
title: 'RegionID',
dataIndex: 'id',
key: 'id',
width: '7%',
},
{
title: 'Region名称',
dataIndex: 'name',
key: 'name',
width: '13%',
onCell: () => ({
style: {
maxWidth: 160,
...cellStyle,
},
}),
sorter: (a: IBrokersRegions, b: IBrokersRegions) => a.name.charCodeAt(0) - b.name.charCodeAt(0),
render: (text: string, r: IBrokersRegions) => (
<Tooltip placement="bottomLeft" title={text}>
{text}
</Tooltip>),
},
{
title: 'BrokerIdList',
dataIndex: 'brokerIdList',
key: 'brokerIdList',
width: '10%',
onCell: () => ({
style: {
maxWidth: 200,
...cellStyle,
},
}),
render: (value: number[]) => {
const num = value ? value.join(',') : '';
return(
<Tooltip placement="bottomLeft" title={num}>
{num}
</Tooltip>);
},
},
{
title: '预估容量MB/s',
dataIndex: 'capacity',
key: 'capacity',
width: '10%',
sorter: (a: IBrokersRegions, b: IBrokersRegions) => b.capacity - a.capacity,
render: (t: number) => transBToMB(t),
},
{
title: '实际流量MB/s',
dataIndex: 'realUsed',
key: 'realUsed',
width: '10%',
sorter: (a: IBrokersRegions, b: IBrokersRegions) => b.realUsed - a.realUsed,
render: (t: number) => transBToMB(t),
},
{
title: '预估流量MB/s',
dataIndex: 'estimateUsed',
key: 'estimateUsed',
width: '10%',
sorter: (a: IBrokersRegions, b: IBrokersRegions) => b.estimateUsed - a.estimateUsed,
render: (t: number) => transBToMB(t),
},
{
title: '修改时间',
dataIndex: 'gmtModify',
key: 'gmtModify',
width: '10%',
sorter: (a: IBrokersRegions, b: IBrokersRegions) => b.gmtModify - a.gmtModify,
render: (t: number) => moment(t).format(timeFormat),
},
status,
{
title: '备注',
dataIndex: 'description',
key: 'description',
width: '10%',
onCell: () => ({
style: {
maxWidth: 200,
...cellStyle,
},
}),
render: (text: string, r: IBrokersRegions) => (
<Tooltip placement="bottomLeft" title={text}>
{text}
</Tooltip>),
},
{
title: '操作',
width: '10%',
render: (text: string, record: IBrokersRegions) => {
return (
<span className="table-operation">
<a onClick={() => this.addOrModifyRegion(record)}></a>
<Popconfirm
title="确定删除?"
onConfirm={() => this.handleDeleteRegion(record)}
>
<a></a>
</Popconfirm>
</span>
);
},
},
];
}
public handleDeleteRegion = (record: IBrokersRegions) => {
deleteRegions(record.id).then(() => {
notification.success({ message: '删除成功' });
admin.getBrokersRegions(this.clusterId);
});
}
public addOrModifyRegion(record?: IBrokersRegions) {
const content = this.props.basicInfo as IMetaData;
this.xFormModal = {
formMap: [
{
key: 'name',
label: 'Region名称',
rules: [{ required: true, message: '请输入Region名称' }],
attrs: { placeholder: '请输入Region名称' },
},
{
key: 'clusterName',
label: '集群名称',
rules: [{ required: true, message: '请输入集群名称' }],
defaultValue: content.clusterName,
attrs: {
disabled: true,
placeholder: '请输入集群名称',
},
},
{
key: 'brokerIdList',
label: 'Broker列表',
defaultValue: record ? record.brokerIdList.join(',') : [],
rules: [{ required: true, message: '请输入BrokerIdList' }],
attrs: {
placeholder: '请输入BrokerIdList',
},
},
{
key: 'status',
label: '状态',
type: 'select',
options: [
{
label: '正常',
value: 0,
},
{
label: '容量已满',
value: 1,
},
],
defaultValue: 0,
rules: [{ required: true, message: '请选择状态' }],
attrs: {
placeholder: '请选择状态',
},
},
{
key: 'description',
label: '备注',
type: 'text_area',
rules: [{
required: false,
}],
attrs: {
placeholder: '请输入备注',
},
},
],
formData: record,
visible: true,
title: `${record ? '编辑' : '新增Region'}`,
onSubmit: (value: INewRegions) => {
value.clusterId = this.clusterId;
value.brokerIdList = value.brokerIdList && Array.isArray(value.brokerIdList) ?
value.brokerIdList : value.brokerIdList.split(',');
if (record) {
value.id = record.id;
}
delete value.clusterName;
if (record) {
return admin.editRegions(this.clusterId, value).then(data => {
notification.success({ message: '编辑Region成功' });
});
}
return admin.addNewRegions(this.clusterId, value).then(data => {
notification.success({ message: '新建Region成功' });
});
},
};
wrapper.open(this.xFormModal);
}
public componentDidMount() {
admin.getBrokersRegions(this.clusterId);
admin.getBrokersMetadata(this.clusterId);
}
public getData<T extends IBrokersRegions>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IBrokersRegions) =>
(item.name !== undefined && item.name !== null) && item.name.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public renderRegion() {
return (
<Table
columns={this.renderColumns()}
dataSource={this.getData(admin.brokersRegions)}
pagination={pagination}
rowKey="id"
/>
);
}
public render() {
return (
<div className="k-row">
<ul className="k-tab">
<li>{this.props.tab}</li>
<li className="k-add" onClick={() => this.addOrModifyRegion()}>
<i className="k-icon-xinjian didi-theme" />
<span>Region</span>
</li>
{this.renderSearch('', '请输入Region名称')}
</ul>
{this.renderRegion()}
</div>
);
}
}

View File

@@ -0,0 +1,84 @@
.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;
}
}
}
.traffic-table {
margin: 10px 0;
min-height: 450px;
.traffic-header {
width: 100%;
height: 44px;
font-weight: bold;
background: rgb(245, 245, 245);
border: 1px solid #e8e8e8;
padding: 0 10px;
display: flex;
justify-content: space-between;
span {
color: rgba(0, 0, 0, 0.65);
font-size: 13px;
line-height: 44px;
font-weight: 100;
}
.k-abs {
font-size: 12px;
}
}
}
.implement-button {
float: right;
margin-right: -120px;
}
.diagram {
min-width: 900px;
background: white;
height: 400px;
display: flex;
justify-content: space-around;
.diagram-box {
float: left;
margin-right: 10px;
width: 46%;
height: 100%;
h2 {
line-height: 30px;
font-size: 13px;
margin-left: 15px;
margin-top: 10px;
}
}
}
.descriptions {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
}
.leader-seacrh {
z-index: 9999999;
margin-top: 10px;
.search-top {
width: 100%;
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
}

View File

@@ -0,0 +1,73 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import { Tabs, PageHeader } from 'antd';
import { IMetaData } from 'types/base-type';
import { ClusterOverview } from './cluster-overview';
import { ClusterTopic } from './cluster-topic';
import { ClusterBroker } from './cluster-broker';
import { ClusterConsumer } from './cluster-consumer';
import { ExclusiveCluster } from './exclusive-cluster';
import { LogicalCluster } from './logical-cluster';
import { ClusterController } from './cluster-controller';
import { CurrentLimiting } from './current-limiting';
import { handleTabKey } from 'lib/utils';
import { admin } from 'store/admin';
import { handlePageBack } from 'lib/utils';
import Url from 'lib/url-parser';
const { TabPane } = Tabs;
@observer
export class ClusterDetail extends React.Component {
public clusterId: number;
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
}
public componentDidMount() {
admin.getBasicInfo(this.clusterId);
}
public render() {
let content = {} as IMetaData;
content = admin.basicInfo ? admin.basicInfo : content;
return (
<>
<PageHeader
className="detail topic-detail-header"
onBack={() => handlePageBack('/admin')}
title={`集群列表/${content.clusterName || ''}`}
/>
<Tabs activeKey={location.hash.substr(1) || '1'} type="card" onChange={handleTabKey}>
<TabPane tab="集群概览" key="1">
<ClusterOverview basicInfo={content} />
</TabPane>
<TabPane tab="Topic信息" key="2">
<ClusterTopic tab={'Topic信息'}/>
</TabPane>
<TabPane tab="Broker信息" key="3">
<ClusterBroker tab={'Broker信息'} basicInfo={content} />
</TabPane>
<TabPane tab="消费组信息" key="4">
<ClusterConsumer tab={'消费组信息'} />
</TabPane>
<TabPane tab="Region信息" key="5">
<ExclusiveCluster tab={'Region信息'} basicInfo={content} />
</TabPane>
<TabPane tab="逻辑集群信息" key="6">
<LogicalCluster tab={'逻辑集群信息'} basicInfo={content} />
</TabPane>
<TabPane tab="Controller变更历史" key="7">
<ClusterController tab={'Controller变更历史'} />
</TabPane>
<TabPane tab="限流信息" key="8">
<CurrentLimiting tab={'限流信息'}/>
</TabPane>
</Tabs>
</>
);
}
}

View File

@@ -0,0 +1,169 @@
import * as React from 'react';
import { Table, notification, Popconfirm } from 'component/antd';
import { observer } from 'mobx-react';
import { SearchAndFilterContainer } from 'container/search-filter';
import { pagination } from 'constants/table';
import Url from 'lib/url-parser';
import moment from 'moment';
import { admin } from 'store/admin';
import { cluster } from 'store/cluster';
import { ILogicalCluster } from 'types/base-type';
import './index.less';
import { app } from 'store/app';
import { showLogicalClusterOpModal } from 'container/modal';
import { timeFormat } from 'constants/strategy';
@observer
export class LogicalCluster extends SearchAndFilterContainer {
public clusterId: number;
public state = {
searchKey: '',
filterStatus: false,
};
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
}
public renderColumns = () => {
return [
{
title: '逻辑集群ID',
dataIndex: 'logicalClusterId',
key: 'logicalClusterId',
},
{
title: '逻辑集群名称',
dataIndex: 'logicalClusterName',
key: 'logicalClusterName',
},
{
title: '应用ID',
dataIndex: 'appId',
key: 'appId',
},
{
title: 'RegionIdList',
dataIndex: 'regionIdList',
key: 'regionIdList',
render: (value: number[]) => {
const num = value ? `[${value.join(',')}]` : '';
return(
<span>{num}</span>
);
},
},
{
title: '集群模式',
dataIndex: 'mode',
key: 'mode',
render: (value: number) => {
let val = '';
cluster.clusterModes.forEach((ele: any) => {
if (value === ele.code) {
val = ele.message;
}
});
return(<span>{val}</span>);
},
},
{
title: '修改时间',
dataIndex: 'gmtModify',
key: 'gmtModify',
render: (t: number) => moment(t).format(timeFormat),
},
{
title: '备注',
dataIndex: 'description',
key: 'description',
},
{
title: '操作',
render: (text: string, record: ILogicalCluster) => {
return (
<span className="table-operation">
<a onClick={() => this.editRegion(record)}></a>
<Popconfirm
title="确定删除?"
onConfirm={() => this.handleDeleteRegion(record)}
>
<a></a>
</Popconfirm>
</span>
);
},
},
];
}
public handleDeleteRegion = (record: ILogicalCluster) => {
admin.deteleLogicalClusters(this.clusterId, record.logicalClusterId).then(() => {
notification.success({ message: '删除成功' });
});
}
public async editRegion(record: ILogicalCluster) {
await admin.queryLogicalClusters(record.logicalClusterId);
await this.addOrEditLogicalCluster(admin.queryLogical);
}
public addOrEditLogicalCluster(record?: ILogicalCluster) {
showLogicalClusterOpModal(this.clusterId, record);
}
public componentDidMount() {
admin.getLogicalClusters(this.clusterId);
cluster.getClusterModes();
admin.getBrokersRegions(this.clusterId);
if (!app.adminAppData.length) {
app.getAdminAppList();
}
}
public getData<T extends ILogicalCluster>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: ILogicalCluster) =>
(item.logicalClusterName !== undefined && item.logicalClusterName !== null)
&& item.logicalClusterName.toLowerCase().includes(searchKey as string)
|| (item.appId !== undefined && item.appId !== null) && item.appId.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public renderLogicalCluster() {
return (
<Table
columns={this.renderColumns()}
dataSource={this.getData(admin.logicalClusters)}
pagination={pagination}
rowKey="key"
/>
);
}
public render() {
return (
<div className="k-row">
<ul className="k-tab">
<li>{this.props.tab}</li>
<li className="k-add" onClick={() => this.addOrEditLogicalCluster()}>
<i className="k-icon-xinjian didi-theme" />
<span></span>
</li>
{this.renderSearch('', '请输入逻辑集群名称或AppId')}
</ul>
{this.renderLogicalCluster()}
</div>
);
}
}