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,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}&region=${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()}
</>
);
}
}

View File

@@ -0,0 +1,4 @@
export * from './topic-hotspot';
export * from './topic-partition';
export * from './topic-governance';
export * from './diagnosis';

View File

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

View File

@@ -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&region=${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))}
</>
);
}
}

View File

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

View File

@@ -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&region=${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>
</>
);
}
}

View File

@@ -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&region=${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()}
</>
);
}
}

View File

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

View File

@@ -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&region=${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} />
);
}
}