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,337 @@
import * as React from 'react';
import { IBtn, ITopic, IQuotaQuery, IConnectionInfo } from 'types/base-type';
import { showAllPermissionModal, showPermissionModal, showTopicEditModal, showApplyQuatoModal, applyExpandModal, showTopicApplyQuatoModal } from 'container/modal';
import { applyOnlineModal } from 'container/modal';
import { Tooltip } from 'component/antd';
import { topic } from 'store/topic';
import { region } from 'store/region';
import { cellStyle } from 'constants/table';
import { MoreBtns } from 'container/custom-component';
import { app } from 'store/app';
import './index.less';
/**
* 0: 无权限 申请权限
* 1: 可消费 申请配额,只能申请消费配额, 申请权限(申请发送)
* 2: 可发送 申请配额,只能申请发送配额, 申请权限(申请消费)
* 3: 可消费发送 申请配额,可以申请发送及消费配额
* 4: 可管理 可编辑,可以申请发送及消费配额
*/
export const renderMyTopicOperation = (record: ITopic) => {
const twoBtns = getTopicBtn(record).splice(0, 2);
const leftBtns = getTopicBtn(record).splice(2);
return (
<>
<span className="table-operation">
{twoBtns.map((item, index) => (
item.clickFunc ? <a type="javascript;" key={index} onClick={() => item.clickFunc(record)}>{item.label}</a> :
<span key={index} className="mr-10">{item.label}</span>
))}
{getTopicBtn(record).length > 2 && <MoreBtns btns={leftBtns} data={record}/>}
</span>
</>);
};
const getTopicBtn = (record: ITopic) => {
const btnList: IBtn[] = [{
label: '申请权限',
show: [0, 1, 2].indexOf(record.access) > -1,
clickFunc: (item: ITopic) => {
showPermissionModal(item);
},
}, {
label: '申请配额',
show: [1, 2, 3, 4].indexOf(record.access) > -1,
clickFunc: (item: ITopic) => {
applyQuotaQuery(item);
},
}, {
label: '申请分区',
show: true,
clickFunc: (item: ITopic) => {
applyExpandModal(item);
},
}, {
label: '申请下线',
show: record.access === 4,
clickFunc: (item: ITopic) => {
applyOnlineModal(item);
},
}, {
label: '编辑',
show: record.access === 4,
clickFunc: (item: ITopic) => {
showTopicEditModal(item);
},
}];
const origin = btnList.filter((item: IBtn) => item.show);
return origin;
};
export const renderAllTopicOperation = (item: ITopic) => {
return (
<>
{item.needAuth && <a className="mr-10" onClick={() => showAllPermissionModal(item)}></a>}
</>
);
};
export const applyQuotaQuery = (item: ITopic) => {
topic.getQuotaQuery(item.appId, item.clusterId, item.topicName).then((data) => {
const record = data && data.length ? data[0] : {} as IQuotaQuery;
showApplyQuatoModal(item, record);
});
};
export const applyTopicQuotaQuery = async (item: ITopic) => {
await app.getTopicAppQuota(item.clusterId, item.topicName);
await showTopicApplyQuatoModal(item);
};
export const getOnlineColumns = () => {
const columns = [
{
title: 'AppID',
dataIndex: 'appId',
key: 'appId',
},
{
title: 'HostName',
dataIndex: 'hostname',
key: 'hostname',
onCell: () => ({
style: {
maxWidth: 120,
...cellStyle,
},
}),
render: (text: string) => {
return (
<Tooltip placement="bottomLeft" title={text} >
{text}
</Tooltip>);
},
},
{
title: 'ClientType',
dataIndex: 'clientType',
key: 'clientType',
},
];
return columns;
};
export const getAllTopicColumns = (urlPrefix: string) => {
const columns = [
{
title: 'Topic名称',
dataIndex: 'topicName',
key: 'topicName',
width: '25%',
onCell: () => ({
style: {
maxWidth: 250,
...cellStyle,
},
}),
sorter: (a: ITopic, b: ITopic) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
render: (text: string, record: ITopic) => {
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}&region=${region.currentRegion}`}
>
{text}
</a>
</Tooltip>);
},
}, {
title: '集群名称',
dataIndex: 'clusterName',
key: 'clusterName',
width: '20%',
}, {
title: 'Topic描述',
dataIndex: 'description',
key: 'description',
width: '25%',
onCell: () => ({
style: {
maxWidth: 250,
...cellStyle,
},
}),
render: (text: string) => <Tooltip placement="bottomLeft" title={text} >{text}</Tooltip>,
}, {
title: '负责人',
dataIndex: 'appPrincipals',
key: 'appPrincipals',
width: '20%',
onCell: () => ({
style: {
maxWidth: 100,
...cellStyle,
},
}),
render: (text: string) => <Tooltip placement="bottomLeft" title={text} >{text}</Tooltip>,
}, {
title: '操作',
dataIndex: 'operation',
key: 'operation',
width: '10%',
render: (text: string, item: ITopic) => (
renderAllTopicOperation(item)
),
},
];
return columns;
};
export const getExpireColumns = (urlPrefix: string) => {
return [
{
title: 'Topic名称',
dataIndex: 'topicName',
key: 'topicName',
width: '35%',
sorter: (a: ITopic, b: ITopic) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
render: (t: string, r: ITopic) => {
return (
<Tooltip placement="bottomLeft" title={r.topicName} >
<a
// tslint:disable-next-line:max-line-length
href={`${urlPrefix}/topic/topic-detail?clusterId=${r.clusterId}&topic=${r.topicName}&region=${region.currentRegion}`}
>
{t}
</a>
</Tooltip>);
},
},
{
title: '所属集群',
dataIndex: 'clusterName',
key: 'clusterName',
width: '20%',
},
{
title: '关联应用',
dataIndex: 'appName',
key: 'appName',
width: '20%',
},
{
title: '消费者个数',
dataIndex: 'fetchConnectionNum',
key: 'fetchConnectionNum',
width: '10%',
},
];
};
export const getMyTopicColumns = (urlPrefix: string) => {
return [
{
title: 'Topic名称',
dataIndex: 'topicName',
key: 'topicName',
width: '21%',
sorter: (a: ITopic, b: ITopic) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
render: (t: string, r: ITopic) => {
return (
<Tooltip placement="bottomLeft" title={r.topicName} >
<a
// tslint:disable-next-line:max-line-length
href={`${urlPrefix}/topic/topic-detail?clusterId=${r.clusterId}&topic=${r.topicName}&region=${region.currentRegion}`}
>
{t}
</a>
</Tooltip>);
},
}, {
title: 'Bytes in(KB/s)',
dataIndex: 'bytesIn',
key: 'bytesIn',
width: '12%',
sorter: (a: ITopic, b: ITopic) => b.bytesIn - a.bytesIn,
render: (t: number) => t === null ? '' : (t / 1024).toFixed(2),
}, {
title: 'Bytes out(KB/s)',
dataIndex: 'bytesOut',
key: 'bytesOut',
width: '12%',
sorter: (a: ITopic, b: ITopic) => b.bytesOut - a.bytesOut,
render: (t: number) => t === null ? '' : (t / 1024).toFixed(2),
}, {
title: '所属集群',
dataIndex: 'clusterName',
key: 'clusterName',
width: '15%',
}, {
title: '关联应用',
dataIndex: 'appName',
key: 'appName',
width: '12%',
render: (text: string, record: ITopic) => (
<>
<Tooltip placement="bottomLeft" title={record.appId} >
{text}
</Tooltip>
</>
),
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
width: '35%',
render: (val: string, item: ITopic, index: number) => (
renderMyTopicOperation(item)
),
},
];
};
export const getApplyOnlineColumns = () => {
const columns = [
{
title: 'AppID',
dataIndex: 'appId',
key: 'appId',
width: '22%',
sorter: (a: IConnectionInfo, b: IConnectionInfo) => a.appId.charCodeAt(0) - b.appId.charCodeAt(0),
},
{
title: '主机名',
dataIndex: 'hostname',
key: 'hostname',
width: '40%',
onCell: () => ({
style: {
maxWidth: 250,
...cellStyle,
},
}),
render: (t: string) => {
return (
<Tooltip placement="bottomLeft" title={t} >{t}</Tooltip>
);
},
},
{
title: '客户端版本',
dataIndex: 'clientVersion',
key: 'clientVersion',
width: '20%',
},
{
title: '客户端类型',
dataIndex: 'clientType',
key: 'clientType',
width: '18%',
render: (t: string) => <span>{t === 'consumer' ? '消费' : '生产'}</span>,
},
];
return columns;
};

View File

@@ -0,0 +1,58 @@
.commonbox {
width: 100%;
display: flex;
justify-content: space-between;
ul {
min-width: 720px;
li {
margin: 0 5px;
float: left;
}
}
}
.expand-text{
float: right;
color: #F5222D;
}
.offline_span{
font-size: 15px;
}
.render_offline{
padding: 20px 0;
margin-left: -35px;
}
.min-width{
min-width: 900px;
}
.table-operation {
a {
color: @primary-color;
}
a+a {
margin-left: 10px;
}
}
.mr--10{
margin-top: -10px;
}
.mr-10 {
margin-right: 10px;
}
.mb-30{
margin-bottom: 30px;
}
.form-tip{
color: #F5222D;
}

View File

@@ -0,0 +1,4 @@
export * from './topic-all';
export * from './topic-mine';
export * from './topic-detail';
export * from './topic-app-list';

View File

@@ -0,0 +1,39 @@
import { Input } from 'component/antd';
import { region } from 'store';
import * as React from 'react';
interface IPeakFlowProps {
value?: any;
onChange?: (result: any) => any;
}
export class PeakFlowInput extends React.Component<IPeakFlowProps> {
public render() {
const { value } = this.props;
return (
<>
<Input
placeholder="请输入峰值流量"
suffix="MB/s"
value={value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.handleChange(e)}
/>
<span>
{region.currentRegion === 'cn' ? value * 40 : value * 45}/
<a
// tslint:disable-next-line:max-line-length
href="https://github.com/didi/kafka-manager"
target="_blank"
>kafka计价方式
</a>
</span>
</>
);
}
public handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const { onChange } = this.props;
// tslint:disable-next-line:no-unused-expression
onChange && onChange(e.target.value);
}
}

View File

@@ -0,0 +1,111 @@
import { Table } from 'component/antd';
import { SearchAndFilterContainer } from 'container/search-filter';
import { tableFilter } from 'lib/utils';
import { observer } from 'mobx-react';
import * as React from 'react';
import { cluster } from 'store/cluster';
import { topic } from 'store/topic';
import { app } from 'store/app';
import { getAllTopicColumns } from './config';
import { ITopic } from 'types/base-type';
import { pagination } from 'constants/table';
import { topicStatusMap } from 'constants/status-map';
import 'styles/table-filter.less';
@observer
export class AllTopic extends SearchAndFilterContainer {
public state = {
searchKey: '',
};
public componentDidMount() {
if (!cluster.allData.length) {
cluster.getAllClusters();
}
if (!topic.allTopicData.length) {
topic.getAllTopic();
}
if (!app.data.length) {
app.getAppList();
}
}
public getColumns = (data: ITopic[]) => {
const statusColumn = Object.assign({
title: '状态',
dataIndex: 'access',
key: 'access',
width: '10%',
filters: tableFilter<ITopic>(data, 'access', topicStatusMap),
onFilter: (text: number, record: ITopic) => record.access === text,
render: (val: number) => (
<div className={val === 0 ? '' : 'success'}>
{topicStatusMap[val] || ''}
</div>
),
}, this.renderColumnsFilter('filterStatus')) as any;
const columns = getAllTopicColumns(this.urlPrefix);
// columns.splice(-2, 0, statusColumn);
return columns;
}
public getData<T extends ITopic>(origin: T[]) {
let data: T[] = [];
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
if (cluster.allActive !== -1 || searchKey !== '') {
data = origin.filter(d =>
((d.topicName !== undefined && d.topicName !== null) && d.topicName.toLowerCase().includes(searchKey as string)
|| ((d.appPrincipals !== undefined && d.appPrincipals !== null) && d.appPrincipals.toLowerCase().includes(searchKey as string)))
&& (cluster.allActive === -1 || d.clusterId === cluster.allActive),
);
} else {
data = origin;
}
return data;
}
public renderTableList(data: ITopic[]) {
return (
<Table
rowKey="key"
columns={this.getColumns(data)}
dataSource={data}
pagination={pagination}
loading={topic.loading}
/>
);
}
public renderTable() {
return this.renderTableList(this.getData(topic.allTopicData));
}
public renderOperationPanel() {
return (
<>
{this.renderAllCluster('集群:')}
{this.renderSearch('名称:', '请输入Topic名称或负责人')}
</>
);
}
public render() {
return (
<div className="container">
<div className="table-operation-panel">
<ul>
{this.renderOperationPanel()}
</ul>
</div>
<div className="table-wrapper">
{this.renderTable()}
</div>
</div>
);
}
}

View File

@@ -0,0 +1,61 @@
import { Button } from 'component/antd';
import { observer } from 'mobx-react';
import * as React from 'react';
import 'styles/table-filter.less';
import { app } from 'store/app';
import { CommonAppList } from 'container/app/app-list';
import { showEditModal } from 'container/modal';
import Url from 'lib/url-parser';
@observer
export class TopicAppList extends CommonAppList {
public static defaultProps = {
from: 'topic',
};
constructor(defaultProps: any) {
super(defaultProps);
}
public componentDidMount() {
if (!app.data.length) {
app.getAppList();
}
if (Url().search.hasOwnProperty('application')) {
showEditModal();
}
}
public renderTable() {
return this.renderTableList(this.getData(app.data));
}
public renderOperationPanel() {
return (
<ul>
{this.renderSearch('', '请输入应用名称或者负责人')}
<li className="right-btn-1">
<Button type="primary" onClick={() => showEditModal()}></Button>
</li>
</ul>
);
}
public applyApp() {
showEditModal();
}
public render() {
return (
<div className="container">
<div className="table-operation-panel">
{this.renderOperationPanel()}
</div>
<div className="table-wrapper">
{this.renderTable()}
</div>
</div>
);
}
}

View File

@@ -0,0 +1,61 @@
import { Select, Tooltip } from 'component/antd';
import { urlPrefix } from 'constants/left-menu';
import { ITopic } from 'types/base-type';
import { topic } from 'store/topic';
import { updateAllTopicFormModal } from '../modal/topic';
import { searchProps } from 'constants/table';
import * as React from 'react';
const Option = Select.Option;
interface IStaffSelectProps {
parameter?: ITopic;
selectData?: any[];
onChange?: (result: string) => any;
value?: string;
}
export class TopicAppSelect extends React.Component<IStaffSelectProps> {
public render() {
const { value, selectData } = this.props;
const query = `application=1`;
let appId: string = null;
const index = Array.isArray(selectData) ? selectData.findIndex(row => row.appId === value) : -1;
appId = index > -1 ? value : selectData && selectData.length ? selectData[0].appId : '' ;
return (
<>
<Select
placeholder="请选择"
defaultValue={appId}
onChange={(e: string) => this.handleChange(e)}
{...searchProps}
>
{selectData.map((d: any) => {
const label = `${d.name}${d.appId}`;
return (<Option value={d.appId} key={d.appId}>
{label.length > 25 ? <Tooltip placement="bottomLeft" title={label}>{label}</Tooltip> : label}
</Option>);
})}
</Select>
{
selectData.length ? null : <i>
<a href={`${urlPrefix}/topic/app-list?${query}`}></a>
</i>}
</>
);
}
public handleChange(params: string) {
const { onChange, parameter } = this.props;
topic.getAuthorities(params, parameter.clusterId, parameter.topicName).then(() => {
updateAllTopicFormModal();
});
// tslint:disable-next-line:no-unused-expression
onChange && onChange(params);
}
}

View File

@@ -0,0 +1,140 @@
import * as React from 'react';
import './index.less';
import Url from 'lib/url-parser';
import { observer } from 'mobx-react';
import { topic, IAppsIdInfo } from 'store/topic';
import { Table, Tooltip } from 'component/antd';
import { SearchAndFilterContainer } from 'container/search-filter';
import { IQuotaQuery } from 'types/base-type';
import { showApplyQuatoModal } from 'container/modal';
import { pagination, cellStyle } from 'constants/table';
import { transBToMB } from 'lib/utils';
@observer
export class AppIdInformation extends SearchAndFilterContainer {
public clusterId: number;
public topicName: string;
public state = {
searchKey: '',
};
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
this.topicName = url.search.topic;
}
public renderColumns = () => {
return [{
title: '应用Id',
key: 'appId',
dataIndex: 'appId',
sorter: (a: IAppsIdInfo, b: IAppsIdInfo) => a.appId.charCodeAt(0) - b.appId.charCodeAt(0),
}, {
title: '应用名称',
key: 'appName',
dataIndex: 'appName',
sorter: (a: IAppsIdInfo, b: IAppsIdInfo) => a.appName.charCodeAt(0) - b.appName.charCodeAt(0),
}, {
title: '负责人',
key: 'appPrincipals',
dataIndex: 'appPrincipals',
onCell: () => ({
style: {
maxWidth: 120,
...cellStyle,
},
}),
render: (text: string) => {
return (
<Tooltip placement="bottomLeft" title={text} >
{text}
</Tooltip>);
},
}, {
title: '生产配额(MB/s)',
key: 'produceQuota',
dataIndex: 'produceQuota',
render: (val: number) => transBToMB(val),
}, {
title: '生产是否限流',
key: 'produceThrottled',
dataIndex: 'produceThrottled',
render: (t: boolean) => <span className={t ? 'fail' : 'success'}>{t ? '是' : '否'}</span>,
}, {
title: '消费配额(MB/s)',
key: 'consumerQuota',
dataIndex: 'consumerQuota',
render: (val: number) => transBToMB(val),
}, {
title: '消费是否限流',
key: 'fetchThrottled',
dataIndex: 'fetchThrottled',
render: (t: boolean) => <span className={t ? 'fail' : 'success'}>{t ? '是' : '否'}</span>,
}, {
title: '操作',
key: 'action',
dataIndex: 'action',
render: (val: string, item: IAppsIdInfo) =>
<a onClick={() => this.applyQuotaQuery(item)}></a>,
}];
}
public applyQuotaQuery = (item: IAppsIdInfo) => {
const isPhysicalClusterId = location.search.indexOf('isPhysicalClusterId') > -1;
topic.getQuotaQuery(item.appId, this.clusterId, this.topicName).then((data) => {
const record = data && data.length ? data[0] : {} as IQuotaQuery;
item.clusterId = this.clusterId;
item.isPhysicalClusterId = isPhysicalClusterId;
showApplyQuatoModal(item, record);
});
}
public getData(data: IAppsIdInfo[]) {
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
const filterData = searchKey ?
(data || []).filter(d => ((d.appId !== undefined && d.appId !== null) && d.appId.toLowerCase().includes(searchKey as string))
|| ((d.appName !== undefined && d.appName !== null) && d.appName.toLowerCase().includes(searchKey as string)),
) : topic.appsIdInfo;
return filterData;
}
public renderAppList() {
const { searchKey } = this.state;
return (
<>
<div className="k-row" >
<ul className="k-tab">
<li></li>
{this.renderSearch('', '请输入所属应用信息')}
</ul>
<div style={searchKey ? { minHeight: 700 } : null}>
<Table
loading={topic.loading}
columns={this.renderColumns()}
table-Layout="fixed"
dataSource={this.getData(topic.appsIdInfo)}
rowKey="key"
pagination={pagination}
/>
</div>
</div>
</>
);
}
public componentDidMount() {
topic.getAppsIdInfo(this.clusterId, this.topicName);
}
public render() {
return (
<>{this.renderAppList()}</>
);
}
}

View File

@@ -0,0 +1,245 @@
import * as React from 'react';
import './index.less';
import Url from 'lib/url-parser';
import { observer } from 'mobx-react';
import { topic, IRealConsumeDetail, ITopicBaseInfo, IRealTimeTraffic } from 'store/topic';
import { Table, Tooltip, Icon, PageHeader, Descriptions, Spin } from 'component/antd';
import { ILabelValue } from 'types/base-type';
import { copyString, transMSecondToHour } from 'lib/utils';
import moment from 'moment';
import { StatusGraghCom } from 'component/flow-table';
import { renderTrafficTable } from 'container/network-flow';
import { timeFormat, indexUrl } from 'constants/strategy';
interface IInfoProps {
baseInfo: ITopicBaseInfo;
}
@observer
export class BaseInformation extends React.Component<IInfoProps> {
public clusterId: number;
public topicName: string;
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
this.topicName = url.search.topic;
}
public updateRealStatus = () => {
topic.getRealTimeTraffic(this.clusterId, this.topicName);
}
public updateConsumeStatus = () => {
topic.getRealConsume(this.clusterId, this.topicName);
}
public fillBaseInfo() {
const { baseInfo } = this.props;
const createTime = moment(baseInfo.createTime).format(timeFormat);
const modifyTime = moment(baseInfo.modifyTime).format(timeFormat);
const retentionTime = transMSecondToHour(baseInfo.retentionTime);
if (baseInfo) {
const infoList: ILabelValue[] = [{
label: '健康分',
value: baseInfo.score,
}, {
label: '分区数',
value: baseInfo.partitionNum,
}, {
label: '副本数',
value: baseInfo.replicaNum,
}, {
label: '存储时间',
value: `${retentionTime} 小时`,
}, {
label: '创建时间',
value: createTime,
}, {
label: '更改时间',
value: modifyTime,
}, {
label: '压缩格式',
value: baseInfo.topicCodeC,
}, {
label: '集群ID',
value: baseInfo.clusterId,
}];
const infoHide: ILabelValue[] = [{
label: 'Bootstrap Severs',
value: baseInfo.bootstrapServers,
}, {
label: 'Topic说明',
value: baseInfo.description,
}];
return (
<>
<div className="chart-title"></div>
<PageHeader className="detail" title="">
<Descriptions size="small" column={3}>
<Descriptions.Item key={baseInfo.appName} label="所属应用">{baseInfo.appName}</Descriptions.Item>
<Descriptions.Item key={baseInfo.principals} label="应用负责人">
<Tooltip placement="bottomLeft" title={baseInfo.principals}>
<span className="overview-bootstrap">
<Icon
onClick={() => copyString(baseInfo.principals)}
type="copy"
className="didi-theme overview-theme"
/>
<i className="overview-boot">{baseInfo.principals}</i>
</span>
</Tooltip>
</Descriptions.Item>
{infoList.map((item: ILabelValue, index: number) => (
<Descriptions.Item
key={index}
label={item.label}
>{item.value}
</Descriptions.Item>
))}
{infoHide.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 realTimeTraffic() {
const realTraffic = topic.realTraffic;
if (realTraffic) {
return (
<>
<Spin spinning={topic.realLoading}>
{renderTrafficTable(this.updateRealStatus, StatusGragh)}
</Spin>
</>
);
}
}
public realTimeConsume() {
const consumeColumns = [{
title: ' ',
dataIndex: 'metricsName',
key: 'metricsName',
},
{
title: 'RequestQueueTime',
dataIndex: 'requestQueueTimeMs',
key: 'requestQueueTimeMs',
render: (t: number) => t === null ? '' : (t ? t.toFixed(2) : 0),
},
{
title: 'LocalTime',
dataIndex: 'localTimeMs',
key: 'localTimeMs',
render: (t: number) => t === null ? '' : (t ? t.toFixed(2) : 0),
},
{
title: 'RemoteTime',
dataIndex: 'remoteTimeMs',
key: 'remoteTimeMs',
render: (t: number) => t === null ? '' : (t ? t.toFixed(2) : 0),
},
{
title: 'ThrottleTime',
dataIndex: 'throttleTimeMs',
key: 'throttleTimeMs',
render: (t: number) => t === null ? '' : (t ? t.toFixed(2) : 0),
},
{
title: 'ResponseQueueTime',
dataIndex: 'responseQueueTimeMs',
key: 'responseQueueTimeMs',
render: (t: number) => t === null ? '' : (t ? t.toFixed(2) : 0),
},
{
title: 'ResponseSendTime',
dataIndex: 'responseSendTimeMs',
key: 'responseSendTimeMs',
render: (t: number) => t === null ? '' : (t ? t.toFixed(2) : 0),
},
{
title: 'TotalTime',
dataIndex: 'totalTimeMs',
key: 'totalTimeMs',
render: (t: number) => t === null ? '' : (t ? t.toFixed(2) : 0),
}];
const realConsume = topic.realConsume;
if (realConsume) {
const consumeData: IRealConsumeDetail[] = [];
Object.keys(realConsume).map((key: string) => {
if (realConsume[key]) {
realConsume[key].metricsName = (key === '0') ? 'Produce' : 'Fetch';
consumeData.push(realConsume[key]);
}
});
return (
<>
<div className="traffic-table">
<div className="traffic-header">
<span>
<span className="action-button"></span>
<a href={indexUrl} target="_blank"></a>
</span>
<span className="k-abs" onClick={this.updateConsumeStatus}>
<i className="k-icon-shuaxin didi-theme mr-5" />
<a></a>
</span>
</div>
<Table
columns={consumeColumns}
dataSource={consumeData}
pagination={false}
loading={topic.consumeLoading}
rowKey="metricsName"
/>
</div>
</>
);
}
}
public componentDidMount() {
topic.getRealTimeTraffic(this.clusterId, this.topicName);
topic.getRealConsume(this.clusterId, this.topicName);
}
public render() {
return (
<>
<div className="base-info">
{this.fillBaseInfo()}
{this.realTimeTraffic()}
{this.realTimeConsume()}
</div>
</>
);
}
}
@observer
export class StatusGragh extends StatusGraghCom<IRealTimeTraffic> {
public getData = () => {
return topic.realTraffic;
}
public getLoading = () => {
return topic.realLoading;
}
}

View File

@@ -0,0 +1,118 @@
import * as React from 'react';
import Url from 'lib/url-parser';
import { DatePicker, notification, Icon } from 'component/antd';
import { BarChartComponet } from 'component/chart';
import { SearchAndFilterContainer } from 'container/search-filter';
import { observer } from 'mobx-react';
import { Moment } from 'moment';
import { topic } from 'store/topic';
import { timeMonth } from 'constants/strategy';
import './index.less';
const { RangePicker } = DatePicker;
import moment = require('moment');
@observer
export class BillInformation extends SearchAndFilterContainer {
public clusterId: number;
public topicName: string;
public state = {
mode: ['month', 'month'] as any,
value: [moment(new Date()).subtract(6, 'months'), moment()] as any,
};
private startTime: number = moment(new Date()).subtract(6, 'months').valueOf();
private endTime: number = moment().valueOf();
private chart: any = null;
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
this.topicName = url.search.topic;
}
public getData() {
const { clusterId, topicName, startTime, endTime } = this;
topic.setLoading(true);
return topic.getBillInfo(clusterId, topicName, startTime, endTime);
}
public renderDatePick() {
const { value, mode } = this.state;
return (
<>
<div className="op-panel">
<span>
<RangePicker
ranges={{
: [moment(new Date()).subtract(6, 'months'), moment()],
: [moment().startOf('year'), moment().endOf('year')],
}}
value={value}
mode={mode}
format={timeMonth}
onChange={this.handlePanelChange}
onPanelChange={this.handlePanelChange}
/>
</span>
</div>
</>
);
}
public disabledDateTime = (current: Moment) => {
return current && current > moment().endOf('day');
}
public handleRefreshChart = () => {
this.chart.handleRefreshChart();
}
public handlePanelChange = (value: any, mode: any) => {
this.setState({
value,
mode: ['month', 'month'] as any,
});
this.startTime = value[0].valueOf();
this.endTime = value[1].valueOf();
if (this.startTime >= this.endTime) {
return notification.error({ message: '开始时间不能大于或等于结束时间' });
}
this.getData();
this.handleRefreshChart();
}
public renderChart() {
return (
<div className="chart-box">
<BarChartComponet ref={(ref) => this.chart = ref} getChartData={this.getData.bind(this, null)} />
</div>
);
}
public render() {
return(
<>
<div className="k-row" >
<ul className="k-tab">
<li>&nbsp;
<a
// tslint:disable-next-line:max-line-length
href="https://github.com/didi/kafka-manager"
target="_blank"
>
<Icon type="question-circle" />
</a>
</li>
{this.renderDatePick()}
</ul>
{this.renderChart()}
</div>
</>
);
}
}

View File

@@ -0,0 +1,163 @@
import * as React from 'react';
import './index.less';
import { topic, ITopicBroker, IBrokerInfo } from 'store/topic';
import { Table, Tooltip } from 'component/antd';
import Url from 'lib/url-parser';
import { SearchAndFilterContainer } from 'container/search-filter';
import { observer } from 'mobx-react';
import { pagination } from 'constants/table';
@observer
export class BrokersInformation extends SearchAndFilterContainer {
public clusterId: number;
public topicName: string;
public from: string;
public isPhysical: boolean;
public state = {
searchKey: '',
};
public brokerColumns = [{
title: 'BrokerID',
key: 'brokerId',
dataIndex: 'brokerId',
width: '10%',
sorter: (a: ITopicBroker, b: ITopicBroker) => b.brokerId - a.brokerId,
render: (t: string, record: ITopicBroker) => {
return ( // alive true==可点击false==不可点击
<>
{record.alive ?
<a href={`${this.urlPrefix}/admin/broker-detail?clusterId=${record.clusterId}&brokerId=${t}`}>{t}</a>
: <a style={{ cursor: 'not-allowed', color: '#999' }}>{t}</a>}
</>
);
},
}, {
title: 'Host',
key: 'host',
dataIndex: 'host',
width: '20%',
render: (text: string) => <Tooltip placement="bottomLeft" title={text}>{text}</Tooltip>,
}, {
title: 'Leader个数',
key: 'leaderPartitionIdListLength',
dataIndex: 'leaderPartitionIdList',
width: '10%',
sorter: (a: ITopicBroker, b: ITopicBroker) => b.leaderPartitionIdList.length - a.leaderPartitionIdList.length,
render: (t: []) => t.length,
}, {
title: '分区LeaderID',
key: 'leaderPartitionIdList',
dataIndex: 'leaderPartitionIdList',
width: '25%',
onCell: () => ({
style: {
maxWidth: 180,
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
cursor: 'pointer',
},
}),
render: (t: []) => {
return (
<Tooltip placement="bottomLeft" title={t.join('、')}>
{t.map(i => <span key={i} className="p-params">{i}</span>)}
</Tooltip>
);
},
}, {
title: '分区个数',
key: 'partitionNum',
dataIndex: 'partitionNum',
width: '10%',
sorter: (a: ITopicBroker, b: ITopicBroker) => b.partitionNum - a.partitionNum,
}, {
title: '分区ID',
key: 'partitionIdList',
dataIndex: 'partitionIdList',
width: '25%',
onCell: () => ({
style: {
maxWidth: 180,
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
cursor: 'pointer',
},
}),
render: (t: []) => {
return (
<Tooltip placement="bottomLeft" title={t.join('、')}>
{t.map(i => <span key={i} className="p-params">{i}</span>)}
</Tooltip>
);
},
}];
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
this.topicName = url.search.topic;
this.from = decodeURIComponent(url.search.from);
this.isPhysical = this.from.includes('expert');
}
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 getData<T extends IBrokerInfo>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IBrokerInfo) =>
(item.host !== undefined && item.host !== null) && item.host.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public renderMore() {
const { searchKey } = this.state;
return (
<>
<div className="k-row">
<ul className="k-tab">
<li>Broker信息</li>
{this.renderSearch('', '请输入Host')}
</ul>
<div style={searchKey ? { minHeight: 370 } : null}>
<Table
columns={this.brokerColumns}
dataSource={this.getData(topic.brokerInfo)}
rowKey="key"
pagination={pagination}
/>
</div>
</div>
</>
);
}
public componentDidMount() {
topic.getBrokerInfo(this.clusterId, this.topicName);
}
public render() {
return (
<>
{this.renderMore()}
</>
);
}
}

View File

@@ -0,0 +1,98 @@
import * as React from 'react';
import { Select, Spin, Tooltip } from 'component/antd';
import { topic, IAppsIdInfo } from 'store/topic';
import { ILabelValue } from 'types/base-type';
import { observer } from 'mobx-react';
import { selectOptionMap } from 'constants/status-map';
import { ChartWithDatePicker } from 'component/chart';
import { searchProps } from 'constants/table';
import moment from 'moment';
@observer
export class NetWorkFlow extends React.Component<any> {
public state = {
loading: false,
};
private $chart: any;
public getChartData = (startTime: moment.Moment, endTime: moment.Moment) => {
const { getApi } = this.props;
topic.changeStartTime(startTime);
topic.changeEndTime(endTime);
return getApi();
}
public handleChange = (value: any) => {
const { selectChange } = this.props;
this.$chart.changeChartOptions(selectChange(value));
}
public handleAppChange = (value: string) => {
const {clusterId, topicName} = this.props;
topic.appId = value;
this.setState({ loading: true });
topic.getMetriceInfo(clusterId, topicName).then(data => {
this.$chart.changeChartOptions(data);
this.setState({ loading: false });
});
}
public renderSelect() {
const isTrue = this.props.selectArr === selectOptionMap;
return (
<>
<li>
<span className="label"></span>
<Select
defaultValue={this.props.type}
style={{ width: 230 }}
onChange={this.handleChange}
{...searchProps}
>
{this.props.selectArr.map((item: ILabelValue) => (
<Select.Option key={item.value} value={item.value}>
{item.label.length > 16 ?
<Tooltip placement="bottomLeft" title={item.label}>{item.label}</Tooltip>
: item.label}
</Select.Option>
))}
</Select>
</li>
<li className={!isTrue ? 'is-show' : ''}>
<span className="label"></span>
<Select
placeholder="请选择应用"
style={{ width: 180 }}
onChange={this.handleAppChange}
{...searchProps}
>
{topic.appInfo.map((item: IAppsIdInfo) => (
<Select.Option key={item.appId} value={item.appId}>
{item.appName.length > 16 ?
<Tooltip placement="bottomLeft" title={item.appName}> {item.appName} </Tooltip>
: item.appName}
</Select.Option>
))}
</Select>
</li>
</>
);
}
public componentDidMount() {
topic.initTime();
}
public render() {
return (
<Spin spinning={this.state.loading} className="chart-content">
<ChartWithDatePicker
customerNode={this.renderSelect()}
getChartData={this.getChartData}
ref={chart => this.$chart = chart}
/>
</Spin>
);
}
}

View File

@@ -0,0 +1,112 @@
import * as React from 'react';
import './index.less';
import { observer } from 'mobx-react';
import { topic, IConnectionInfo } from 'store/topic';
import { Table, Tooltip } from 'component/antd';
import { SearchAndFilterContainer } from 'container/search-filter';
import Url from 'lib/url-parser';
import { pagination, cellStyle } from 'constants/table';
@observer
export class ConnectInformation extends SearchAndFilterContainer {
public clusterId: number;
public topicName: string;
public state = {
searchKey: '',
};
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
this.topicName = url.search.topic;
}
public renderConnectionInfo(connectInfo: IConnectionInfo[]) {
const clientType = Object.assign({
title: '客户端类型',
dataIndex: 'clientType',
key: 'clientType',
width: '20%',
filters: [{ text: '消费', value: 'consumer' }, { text: '生产', value: 'produce' }],
onFilter: (value: string, record: IConnectionInfo) => record.clientType.indexOf(value) === 0,
render: (t: string) =>
<span>{t === 'consumer' ? '消费' : '生产'}</span>,
}, this.renderColumnsFilter('filterVisible'));
const columns = [{
title: 'AppID',
dataIndex: 'appId',
key: 'appId',
width: '20%',
sorter: (a: IConnectionInfo, b: IConnectionInfo) => a.appId.charCodeAt(0) - b.appId.charCodeAt(0),
},
{
title: '主机名',
dataIndex: 'hostname',
key: 'hostname',
width: '40%',
onCell: () => ({
style: {
maxWidth: 250,
...cellStyle,
},
}),
render: (t: string) => {
return (
<Tooltip placement="bottomLeft" title={t} >{t}</Tooltip>
);
},
},
{
title: '客户端版本',
dataIndex: 'clientVersion',
key: 'clientVersion',
width: '20%',
},
clientType,
];
if (connectInfo) {
return (
<>
<Table dataSource={connectInfo} columns={columns} pagination={pagination} loading={topic.loading} />
</>
);
}
}
public getData<T extends IConnectionInfo>(origin: T[]) {
let data: T[] = [];
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
if (searchKey !== '') {
data = origin.filter(d =>
((d.appId !== undefined && d.appId !== null) && d.appId.toLowerCase().includes(searchKey as string))
|| ((d.hostname !== undefined && d.hostname !== null) && d.hostname.toLowerCase().includes(searchKey as string)),
);
} else {
data = origin;
}
return data;
}
public componentDidMount() {
const appId = this.props.baseInfo.appId;
topic.getConnectionInfo(this.clusterId, this.topicName, appId);
}
public render() {
return (
<>
<div className="k-row" >
<ul className="k-tab">
<li></li>
{this.renderSearch('', '请输入连接信息', 'searchKey')}
</ul>
{this.renderConnectionInfo(this.getData(topic.connectionInfo))}
</div>
</>
);
}
}

View File

@@ -0,0 +1,264 @@
import * as React from 'react';
import { wrapper, region } from 'store';
import './index.less';
import { topic, IConsumerGroups, IConsumeDetails } from 'store/topic';
import { observer } from 'mobx-react';
import { SearchAndFilterContainer } from 'container/search-filter';
import Url from 'lib/url-parser';
import { pagination } from 'constants/table';
import { IXFormWrapper, IOffset } from 'types/base-type';
import { Table, Button, Tooltip } from 'component/antd';
import ResetOffset from './reset-offset';
import { urlPrefix } from 'constants/left-menu';
import { cellStyle } from 'constants/table';
import './index.less';
@observer
export class GroupID extends SearchAndFilterContainer {
public static getDerivedStateFromProps(nextProps: any, prevState: any) {
const url = Url();
return {
...prevState,
isDetailPage: url.search.consumerGroup && url.search.location,
};
}
public clusterId: number;
public topicName: string;
public consumerGroup: string;
public location: string;
public isPhysicalTrue: string;
public state = {
searchKey: '',
updateRender: false,
isDetailPage: false,
};
private xFormWrapper: IXFormWrapper;
constructor(props: any) {
super(props);
this.handleUrlSearch();
}
public componentDidMount() {
if (!topic.showConsumeDetail) {
return topic.getConsumerGroups(this.clusterId, this.topicName);
}
return topic.getConsumeDetails(this.clusterId, this.topicName, this.consumerGroup, this.location);
}
public componentDidUpdate(prevProps: any, prevState: any) {
if (prevState.isDetailPage !== this.state.isDetailPage) {
this.handleUrlSearch();
if (!topic.showConsumeDetail && !topic.consumerGroups.length) {
topic.getConsumerGroups(this.clusterId, this.topicName);
}
}
}
public handleUrlSearch = () => {
const url = Url();
this.clusterId = Number(url.search.clusterId);
this.topicName = url.search.topic;
const isPhysical = Url().search.hasOwnProperty('isPhysicalClusterId');
this.isPhysicalTrue = isPhysical ? '&isPhysicalClusterId=true' : '';
topic.setConsumeDetail(false);
if (url.search.consumerGroup && url.search.location) {
topic.setConsumeDetail(true);
this.consumerGroup = url.search.consumerGroup;
this.location = url.search.location;
}
}
public showConsumeDetail = (record: IConsumerGroups) => {
// tslint:disable-next-line:max-line-length
const url = `${urlPrefix}/topic/topic-detail?clusterId=${this.clusterId}&topic=${this.topicName}&consumerGroup=${record.consumerGroup}&location=${record.location}${this.isPhysicalTrue}&region=${region.currentRegion}#4`;
history.pushState({ url }, '', url);
this.handleUrlSearch();
topic.setConsumeDetail(true);
topic.getConsumeDetails(this.clusterId, this.topicName, record.consumerGroup, record.location);
this.setState({
updateRender: !this.state.updateRender,
});
}
public updateDetailsStatus = () => {
topic.getConsumeDetails(this.clusterId,
this.topicName, this.consumerGroup, this.location);
}
public backToPage = () => {
// tslint:disable-next-line:max-line-length
const url = `${urlPrefix}/topic/topic-detail?clusterId=${this.clusterId}&topic=${this.topicName}${this.isPhysicalTrue}&region=${region.currentRegion}#4`;
history.pushState({ url }, '', url);
topic.setConsumeDetail(false);
topic.getConsumerGroups(this.clusterId, this.topicName);
this.setState({
updateRender: !this.state.updateRender,
});
}
public showResetOffset() {
this.xFormWrapper = {
type: 'drawer',
formMap: [
],
formData: {
},
visible: true,
width: 600,
title: '重置消费偏移',
customRenderElement: this.renderDrawerInfo(),
noform: true,
nofooter: true,
onSubmit: (value: any) => {
//
},
};
wrapper.open(this.xFormWrapper);
}
public renderDrawerInfo() {
const OffsetReset = {
clusterId: this.clusterId,
topicName: this.topicName,
consumerGroup: this.consumerGroup,
location: this.location,
offsetList: [],
timestamp: 0,
} as IOffset;
return (
<div>
<ResetOffset OffsetReset={OffsetReset} />
</div>
);
}
public renderConsumerDetails() {
const consumerGroup = this.consumerGroup;
const columns = [{
title: 'Partition ID',
dataIndex: 'partitionId',
key: 'partitionId',
width: '10%',
sorter: (a: IConsumeDetails, b: IConsumeDetails) => +b.partitionId - +a.partitionId,
}, {
title: 'Consumer ID',
dataIndex: 'clientId',
key: 'clientId',
width: '40%',
onCell: () => ({
style: {
maxWidth: 300,
...cellStyle,
},
}),
render: (t: IConsumeDetails) => <Tooltip placement="bottomLeft" title={t}> {t} </Tooltip>,
}, {
title: 'Consume Offset',
dataIndex: 'consumeOffset',
key: 'consumeOffset',
width: '20%',
sorter: (a: IConsumeDetails, b: IConsumeDetails) => +b.consumeOffset - +a.consumeOffset,
}, {
title: 'Partition Offset',
dataIndex: 'partitionOffset',
key: 'partitionOffset',
width: '20%',
sorter: (a: IConsumeDetails, b: IConsumeDetails) => +b.partitionOffset - +a.partitionOffset,
}, {
title: 'Lag',
dataIndex: 'lag',
key: 'lag',
width: '10%',
sorter: (a: IConsumeDetails, b: IConsumeDetails) => +b.lag - +a.lag,
}];
return (
<>
<div className="details-box">
<b>{consumerGroup}</b>
<div>
<Button onClick={this.backToPage}></Button>
<Button onClick={this.updateDetailsStatus}></Button>
<Button onClick={() => this.showResetOffset()}>Offset</Button>
</div>
</div>
<Table
columns={columns}
dataSource={topic.consumeDetails}
rowKey="key"
pagination={pagination}
/>
</>
);
}
public renderConsumerTable(consumerData: IConsumerGroups[]) {
const columns = [{
title: '消费组名称',
dataIndex: 'consumerGroup',
key: 'consumerGroup',
width: '36%',
render: (t: string, r: IConsumerGroups) => (
<> <a onClick={() => this.showConsumeDetail(r)}>{t}</a> </>
),
}, {
title: 'AppIds',
dataIndex: 'appIds',
key: 'appIds',
width: '36%',
}, {
title: 'Location',
dataIndex: 'location',
key: 'location',
width: '34%',
},
];
return (
<>
<Table
columns={columns}
dataSource={consumerData}
pagination={pagination}
rowKey="key"
scroll={{ y: 400 }}
/>
</>
);
}
public getData<T extends IConsumerGroups>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IConsumerGroups) =>
(item.consumerGroup !== undefined && item.consumerGroup !== null) && item.consumerGroup.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public renderConsumerGroups() {
return (
<>
<div className="k-row" >
<ul className="k-tab">
<li></li>
{this.renderSearch('', '请输入消费组名称')}
</ul>
{this.renderConsumerTable(this.getData(topic.consumerGroups))}
</div>
</>
);
}
public render() {
return (
<>
{topic.showConsumeDetail ? this.renderConsumerDetails() : this.renderConsumerGroups()}
</>
);
}
}

View File

@@ -0,0 +1,217 @@
.topic-detail-header {
bottom: 10px;
}
.base-info {
width: 100%;
.base-info-header {
background: #fff;
min-width: 960px;
height: 200px;
padding: 20px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
ul {
width: 320px;
position: relative;
li {
float: left;
font-size: 13px;
width: 320px;
white-space: nowrap;
text-overflow: ellipsis;
/**超出部分省略号**/
overflow: hidden;
/** 隐藏超出的内容 **/
height: 30px;
line-height: 30px;
b {
font-weight: 600;
}
.special-li {
min-width: 960px;
position: absolute;
z-index: 9;
left: 0;
bottom: 0;
}
}
}
.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;
}
}
}
}
.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);
}
}
.p-description {
margin-left: 20px;
span {
display: inline-block;
width: 150px;
text-align: right;
margin-right: 10px;
}
}
.o-container {
width: 100%;
padding: 40px 0 0 10px;
}
}
.partionButton {
float: right;
margin: 30px 60px;
}
.b-list {
button {
margin: 0 5px;
}
}
.title-con {
font-size: 14px;
color: @primary-color;
line-height: 50px;
}
.chart-title {
color: rgba(0, 0, 0, 0.65);
font-size: 13px;
line-height: 44px;
font-weight: 100;
background: rgb(245, 245, 245);
border: 1px solid #e8e8e8;
padding: 0 10px;
}
.chart-box-0 {
background-color: white;
width: 100%;
min-height: 520px;
margin-bottom: 20px;
.chart-title {
color: rgba(0, 0, 0, 0.65);
font-size: 13px;
line-height: 44px;
font-weight: 100;
background: rgb(245, 245, 245);
border: 1px solid #e8e8e8;
padding: 0 10px;
}
.chart-divider {
margin-bottom: -10px;
}
.ant-divider-horizontal {
margin: 0px 0px 24px 0px;
}
}
.details-box {
padding: 0 5px;
display: flex;
justify-content: space-between;
margin-bottom: 5px;
b {
font-weight: 100;
line-height: 32px;
font-size: 13px;
}
Button {
margin: 0 5px;
}
}
.sample-button {
margin-right: 100px;
float: right;
}
.topic-detail-sample {
margin-top: 60px;
h2 {
line-height: 35px;
font-size: 15px;
}
.detail-sample-box {
width: 560px;
border: 1px solid #d9d9dd;
border-radius: 3px;
h3 {
font-weight: bold;
height: 45px;
line-height: 45px;
border-bottom: 1px solid #e3e3e4;
background: #f5f7f9;
padding: 0 10px;
}
.detail-sample-span {
height: 500px;
overflow-y: auto;
li {
border-bottom: 1px solid #e3e3e4;
padding: 5px 10px;
line-height: 24px;
font-size: 13px;
color: #333;
}
}
}
}
.is-show {
display: none;
}

View File

@@ -0,0 +1,380 @@
import * as React from 'react';
import './index.less';
import { wrapper, region } from 'store';
import { Tabs, PageHeader, Button, notification, Drawer, message, Icon } from 'antd';
import { observer } from 'mobx-react';
import { BaseInformation } from './base-information';
import { StatusChart } from './status-chart';
import { ConnectInformation } from './connect-information';
import { GroupID } from './group-id';
import { PartitionInformation } from './partition-information';
import { BrokersInformation } from './brokers-information';
import { AppIdInformation } from './appid-information';
import { BillInformation } from './bill-information';
import { IXFormWrapper, ITopic } from 'types/base-type';
import { getTopicCompile, getTopicSampling } from 'lib/api';
import { copyString } from 'lib/utils';
import { topic, ITopicBaseInfo } from 'store/topic';
import { XFormComponent, IFormItem } from 'component/x-form';
import { applyExpandModal } from 'container/modal';
import { applyTopicQuotaQuery } from '../config';
import { users } from 'store/users';
import { urlPrefix } from 'constants/left-menu';
import { handlePageBack } from 'lib/utils';
import Url from 'lib/url-parser';
const { TabPane } = Tabs;
interface IInfoData {
value: string;
}
@observer
export class TopicDetail extends React.Component {
public clusterId: number;
public topicName: string;
public isPhysicalTrue: string;
public state = {
drawerVisible: false,
infoVisible: false,
infoTopicList: [] as IInfoData[],
};
private $formRef: any;
private xFormWrapper: IXFormWrapper;
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
this.topicName = url.search.topic;
const isPhysical = Url().search.hasOwnProperty('isPhysicalClusterId');
this.isPhysicalTrue = isPhysical ? '&isPhysicalClusterId=true' : '';
}
public compileDetails() {
this.xFormWrapper = {
formMap: [
{
key: 'topicName',
label: 'Topic名称',
attrs: {
disabled: true,
},
},
{
key: 'appName',
label: '应用名称',
attrs: {
disabled: true,
},
},
{
key: 'description',
label: '备注',
attrs: {
placeholder: '请输入备注',
},
rules: [{
required: true,
message: '请输入备注',
}],
},
],
formData: {
topicName: this.topicName,
appName: topic.baseInfo.appName,
description: topic.baseInfo.description,
},
okText: '确认',
visible: true,
width: 600,
title: '编辑',
onSubmit: (value: any) => {
const compile = {
appId: topic.baseInfo.appId,
clusterId: this.clusterId,
description: value.description,
topicName: this.topicName,
} as ITopic;
getTopicCompile(compile).then(data => {
notification.success({ message: '编辑成功' });
topic.getTopicBasicInfo(this.clusterId, this.topicName);
});
},
};
wrapper.open(this.xFormWrapper);
}
public drawerRender() {
const formMap = [
{
key: 'maxMsgNum',
label: '最大采样数据条数',
type: 'input_number',
rules: [{
required: true,
message: '请输入最大采样数据条数',
}],
attrs: {
max: 100,
},
},
{
key: 'timeout',
label: '最大采样时间',
type: 'input_number',
rules: [{
required: true,
message: '请输入最大采样时间',
}],
attrs: {
max: 300000,
},
},
{
key: 'partitionId',
label: '分区号',
type: 'input_number',
rules: [{
required: false,
message: '请输入分区号',
}],
},
{
key: 'offset',
label: '偏移量',
type: 'input_number',
rules: [{
required: false,
message: '请输入偏移量',
}],
},
{
key: 'truncate',
label: '是否截断',
type: 'radio_group',
defaultValue: 'true',
options: [{
label: '是',
value: 'true',
}, {
label: '否',
value: 'false',
}],
rules: [{
required: true,
message: '请选择是否截断',
}],
},
] as IFormItem [];
const formData = {
maxMsgNum: 1,
timeout: 3000,
};
const { infoVisible } = this.state;
return(
<>
<Drawer
title="Topic 采样"
placement="right"
closable={false}
onClose={this.drawerClose}
visible={this.state.drawerVisible}
width={600}
key="1"
>
<XFormComponent
ref={form => this.$formRef = form}
formData={formData}
formMap={formMap}
/>
<Button type="primary" onClick={this.drawerSubmit} className="sample-button"></Button>
{infoVisible ? this.renderInfo() : null}
</Drawer>
</>
);
}
public getAllValue = () => {
const { infoTopicList } = this.state;
const text = infoTopicList.map(ele => ele.value );
return text.join('\n\n');
}
public renderInfo() {
const { infoTopicList } = this.state;
return (
<>
<div>
<div className="topic-detail-sample">
<h2></h2>
<div className="detail-sample-box">
<h3>
{this.topicName} <Icon
onClick={() => copyString(this.getAllValue())}
type="copy"
className="didi-theme"
/>
</h3>
<div className="detail-sample-span">
{infoTopicList.map((v, index) => (
<li key={index}>
<Icon
onClick={() => copyString(v.value)}
type="copy"
className="didi-theme"
/> {v.value}
</li>
))}
</div>
</div>
</div>
</div>
</>
);
}
public drawerSubmit = (value: any) => {
this.$formRef.validateFields((error: Error, result: any) => {
if (error) {
return;
}
result.truncate = result.truncate === 'true';
getTopicSampling(result, this.clusterId, this.topicName).then(data => {
this.setState({
infoTopicList: data,
infoVisible: true,
});
message.success('采样成功');
});
});
}
public showDrawer = () => {
this.setState({
drawerVisible: true,
});
}
public drawerClose = () => {
this.setState({
drawerVisible: false,
infoVisible: false,
});
this.resetForm();
}
public resetForm = (resetFields?: string[]) => {
// tslint:disable-next-line:no-unused-expression
this.$formRef && this.$formRef.resetFields(resetFields || '');
}
public cusstr(str: string, findStr: string, num: number) {
let idx = str.indexOf(findStr);
let count = 1;
while (idx >= 0 && count < num) {
idx = str.indexOf(findStr, idx + 1);
count++;
}
if (idx < 0) {
return '';
}
return str.substring(0, idx);
}
public setConsumeUrl = (key: string) => {
// tslint:disable-next-line:max-line-length
const url = `${urlPrefix}/topic/topic-detail?clusterId=${this.clusterId}&topic=${this.topicName}${this.isPhysicalTrue}&region=${region.currentRegion}#${key}`;
history.pushState({ url }, '', url);
}
public handleTabKey = (key: string) => {
location.hash = key;
if (location.search.match(RegExp(/&consumerGroup=/))) {
this.setConsumeUrl(key);
}
}
public onTabClick = (key: string) => {
location.hash = key;
if (key === '4') {
topic.setConsumeDetail(false);
this.setConsumeUrl(key);
}
}
public componentDidMount() {
topic.getTopicBasicInfo(this.clusterId, this.topicName);
topic.getTopicBusiness(this.clusterId, this.topicName);
}
public render() {
const role = users.currentUser.role;
const baseInfo = topic.baseInfo as ITopicBaseInfo;
const showEditBtn = topic.topicBusiness && topic.topicBusiness.principals.includes(users.currentUser.username);
const topicRecord = {
clusterId: this.clusterId,
topicName: this.topicName,
} as ITopic;
return (
<>
{
baseInfo ?
<>
<PageHeader
className="detail topic-detail-header"
onBack={() => handlePageBack(`/topic`)}
title={this.topicName || ''}
extra={
<>
<Button key="1" type="primary" onClick={() => applyTopicQuotaQuery(topicRecord)} ></Button>
<Button key="2" type="primary" onClick={() => applyExpandModal(topicRecord)} ></Button>
<Button key="3" type="primary" onClick={this.showDrawer.bind(this)} ></Button>
{showEditBtn && <Button key="4" onClick={() => this.compileDetails()} type="primary"></Button>}
</>
}
/>
<Tabs
activeKey={location.hash.substr(1) || '1'}
type="card"
onChange={this.handleTabKey}
onTabClick={this.onTabClick}
>
<TabPane tab="基本信息" key="1">
<BaseInformation baseInfo={topic.baseInfo} />
</TabPane>
<TabPane tab="状态图" key="2">
<StatusChart />
</TabPane>
<TabPane tab="连接信息" key="3">
<ConnectInformation baseInfo={topic.baseInfo} />
</TabPane>
<TabPane tab="消费组信息" key="4">
<GroupID />
</TabPane>
<TabPane tab="分区信息" key="5">
<PartitionInformation />
</TabPane>
{
role === 0 ? null :
<TabPane tab="Broker信息" key="6">
<BrokersInformation />
</TabPane>
}
<TabPane tab="应用信息" key="7">
<AppIdInformation />
</TabPane>
<TabPane tab="账单信息" key="8">
<BillInformation />
</TabPane>
</Tabs>
</>
: null
}
{this.drawerRender()}
</>
);
}
}

View File

@@ -0,0 +1,132 @@
import * as React from 'react';
import './index.less';
import { observer } from 'mobx-react';
import { topic, IPartitionsInfo } from 'store/topic';
import { Table, Tooltip } from 'component/antd';
import { SearchAndFilterContainer } from 'container/search-filter';
import Url from 'lib/url-parser';
import { transBToMB } from 'lib/utils';
import { pagination } from 'constants/table';
@observer
export class PartitionInformation extends SearchAndFilterContainer {
public clusterId: number;
public topicName: string;
public state = {
brokerKey: '',
searchKey: '',
};
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
this.topicName = url.search.topic;
}
public renderColumns = () => {
const underReplicated = Object.assign({
title: '是否同步',
key: 'underReplicated',
dataIndex: 'underReplicated',
filters: [{ text: '是', value: 'true' }, { text: '否', value: 'false' }],
onFilter: (value: string, record: IPartitionsInfo) => record.underReplicated + '' === value,
render: (t: any) => <span className={t ? 'success' : 'fail'}>{t ? '是' : '否'}</span>,
}, this.renderColumnsFilter('filterVisible'));
return [{
title: '分区ID',
key: 'partitionId',
dataIndex: 'partitionId',
sorter: (a: IPartitionsInfo, b: IPartitionsInfo) => b.partitionId - a.partitionId,
}, {
title: 'beginningOffset',
key: 'beginningOffset',
dataIndex: 'beginningOffset',
sorter: (a: IPartitionsInfo, b: IPartitionsInfo) => b.beginningOffset - a.beginningOffset,
}, {
title: 'endOffset',
key: 'endOffset',
dataIndex: 'endOffset',
sorter: (a: IPartitionsInfo, b: IPartitionsInfo) => b.endOffset - a.endOffset,
}, {
title: 'msgNum',
key: 'msgNum',
dataIndex: 'msgNum',
sorter: (a: IPartitionsInfo, b: IPartitionsInfo) => b.msgNum - a.msgNum,
}, {
title: 'Leader Broker',
key: 'leaderBrokerId',
dataIndex: 'leaderBrokerId',
sorter: (a: IPartitionsInfo, b: IPartitionsInfo) => b.leaderBrokerId - a.leaderBrokerId,
}, {
title: 'LogSizeMB',
key: 'logSize',
dataIndex: 'logSize',
sorter: (a: IPartitionsInfo, b: IPartitionsInfo) => b.logSize - a.logSize,
render: (t: number) => transBToMB(t),
}, {
title: '优选副本',
key: 'preferredBrokerId',
dataIndex: 'preferredBrokerId',
sorter: (a: IPartitionsInfo, b: IPartitionsInfo) => b.preferredBrokerId - a.preferredBrokerId,
}, {
title: 'AR',
key: 'replicaBrokerIdList',
dataIndex: 'replicaBrokerIdList',
render: (t: []) => {
return (
<Tooltip placement="bottomLeft" title={t.join('、')}>
{t ? t.map(i => <span key={i} className="p-params">{i}</span>) : null}
</Tooltip>
);
},
}, {
title: 'ISR',
key: 'isrBrokerIdList',
dataIndex: 'isrBrokerIdList',
render: (t: []) => {
return (
<Tooltip placement="bottomLeft" title={t.join('、')}>
{t ? t.map(i => <span key={i} className="p-params">{i}</span>) : null}
</Tooltip>
);
},
},
underReplicated];
}
public renderPartitionsInfo() {
const { searchKey } = this.state;
const data = searchKey ?
topic.partitionsInfo.filter((d) => d.partitionId + '' === searchKey) : topic.partitionsInfo;
return (
<>
<div className="k-row" >
<ul className="k-tab">
<li></li>
{this.renderSearch('', '请输入分区号')}
</ul>
<div style={searchKey ? { minHeight: 700 } : null}>
<Table
columns={this.renderColumns()}
table-Layout="fixed"
dataSource={data}
rowKey="key"
pagination={pagination}
/>
</div>
</div>
</>
);
}
public componentDidMount() {
topic.getPartitionsInfo(this.clusterId, this.topicName);
}
public render() {
return (
<>{this.renderPartitionsInfo()}</>
);
}
}

View File

@@ -0,0 +1,173 @@
import * as React from 'react';
import { Form, Row, Button, Input, DatePicker, Col, Select, Radio, Alert, notification, Tooltip } from 'component/antd';
import { topic } from 'store/topic';
import { consume } from 'store/consume';
import { observer } from 'mobx-react';
import { resetOffset } from 'lib/api';
import { timeMinute } from 'constants/strategy';
import { searchProps } from 'constants/table';
import { disabledDate, disabledDateTime } from 'lib/utils';
import moment, { Moment } from 'moment';
import './index.less';
@observer
class ResetOffset extends React.Component<any> {
public state = {
typeValue: 'time',
offsetValue: 'offset',
};
constructor(props: any) {
super(props);
}
public handleSubmit = (e: React.MouseEvent<any, MouseEvent>) => {
e.preventDefault();
const { typeValue, offsetValue } = this.state;
if (typeValue === 'time') {
this.props.form.validateFields((err: Error, values: any) => {
if (err) return;
const { timestamp } = values;
this.props.OffsetReset.offsetList = [];
if (offsetValue === 'offset') {
this.props.OffsetReset.offsetPos = 2;
this.props.OffsetReset.timestamp = 0;
} else {
this.props.OffsetReset.offsetPos = 0;
this.props.OffsetReset.timestamp = +moment(timestamp).format('x');
}
resetOffset(this.props.OffsetReset).then(data => {
notification.success({ message: '重置时间成功' });
});
});
} else {
const offsetData = consume.offsetList.filter(ele => ele.offset || ele.partitionId);
if (offsetData && offsetData.length) {
this.props.OffsetReset.offsetPos = 0;
this.props.OffsetReset.timestamp = 0;
this.props.OffsetReset.offsetList = offsetData;
return resetOffset(this.props.OffsetReset).then(data => {
notification.success({ message: '重置分区及偏移成功' });
});
}
return notification.warning({ message: '请先填写分区及偏移!!!' });
}
}
public onChangeType = (e: any) => {
this.setState({
typeValue: e.target.value,
});
}
public onChangeOffset = (e: any) => {
this.setState({
offsetValue: e.target.value,
});
}
public render() {
const { getFieldDecorator } = this.props.form;
const { typeValue, offsetValue } = this.state;
return (
<>
<Alert message="重置之前一定要关闭消费客户端!!!" type="warning" showIcon={true} />
<Alert message="重置之前一定要关闭消费客户端!!!" type="warning" showIcon={true} />
<Alert message="重置之前一定要关闭消费客户端!!!" type="warning" showIcon={true} className="mb-30"/>
<div className="o-container">
<Form labelAlign="left" onSubmit={this.handleSubmit} >
<Radio.Group onChange={this.onChangeType} value={typeValue}>
<Radio value="time"><span className="title-con"></span></Radio>
<Row>
<Col span={26}>
<Form.Item label="" >
<Radio.Group
onChange={this.onChangeOffset}
value={offsetValue}
disabled={typeValue === 'partition'}
defaultValue="offset"
className="mr-10"
>
<Radio.Button value="offset">offset</Radio.Button>
<Radio.Button value="custom"></Radio.Button>
</Radio.Group>
{typeValue === 'time' && offsetValue === 'custom' &&
getFieldDecorator('timestamp', {
rules: [{ required: false, message: '' }],
initialValue: moment(),
})(
<DatePicker
showTime={true}
format={timeMinute}
disabledDate={disabledDate}
disabledTime={disabledDateTime}
style={{ width: '50%' }}
/>,
)}
</Form.Item>
</Col>
</Row>
<Radio value="partition"><span className="title-con"></span></Radio>
</Radio.Group>
<Row>
<Form.Item>
<Row>
<Col span={8}>ID</Col>
<Col span={16}></Col>
</Row>
{consume.offsetList.map((ele, index) => {
return (
<Row key={index} gutter={16}>
<Col span={8}>
<Select
placeholder="请选择"
onChange={consume.selectChange.bind(consume, index)}
disabled={typeValue === 'time'}
{...searchProps}
>
{topic.consumeDetails.map((r, k) => {
return <Select.Option key={k} value={r.partitionId}>
{(r.partitionId + '').length > 16 ?
<Tooltip placement="bottomLeft" title={r.partitionId}>{r.partitionId}</Tooltip>
: r.partitionId}
</Select.Option>;
})}
</Select></Col>
<Col span={10}>
<Input
placeholder="请输入partition offset"
disabled={typeValue === 'time'}
onChange={consume.inputChange.bind(consume, index)}
/>
</Col>
<Col span={6} className="b-list">
<Button
type="dashed"
icon="plus"
disabled={typeValue === 'time'}
onClick={consume.handleList.bind(null, null)}
/>
<Button
type="dashed"
icon="minus"
disabled={index === 0 || typeValue === 'time'}
onClick={consume.handleList.bind(null, index)}
/>
</Col>
</Row>
);
})}
<Row className="partionButton">
<Button type="primary" htmlType="submit" onClick={this.handleSubmit}></Button>
</Row>
</Form.Item>
</Row>
</Form>
</div>
</>
);
}
}
export default Form.create<any>({ name: 'topicSample' })(ResetOffset);

View File

@@ -0,0 +1,78 @@
import * as React from 'react';
import './index.less';
import Url from 'lib/url-parser';
import { topic } from 'store/topic';
import { NetWorkFlow } from './common-detail';
import { IOptionType, ITakeType } from 'types/base-type';
import { selectOptionMap, selectTakeMap } from 'constants/status-map';
import { Divider } from 'component/antd';
import { indexUrl } from 'constants/strategy';
export class StatusChart extends React.Component {
public clusterId: number;
public topicName: string;
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
this.topicName = url.search.topic;
}
public onSelectChange(e: IOptionType) {
return topic.changeType(e);
}
public onSelectTakeChange(e: ITakeType) {
return topic.changeTakeType(e);
}
public getOptionApi = () => {
return topic.getMetriceInfo(this.clusterId, this.topicName);
}
public getTakeApi = () => {
return topic.getMetriceTake(this.clusterId, this.topicName);
}
public componentDidMount() {
topic.getAppsIdInfo(this.clusterId, this.topicName);
}
public render() {
return (
<>
<div className="chart-box-0">
<div className="chart-title">
<span className="action-button"></span>
<a href={indexUrl} target="_blank"></a>
</div>
<Divider className="chart-divider" />
<NetWorkFlow
key="1"
selectArr={selectOptionMap}
type={topic.type}
selectChange={(value: IOptionType) => this.onSelectChange(value)}
getApi={() => this.getOptionApi()}
clusterId={this.clusterId}
topicName={this.topicName}
/>
</div>
<div className="chart-box-0">
<div className="chart-title">
<span className="action-button"></span>
<a href={indexUrl} target="_blank"></a>
</div>
<Divider className="chart-divider" />
<NetWorkFlow
key="2"
selectArr={selectTakeMap}
type={topic.takeType}
selectChange={(value: ITakeType) => this.onSelectTakeChange(value)}
getApi={() => this.getTakeApi()}
/>
</div>
</>
);
}
}

View File

@@ -0,0 +1,170 @@
import * as React from 'react';
import { Tabs, Table, Button } from 'component/antd';
import { cluster } from 'store/cluster';
import { observer } from 'mobx-react';
import { topic } from 'store/topic';
import { app } from 'store/app';
import { SearchAndFilterContainer } from 'container/search-filter';
import { ITopic } from 'types/base-type';
import { getExpireColumns, getMyTopicColumns } from './config';
import { applyTopic, deferTopic, applyOnlineModal } from 'container/modal';
import { pagination } from 'constants/table';
import { tableFilter } from 'lib/utils';
import { topicStatusMap } from 'constants/status-map';
import './index.less';
const { TabPane } = Tabs;
@observer
export class MineTopic extends SearchAndFilterContainer {
public state = {
searchKey: '',
filterAccess: false,
};
public getData<T extends ITopic>(origin: T[]) {
let data: T[] = [];
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
if (cluster.active !== -1 || app.active !== '-1' || searchKey !== '') {
data = origin.filter(d =>
((d.topicName !== undefined && d.topicName !== null) && d.topicName.toLowerCase().includes(searchKey as string)
|| ((d.appName !== undefined && d.appName !== null) && d.appName.toLowerCase().includes(searchKey as string)))
&& (cluster.active === -1 || d.clusterId === cluster.active)
&& (app.active === '-1' || d.appId === (app.active + '')),
);
} else {
data = origin;
}
return data;
}
public componentDidMount() {
topic.getTopic();
topic.getExpired();
if (!cluster.clusterData.length) {
cluster.getClusters();
}
if (!app.data.length) {
app.getAppList();
}
}
public renderOperationPanel(key: number) {
return (
<div className="table-operation-panel">
<ul>
{this.renderApp('关联应用:')}
{this.renderCluster('集群:')}
{this.renderSearch('名称:', '请输入Topic名称或者应用')}
{
key === 1 && <li className="right-btn-1">
<Button type="primary" onClick={() => { applyTopic(); }}>
Topic
</Button>
</li>
}
</ul>
</div>
);
}
public getColumns = (mytopicData: ITopic[]) => {
const access = Object.assign({
title: '权限',
dataIndex: 'access',
key: 'access',
width: '10%',
filters: tableFilter<ITopic>(mytopicData, 'access', topicStatusMap),
onFilter: (text: number, record: ITopic) => record.access === text,
render: (val: number) => (
<div className={val === 0 ? '' : 'success'}>
{topicStatusMap[val] || ''}
</div>
),
}, this.renderColumnsFilter('filterAccess')) as any;
const columns = getMyTopicColumns(this.urlPrefix);
columns.splice(-2, 0, access);
return columns;
}
public renderMyTopicTable(mytopicData: ITopic[]) {
return (
<div>
<Table
rowKey="key"
loading={topic.loading}
dataSource={mytopicData}
columns={this.getColumns(mytopicData)}
pagination={pagination}
/>
</div>
);
}
public renderDeprecatedTopicTable(expireData: ITopic[]) {
const operationCol = {
title: '操作',
dataIndex: 'action',
key: 'action',
width: '15%',
render: (val: string, item: ITopic, index: number) => (
<>
<span>
<a style={{ marginRight: 16 }} onClick={() => deferTopic(item)}></a>
<a style={{ marginRight: 16 }} onClick={() => applyOnlineModal(item)}>线</a>
</span>
</>
),
} as any;
const expireColumns = getExpireColumns(this.urlPrefix);
expireColumns.push(operationCol);
return (
<div>
<Table
rowKey="key"
dataSource={expireData}
columns={expireColumns}
pagination={pagination}
loading={topic.expiredLoading}
/>
</div>
);
}
public handleTabKey(key: string) {
location.hash = key;
cluster.changeCluster(-1);
app.changeActiveApp('-1');
this.setState({
searchKey: '',
});
topic.setLoading(false);
}
public render() {
return (
<>
<div className="min-width">
<Tabs activeKey={location.hash.substr(1) || '1'} type="card" onChange={(key) => this.handleTabKey(key)}>
<TabPane tab="有效Topic" key="1" >
{this.renderOperationPanel(1)}
{this.renderMyTopicTable(this.getData(topic.mytopicData))}
</TabPane>
<TabPane tab="已过期Topic" key="2">
{this.renderOperationPanel(2)}
{this.renderDeprecatedTopicTable(this.getData(topic.expireData))}
</TabPane>
</Tabs>
</div>
</>
);
}
}