mirror of
https://github.com/didi/KnowStreaming.git
synced 2026-01-04 11:52:07 +08:00
kafka-manager 2.0
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
import * as React from 'react';
|
||||
import { Table } from 'component/antd';
|
||||
import Url from 'lib/url-parser';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { observer } from 'mobx-react';
|
||||
import { pagination } from 'constants/table';
|
||||
import { IBrokerData, IEnumsMap } from 'types/base-type';
|
||||
import { admin } from 'store/admin';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { transBToMB } from 'lib/utils';
|
||||
import moment from 'moment';
|
||||
import './index.less';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
|
||||
@observer
|
||||
export class ClusterBroker extends SearchAndFilterContainer {
|
||||
public clusterId: number;
|
||||
public clusterName: string;
|
||||
|
||||
public state = {
|
||||
filterPeakFlowVisible: false,
|
||||
filterReplicatedVisible: false,
|
||||
filterStatusVisible: false,
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
this.clusterName = decodeURI(url.search.clusterName);
|
||||
}
|
||||
|
||||
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 renderBrokerData() {
|
||||
let peakFlow = [] as IEnumsMap[];
|
||||
peakFlow = admin.peakFlowStatusList ? admin.peakFlowStatusList : peakFlow;
|
||||
const peakFlowStatus = Object.assign({
|
||||
title: '峰值状态',
|
||||
dataIndex: 'peakFlowStatus',
|
||||
key: 'peakFlowStatus',
|
||||
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',
|
||||
filters: [{ text: '同步', value: 'false' }, { text: '未同步', value: 'true' }],
|
||||
onFilter: (value: string, record: IBrokerData) => record.underReplicated === (value === 'true') ? true : false,
|
||||
render: (t: boolean) => <span className={t ? 'fail' : 'success'}>{t ? '未同步' : '同步'}</span>,
|
||||
}, this.renderColumnsFilter('filterReplicatedVisible'));
|
||||
|
||||
const status = Object.assign({
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
filters: [{ text: '未使用', value: '-1' }, { text: '使用中', value: '0' }],
|
||||
onFilter: (value: string, record: IBrokerData) => record.status === Number(value),
|
||||
render: (t: number) => t === 0 ? '使用中' : '未使用',
|
||||
}, this.renderColumnsFilter('filterStatusVisible'));
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'brokerId',
|
||||
key: 'brokerId',
|
||||
sorter: (a: IBrokerData, b: IBrokerData) => b.brokerId - a.brokerId,
|
||||
render: (text: number, record: IBrokerData) => <span>{text}</span>,
|
||||
},
|
||||
{
|
||||
title: '主机',
|
||||
dataIndex: 'host',
|
||||
key: 'host',
|
||||
sorter: (a: any, b: any) => a.host.charCodeAt(0) - b.host.charCodeAt(0),
|
||||
},
|
||||
{
|
||||
title: 'Port',
|
||||
dataIndex: 'port',
|
||||
key: 'port',
|
||||
sorter: (a: IBrokerData, b: IBrokerData) => b.port - a.port,
|
||||
},
|
||||
{
|
||||
title: 'JMX Port',
|
||||
dataIndex: 'jmxPort',
|
||||
key: 'jmxPort',
|
||||
sorter: (a: IBrokerData, b: IBrokerData) => b.jmxPort - a.jmxPort,
|
||||
},
|
||||
{
|
||||
title: '启动时间',
|
||||
dataIndex: 'startTime',
|
||||
key: 'startTime',
|
||||
sorter: (a: IBrokerData, b: IBrokerData) => b.startTime - a.startTime,
|
||||
render: (time: number) => moment(time).format(timeFormat),
|
||||
},
|
||||
{
|
||||
title: 'Bytes In(MB/s)',
|
||||
dataIndex: 'byteIn',
|
||||
key: 'byteIn',
|
||||
sorter: (a: IBrokerData, b: IBrokerData) => b.byteIn - a.byteIn,
|
||||
render: (t: number) => transBToMB(t),
|
||||
},
|
||||
{
|
||||
title: 'Bytes Out(MB/s)',
|
||||
dataIndex: 'byteOut',
|
||||
key: 'byteOut',
|
||||
sorter: (a: IBrokerData, b: IBrokerData) => b.byteOut - a.byteOut,
|
||||
render: (t: number) => transBToMB(t),
|
||||
},
|
||||
// peakFlowStatus,
|
||||
underReplicated,
|
||||
status,
|
||||
];
|
||||
return (
|
||||
<Table dataSource={this.getData(cluster.clusterBroker)} columns={columns} pagination={pagination} loading={cluster.loading} />
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
cluster.getClusterDetailBroker(this.clusterId);
|
||||
// admin.getBrokersMetadata(this.clusterId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<div className="k-row">
|
||||
<ul className="k-tab">
|
||||
<li>{this.props.tab}</li>
|
||||
{this.renderSearch('', '请输入ID或主机')}
|
||||
</ul>
|
||||
{this.renderBrokerData()}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import * as React from 'react';
|
||||
import { PageHeader, Descriptions, Tooltip, Icon, Spin } from 'component/antd';
|
||||
import { ILabelValue, IBasicInfo, IOptionType, IClusterReal } from 'types/base-type';
|
||||
import { selectOptionMap } from 'constants/status-map';
|
||||
import { observer } from 'mobx-react';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { clusterTypeMap } from 'constants/status-map';
|
||||
import { copyString } from 'lib/utils';
|
||||
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: IBasicInfo;
|
||||
}
|
||||
|
||||
@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 IBasicInfo;
|
||||
const clusterContent = [{
|
||||
value: content.clusterName,
|
||||
label: '集群名称',
|
||||
}, {
|
||||
value: clusterTypeMap[content.mode],
|
||||
label: '集群类型',
|
||||
}, {
|
||||
value: moment(content.gmtCreate).format(timeFormat),
|
||||
label: '接入时间',
|
||||
}, {
|
||||
value: content.physicalClusterId,
|
||||
label: '物理集群ID',
|
||||
}];
|
||||
const clusterInfo = [{
|
||||
value: content.clusterVersion,
|
||||
label: 'kafka版本',
|
||||
}, {
|
||||
value: content.bootstrapServers,
|
||||
label: 'Bootstrap Severs',
|
||||
}];
|
||||
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 = () => {
|
||||
cluster.getClusterDetailRealTime(this.clusterId);
|
||||
}
|
||||
|
||||
public onSelectChange(e: IOptionType) {
|
||||
return cluster.changeType(e);
|
||||
}
|
||||
|
||||
public getOptionApi = () => {
|
||||
return cluster.getClusterDetailMetrice(this.clusterId);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
cluster.getClusterBasicInfo(this.clusterId);
|
||||
cluster.getClusterDetailRealTime(this.clusterId);
|
||||
}
|
||||
|
||||
public renderHistoryTraffic() {
|
||||
return (
|
||||
<NetWorkFlow
|
||||
key="1"
|
||||
selectArr={selectOptionMap}
|
||||
type={cluster.type}
|
||||
selectChange={(value: IOptionType) => this.onSelectChange(value)}
|
||||
getApi={() => this.getOptionApi()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public renderTrafficInfo = () => {
|
||||
return (
|
||||
<Spin spinning={cluster.realLoading}>
|
||||
{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 cluster.clusterRealData;
|
||||
}
|
||||
public getLoading = () => {
|
||||
return cluster.realLoading;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import * as React from 'react';
|
||||
import Url from 'lib/url-parser';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { Table, Tooltip } from 'antd';
|
||||
import { pagination, cellStyle } from 'constants/table';
|
||||
import { observer } from 'mobx-react';
|
||||
import { IClusterTopics } from 'types/base-type';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { urlPrefix } from 'constants/left-menu';
|
||||
import { transMSecondToHour } from 'lib/utils';
|
||||
import { region } from 'store/region';
|
||||
import './index.less';
|
||||
|
||||
import moment = require('moment');
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
|
||||
@observer
|
||||
export class ClusterTopic extends SearchAndFilterContainer {
|
||||
public clusterId: number;
|
||||
public clusterTopicsFrom: IClusterTopics;
|
||||
|
||||
public state = {
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.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.topicName !== undefined && item.topicName !== null) && item.topicName.toLowerCase().includes(searchKey as string)
|
||||
|| (item.appName !== undefined && item.appName !== null) && item.appName.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public clusterTopicList() {
|
||||
const columns = [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
width: '20%',
|
||||
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.logicalClusterId}&topic=${record.topicName}®ion=${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: (val: number) => val === null ? '' : (val / 1024).toFixed(2),
|
||||
},
|
||||
{
|
||||
title: '所属应用',
|
||||
dataIndex: 'appName',
|
||||
key: 'appName',
|
||||
width: '15%',
|
||||
render: (val: string, record: IClusterTopics) => (
|
||||
<Tooltip placement="bottomLeft" title={record.appId} >
|
||||
{val}
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '保存时间(h)',
|
||||
dataIndex: 'retentionTime',
|
||||
key: 'retentionTime',
|
||||
width: '15%',
|
||||
sorter: (a: IClusterTopics, b: IClusterTopics) => b.retentionTime - a.retentionTime,
|
||||
render: (time: any) => transMSecondToHour(time),
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updateTime',
|
||||
key: 'updateTime',
|
||||
width: '20%',
|
||||
render: (t: number) => moment(t).format(timeFormat),
|
||||
},
|
||||
{
|
||||
title: 'Topic说明',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
width: '30%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 200,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
render: (val: string) => (
|
||||
<Tooltip placement="topLeft" title={val} >
|
||||
{val}
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<div className="k-row">
|
||||
<ul className="k-tab">
|
||||
<li>{this.props.tab}</li>
|
||||
{this.renderSearch('', '请输入Topic名称或所属应用')}
|
||||
</ul>
|
||||
<Table
|
||||
loading={cluster.loading}
|
||||
rowKey="topicName"
|
||||
dataSource={this.getData(cluster.clusterTopics)}
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
cluster.getClusterDetailTopics(this.clusterId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
cluster.clusterTopics ? <> {this.clusterTopicList()} </> : null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
|
||||
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 { cluster } from 'store/cluster';
|
||||
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 getData<T extends IThrottles>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: IThrottles) =>
|
||||
(item.topicName !== undefined && item.topicName !== null) && item.topicName.toLowerCase().includes(searchKey as string)
|
||||
|| (item.appId !== undefined && item.appId !== null) && item.appId.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public renderController() {
|
||||
const clientType = Object.assign({
|
||||
title: '类型',
|
||||
dataIndex: 'throttleClientType',
|
||||
key: 'throttleClientType',
|
||||
filters: [{ text: 'fetch', value: 'FetchThrottleTime' }, { text: 'produce', value: 'ProduceThrottleTime' }],
|
||||
onFilter: (value: string, record: IThrottles) => record.throttleClientType === value,
|
||||
render: (t: string) => t,
|
||||
}, this.renderColumnsFilter('filterStatus'));
|
||||
const columns = [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
key: 'topicName',
|
||||
dataIndex: 'topicName',
|
||||
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',
|
||||
sorter: (a: IThrottles, b: IThrottles) => a.appId.charCodeAt(0) - b.appId.charCodeAt(0),
|
||||
},
|
||||
clientType,
|
||||
{
|
||||
title: 'Broker',
|
||||
dataIndex: 'brokerIdList',
|
||||
key: 'brokerIdList',
|
||||
render: (value: number[]) => {
|
||||
const num = value ? `[${value.join(',')}]` : '';
|
||||
return(
|
||||
<span>{num}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={this.getData(cluster.clustersThrottles)}
|
||||
pagination={pagination}
|
||||
rowKey="key"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
cluster.getClusterDetailThrottles(this.clusterId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<div className="k-row">
|
||||
<ul className="k-tab">
|
||||
<li>{this.props.tab}</li>
|
||||
{this.renderSearch('', '请输入Topic名称或AppId')}
|
||||
</ul>
|
||||
{this.renderController()}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
|
||||
.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;
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.overview-bootstrap {
|
||||
width: 200px;
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
line-height: 20px;
|
||||
.overview-theme {
|
||||
margin: 3px;
|
||||
}
|
||||
.overview-boot {
|
||||
font-style: normal;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Tabs, PageHeader } from 'antd';
|
||||
import { ClusterOverview } from './cluster-overview';
|
||||
import { ClusterTopic } from './cluster-topic';
|
||||
import { ClusterBroker } from './cluster-broker';
|
||||
import { CurrentLimiting } from './current-limiting';
|
||||
import { IBasicInfo } from 'types/base-type';
|
||||
import { handleTabKey } from 'lib/utils';
|
||||
import { cluster } from 'store/cluster';
|
||||
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() {
|
||||
cluster.getClusterBasicInfo(this.clusterId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
let content = {} as IBasicInfo;
|
||||
content = cluster.basicInfo ? cluster.basicInfo : content;
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
className="detail topic-detail-header"
|
||||
onBack={() => handlePageBack('/cluster')}
|
||||
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信息'} />
|
||||
</TabPane>
|
||||
<TabPane tab="限流信息" key="7">
|
||||
<CurrentLimiting tab={'限流信息'} />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
95
kafka-manager-console/src/container/cluster/config.tsx
Normal file
95
kafka-manager-console/src/container/cluster/config.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import * as React from 'react';
|
||||
import { clusterTypeMap } from 'constants/status-map';
|
||||
import { notification, Tooltip, Modal, Table, message, Icon } from 'component/antd';
|
||||
import { IClusterData } from 'types/base-type';
|
||||
import { showCpacityModal } from 'container/modal';
|
||||
import moment = require('moment');
|
||||
import { cellStyle } from 'constants/table';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
import { modal } from 'store/modal';
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
||||
export const getClusterColumns = (urlPrefix: string) => {
|
||||
return [
|
||||
{
|
||||
title: '集群名称',
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
width: '15%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 120,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
sorter: (a: IClusterData, b: IClusterData) => a.clusterName.charCodeAt(0) - b.clusterName.charCodeAt(0),
|
||||
render: (text: string, record: IClusterData) => (
|
||||
<Tooltip placement="bottomLeft" title={text} >
|
||||
<a href={`${urlPrefix}/cluster/cluster-detail?clusterId=${record.clusterId}`}> {text} </a>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Topic数量',
|
||||
dataIndex: 'topicNum',
|
||||
key: 'topicNum',
|
||||
width: '10%',
|
||||
sorter: (a: IClusterData, b: IClusterData) => b.topicNum - a.topicNum,
|
||||
},
|
||||
{
|
||||
title: '集群类型',
|
||||
dataIndex: 'mode',
|
||||
key: 'mode',
|
||||
width: '10%',
|
||||
render: (text: number) => (clusterTypeMap[text] || ''),
|
||||
},
|
||||
{
|
||||
title: '集群版本',
|
||||
dataIndex: 'clusterVersion',
|
||||
key: 'clusterVersion',
|
||||
width: '25%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 200,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
render: (text: string) => <Tooltip placement="bottomLeft" title={text} >{text}</Tooltip>,
|
||||
}, {
|
||||
title: '接入时间',
|
||||
dataIndex: 'gmtCreate',
|
||||
key: 'gmtCreate',
|
||||
width: '15%',
|
||||
sorter: (a: IClusterData, b: IClusterData) => b.gmtCreate - a.gmtCreate,
|
||||
render: (t: number) => moment(t).format(timeFormat),
|
||||
}, {
|
||||
title: '修改时间',
|
||||
dataIndex: 'gmtModify',
|
||||
key: 'gmtModify',
|
||||
width: '15%',
|
||||
sorter: (a: IClusterData, b: IClusterData) => b.gmtModify - a.gmtModify,
|
||||
render: (t: number) => moment(t).format(timeFormat),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
width: '10%',
|
||||
render: (val: string, record: IClusterData) => (
|
||||
<>
|
||||
{
|
||||
record.mode !== 0 ? <>
|
||||
<a onClick={() => showConfirm(record)} className="action-button">申请下线</a>
|
||||
<a onClick={() => showCpacityModal(record)}>扩缩容</a>
|
||||
</> : null
|
||||
}
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const showConfirm = (record: IClusterData) => {
|
||||
modal.showOfflineClusterModal(record.clusterId);
|
||||
};
|
||||
24
kafka-manager-console/src/container/cluster/index.less
Normal file
24
kafka-manager-console/src/container/cluster/index.less
Normal file
@@ -0,0 +1,24 @@
|
||||
.apply-button {
|
||||
Button {
|
||||
position: absolute;
|
||||
right : 25px;
|
||||
z-index: 999999;
|
||||
}
|
||||
Table {
|
||||
margin-top: 40px;
|
||||
}
|
||||
.new_task_but{
|
||||
right: 5px;
|
||||
}
|
||||
}
|
||||
.radio-and-input{
|
||||
.radio-and-input-inputNuber{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 251px;
|
||||
justify-content: space-between;
|
||||
.ant-input-number {
|
||||
width: 210px;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
kafka-manager-console/src/container/cluster/index.tsx
Normal file
2
kafka-manager-console/src/container/cluster/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './my-cluster';
|
||||
export * from './cluster-detail';
|
||||
254
kafka-manager-console/src/container/cluster/my-cluster.tsx
Normal file
254
kafka-manager-console/src/container/cluster/my-cluster.tsx
Normal file
@@ -0,0 +1,254 @@
|
||||
import * as React from 'react';
|
||||
import './index.less';
|
||||
import { wrapper } from 'store';
|
||||
import { users } from 'store/users';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
notification,
|
||||
Radio,
|
||||
RadioChangeEvent,
|
||||
InputNumber,
|
||||
} from 'component/antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import { pagination } from 'constants/table';
|
||||
import { urlPrefix } from 'constants/left-menu';
|
||||
import { getClusterColumns } from './config';
|
||||
import { app } from 'store/app';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { IOrderParams, IClusterData, IRadioItem } from 'types/base-type';
|
||||
import { region } from 'store';
|
||||
|
||||
@observer
|
||||
export class MyCluster extends SearchAndFilterContainer {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
public applyCluster() {
|
||||
const xFormModal = {
|
||||
formMap: [
|
||||
{
|
||||
key: 'idc',
|
||||
label: '数据中心',
|
||||
defaultValue: region.regionName,
|
||||
rules: [{ required: true, message: '请输入数据中心' }],
|
||||
attrs: {
|
||||
placeholder: '请输入数据中心',
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'appId',
|
||||
label: '所属应用',
|
||||
rules: [{ required: true, message: '请选择所属应用' }],
|
||||
type: 'select',
|
||||
options: app.data.map((item) => {
|
||||
return {
|
||||
label: item.name,
|
||||
value: item.appId,
|
||||
};
|
||||
}),
|
||||
attrs: {
|
||||
placeholder: '请选择所属应用',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'mode',
|
||||
label: '集群类型',
|
||||
type: 'radio_group',
|
||||
options: cluster.clusterMode,
|
||||
rules: [{ required: true, message: '请选择' }],
|
||||
attrs: {
|
||||
placeholder: '请选择集群',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'bytesIn',
|
||||
label: '峰值流量',
|
||||
type: 'custom',
|
||||
rules: [{ required: true, message: '请选择' }],
|
||||
customFormItem: <RadioAndInput />,
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
label: '申请原因',
|
||||
type: 'text_area',
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
pattern: /^.{5,}.$/,
|
||||
message: '请输入至少5个字符',
|
||||
},
|
||||
],
|
||||
attrs: {
|
||||
placeholder: `请大致说明集群申请的原因、用途,对稳定性SLA的要求等。
|
||||
例如:
|
||||
原因:xxx, 用途:xxx, 稳定性:xxx`,
|
||||
},
|
||||
},
|
||||
],
|
||||
formData: {},
|
||||
visible: true,
|
||||
title: '申请集群',
|
||||
okText: '确认',
|
||||
onSubmit: (value: any) => {
|
||||
value.idc = region.currentRegion;
|
||||
const params = JSON.parse(JSON.stringify(value));
|
||||
delete params.description;
|
||||
if (typeof params.bytesIn === 'number') {
|
||||
params.bytesIn = params.bytesIn * 1024 * 1024;
|
||||
}
|
||||
const clusterParams: IOrderParams = {
|
||||
type: 4,
|
||||
applicant: users.currentUser.username,
|
||||
description: value.description,
|
||||
extensions: JSON.stringify(params),
|
||||
};
|
||||
cluster.applyCluster(clusterParams).then((data) => {
|
||||
notification.success({
|
||||
message: '申请集群成功',
|
||||
// description: this.aHrefUrl(data.id),
|
||||
});
|
||||
window.location.href = `${urlPrefix}/user/order-detail/?orderId=${data.id}®ion=${region.currentRegion}`;
|
||||
});
|
||||
},
|
||||
};
|
||||
wrapper.open(xFormModal);
|
||||
}
|
||||
|
||||
public aHrefUrl(id: number) {
|
||||
return (
|
||||
<>
|
||||
<a href={urlPrefix + '/user/order-detail/?orderId=' + id}>
|
||||
是否跳转到集群审批页?
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
if (!cluster.clusterData.length) {
|
||||
cluster.getClusters();
|
||||
}
|
||||
if (!cluster.clusterModes.length) {
|
||||
cluster.getClusterModes();
|
||||
}
|
||||
if (!app.data.length) {
|
||||
app.getAppList();
|
||||
}
|
||||
}
|
||||
|
||||
public renderOperationPanel() {
|
||||
return (
|
||||
<ul>
|
||||
{this.renderSearch('', '请输入集群名称')}
|
||||
<li className="right-btn-1">
|
||||
<Button type="primary" onClick={() => this.applyCluster()}>
|
||||
申请集群
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
public getData<T extends IClusterData>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: IClusterData) =>
|
||||
(item.clusterName !== undefined && item.clusterName !== null) && item.clusterName.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public renderTable() {
|
||||
return (
|
||||
<Table
|
||||
loading={cluster.loading}
|
||||
rowKey="clusterId"
|
||||
dataSource={this.getData(cluster.clusterData)}
|
||||
columns={getClusterColumns(urlPrefix)}
|
||||
pagination={pagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="table-operation-panel">
|
||||
{this.renderOperationPanel()}
|
||||
</div>
|
||||
<div className="table-wrapper">{this.renderTable()}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface IRadioProps {
|
||||
onChange?: (result: any) => any;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
@observer
|
||||
class RadioAndInput extends React.Component<IRadioProps> {
|
||||
public state = {
|
||||
value: null as number,
|
||||
};
|
||||
|
||||
public onRadioChange = (e: RadioChangeEvent) => {
|
||||
const { onChange } = this.props;
|
||||
if (onChange) {
|
||||
onChange(e.target.value);
|
||||
this.setState({
|
||||
value: e.target.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onInputChange = (e: number) => {
|
||||
const { onChange } = this.props;
|
||||
if (onChange) {
|
||||
onChange(e);
|
||||
this.setState({
|
||||
value: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
if (!cluster.clusterComboList.length) {
|
||||
cluster.getClusterComboList();
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const options = cluster.clusterComboList;
|
||||
const attrs = {
|
||||
min: 0,
|
||||
placeholder: '没合适?手动输入试试。',
|
||||
};
|
||||
return (
|
||||
<div className="radio-and-input">
|
||||
<Radio.Group value={this.state.value} onChange={this.onRadioChange}>
|
||||
{options.map((v: IRadioItem, index: number) => (
|
||||
<Radio.Button key={v.value || index} value={parseInt(v.label)}>
|
||||
{v.label}
|
||||
</Radio.Button>
|
||||
))}
|
||||
</Radio.Group>
|
||||
<div className="radio-and-input-inputNuber">
|
||||
<InputNumber
|
||||
{...attrs}
|
||||
value={this.state.value}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
<span>MB/s</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user