mirror of
https://github.com/didi/KnowStreaming.git
synced 2026-01-08 15:52:15 +08:00
kafka-manager 2.0
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import * as React from 'react';
|
||||
import './index.less';
|
||||
import { Table, DatePicker } from 'antd';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { expert } from 'store/expert';
|
||||
import { observer } from 'mobx-react';
|
||||
import { IAnomalyFlow } from 'types/base-type';
|
||||
import { pagination } from 'constants/table';
|
||||
import { timeMinute } from 'constants/strategy';
|
||||
|
||||
import moment = require('moment');
|
||||
import { region } from 'store/region';
|
||||
|
||||
@observer
|
||||
export class Diagnosis extends SearchAndFilterContainer {
|
||||
public onOk(value: any) {
|
||||
const timestamp = +moment(value).format('x');
|
||||
expert.getAnomalyFlow(timestamp);
|
||||
}
|
||||
|
||||
public selectTime() {
|
||||
return (
|
||||
<>
|
||||
<div className="zoning-otspots">
|
||||
<div>
|
||||
<span>选择时间:</span>
|
||||
<DatePicker showTime={true} format={timeMinute} defaultValue={moment()} onOk={this.onOk} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public pendingTopic() {
|
||||
const columns = [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
width: '30%',
|
||||
sorter: (a: IAnomalyFlow, b: IAnomalyFlow) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
render: (text: string, item: IAnomalyFlow) =>
|
||||
(
|
||||
<a
|
||||
// tslint:disable-next-line:max-line-length
|
||||
href={`${this.urlPrefix}/topic/topic-detail?clusterId=${item.clusterId}&topic=${item.topicName}®ion=${region.currentRegion}`}
|
||||
>
|
||||
{text}
|
||||
</a>),
|
||||
},
|
||||
{
|
||||
title: '所在独享集群',
|
||||
dataIndex: 'clusterName',
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
title: 'IOPS',
|
||||
dataIndex: 'iops',
|
||||
width: '20%',
|
||||
sorter: (a: IAnomalyFlow, b: IAnomalyFlow) => b.iops - a.iops,
|
||||
render: (val: number, item: IAnomalyFlow) => (
|
||||
// tslint:disable-next-line:max-line-length
|
||||
<span> {val === null ? '' : (val / 1024).toFixed(2)}{item.iopsIncr === null ? '' : `(${(item.iopsIncr / 1024).toFixed(2)})`}</span> ),
|
||||
},
|
||||
{
|
||||
title: '流量',
|
||||
dataIndex: 'bytesIn',
|
||||
width: '20%',
|
||||
sorter: (a: IAnomalyFlow, b: IAnomalyFlow) => b.bytesIn - a.bytesIn,
|
||||
render: (val: number, item: IAnomalyFlow) => (
|
||||
// tslint:disable-next-line:max-line-length
|
||||
<span> {val === null ? '' : (val / 1024).toFixed(2)}{item.bytesInIncr === null ? '' : `(${(item.bytesInIncr / 1024).toFixed(2)})`}</span> ),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
{this.selectTime()}
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={expert.anomalyFlowData}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const timestamp = +moment(moment()).format('x');
|
||||
expert.getAnomalyFlow(timestamp);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
{this.pendingTopic()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
4
kafka-manager-console/src/container/expert/index.tsx
Normal file
4
kafka-manager-console/src/container/expert/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './topic-hotspot';
|
||||
export * from './topic-partition';
|
||||
export * from './topic-governance';
|
||||
export * from './diagnosis';
|
||||
@@ -0,0 +1,19 @@
|
||||
.k-collect {
|
||||
width: 250px;
|
||||
position: relative;
|
||||
margin-bottom: 12px;
|
||||
.ant-alert-close-icon {
|
||||
top: 7px;
|
||||
}
|
||||
.k-coll-btn {
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
right: 15px;
|
||||
.btn-right{
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.action-button{
|
||||
margin-right: 5px;
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import * as React from 'react';
|
||||
import './index.less';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Tabs, Table, Alert, Modal, Tooltip, notification } from 'antd';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { expert } from 'store/expert';
|
||||
import { observer } from 'mobx-react';
|
||||
import { IUtils, IResource } from 'types/base-type';
|
||||
import { getUtilsTopics } from 'lib/api';
|
||||
import { offlineStatusMap } from 'constants/status-map';
|
||||
import { region } from 'store/region';
|
||||
|
||||
interface IUncollect {
|
||||
clusterId: number;
|
||||
code: number;
|
||||
message: string;
|
||||
topicName: string;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class GovernanceTopic extends SearchAndFilterContainer {
|
||||
|
||||
public unpendinngRef: HTMLDivElement = null;
|
||||
public unofflineRef: HTMLDivElement = null;
|
||||
|
||||
public state = {
|
||||
searchKey: '',
|
||||
};
|
||||
public onSelectChange = {
|
||||
onChange: (selectedRowKeys: string[], selectedRows: []) => {
|
||||
this.setState({
|
||||
hasSelected: !!selectedRowKeys.length,
|
||||
});
|
||||
const num = selectedRows.length;
|
||||
ReactDOM.render(
|
||||
selectedRows.length ? (
|
||||
<>
|
||||
<Alert
|
||||
type="warning"
|
||||
message={`已选择 ${num} 项 `}
|
||||
showIcon={true}
|
||||
closable={false}
|
||||
/>
|
||||
<a className="k-coll-btn" onClick={this.uncollect.bind(this, selectedRows)}>下线</a>
|
||||
</>) : null,
|
||||
this.unpendinngRef,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
public onOfflineChange = {
|
||||
onChange: (selectedRowKeys: string[], selectedRows: []) => {
|
||||
const num = selectedRows.length;
|
||||
ReactDOM.render(
|
||||
selectedRows.length ? (
|
||||
<>
|
||||
<Alert
|
||||
type="warning"
|
||||
message={`已选择 ${num} 项 `}
|
||||
showIcon={true}
|
||||
closable={false}
|
||||
/>
|
||||
</>) : null,
|
||||
this.unofflineRef,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
public uncollect = (selectedRowKeys: IResource) => {
|
||||
let selectedRow = [] as IResource[];
|
||||
if (selectedRowKeys instanceof Object) {
|
||||
selectedRow.push(selectedRowKeys);
|
||||
} else {
|
||||
selectedRow = selectedRowKeys;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: `确认下线?`,
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
ReactDOM.unmountComponentAtNode(this.unpendinngRef);
|
||||
const paramsData = [] as IUtils[];
|
||||
let params = {} as IUtils;
|
||||
selectedRow.forEach((item: IResource) => {
|
||||
params = {
|
||||
clusterId: item.clusterId,
|
||||
force: true,
|
||||
topicName: item.topicName,
|
||||
};
|
||||
paramsData.push(params);
|
||||
});
|
||||
getUtilsTopics(params).then((data: IUncollect[]) => {
|
||||
if (data) {
|
||||
data.map((ele: IUncollect) => {
|
||||
if (ele.code === 0) {
|
||||
notification.success({ message: `${ele.topicName}下线成功` });
|
||||
} else {
|
||||
notification.error({ message: `${ele.topicName}下线失败` });
|
||||
}
|
||||
});
|
||||
}
|
||||
}, (err) => {
|
||||
notification.error({ message: '操作失败' });
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public getData(origin: IResource[]) {
|
||||
let data: IResource[] = [];
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
if (expert.active !== -1 || searchKey !== '') {
|
||||
data = origin.filter(d =>
|
||||
(
|
||||
((d.topicName !== undefined && d.topicName !== null) && d.topicName.toLowerCase().includes(searchKey as string))
|
||||
|| ((d.appId !== undefined && d.appId !== null) && d.appId.toLowerCase().includes(searchKey as string))
|
||||
)
|
||||
&& (expert.active === -1 || +d.clusterId === expert.active),
|
||||
);
|
||||
} else {
|
||||
data = origin;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
public pendingTopic(resourceData: IResource[]) {
|
||||
const columns = [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
width: '30%',
|
||||
sorter: (a: IResource, b: IResource) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
render: (text: string, item: IResource) =>
|
||||
(
|
||||
<Tooltip placement="bottomLeft" title={text}>
|
||||
<a
|
||||
// tslint:disable-next-line:max-line-length
|
||||
href={`${this.urlPrefix}/topic/topic-detail?clusterId=${item.clusterId}&topic=${item.topicName}&isPhysicalClusterId=true®ion=${region.currentRegion}`}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
</Tooltip>),
|
||||
},
|
||||
{
|
||||
title: '所在集群',
|
||||
dataIndex: 'clusterName',
|
||||
width: '10%',
|
||||
},
|
||||
{
|
||||
title: '过期天数(天)',
|
||||
dataIndex: 'expiredDay',
|
||||
width: '10%',
|
||||
},
|
||||
{
|
||||
title: '发送连接',
|
||||
dataIndex: 'produceConnectionNum',
|
||||
width: '10%',
|
||||
},
|
||||
{
|
||||
title: '消费连接',
|
||||
dataIndex: 'fetchConnectionNum',
|
||||
width: '10%',
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
dataIndex: 'appName',
|
||||
width: '10%',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: '10%',
|
||||
render: (text: number) => <span>{offlineStatusMap[Number(text)]}</span>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
width: '10%',
|
||||
render: (val: string, item: IResource, index: number) => (
|
||||
<>
|
||||
{item.status !== -1 && <a className="action-button" >通知用户</a>}
|
||||
{item.status === -1 && <a onClick={this.uncollect.bind(this, item)}>下线</a>}
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<div className="table-operation-panel">
|
||||
<ul>
|
||||
{this.renderPhysical('物理集群:')}
|
||||
{this.renderSearch('名称:', '请输入Topic名称')}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="k-collect" ref={(id) => this.unpendinngRef = id} />
|
||||
<Table
|
||||
rowKey="key"
|
||||
columns={columns}
|
||||
dataSource={resourceData}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
expert.getResourceManagement();
|
||||
if (!expert.metaData.length) {
|
||||
expert.getMetaData(false);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.pendingTopic(this.getData(expert.resourceData))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
.data-migration {
|
||||
b {
|
||||
font-weight: 100;
|
||||
font-size : 13px;
|
||||
line-height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.th-number{
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.transfer-button{
|
||||
float: right;
|
||||
z-index: 9999;
|
||||
Button{
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.migration-table{
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import * as React from 'react';
|
||||
import './index.less';
|
||||
import { Tabs, Table, Button, Tooltip } from 'component/antd';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { expert } from 'store/expert';
|
||||
import { handleTabKey } from 'lib/utils';
|
||||
import { observer } from 'mobx-react';
|
||||
import { IDetailData, IHotTopics } from 'types/base-type';
|
||||
import { pagination } from 'constants/table';
|
||||
import { IRenderData } from 'container/modal/expert';
|
||||
import { migrationModal } from 'container/modal/expert';
|
||||
|
||||
import './index.less';
|
||||
import { region } from 'store/region';
|
||||
import { MigrationTask } from 'container/admin/operation-management/migration-task';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
@observer
|
||||
export class HotSpotTopic extends SearchAndFilterContainer {
|
||||
|
||||
public selectedData: IHotTopics[];
|
||||
|
||||
public state = {
|
||||
loading: false,
|
||||
hasSelected: false,
|
||||
migrationVisible: false,
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
public onSelectChange = {
|
||||
onChange: (selectedRowKeys: string[], selectedRows: IHotTopics[]) => {
|
||||
this.selectedData = selectedRows ? selectedRows : [];
|
||||
this.setState({
|
||||
hasSelected: !!selectedRowKeys.length,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
public getData(origin: IHotTopics[]) {
|
||||
let data: IHotTopics[] = [];
|
||||
const { searchKey } = this.state;
|
||||
if (expert.active !== -1 || searchKey !== '') {
|
||||
data = origin.filter(d =>
|
||||
((d.topicName !== undefined && d.topicName !== null) && d.topicName.toLowerCase().includes(searchKey.toLowerCase()))
|
||||
&& (expert.active === -1 || +d.clusterId === expert.active),
|
||||
);
|
||||
} else {
|
||||
data = origin;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
public zoningHotspots(hotData: IHotTopics[]) {
|
||||
const { loading, hasSelected } = this.state;
|
||||
const columns = [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
width: '30%',
|
||||
sorter: (a: IHotTopics, b: IHotTopics) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
render: (text: string, item: IHotTopics) => (
|
||||
<Tooltip placement="bottomLeft" title={text}>
|
||||
<a
|
||||
// tslint:disable-next-line:max-line-length
|
||||
href={`${this.urlPrefix}/topic/topic-detail?clusterId=${item.clusterId}&topic=${item.topicName}&isPhysicalClusterId=true®ion=${region.currentRegion}`}
|
||||
>{text}
|
||||
</a>
|
||||
</Tooltip>),
|
||||
},
|
||||
{
|
||||
title: '所在集群',
|
||||
dataIndex: 'clusterName',
|
||||
width: '30%',
|
||||
},
|
||||
{
|
||||
title: '分区热点状态',
|
||||
dataIndex: 'detailList',
|
||||
width: '30%',
|
||||
render: (detailList: IDetailData[], item: any, index: number) => (
|
||||
<>
|
||||
<Tooltip
|
||||
placement="rightTop"
|
||||
title={() => this.ReactNode(detailList)}
|
||||
>
|
||||
<span>查看状态</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
width: '10%',
|
||||
render: (value: any, item: IHotTopics) => (
|
||||
<span onClick={this.dataMigration.bind(this, item)}><a>数据迁移</a></span>
|
||||
),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<div className="table-operation-panel">
|
||||
<ul>
|
||||
{this.renderPhysical('物理集群:')}
|
||||
{this.renderSearch('名称:', '请输入Topic名称')}
|
||||
</ul>
|
||||
</div>
|
||||
<Table
|
||||
rowKey="key"
|
||||
rowSelection={this.onSelectChange}
|
||||
columns={columns}
|
||||
dataSource={hotData}
|
||||
pagination={pagination}
|
||||
/>
|
||||
<div className="zoning-button">
|
||||
<Button onClick={() => this.dataMigration()} disabled={!hasSelected} loading={loading}>
|
||||
批量数据迁移
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public ReactNode(detailList: IDetailData[]) {
|
||||
return (
|
||||
<ul>
|
||||
{detailList.map((record: IDetailData) => (
|
||||
<li>broker{record.brokeId}:{record.partitionNum}个分区</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
public dataMigration(item?: IHotTopics) {
|
||||
let migrateData = [] as IHotTopics[];
|
||||
const renderData = [] as IRenderData[];
|
||||
if (item) {
|
||||
migrateData.push(item);
|
||||
} else {
|
||||
migrateData = this.selectedData;
|
||||
}
|
||||
migrateData.forEach((ele, index) => {
|
||||
const brokerId = [] as number[];
|
||||
ele.detailList.forEach(t => {
|
||||
brokerId.push(t.brokeId);
|
||||
});
|
||||
const item = {
|
||||
brokerIdList: brokerId,
|
||||
partitionIdList: [],
|
||||
topicName: ele.topicName,
|
||||
clusterId: ele.clusterId,
|
||||
clusterName: ele.clusterName,
|
||||
retentionTime: ele.retentionTime,
|
||||
key: index,
|
||||
} as IRenderData;
|
||||
renderData.push(item);
|
||||
});
|
||||
this.migrationInterface(renderData);
|
||||
}
|
||||
|
||||
public migrationInterface(renderData: IRenderData[]) {
|
||||
migrationModal(renderData);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
expert.getHotTopics();
|
||||
if (!expert.metaData.length) {
|
||||
expert.getMetaData(false);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<Tabs activeKey={location.hash.substr(1) || '1'} type="card" onChange={handleTabKey}>
|
||||
<TabPane tab="分区热点Topic" key="1">
|
||||
{this.zoningHotspots(this.getData(expert.hotTopics))}
|
||||
</TabPane>
|
||||
<TabPane tab="迁移任务" key="2">
|
||||
<MigrationTask />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import * as React from 'react';
|
||||
import './index.less';
|
||||
import { Table, Button, InputNumber, notification, Tooltip } from 'antd';
|
||||
import { IPartition, IEepand } from 'types/base-type';
|
||||
import { pagination } from 'constants/table';
|
||||
import { observer } from 'mobx-react';
|
||||
import { getExpandTopics } from 'lib/api';
|
||||
import { urlPrefix } from 'constants/left-menu';
|
||||
import { region } from 'store/region';
|
||||
|
||||
@observer
|
||||
export class BatchExpansion extends React.Component<any> {
|
||||
public estimateData = [] as IPartition[];
|
||||
|
||||
public onPartitionChange(value: number, item: IPartition, index: number) {
|
||||
this.estimateData.forEach((element: IPartition) => {
|
||||
if (element.topicName === item.topicName) {
|
||||
element.suggestedPartitionNum = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public createPartition() {
|
||||
return (
|
||||
<>
|
||||
<div className="create-partition">
|
||||
<h2>新建Topic分区扩容</h2>
|
||||
<div>
|
||||
<Button onClick={() => this.cancelExpansion()} className="create-button">取消</Button>
|
||||
<Button type="primary" onClick={() => this.ConfirmExpansion()}>确认扩容</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public ConfirmExpansion() {
|
||||
const paramsData = [] as IEepand[];
|
||||
this.estimateData.forEach(item => {
|
||||
const hash = {
|
||||
brokerIdList: item.brokerIdList,
|
||||
clusterId: item.clusterId,
|
||||
topicName: item.topicName,
|
||||
partitionNum: item.suggestedPartitionNum,
|
||||
regionId: '',
|
||||
} as IEepand;
|
||||
paramsData.push(hash);
|
||||
});
|
||||
getExpandTopics(paramsData).then(data => {
|
||||
notification.success({ message: '扩容成功' });
|
||||
this.props.history.push(`${urlPrefix}/expert#2`);
|
||||
});
|
||||
}
|
||||
|
||||
public cancelExpansion() {
|
||||
const { onChange } = this.props;
|
||||
onChange(true);
|
||||
}
|
||||
|
||||
public partitionExpansion() {
|
||||
const columns = [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
sorter: (a: IPartition, b: IPartition) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
render: (text: string, item: IPartition) => (
|
||||
<Tooltip placement="bottomLeft" title={text}>
|
||||
<a
|
||||
// tslint:disable-next-line:max-line-length
|
||||
href={`${urlPrefix}/topic/topic-detail?clusterId=${item.clusterId}&topic=${item.topicName}&isPhysicalClusterId=true®ion=${region.currentRegion}`}
|
||||
>{text}
|
||||
</a>
|
||||
</Tooltip>),
|
||||
},
|
||||
{
|
||||
title: '所在集群',
|
||||
dataIndex: 'clusterName',
|
||||
},
|
||||
{
|
||||
title: '当前分区数量',
|
||||
dataIndex: 'presentPartitionNum',
|
||||
},
|
||||
{
|
||||
title: '预计分区数量',
|
||||
dataIndex: 'suggestedPartitionNum',
|
||||
render: (val: number, item: IPartition, index: number) => (
|
||||
// tslint:disable-next-line:max-line-length
|
||||
<InputNumber className="batch-input" min={0} defaultValue={val} onChange={(value) => this.onPartitionChange(value, item, index)} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '新分区broker',
|
||||
dataIndex: 'brokerIdList',
|
||||
render: (val: number, item: IPartition, index: number) => (
|
||||
item.brokerIdList.map((record: number) => (
|
||||
<span className="p-params">{record}</span>
|
||||
))
|
||||
),
|
||||
},
|
||||
];
|
||||
this.estimateData = this.props.capacityData;
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
rowKey="key"
|
||||
columns={columns}
|
||||
dataSource={this.estimateData}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
{this.createPartition()}
|
||||
{this.partitionExpansion()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
.zoning-otspots{
|
||||
min-width: 250px;
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.zoning-button{
|
||||
margin-top: -50px;
|
||||
}
|
||||
|
||||
.create-partition{
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
h2{
|
||||
padding-left: 10px;
|
||||
font-size: 15px;
|
||||
color: rgba(0, 0, 0, .65);
|
||||
line-height: 24px;
|
||||
}
|
||||
.create-button{
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.batch-input{
|
||||
width: 100px;
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import * as React from 'react';
|
||||
import './index.less';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { expert } from 'store/expert';
|
||||
import { Table, Button, Tooltip } from 'antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import { IPartition } from 'types/base-type';
|
||||
import { pagination } from 'constants/table';
|
||||
import { BatchExpansion } from './batch-expansion';
|
||||
import { region } from 'store/region';
|
||||
import { transBToMB } from 'lib/utils';
|
||||
|
||||
@observer
|
||||
export class PartitionTopic extends SearchAndFilterContainer {
|
||||
public capacityData: IPartition[];
|
||||
public selectedRows: IPartition[];
|
||||
|
||||
public state = {
|
||||
searchKey: '',
|
||||
loading: false,
|
||||
hasSelected: false,
|
||||
partitionVisible: true,
|
||||
};
|
||||
|
||||
public onSelectChange = {
|
||||
onChange: (selectedRowKeys: string[], selectedRows: []) => {
|
||||
this.selectedRows = selectedRows;
|
||||
this.setState({
|
||||
hasSelected: !!selectedRowKeys.length,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
public InsufficientPartition(partitionData: IPartition[]) {
|
||||
const columns = [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
width: '30%',
|
||||
sorter: (a: IPartition, b: IPartition) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
render: (text: string, item: IPartition) => (
|
||||
<Tooltip placement="bottomLeft" title={item.topicName} >
|
||||
<a
|
||||
// tslint:disable-next-line:max-line-length
|
||||
href={`${this.urlPrefix}/topic/topic-detail?clusterId=${item.clusterId}&topic=${item.topicName}&isPhysicalClusterId=true®ion=${region.currentRegion}`}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
</Tooltip>),
|
||||
},
|
||||
{
|
||||
title: '所在集群',
|
||||
dataIndex: 'clusterName',
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
title: '分区个数',
|
||||
dataIndex: 'presentPartitionNum',
|
||||
width: '10%',
|
||||
},
|
||||
{
|
||||
title: '分区平均流量(MB/s)',
|
||||
dataIndex: 'bytesInPerPartition',
|
||||
width: '10%',
|
||||
sorter: (a: IPartition, b: IPartition) => b.bytesInPerPartition - a.bytesInPerPartition,
|
||||
render: (t: number) => transBToMB(t),
|
||||
},
|
||||
{
|
||||
title: '近三天峰值流量(MB/s)',
|
||||
dataIndex: 'maxAvgBytesInList',
|
||||
width: '25%',
|
||||
render: (val: number, item: IPartition, index: number) => (
|
||||
item.maxAvgBytesInList.map((record: number) => (
|
||||
<span className="p-params">{transBToMB(record)}</span>
|
||||
))
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
width: '10%',
|
||||
render: (val: string, item: IPartition, index: number) => (
|
||||
<>
|
||||
<a onClick={() => this.dataMigration([item])}>扩分区</a>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
const { loading, hasSelected } = this.state;
|
||||
return (
|
||||
<>
|
||||
<div className="table-operation-panel">
|
||||
<ul>
|
||||
{this.renderPhysical('物理集群:')}
|
||||
{this.renderSearch('Topic名称:', '请输入Topic名称')}
|
||||
</ul>
|
||||
</div>
|
||||
<Table
|
||||
rowKey="key"
|
||||
rowSelection={this.onSelectChange}
|
||||
columns={columns}
|
||||
dataSource={partitionData}
|
||||
pagination={pagination}
|
||||
/>
|
||||
<div className="zoning-button">
|
||||
<Button disabled={!hasSelected} loading={loading}>
|
||||
<a onClick={() => this.dataMigration(this.selectedRows)}>批量扩分区</a>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public dataMigration(item: IPartition[]) {
|
||||
this.capacityData = item;
|
||||
this.setState({
|
||||
partitionVisible: false,
|
||||
});
|
||||
}
|
||||
|
||||
public getData(origin: IPartition[]) {
|
||||
let data: IPartition[] = [];
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
if (expert.active !== -1 || searchKey !== '') {
|
||||
data = origin.filter(d =>
|
||||
((d.topicName !== undefined && d.topicName !== null) && d.topicName.toLowerCase().includes(searchKey as string))
|
||||
&& (expert.active === -1 || +d.clusterId === expert.active),
|
||||
);
|
||||
} else {
|
||||
data = origin;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
expert.getInsufficientPartition();
|
||||
if (!expert.metaData.length) {
|
||||
expert.getMetaData(false);
|
||||
}
|
||||
}
|
||||
|
||||
public onChangeVisible(value?: boolean) {
|
||||
this.setState({
|
||||
partitionVisible: value,
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
this.state.partitionVisible ?
|
||||
<>{this.InsufficientPartition(this.getData(expert.partitionedData))}</>
|
||||
: <BatchExpansion onChange={(value: boolean) => this.onChangeVisible(value)} capacityData={this.capacityData} />
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user