mirror of
https://github.com/didi/KnowStreaming.git
synced 2026-01-03 11:28:12 +08:00
kafka-manager 2.0
This commit is contained in:
47
kafka-manager-console/src/container/admin/admin-app-list.tsx
Normal file
47
kafka-manager-console/src/container/admin/admin-app-list.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
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';
|
||||
|
||||
@observer
|
||||
export class AdminAppList extends CommonAppList {
|
||||
public static defaultProps = {
|
||||
from: 'admin',
|
||||
};
|
||||
|
||||
constructor(defaultProps: any) {
|
||||
super(defaultProps);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
if (!app.adminAppData.length) {
|
||||
app.getAdminAppList();
|
||||
}
|
||||
}
|
||||
|
||||
public renderTable() {
|
||||
return this.renderTableList(this.getData(app.adminAppData));
|
||||
}
|
||||
|
||||
public renderOperationPanel() {
|
||||
return (
|
||||
<ul>
|
||||
{this.renderSearch('', '请输入应用名称或者负责人')}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="table-operation-panel">
|
||||
{this.renderOperationPanel()}
|
||||
</div>
|
||||
<div className="table-wrapper">
|
||||
{this.renderTable()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
103
kafka-manager-console/src/container/admin/bill-detail.tsx
Normal file
103
kafka-manager-console/src/container/admin/bill-detail.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import * as React from 'react';
|
||||
import { Table, Tabs, Icon, Spin } from 'component/antd';
|
||||
import { pagination } from 'constants/table';
|
||||
import { observer } from 'mobx-react';
|
||||
import { bill } from 'store/bill';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { billDetailCols } from '../user-center/config';
|
||||
import { admin } from 'store/admin';
|
||||
import { IBillDetail } from 'types/base-type';
|
||||
import { getCookie } from 'lib/utils';
|
||||
import { timeMonth } from 'constants/strategy';
|
||||
import Url from 'lib/url-parser';
|
||||
import * as XLSX from 'xlsx';
|
||||
import moment from 'moment';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
@observer
|
||||
export class BillDetail extends SearchAndFilterContainer {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
private timestamp: number = null;
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.timestamp = Number(url.search.timestamp);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
admin.getBillDetailStaffList(getCookie('username'), this.timestamp);
|
||||
}
|
||||
|
||||
public getData<T extends IBillDetail>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: IBillDetail) =>
|
||||
(item.topicName !== undefined && item.topicName !== null) && item.topicName.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public handleDownLoad() {
|
||||
const tableData = admin.billDetailStaffData.map(item => {
|
||||
return {
|
||||
// tslint:disable
|
||||
'集群ID': item.clusterId,
|
||||
'集群名称': item.clusterName,
|
||||
'quota数量': item.quota,
|
||||
'Topic名称': item.topicName,
|
||||
'金额': item.cost,
|
||||
};
|
||||
});
|
||||
const data = [].concat(tableData);
|
||||
const wb = XLSX.utils.book_new();
|
||||
// json转sheet
|
||||
const ws = XLSX.utils.json_to_sheet(data, {
|
||||
header: ['集群ID', '集群名称', 'quota数量', 'Topic名称', '金额'],
|
||||
});
|
||||
// XLSX.utils.
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'bill');
|
||||
// 输出
|
||||
XLSX.writeFile(wb, 'bill-' + moment(this.timestamp).format(timeMonth) + '.xlsx');
|
||||
}
|
||||
|
||||
public renderTableList() {
|
||||
return (
|
||||
<Spin spinning={bill.loading}>
|
||||
<Table
|
||||
rowKey="key"
|
||||
columns={billDetailCols}
|
||||
dataSource={this.getData(admin.billDetailStaffData)}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<div className="container">
|
||||
<Tabs defaultActiveKey="1" type="card">
|
||||
<TabPane tab={`账单详情-${moment(this.timestamp).format(timeMonth)}`} key="1">
|
||||
{this.renderTableList()}
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
<div className="operation-panel special">
|
||||
<ul>
|
||||
{this.renderSearch('', '请输入TopicName')}
|
||||
<li className="right-btn-1">
|
||||
<Icon type="download" onClick={this.handleDownLoad.bind(this, null)} />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.bill-head{
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as React from 'react';
|
||||
import { handleTabKey } from 'lib/utils';
|
||||
import { PersonalBill } from './personal-bill';
|
||||
import { Tabs } from 'antd';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
export class BillManagement extends React.Component {
|
||||
public render() {
|
||||
return(
|
||||
<>
|
||||
<Tabs activeKey={location.hash.substr(1) || '1'} type="card" onChange={handleTabKey}>
|
||||
<TabPane tab="个人账单" key="1">
|
||||
<PersonalBill />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import * as React from 'react';
|
||||
import { Table, DatePicker } from 'antd';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { admin } from 'store/admin';
|
||||
import { Moment } from 'moment';
|
||||
import { observer } from 'mobx-react';
|
||||
import { IStaffSummary } from 'types/base-type';
|
||||
import { pagination } from 'constants/table';
|
||||
|
||||
import './index.less';
|
||||
const { MonthPicker } = DatePicker;
|
||||
|
||||
import moment = require('moment');
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
|
||||
@observer
|
||||
export class PersonalBill extends SearchAndFilterContainer {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
public handleTimeChange = (value: Moment) => {
|
||||
const timestamp = value.valueOf();
|
||||
admin.getStaffSummary(timestamp);
|
||||
}
|
||||
|
||||
public selectTime() {
|
||||
return (
|
||||
<>
|
||||
<div className="zoning-otspots">
|
||||
<div>
|
||||
<span>选择月份:</span>
|
||||
<MonthPicker
|
||||
placeholder="Select month"
|
||||
defaultValue={moment()}
|
||||
onChange={this.handleTimeChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public getData<T extends IStaffSummary>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: IStaffSummary) =>
|
||||
(item.username !== undefined && item.username !== null) && item.username.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public pendingTopic() {
|
||||
const columns = [
|
||||
{
|
||||
title: '月份',
|
||||
dataIndex: 'gmtMonth',
|
||||
width: '15%',
|
||||
sorter: (a: IStaffSummary, b: IStaffSummary) => b.timestamp - a.timestamp,
|
||||
render: (text: string, record: IStaffSummary) => (
|
||||
<a href={`${this.urlPrefix}/admin/bill-individual`}> {text} </a>),
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
width: '20%',
|
||||
sorter: (a: IStaffSummary, b: IStaffSummary) => a.username.charCodeAt(0) - b.username.charCodeAt(0),
|
||||
},
|
||||
{
|
||||
title: 'Topic数量',
|
||||
dataIndex: 'topicNum',
|
||||
width: '15%',
|
||||
sorter: (a: IStaffSummary, b: IStaffSummary) => b.topicNum - a.topicNum,
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'timestamp',
|
||||
width: '20%',
|
||||
sorter: (a: IStaffSummary, b: IStaffSummary) => b.timestamp - a.timestamp,
|
||||
render: (t: number) => moment(t).format(timeFormat),
|
||||
},
|
||||
{
|
||||
title: 'Quota(M/S)',
|
||||
dataIndex: 'quota',
|
||||
width: '15%',
|
||||
sorter: (a: IStaffSummary, b: IStaffSummary) => b.quota - a.quota,
|
||||
render: (t: number) => t === null ? '' : Number.isInteger(t) ? t : (t).toFixed(2),
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
dataIndex: 'cost',
|
||||
width: '15%',
|
||||
sorter: (a: IStaffSummary, b: IStaffSummary) => b.cost - a.cost,
|
||||
render: (t: number) => t === null ? '' : Number.isInteger(t) ? t : (t).toFixed(2),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<ul className="bill-head">
|
||||
{this.renderSearch('名称:', '请输入用户名')}
|
||||
{this.selectTime()}
|
||||
</ul>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={this.getData(admin.staffSummary)}
|
||||
pagination={pagination}
|
||||
rowKey="key"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const timestamp = +moment().format('x');
|
||||
admin.getStaffSummary(timestamp);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return(
|
||||
<>
|
||||
{admin.staffSummary ? this.pendingTopic() : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import * as React from 'react';
|
||||
import { ILabelValue, IBrokersBasicInfo, IOptionType, IClusterReal } from 'types/base-type';
|
||||
import { observer } from 'mobx-react';
|
||||
import moment from 'moment';
|
||||
import Url from 'lib/url-parser';
|
||||
import { admin } from 'store/admin';
|
||||
import { PageHeader, Descriptions, Spin } from 'component/antd';
|
||||
import { selectBrokerMap } from 'constants/status-map';
|
||||
import { StatusGraghCom } from 'component/flow-table';
|
||||
import { NetWorkFlow, renderTrafficTable } from 'container/network-flow';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
|
||||
@observer
|
||||
export class BaseInfo extends React.Component {
|
||||
public clusterId: number;
|
||||
public brokerId: number;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
this.brokerId = Number(url.search.brokerId);
|
||||
}
|
||||
|
||||
public updateRealStatus = () => {
|
||||
admin.getBrokersMetrics(this.clusterId, this.brokerId);
|
||||
}
|
||||
|
||||
public onSelectChange(e: IOptionType) {
|
||||
return admin.changeBrokerType(e);
|
||||
}
|
||||
|
||||
public getOptionApi = () => {
|
||||
return admin.getBrokersMetricsHistory(this.clusterId, this.brokerId);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
admin.getBrokersBasicInfo(this.clusterId, this.brokerId);
|
||||
admin.getBrokersMetrics(this.clusterId, this.brokerId);
|
||||
}
|
||||
|
||||
public renderBrokerContent() {
|
||||
let content = {} as IBrokersBasicInfo;
|
||||
content = admin.brokersBasicInfo ? admin.brokersBasicInfo : content;
|
||||
const brokerContent = [{
|
||||
value: content.host,
|
||||
label: '主机名',
|
||||
}, {
|
||||
value: content.port,
|
||||
label: '服务端口',
|
||||
}, {
|
||||
value: content.jmxPort,
|
||||
label: 'JMX端口',
|
||||
}, {
|
||||
value: content.topicNum,
|
||||
label: 'Topic数',
|
||||
}, {
|
||||
value: content.leaderCount,
|
||||
label: 'Leader分区数',
|
||||
}, {
|
||||
value: content.partitionCount,
|
||||
label: '分区数',
|
||||
}, {
|
||||
value: moment(content.startTime).format(timeFormat),
|
||||
label: '启动时间',
|
||||
}];
|
||||
return (
|
||||
<>
|
||||
<div className="chart-title">基本信息</div>
|
||||
<PageHeader className="detail" title="">
|
||||
<Descriptions size="small" column={3}>
|
||||
{brokerContent.map((item: ILabelValue, index: number) => (
|
||||
<Descriptions.Item key={index} label={item.label}>{item.value}</Descriptions.Item>
|
||||
))}
|
||||
</Descriptions>
|
||||
</PageHeader>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public renderHistoryTraffic() {
|
||||
return (
|
||||
<NetWorkFlow
|
||||
key="1"
|
||||
selectArr={selectBrokerMap}
|
||||
type={admin.type}
|
||||
selectChange={(value: IOptionType) => this.onSelectChange(value)}
|
||||
getApi={() => this.getOptionApi()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public renderTrafficInfo = () => {
|
||||
return (
|
||||
<Spin spinning={admin.realBrokerLoading}>
|
||||
{renderTrafficTable(this.updateRealStatus, StatusGragh)}
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
{this.renderBrokerContent()}
|
||||
{this.renderTrafficInfo()}
|
||||
{this.renderHistoryTraffic()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
class StatusGragh extends StatusGraghCom<IClusterReal> {
|
||||
public getData = () => {
|
||||
return admin.brokersMetrics;
|
||||
}
|
||||
|
||||
public getLoading = () => {
|
||||
return admin.realBrokerLoading;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Table, Tooltip } from 'component/antd';
|
||||
import { diskDefault } from 'constants/status-map';
|
||||
import Url from 'lib/url-parser';
|
||||
import { pagination } from 'constants/table';
|
||||
import { admin } from 'store/admin';
|
||||
import { IPartitionsLocation } from 'types/base-type';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import './index.less';
|
||||
|
||||
@observer
|
||||
export class DiskInfo extends SearchAndFilterContainer {
|
||||
public clusterId: number;
|
||||
public brokerId: number;
|
||||
|
||||
public state = {
|
||||
searchKey: '',
|
||||
filterStatusVisible: false,
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
this.brokerId = Number(url.search.brokerId);
|
||||
}
|
||||
|
||||
public getDescription = (value: any, record: any) => {
|
||||
return Object.keys(value).map((key: keyof any, index: any) => {
|
||||
return (
|
||||
<>
|
||||
<p key={index}>
|
||||
<span>{value[key]}</span>
|
||||
{(record[key] as []).join(',')}(共{(record[key] as []).length}个)
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public getMoreDetail = (record: IPartitionsLocation) => {
|
||||
return (
|
||||
<div className="p-description" key={record.key}>
|
||||
<p><span>diskName: </span>{record.diskName}</p>
|
||||
<p><span>brokerId: </span>{record.brokerId}</p>
|
||||
<p><span>isUnderReplicated:</span>{record.underReplicated + ''}</p>
|
||||
<p><span>topic: </span>{record.topicName}</p>
|
||||
{this.getDescription(diskDefault, record)}
|
||||
<p><span>clusterId: </span>{record.clusterId}</p>
|
||||
<p><span>underReplicatedPartitions: </span></p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
admin.getPartitionsLocation(this.clusterId, this.brokerId);
|
||||
}
|
||||
|
||||
public getData<T extends IPartitionsLocation>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: IPartitionsLocation) =>
|
||||
(item.diskName !== undefined && item.diskName !== null) && item.diskName.toLowerCase().includes(searchKey as string)
|
||||
|| (item.topicName !== undefined && item.topicName !== null) && item.topicName.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public renderDiskInfo() {
|
||||
const underReplicated = Object.assign({
|
||||
title: '状态',
|
||||
dataIndex: 'underReplicated',
|
||||
key: 'underReplicated',
|
||||
filters: [{ text: '已同步', value: 'false' }, { text: '未同步', value: 'true' }],
|
||||
onFilter: (value: string, record: IPartitionsLocation) => record.underReplicated + '' === value,
|
||||
render: (t: boolean) => <span>{t ? '未同步' : '已同步'}</span>,
|
||||
}, this.renderColumnsFilter('filterStatusVisible'));
|
||||
const columns = [{
|
||||
title: '磁盘名称',
|
||||
dataIndex: 'diskName',
|
||||
key: 'diskName',
|
||||
sorter: (a: IPartitionsLocation, b: IPartitionsLocation) => a.diskName.charCodeAt(0) - b.diskName.charCodeAt(0),
|
||||
render: (val: string) => <Tooltip placement="bottomLeft" title={val}> {val} </Tooltip>,
|
||||
}, {
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
sorter: (a: IPartitionsLocation, b: IPartitionsLocation) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
render: (val: string) => <Tooltip placement="bottomLeft" title={val}> {val} </Tooltip>,
|
||||
}, {
|
||||
title: 'Leader分区',
|
||||
dataIndex: 'leaderPartitions',
|
||||
key: 'leaderPartitions',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 250,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
render: (value: number[]) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={value.join('、')}>
|
||||
{value.map(i => <span key={i} className="p-params">{i}</span>)}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
title: 'Follow分区',
|
||||
dataIndex: 'followerPartitions',
|
||||
key: 'followerPartitions',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 250,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
render: (value: number[]) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={value.join('、')}>
|
||||
{value.map(i => <span key={i} className="p-params">{i}</span>)}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
title: '未同步副本',
|
||||
dataIndex: 'notUnderReplicatedPartitions',
|
||||
render: (value: number[]) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={value.join('、')}>
|
||||
{value.map(i => <span key={i} className="p-params p-params-unFinished">{i}</span>)}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
underReplicated,
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="k-row">
|
||||
<ul className="k-tab">
|
||||
<li>{this.props.tab}</li>
|
||||
{this.renderSearch('', '请输入磁盘名或者Topic名称')}
|
||||
</ul>
|
||||
<Table
|
||||
columns={columns}
|
||||
expandIconColumnIndex={-1}
|
||||
expandedRowRender={this.getMoreDetail}
|
||||
dataSource={this.getData(admin.partitionsLocation)}
|
||||
rowKey="key"
|
||||
pagination={pagination}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
admin.partitionsLocation ? <> {this.renderDiskInfo()} </> : null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
.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: 180px;
|
||||
text-align: right;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
p.k-title {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
font-family: PingFangSC-Medium;
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
padding-left: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.right-flow {
|
||||
.k-abs {
|
||||
right: 24px;
|
||||
cursor: pointer;
|
||||
& > i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title-flow{
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
.k-text{
|
||||
width: 420px;
|
||||
line-height: 50px;
|
||||
background: #00000005;
|
||||
}
|
||||
}
|
||||
|
||||
.mb-24 {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.k-summary {
|
||||
width: 100%;
|
||||
font-family: PingFangSC-Regular;
|
||||
background: #fff;
|
||||
.k-row-1 {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
border-bottom: solid 1px #e8e8e8;
|
||||
div {
|
||||
flex: 1;
|
||||
line-height: 48px;
|
||||
padding-left: 32px;
|
||||
font-size: 15px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
div + div {
|
||||
border-left: solid 1px #e8e8e8;
|
||||
}
|
||||
}
|
||||
.k-row-2 {
|
||||
width: 100%;
|
||||
padding: 24px 0;
|
||||
border-top: solid 1px #e8e8e8;
|
||||
border-bottom: solid 1px #e8e8e8;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
div {
|
||||
height: 58px;
|
||||
flex: 1;
|
||||
span {
|
||||
display: block;
|
||||
line-height: 22px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 14px;
|
||||
}
|
||||
p {
|
||||
line-height: 32px;
|
||||
margin-top: 4px;
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
}
|
||||
div + div {
|
||||
border-left: solid 2px #e8e8e8;
|
||||
}
|
||||
}
|
||||
.k-row-3 {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
div {
|
||||
padding: 9px 0;
|
||||
flex: 1;
|
||||
span {
|
||||
display: block;
|
||||
line-height: 22px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 14px;
|
||||
}
|
||||
p {
|
||||
line-height: 22px;
|
||||
margin-top: 1px;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
}
|
||||
.long-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
div + div {
|
||||
border-left: solid 2px #e8e8e8;
|
||||
}
|
||||
}
|
||||
}
|
||||
.option-map {
|
||||
min-width: 1200px;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Tabs, PageHeader } from 'antd';
|
||||
import { handleTabKey } from 'lib/utils';
|
||||
import { IMetaData } from 'types/base-type';
|
||||
import Url from 'lib/url-parser';
|
||||
import { BaseInfo } from './base-info';
|
||||
import { MonitorInfo } from './monitor-info';
|
||||
import { TopicInfo } from './topic-info';
|
||||
import { DiskInfo } from './disk-info';
|
||||
import { PartitionInfo } from './partition-info';
|
||||
import { TopicAnalysis } from './topic-analysis';
|
||||
import { handlePageBack } from 'lib/utils';
|
||||
import { admin } from 'store/admin';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
@observer
|
||||
export class BrokerDetail extends React.Component {
|
||||
public clusterId: number;
|
||||
public brokerId: number;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
this.brokerId = Number(url.search.brokerId);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
admin.getBasicInfo(this.clusterId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
let content = {} as IMetaData;
|
||||
content = admin.basicInfo ? admin.basicInfo : content;
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
className="detail topic-detail-header"
|
||||
onBack={() => handlePageBack(`/admin/cluster-detail?clusterId=${this.clusterId}#3`)}
|
||||
title={`集群列表/${content.clusterName || ''}/${this.brokerId || ''}`}
|
||||
/>
|
||||
<Tabs activeKey={location.hash.substr(1) || '1'} type="card" onChange={handleTabKey}>
|
||||
<TabPane tab="基本信息" key="1">
|
||||
<BaseInfo/>
|
||||
</TabPane>
|
||||
<TabPane tab="监控信息" key="2">
|
||||
<MonitorInfo />
|
||||
</TabPane>
|
||||
<TabPane tab="Topic信息" key="3">
|
||||
<TopicInfo tab={'Topic信息'} />
|
||||
</TabPane>
|
||||
<TabPane tab="磁盘信息" key="4">
|
||||
<DiskInfo tab={'磁盘信息'} />
|
||||
</TabPane>
|
||||
<TabPane tab="partition信息" key="5">
|
||||
<PartitionInfo tab={'partition信息'} />
|
||||
</TabPane>
|
||||
<TabPane tab="Topic分析" key="6">
|
||||
<TopicAnalysis />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Url from 'lib/url-parser';
|
||||
import { adminMonitor } from 'store/admin-monitor';
|
||||
import moment from 'moment';
|
||||
import './index.less';
|
||||
import { ExpandCard } from 'component/expand-card';
|
||||
import { DataCurveFilter } from '../data-curve';
|
||||
import { allCurves, ICurveType } from '../data-curve/config';
|
||||
import { CommonCurve } from 'container/common-curve';
|
||||
|
||||
@observer
|
||||
export class MonitorInfo extends React.Component {
|
||||
public clusterId: number;
|
||||
public brokerId: number;
|
||||
public $chart: any;
|
||||
public chart11: any;
|
||||
|
||||
public state = {
|
||||
startTime: moment().subtract(1, 'hour'),
|
||||
endTime: moment(),
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
this.brokerId = Number(url.search.brokerId);
|
||||
adminMonitor.setCurrentBrokerId(this.brokerId);
|
||||
adminMonitor.setCurrentClusterId(this.clusterId);
|
||||
}
|
||||
|
||||
public handleRef(chart: any, index: number) {
|
||||
this.$chart[index] = chart;
|
||||
}
|
||||
|
||||
public getCurves = (curveType: ICurveType) => {
|
||||
return curveType.curves.map(o => {
|
||||
return <CommonCurve key={o.path} options={o} parser={curveType.parser} />;
|
||||
});
|
||||
}
|
||||
|
||||
public renderChart() {
|
||||
return (
|
||||
<div className="curve-wrapper">
|
||||
<DataCurveFilter />
|
||||
{allCurves.map(c => {
|
||||
return <ExpandCard key={c.type} title={c.title} charts={this.getCurves(c)} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
adminMonitor.getBrokersMetricsList(moment().subtract(1, 'hour').format('x'), moment().format('x'));
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
{adminMonitor.brokersMetricsHistory ? this.renderChart() : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Table, Tooltip } from 'component/antd';
|
||||
import { columsDefault } from 'constants/status-map';
|
||||
import Url from 'lib/url-parser';
|
||||
import { pagination } from 'constants/table';
|
||||
import { admin } from 'store/admin';
|
||||
import { IBrokersPartitions } from 'types/base-type';
|
||||
import './index.less';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { getPartitionInfoColumns } from '../config';
|
||||
|
||||
@observer
|
||||
export class PartitionInfo extends SearchAndFilterContainer {
|
||||
public clusterId: number;
|
||||
public brokerId: number;
|
||||
public state = {
|
||||
searchKey: '',
|
||||
filterStatusVisible: false,
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
this.brokerId = Number(url.search.brokerId);
|
||||
}
|
||||
|
||||
public getColumns = () => {
|
||||
const columns = getPartitionInfoColumns();
|
||||
const status = Object.assign({
|
||||
title: '状态',
|
||||
dataIndex: 'underReplicated',
|
||||
key: 'underReplicated',
|
||||
onCell: null,
|
||||
width: '7%',
|
||||
filters: [{ text: '已同步', value: true }, { text: '未同步', value: false }],
|
||||
onFilter: (value: string, record: IBrokersPartitions) => record.underReplicated === Boolean(value),
|
||||
render: (value: string) => <span>{Boolean(value) ? '已同步' : '未同步'}</span>,
|
||||
}, this.renderColumnsFilter('filterStatusVisible'));
|
||||
|
||||
const col = columns.splice(4, 0, status);
|
||||
return columns;
|
||||
}
|
||||
|
||||
public getDescription = (value: any, record: any) => {
|
||||
return Object.keys(value).map((key: keyof any, index: number) => {
|
||||
return (
|
||||
<>
|
||||
<p key={index}>
|
||||
<span>{value[key]}</span>
|
||||
{(record[key] as []).join(',')}(共{(record[key] as []).length}个)
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public getMoreDetail = (record: IBrokersPartitions) => {
|
||||
return (
|
||||
<div className="p-description">
|
||||
<p><span>Topic: </span>{record.topicName}</p>
|
||||
<p><span>isUnderReplicated:</span>{record.underReplicated ? '已同步' : '未同步'}</p>
|
||||
{this.getDescription(columsDefault, record)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public getData<T extends IBrokersPartitions>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: IBrokersPartitions) =>
|
||||
(item.topicName !== undefined && item.topicName !== null) && item.topicName.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
admin.getBrokersPartitions(this.clusterId, this.brokerId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="k-row">
|
||||
<ul className="k-tab">
|
||||
<li>{this.props.tab}</li>
|
||||
{this.renderSearch('', '请输入Topic')}
|
||||
</ul>
|
||||
<Table
|
||||
loading={admin.realBrokerLoading}
|
||||
columns={this.getColumns()}
|
||||
expandIconAsCell={true}
|
||||
expandIconColumnIndex={-1}
|
||||
expandedRowRender={this.getMoreDetail}
|
||||
dataSource={this.getData(admin.brokersPartitions)}
|
||||
rowKey="key"
|
||||
pagination={pagination}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import * as React from 'react';
|
||||
import { Table, Tooltip } from 'component/antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import { admin } from 'store/admin';
|
||||
import { brokerMetrics } from 'constants/status-map';
|
||||
import { IBrokerHistory, IAnalysisTopicVO } from 'types/base-type';
|
||||
import Url from 'lib/url-parser';
|
||||
import './index.less';
|
||||
|
||||
const columns = [{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
sorter: (a: IAnalysisTopicVO, b: IAnalysisTopicVO) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
render: (val: string) => <Tooltip placement="bottomLeft" title={val}> {val} </Tooltip>,
|
||||
},
|
||||
{
|
||||
title: 'Bytes In(KB/s)',
|
||||
dataIndex: 'bytesInRate',
|
||||
key: 'bytesInRate',
|
||||
sorter: (a: IAnalysisTopicVO, b: IAnalysisTopicVO) => Number(b.bytesIn) - Number(a.bytesIn),
|
||||
render: (t: number, record: any) => `${record && (record.bytesIn / 1024).toFixed(2)} (${+Math.ceil((t * 100))}%)`,
|
||||
},
|
||||
{
|
||||
title: 'Bytes Out(KB/s)',
|
||||
dataIndex: 'bytesOutRate',
|
||||
key: 'bytesOutRate',
|
||||
sorter: (a: IAnalysisTopicVO, b: IAnalysisTopicVO) => Number(b.bytesOut) - Number(a.bytesOut),
|
||||
render: (t: number, record: any) => `${record && (record.bytesOut / 1024).toFixed(2)} (${+Math.ceil((t * 100))}%)`,
|
||||
},
|
||||
{
|
||||
title: 'Message In(秒)',
|
||||
dataIndex: 'messagesInRate',
|
||||
key: 'messagesInRate',
|
||||
sorter: (a: IAnalysisTopicVO, b: IAnalysisTopicVO) => Number(b.messagesIn) - Number(a.messagesIn),
|
||||
render: (t: number, record: any) => `${record && record.messagesIn} (${+Math.ceil((t * 100))}%)`,
|
||||
},
|
||||
{
|
||||
title: 'Total Fetch Requests(秒)',
|
||||
dataIndex: 'totalFetchRequestsRate',
|
||||
key: 'totalFetchRequestsRate',
|
||||
sorter: (a: IAnalysisTopicVO, b: IAnalysisTopicVO) => Number(b.totalFetchRequests) - Number(a.totalFetchRequests),
|
||||
render: (t: number, record: any) => `${record && record.totalFetchRequests} (${+Math.ceil((t * 100))}%)`,
|
||||
},
|
||||
{
|
||||
title: 'Total Produce Requests(秒)',
|
||||
dataIndex: 'totalProduceRequestsRate',
|
||||
key: 'totalProduceRequestsRate',
|
||||
sorter: (a: IAnalysisTopicVO, b: IAnalysisTopicVO) => Number(b.totalProduceRequests) - Number(a.totalProduceRequests),
|
||||
render: (t: number, record: any) => `${record && record.totalProduceRequests} (${+Math.ceil((t * 100))}%)`,
|
||||
}];
|
||||
|
||||
@observer
|
||||
export class TopicAnalysis extends React.Component {
|
||||
public clusterId: number;
|
||||
public brokerId: number;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
this.brokerId = Number(url.search.brokerId);
|
||||
}
|
||||
|
||||
public brokerStatus() {
|
||||
return (
|
||||
<div className="k-summary">
|
||||
<div className="k-row-3">
|
||||
<div>
|
||||
<span>Broker ID</span>
|
||||
<p>{this.brokerId}</p>
|
||||
</div>
|
||||
{admin.brokersAnalysis ?
|
||||
Object.keys(brokerMetrics).map((i: keyof IBrokerHistory) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<span className={brokerMetrics[i].length > 25 ? 'long-text' : ''}>
|
||||
{brokerMetrics[i]}</span>
|
||||
<p>{(admin.brokersAnalysis[i] === null || admin.brokersAnalysis[i] === undefined) ?
|
||||
'' : admin.brokersAnalysis[i].toFixed(2)}</p>
|
||||
</div>
|
||||
);
|
||||
}) : ''}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
admin.getBrokersAnalysis(this.clusterId, this.brokerId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
let analysisTopic = [] as IAnalysisTopicVO[];
|
||||
analysisTopic = admin.brokersAnalysisTopic ? admin.brokersAnalysisTopic : analysisTopic;
|
||||
return(
|
||||
<>
|
||||
<div className="k-row right-flow mb-24">
|
||||
<p className="k-title">Broker 状态</p>
|
||||
{this.brokerStatus()}
|
||||
</div>
|
||||
<div className="k-row right-flow">
|
||||
<div className="title-flow">
|
||||
<p className="k-title">Topic 状态</p>
|
||||
<span className="didi-theme k-text">说明:数值后的百分比表示“占Broker总量的百分比”</span>
|
||||
</div>
|
||||
<Table
|
||||
rowKey="key"
|
||||
columns={columns}
|
||||
dataSource={analysisTopic}
|
||||
pagination={false}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import * as React from 'react';
|
||||
import moment from 'moment';
|
||||
import { observer } from 'mobx-react';
|
||||
import { pagination, cellStyle } from 'constants/table';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { IBrokersTopics } from 'types/base-type';
|
||||
import Url from 'lib/url-parser';
|
||||
import { Table, Tooltip } from 'component/antd';
|
||||
import { admin } from 'store/admin';
|
||||
import { region } from 'store/region';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
|
||||
@observer
|
||||
export class TopicInfo extends SearchAndFilterContainer {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
public clusterId: number;
|
||||
public brokerId: number;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
this.brokerId = Number(url.search.brokerId);
|
||||
}
|
||||
|
||||
public getData<T extends IBrokersTopics>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: IBrokersTopics) =>
|
||||
(item.appName !== undefined && item.appName !== null) && item.appName.toLowerCase().includes(searchKey as string)
|
||||
|| (item.topicName !== undefined && item.topicName !== null) && item.topicName.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public renderTopicInfo() {
|
||||
const cloumns = [{
|
||||
title: 'Topic名称',
|
||||
key: 'topicName',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 250,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
sorter: (a: IBrokersTopics, b: IBrokersTopics) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
render: (t: string, r: IBrokersTopics) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={r.topicName} >
|
||||
<a
|
||||
// tslint:disable-next-line:max-line-length
|
||||
href={`${this.urlPrefix}/topic/topic-detail?clusterId=${this.clusterId}&topic=${r.topicName || ''}&isPhysicalClusterId=true®ion=${region.currentRegion}`}
|
||||
>
|
||||
{r.topicName}
|
||||
</a>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
title: '分区数',
|
||||
dataIndex: 'partitionNum',
|
||||
key: 'partitionNum',
|
||||
sorter: (a: IBrokersTopics, b: IBrokersTopics) => b.partitionNum - a.partitionNum,
|
||||
}, {
|
||||
title: '副本数',
|
||||
dataIndex: 'replicaNum',
|
||||
key: 'replicaNum',
|
||||
sorter: (a: IBrokersTopics, b: IBrokersTopics) => b.replicaNum - a.replicaNum,
|
||||
}, {
|
||||
title: 'Bytes In(KB/s)',
|
||||
dataIndex: 'byteIn',
|
||||
key: 'byteIn',
|
||||
sorter: (a: IBrokersTopics, b: IBrokersTopics) => b.byteIn - a.byteIn,
|
||||
render: (t: number) => t === null ? '' : (t / 1024).toFixed(2),
|
||||
}, {
|
||||
title: 'QPS',
|
||||
dataIndex: 'produceRequest',
|
||||
key: 'produceRequest',
|
||||
width: '10%',
|
||||
sorter: (a: IBrokersTopics, b: IBrokersTopics) => b.produceRequest - a.produceRequest,
|
||||
render: (t: number) => t === null ? '' : t.toFixed(2),
|
||||
}, {
|
||||
title: '所属应用',
|
||||
dataIndex: 'appName',
|
||||
key: 'appName',
|
||||
width: '10%',
|
||||
render: (val: string, record: IBrokersTopics) => (
|
||||
<Tooltip placement="bottomLeft" title={record.appId} >
|
||||
{val}
|
||||
</Tooltip>
|
||||
),
|
||||
}, {
|
||||
title: '修改时间',
|
||||
dataIndex: 'updateTime',
|
||||
key: 'updateTime',
|
||||
sorter: (a: IBrokersTopics, b: IBrokersTopics) => b.updateTime - a.updateTime,
|
||||
render: (t: number) => moment(t).format(timeFormat),
|
||||
}];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="k-row">
|
||||
<ul className="k-tab">
|
||||
<li>{this.props.tab}</li>
|
||||
{this.renderSearch('', '请输入Topic名称或者负责人')}
|
||||
</ul>
|
||||
<Table columns={cloumns} dataSource={this.getData(admin.brokersTopics)} rowKey="key" pagination={pagination} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
admin.getBrokersTopics(this.clusterId, this.brokerId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
admin.brokersTopics ? <> {this.renderTopicInfo()} </> : null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
import * as React from 'react';
|
||||
import { Table, notification, Button, Divider, Popconfirm } from 'component/antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import { pagination } from 'constants/table';
|
||||
import { IBrokerData, IEnumsMap, IMetaData } from 'types/base-type';
|
||||
import { admin } from 'store/admin';
|
||||
import { tableFilter, transBToMB } from 'lib/utils';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { DoughnutChart } from 'component/chart';
|
||||
import { LeaderRebalanceWrapper } from 'container/modal/admin/leader-rebalance';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
import Url from 'lib/url-parser';
|
||||
import moment from 'moment';
|
||||
import './index.less';
|
||||
|
||||
@observer
|
||||
export class ClusterBroker extends SearchAndFilterContainer {
|
||||
public clusterId: number;
|
||||
public clusterName: string;
|
||||
|
||||
public state = {
|
||||
searchKey: '',
|
||||
filterPeakFlowVisible: false,
|
||||
filterReplicatedVisible: false,
|
||||
filterRegionVisible: false,
|
||||
filterStatusVisible: false,
|
||||
reblanceVisible: false,
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
}
|
||||
|
||||
public renderBrokerData(clusterBroker: IBrokerData[]) {
|
||||
let peakFlow = [] as IEnumsMap[];
|
||||
peakFlow = admin.peakFlowStatusList ? admin.peakFlowStatusList : peakFlow;
|
||||
|
||||
const peakFlowStatus = Object.assign({
|
||||
title: '峰值状态',
|
||||
dataIndex: 'peakFlowStatus',
|
||||
key: 'peakFlowStatus',
|
||||
width: '8%',
|
||||
filters: peakFlow.map(ele => ({ text: ele.message, value: ele.code + '' })),
|
||||
onFilter: (value: string, record: IBrokerData) => record.peakFlowStatus === +value,
|
||||
render: (value: number) => {
|
||||
let messgae: string;
|
||||
peakFlow.map(ele => {
|
||||
if (ele.code === value) {
|
||||
messgae = ele.message;
|
||||
}
|
||||
});
|
||||
return (
|
||||
<span>{messgae}</span>
|
||||
);
|
||||
},
|
||||
}, this.renderColumnsFilter('filterPeakFlowVisible'));
|
||||
|
||||
const underReplicated = Object.assign({
|
||||
title: '副本状态',
|
||||
dataIndex: 'underReplicated',
|
||||
key: 'underReplicated',
|
||||
width: '8%',
|
||||
filters: [{ text: '同步', value: 'false' }, { text: '未同步', value: 'true' }],
|
||||
onFilter: (value: string, record: IBrokerData) => record.underReplicated === (value === 'true') ? true : false,
|
||||
render: (t: boolean) => t !== null ? <span className={t ? 'fail' : 'success'}>{t ? '未同步' : '同步'}</span> : '',
|
||||
}, this.renderColumnsFilter('filterReplicatedVisible'));
|
||||
|
||||
const region = Object.assign({
|
||||
title: 'regionName',
|
||||
dataIndex: 'regionName',
|
||||
key: 'regionName',
|
||||
width: '10%',
|
||||
filters: tableFilter<any>(clusterBroker, 'regionName'),
|
||||
onFilter: (value: string, record: IBrokerData) => record.regionName === value,
|
||||
}, this.renderColumnsFilter('filterRegionVisible'));
|
||||
|
||||
const status = Object.assign({
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: '8%',
|
||||
filters: [{ text: '未使用', value: '-1' }, { text: '使用中', value: '0' }],
|
||||
onFilter: (value: string, record: IBrokerData) => record.status === Number(value),
|
||||
render: (t: number) => <span className={t === 0 ? 'success' : 'fail'}>{t === 0 ? '使用中' : '未使用'}</span>,
|
||||
}, this.renderColumnsFilter('filterStatusVisible'));
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'brokerId',
|
||||
key: 'brokerId',
|
||||
width: '5%',
|
||||
sorter: (a: IBrokerData, b: IBrokerData) => b.brokerId - a.brokerId,
|
||||
render: (text: number, record: IBrokerData) => {
|
||||
// tslint:disable-next-line:max-line-length
|
||||
const query = `clusterId=${this.clusterId}&brokerId=${record.brokerId}`;
|
||||
const judge = record.underReplicated === false && record.status !== 0;
|
||||
return (
|
||||
<span className={judge ? 'fail' : ''}>
|
||||
{
|
||||
// tslint:disable-next-line:max-line-length
|
||||
record.status === 0 ? <a href={`${this.urlPrefix}/admin/broker-detail?${query}`}>{text}</a>
|
||||
: <a style={{ cursor: 'not-allowed', color: '#999' }}>{text}</a>}
|
||||
</span>);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '主机',
|
||||
dataIndex: 'host',
|
||||
key: 'host',
|
||||
width: '10%',
|
||||
sorter: (a: any, b: any) => a.host.charCodeAt(0) - b.host.charCodeAt(0),
|
||||
},
|
||||
{
|
||||
title: 'Port',
|
||||
dataIndex: 'port',
|
||||
key: 'port',
|
||||
width: '6%',
|
||||
sorter: (a: IBrokerData, b: IBrokerData) => b.port - a.port,
|
||||
},
|
||||
{
|
||||
title: 'JMX Port',
|
||||
dataIndex: 'jmxPort',
|
||||
key: 'jmxPort',
|
||||
width: '7%',
|
||||
sorter: (a: IBrokerData, b: IBrokerData) => b.jmxPort - a.jmxPort,
|
||||
},
|
||||
{
|
||||
title: '启动时间',
|
||||
dataIndex: 'startTime',
|
||||
key: 'startTime',
|
||||
width: '10%',
|
||||
sorter: (a: IBrokerData, b: IBrokerData) => b.startTime - a.startTime,
|
||||
render: (time: number) => moment(time).format(timeFormat),
|
||||
},
|
||||
{
|
||||
title: 'Bytes In(MB/s)',
|
||||
dataIndex: 'byteIn',
|
||||
key: 'byteIn',
|
||||
width: '10%',
|
||||
sorter: (a: IBrokerData, b: IBrokerData) => b.byteIn - a.byteIn,
|
||||
render: (t: number) => transBToMB(t),
|
||||
},
|
||||
{
|
||||
title: 'Bytes Out(MB/s)',
|
||||
dataIndex: 'byteOut',
|
||||
key: 'byteOut',
|
||||
width: '10%',
|
||||
sorter: (a: IBrokerData, b: IBrokerData) => b.byteOut - a.byteOut,
|
||||
render: (t: number) => transBToMB(t),
|
||||
},
|
||||
peakFlowStatus,
|
||||
underReplicated,
|
||||
region,
|
||||
status,
|
||||
{
|
||||
title: '操作',
|
||||
width: '10%',
|
||||
render: (text: string, record: IBrokerData) => {
|
||||
// tslint:disable-next-line:max-line-length
|
||||
const query = `clusterId=${this.clusterId}&brokerId=${record.brokerId}`;
|
||||
return ( // 0 监控中 可点击详情,不可删除 -1 暂停监控 不可点击详情,可删除
|
||||
<>
|
||||
{record.status === 0 ?
|
||||
<a href={`${this.urlPrefix}/admin/broker-detail?${query}`} className="action-button">详情</a>
|
||||
: <a style={{ cursor: 'not-allowed', color: '#999' }}>详情</a>}
|
||||
<Popconfirm
|
||||
title="确定删除?"
|
||||
onConfirm={() => this.deteleTopic(record)}
|
||||
disabled={record.status === 0}
|
||||
>
|
||||
<a style={record.status === 0 ? { cursor: 'not-allowed', color: '#999' } : {}}>
|
||||
删除
|
||||
</a>
|
||||
</Popconfirm>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Table dataSource={clusterBroker} columns={columns} pagination={pagination} />
|
||||
);
|
||||
}
|
||||
|
||||
public deteleTopic(record: any) {
|
||||
admin.deteleClusterBrokers(this.clusterId, record.brokerId).then(data => {
|
||||
notification.success({ message: '删除成功' });
|
||||
});
|
||||
}
|
||||
|
||||
public reblanceInfo() {
|
||||
this.setState({ reblanceVisible: true });
|
||||
}
|
||||
|
||||
public handleVisible(val: boolean) {
|
||||
this.setState({ reblanceVisible: val });
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
await admin.getPeakFlowStatus();
|
||||
admin.getClusterBroker(this.clusterId);
|
||||
admin.getBrokersMetadata(this.clusterId);
|
||||
admin.getBrokersStatus(this.clusterId);
|
||||
}
|
||||
|
||||
public getData<T extends IBrokerData>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: IBrokerData) =>
|
||||
(item.brokerId !== undefined && item.brokerId !== null) && (item.brokerId + '').toLowerCase().includes(searchKey as string)
|
||||
|| (item.host !== undefined && item.host !== null) && item.host.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const content = this.props.basicInfo as IMetaData;
|
||||
if (content) {
|
||||
this.clusterName = content.clusterName;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="diagram">
|
||||
<div className="diagram-box">
|
||||
<h2>峰值使用率分布图</h2>
|
||||
<Divider className="hotspot-divider" />
|
||||
{admin.peakValueList.length ? <DoughnutChart
|
||||
getChartData={() => admin.getPeakFlowChartData(admin.peakValueList, admin.peakValueMap)}
|
||||
/> : null}
|
||||
</div>
|
||||
<div className="diagram-box">
|
||||
<h2>副本状态图</h2>
|
||||
<Divider className="hotspot-divider" />
|
||||
{admin.copyValueList.length ? <DoughnutChart
|
||||
getChartData={() => admin.getSideStatusChartData(admin.copyValueList)}
|
||||
/> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="leader-seacrh">
|
||||
<div className="search-top">
|
||||
{this.renderSearch('', '请输入ID或主机')}
|
||||
<Button onClick={() => this.reblanceInfo()} type="primary">Leader Rebalance</Button>
|
||||
</div>
|
||||
{this.renderBrokerData(this.getData(admin.clusterBroker))}
|
||||
</div>
|
||||
{ this.state.reblanceVisible && <LeaderRebalanceWrapper
|
||||
changeVisible={(val: boolean) => this.handleVisible(val)}
|
||||
visible={this.state.reblanceVisible}
|
||||
clusterId={this.clusterId}
|
||||
clusterName={this.clusterName}
|
||||
/> }
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Table, Modal, Tooltip } from 'component/antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import Url from 'lib/url-parser';
|
||||
import { IOffset, IXFormWrapper } from 'types/base-type';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { pagination } from 'constants/table';
|
||||
import { admin } from 'store/admin';
|
||||
import { getConsumerDetails } from 'lib/api';
|
||||
import './index.less';
|
||||
|
||||
@observer
|
||||
export class ClusterConsumer extends SearchAndFilterContainer {
|
||||
public clusterId: number;
|
||||
public consumerDetails = [] as string[];
|
||||
|
||||
public state = {
|
||||
searchKey: '',
|
||||
detailsVisible: false,
|
||||
};
|
||||
|
||||
public columns = [{
|
||||
title: '消费组名称',
|
||||
dataIndex: 'consumerGroup',
|
||||
key: 'consumerGroup',
|
||||
width: '70%',
|
||||
sorter: (a: IOffset, b: IOffset) => a.consumerGroup.charCodeAt(0) - b.consumerGroup.charCodeAt(0),
|
||||
render: (text: string) => <Tooltip placement="bottomLeft" title={text} >{text}</Tooltip>,
|
||||
}, {
|
||||
title: 'Location',
|
||||
dataIndex: 'location',
|
||||
key: 'location',
|
||||
width: '20%',
|
||||
render: (t: string) => t.toLowerCase(),
|
||||
}, {
|
||||
title: '操作',
|
||||
key: 'operation',
|
||||
width: '10%',
|
||||
render: (t: string, item: IOffset) => {
|
||||
return (<a onClick={() => this.getConsumeDetails(item)}>详情</a>);
|
||||
},
|
||||
}];
|
||||
private xFormModal: IXFormWrapper;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
}
|
||||
|
||||
public getConsumeDetails(record: IOffset) {
|
||||
getConsumerDetails(this.clusterId, record.consumerGroup, record.location).then((data: string[]) => {
|
||||
this.consumerDetails = data;
|
||||
this.setState({ detailsVisible: true });
|
||||
});
|
||||
}
|
||||
|
||||
public handleDetailsOk() {
|
||||
this.setState({ detailsVisible: false });
|
||||
}
|
||||
|
||||
public handleDetailsCancel() {
|
||||
this.setState({ detailsVisible: false });
|
||||
}
|
||||
|
||||
public getData<T extends IOffset>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: IOffset) =>
|
||||
(item.consumerGroup !== undefined && item.consumerGroup !== null) && item.consumerGroup.toLowerCase().includes(searchKey as string)
|
||||
|| (item.location !== undefined && item.location !== null) && item.location.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
admin.getClusterConsumer(this.clusterId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
let details: any[];
|
||||
details = this.consumerDetails ? this.consumerDetails.map((ele, index) => {
|
||||
return {
|
||||
key: index,
|
||||
topicName: ele,
|
||||
};
|
||||
}) : [];
|
||||
|
||||
const consumptionColumns = [{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
}];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="k-row">
|
||||
<ul className="k-tab">
|
||||
<li>{this.props.tab}</li>
|
||||
{this.renderSearch()}
|
||||
</ul>
|
||||
<Table
|
||||
columns={this.columns}
|
||||
dataSource={this.getData(admin.consumerData)}
|
||||
pagination={pagination}
|
||||
rowKey="key"
|
||||
/>
|
||||
</div>
|
||||
<Modal
|
||||
title="消费的Topic"
|
||||
visible={this.state.detailsVisible}
|
||||
onOk={() => this.handleDetailsOk()}
|
||||
onCancel={() => this.handleDetailsCancel()}
|
||||
maskClosable={false}
|
||||
footer={null}
|
||||
>
|
||||
<Table
|
||||
columns={consumptionColumns}
|
||||
dataSource={details}
|
||||
pagination={pagination}
|
||||
rowKey="key"
|
||||
scroll={{ y: 260 }}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { Table } from 'component/antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import { pagination } from 'constants/table';
|
||||
import Url from 'lib/url-parser';
|
||||
import { IController } from 'types/base-type';
|
||||
import { admin } from 'store/admin';
|
||||
import './index.less';
|
||||
import moment from 'moment';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
|
||||
@observer
|
||||
export class ClusterController extends SearchAndFilterContainer {
|
||||
public clusterId: number;
|
||||
|
||||
public state = {
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
}
|
||||
|
||||
public getData<T extends IController>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: IController) =>
|
||||
(item.host !== undefined && item.host !== null) && item.host.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public renderController() {
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'BrokerId',
|
||||
dataIndex: 'brokerId',
|
||||
key: 'brokerId',
|
||||
width: '30%',
|
||||
sorter: (a: IController, b: IController) => b.brokerId - a.brokerId,
|
||||
},
|
||||
{
|
||||
title: 'BrokerHost',
|
||||
key: 'host',
|
||||
dataIndex: 'host',
|
||||
width: '30%',
|
||||
render: (r: string, t: IController) => {
|
||||
return (
|
||||
<a href={`${this.urlPrefix}/admin/broker-detail?clusterId=${this.clusterId}&brokerId=${t.brokerId}`}>{r}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '变更时间',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
width: '40%',
|
||||
sorter: (a: IController, b: IController) => b.timestamp - a.timestamp,
|
||||
render: (t: number) => moment(t).format(timeFormat),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={this.getData(admin.controllerHistory)}
|
||||
pagination={pagination}
|
||||
rowKey="key"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
admin.getControllerHistory(this.clusterId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="k-row">
|
||||
<ul className="k-tab">
|
||||
<li>{this.props.tab}</li>
|
||||
{this.renderSearch('', '请输入Host')}
|
||||
</ul>
|
||||
{this.renderController()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import * as React from 'react';
|
||||
import { PageHeader, Descriptions, Divider, Tooltip, Icon, Spin } from 'component/antd';
|
||||
import { ILabelValue, IMetaData, IOptionType, IClusterReal } from 'types/base-type';
|
||||
import { controlOptionMap, clusterTypeMap } from 'constants/status-map';
|
||||
import { copyString } from 'lib/utils';
|
||||
import { observer } from 'mobx-react';
|
||||
import { admin } from 'store/admin';
|
||||
import Url from 'lib/url-parser';
|
||||
import moment from 'moment';
|
||||
import './index.less';
|
||||
import { StatusGraghCom } from 'component/flow-table';
|
||||
import { renderTrafficTable, NetWorkFlow } from 'container/network-flow';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
|
||||
interface IOverview {
|
||||
basicInfo: IMetaData;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClusterOverview extends React.Component<IOverview> {
|
||||
public clusterId: number;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
}
|
||||
|
||||
public clusterContent() {
|
||||
const content = this.props.basicInfo as IMetaData;
|
||||
const gmtCreate = moment(content.gmtCreate).format(timeFormat);
|
||||
const clusterContent = [{
|
||||
value: content.clusterName,
|
||||
label: '集群名称',
|
||||
}, {
|
||||
value: clusterTypeMap[content.mode],
|
||||
label: '集群类型',
|
||||
}, {
|
||||
value: gmtCreate,
|
||||
label: '接入时间',
|
||||
}];
|
||||
const clusterInfo = [{
|
||||
value: content.kafkaVersion,
|
||||
label: 'kafka版本',
|
||||
}, {
|
||||
value: content.bootstrapServers,
|
||||
label: 'Bootstrap Severs',
|
||||
}, {
|
||||
value: content.zookeeper,
|
||||
label: 'Zookeeper',
|
||||
}];
|
||||
return (
|
||||
<>
|
||||
<div className="chart-title">基本信息</div>
|
||||
<PageHeader className="detail" title="">
|
||||
<Descriptions size="small" column={3}>
|
||||
{clusterContent.map((item: ILabelValue, index: number) => (
|
||||
<Descriptions.Item
|
||||
key={index}
|
||||
label={item.label}
|
||||
>{item.value}
|
||||
</Descriptions.Item>
|
||||
))}
|
||||
{clusterInfo.map((item: ILabelValue, index: number) => (
|
||||
<Descriptions.Item key={index} label={item.label}>
|
||||
<Tooltip placement="bottomLeft" title={item.value}>
|
||||
<span className="overview-bootstrap">
|
||||
<Icon
|
||||
onClick={() => copyString(item.value)}
|
||||
type="copy"
|
||||
className="didi-theme overview-theme"
|
||||
/>
|
||||
<i className="overview-boot">{item.value}</i>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Descriptions.Item>
|
||||
))}
|
||||
</Descriptions>
|
||||
</PageHeader>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public updateRealStatus = () => {
|
||||
admin.getClusterRealTime(this.clusterId);
|
||||
}
|
||||
|
||||
public onSelectChange(e: IOptionType) {
|
||||
return admin.changeType(e);
|
||||
}
|
||||
|
||||
public getOptionApi = () => {
|
||||
return admin.getClusterMetrice(this.clusterId);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
admin.getClusterRealTime(this.clusterId);
|
||||
}
|
||||
|
||||
public renderHistoryTraffic() {
|
||||
return (
|
||||
<NetWorkFlow
|
||||
key="1"
|
||||
selectArr={controlOptionMap}
|
||||
type={admin.type}
|
||||
selectChange={(value: IOptionType) => this.onSelectChange(value)}
|
||||
getApi={() => this.getOptionApi()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public renderTrafficInfo = () => {
|
||||
return (
|
||||
<Spin spinning={admin.realClusterLoading}>
|
||||
{renderTrafficTable(this.updateRealStatus, StatusGragh)}
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<div className="base-info">
|
||||
{this.clusterContent()}
|
||||
{this.renderTrafficInfo()}
|
||||
{this.renderHistoryTraffic()}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export class StatusGragh extends StatusGraghCom<IClusterReal> {
|
||||
public getData = () => {
|
||||
return admin.clusterRealData;
|
||||
}
|
||||
public getLoading = () => {
|
||||
return admin.realClusterLoading;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import * as React from 'react';
|
||||
import Url from 'lib/url-parser';
|
||||
import { region } from 'store';
|
||||
import { admin } from 'store/admin';
|
||||
import { Table, notification, Tooltip, Popconfirm } from 'antd';
|
||||
import { pagination, cellStyle } from 'constants/table';
|
||||
import { observer } from 'mobx-react';
|
||||
import { IClusterTopics } from 'types/base-type';
|
||||
import { deleteClusterTopic } from 'lib/api';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { users } from 'store/users';
|
||||
import { urlPrefix } from 'constants/left-menu';
|
||||
import { transMSecondToHour } from 'lib/utils';
|
||||
import './index.less';
|
||||
|
||||
import moment = require('moment');
|
||||
import { ExpandPartitionFormWrapper } from 'container/modal/admin/expand-partition';
|
||||
import { showEditClusterTopic } from 'container/modal/admin';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
|
||||
@observer
|
||||
export class ClusterTopic extends SearchAndFilterContainer {
|
||||
public clusterId: number;
|
||||
public clusterTopicsFrom: IClusterTopics;
|
||||
|
||||
public state = {
|
||||
searchKey: '',
|
||||
expandVisible: false,
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
}
|
||||
|
||||
public getBaseInfo(item: IClusterTopics) {
|
||||
admin.getTopicsBasicInfo(item.clusterId, item.topicName).then(data => {
|
||||
showEditClusterTopic(data);
|
||||
});
|
||||
}
|
||||
|
||||
public handleVisible(val: boolean) {
|
||||
this.setState({ expandVisible: val });
|
||||
}
|
||||
|
||||
public expandPartition(item: IClusterTopics) {
|
||||
this.clusterTopicsFrom = item;
|
||||
this.setState({
|
||||
expandVisible: true,
|
||||
});
|
||||
}
|
||||
|
||||
public deleteTopic(item: IClusterTopics) {
|
||||
const value = [{
|
||||
clusterId: item.clusterId,
|
||||
topicName: item.topicName,
|
||||
unForce: false,
|
||||
}];
|
||||
deleteClusterTopic(value).then(data => {
|
||||
notification.success({ message: '删除成功' });
|
||||
admin.getClusterTopics(this.clusterId);
|
||||
});
|
||||
}
|
||||
|
||||
public getData<T extends IClusterTopics>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: IClusterTopics) =>
|
||||
(item.appName !== undefined && item.appName !== null) && item.appName.toLowerCase().includes(searchKey as string)
|
||||
|| (item.topicName !== undefined && item.topicName !== null) && item.topicName.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
admin.getClusterTopics(this.clusterId);
|
||||
}
|
||||
|
||||
public renderClusterTopicList() {
|
||||
const clusterColumns = [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
width: '15%',
|
||||
sorter: (a: IClusterTopics, b: IClusterTopics) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
render: (text: string, record: IClusterTopics) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={record.topicName} >
|
||||
<a
|
||||
// tslint:disable-next-line:max-line-length
|
||||
href={`${urlPrefix}/topic/topic-detail?clusterId=${record.clusterId || ''}&topic=${record.topicName || ''}&isPhysicalClusterId=true®ion=${region.currentRegion}`}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
</Tooltip>);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'QPS',
|
||||
dataIndex: 'produceRequest',
|
||||
key: 'produceRequest',
|
||||
width: '10%',
|
||||
sorter: (a: IClusterTopics, b: IClusterTopics) => b.produceRequest - a.produceRequest,
|
||||
render: (t: number) => t === null ? '' : t.toFixed(2),
|
||||
},
|
||||
{
|
||||
title: 'Bytes In(KB/s)',
|
||||
dataIndex: 'byteIn',
|
||||
key: 'byteIn',
|
||||
width: '15%',
|
||||
sorter: (a: IClusterTopics, b: IClusterTopics) => b.byteIn - a.byteIn,
|
||||
render: (t: number) => t === null ? '' : (t / 1024).toFixed(2),
|
||||
},
|
||||
{
|
||||
title: '所属应用',
|
||||
dataIndex: 'appName',
|
||||
key: 'appName',
|
||||
width: '10%',
|
||||
render: (val: string, record: IClusterTopics) => (
|
||||
<Tooltip placement="bottomLeft" title={record.appId} >
|
||||
{val}
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '保存时间(h)',
|
||||
dataIndex: 'retentionTime',
|
||||
key: 'retentionTime',
|
||||
width: '10%',
|
||||
sorter: (a: IClusterTopics, b: IClusterTopics) => b.retentionTime - a.retentionTime,
|
||||
render: (time: any) => transMSecondToHour(time),
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updateTime',
|
||||
key: 'updateTime',
|
||||
render: (t: number) => moment(t).format(timeFormat),
|
||||
width: '10%',
|
||||
},
|
||||
{
|
||||
title: 'Topic说明',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
width: '15%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 180,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: '30%',
|
||||
render: (value: string, item: IClusterTopics) => (
|
||||
<>
|
||||
<a onClick={() => this.getBaseInfo(item)} className="action-button">编辑</a>
|
||||
<a onClick={() => this.expandPartition(item)} className="action-button">扩分区</a>
|
||||
<Popconfirm
|
||||
title="确定删除?"
|
||||
onConfirm={() => this.deleteTopic(item)}
|
||||
>
|
||||
<a>删除</a>
|
||||
</Popconfirm>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
if (users.currentUser.role !== 2) {
|
||||
clusterColumns.splice(-1, 1);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="k-row">
|
||||
<ul className="k-tab">
|
||||
<li>{this.props.tab}</li>
|
||||
{this.renderSearch('', '请输入Topic名称,应用名称')}
|
||||
</ul>
|
||||
<Table
|
||||
loading={admin.loading}
|
||||
rowKey="key"
|
||||
dataSource={this.getData(admin.clusterTopics)}
|
||||
columns={clusterColumns}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</div>
|
||||
{this.renderExpandModal()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public renderExpandModal() {
|
||||
let formData = {} as IClusterTopics;
|
||||
formData = this.clusterTopicsFrom ? this.clusterTopicsFrom : formData;
|
||||
return (
|
||||
<>
|
||||
{this.state.expandVisible && <ExpandPartitionFormWrapper
|
||||
handleVisible={(val: boolean) => this.handleVisible(val)}
|
||||
visible={this.state.expandVisible}
|
||||
formData={formData}
|
||||
clusterId={this.clusterId}
|
||||
/>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
admin.clusterTopics ? <> {this.renderClusterTopicList()} </> : null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { Table, Tooltip } from 'component/antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import { pagination } from 'constants/table';
|
||||
import Url from 'lib/url-parser';
|
||||
import { IThrottles } from 'types/base-type';
|
||||
import { admin } from 'store/admin';
|
||||
import './index.less';
|
||||
|
||||
@observer
|
||||
export class CurrentLimiting extends SearchAndFilterContainer {
|
||||
public clusterId: number;
|
||||
|
||||
public state = {
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
}
|
||||
|
||||
public renderController() {
|
||||
const clientType = Object.assign({
|
||||
title: '类型',
|
||||
dataIndex: 'throttleClientType',
|
||||
key: 'throttleClientType',
|
||||
width: '15%',
|
||||
filters: [{ text: 'fetch', value: 'Fetch' }, { text: 'produce', value: 'Produce' }],
|
||||
onFilter: (value: string, record: IThrottles) => record.throttleClientType === value,
|
||||
render: (t: string) => t,
|
||||
}, this.renderColumnsFilter('filterStatus'));
|
||||
const columns = [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
key: 'topicName',
|
||||
dataIndex: 'topicName',
|
||||
width: '50%',
|
||||
sorter: (a: IThrottles, b: IThrottles) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
render: (val: string) => <Tooltip placement="bottomLeft" title={val}> {val} </Tooltip>,
|
||||
},
|
||||
{
|
||||
title: '应用ID',
|
||||
dataIndex: 'appId',
|
||||
key: 'appId',
|
||||
width: '15%',
|
||||
sorter: (a: IThrottles, b: IThrottles) => a.appId.charCodeAt(0) - b.appId.charCodeAt(0),
|
||||
},
|
||||
clientType,
|
||||
{
|
||||
title: 'Broker',
|
||||
dataIndex: 'brokerIdList',
|
||||
key: 'brokerIdList',
|
||||
width: '20%',
|
||||
render: (value: number[]) => {
|
||||
const num = value ? `[${value.join(',')}]` : '';
|
||||
return(
|
||||
<span>{num}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
const { searchKey } = this.state;
|
||||
if (!admin.clustersThrottles ) return null;
|
||||
const clustersThrottles = admin.clustersThrottles.filter(d =>
|
||||
((d.topicName !== undefined && d.topicName !== null) && d.topicName.toLowerCase().includes(searchKey.toLowerCase()))
|
||||
|| ((d.appId !== undefined && d.appId !== null) && d.appId.toLowerCase().includes(searchKey.toLowerCase())));
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={clustersThrottles}
|
||||
pagination={pagination}
|
||||
rowKey="key"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
admin.getClustersThrottles(this.clusterId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="k-row">
|
||||
<ul className="k-tab">
|
||||
<li>{this.props.tab}</li>
|
||||
{this.renderSearch()}
|
||||
</ul>
|
||||
{this.renderController()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Table, notification, Tooltip, Popconfirm } from 'component/antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { pagination, cellStyle } from 'constants/table';
|
||||
import { wrapper } from 'store';
|
||||
import { admin } from 'store/admin';
|
||||
import { IXFormWrapper, IBrokersRegions, INewRegions, IMetaData } from 'types/base-type';
|
||||
import { deleteRegions } from 'lib/api';
|
||||
import { transBToMB } from 'lib/utils';
|
||||
import Url from 'lib/url-parser';
|
||||
import moment from 'moment';
|
||||
import './index.less';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
|
||||
@observer
|
||||
export class ExclusiveCluster extends SearchAndFilterContainer {
|
||||
public clusterId: number;
|
||||
|
||||
public state = {
|
||||
searchKey: '',
|
||||
filterStatus: false,
|
||||
};
|
||||
|
||||
private xFormModal: IXFormWrapper;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
}
|
||||
|
||||
public renderColumns = () => {
|
||||
const status = Object.assign({
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: '10%',
|
||||
filters: [{ text: '正常', value: '0' }, { text: '容量已满', value: '1' }],
|
||||
onFilter: (value: string, record: IBrokersRegions) => record.status === Number(value),
|
||||
render: (t: number) => <span className={t === 0 ? 'success' : 'fail'}>{t === 0 ? '正常' : '容量已满'}</span>,
|
||||
}, this.renderColumnsFilter('filterStatus'));
|
||||
return [
|
||||
{
|
||||
title: 'RegionID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: '7%',
|
||||
},
|
||||
{
|
||||
title: 'Region名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: '13%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 160,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
sorter: (a: IBrokersRegions, b: IBrokersRegions) => a.name.charCodeAt(0) - b.name.charCodeAt(0),
|
||||
render: (text: string, r: IBrokersRegions) => (
|
||||
<Tooltip placement="bottomLeft" title={text}>
|
||||
{text}
|
||||
</Tooltip>),
|
||||
},
|
||||
{
|
||||
title: 'BrokerIdList',
|
||||
dataIndex: 'brokerIdList',
|
||||
key: 'brokerIdList',
|
||||
width: '10%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 200,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
render: (value: number[]) => {
|
||||
const num = value ? value.join(',') : '';
|
||||
return(
|
||||
<Tooltip placement="bottomLeft" title={num}>
|
||||
{num}
|
||||
</Tooltip>);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '预估容量(MB/s)',
|
||||
dataIndex: 'capacity',
|
||||
key: 'capacity',
|
||||
width: '10%',
|
||||
sorter: (a: IBrokersRegions, b: IBrokersRegions) => b.capacity - a.capacity,
|
||||
render: (t: number) => transBToMB(t),
|
||||
},
|
||||
{
|
||||
title: '实际流量(MB/s)',
|
||||
dataIndex: 'realUsed',
|
||||
key: 'realUsed',
|
||||
width: '10%',
|
||||
sorter: (a: IBrokersRegions, b: IBrokersRegions) => b.realUsed - a.realUsed,
|
||||
render: (t: number) => transBToMB(t),
|
||||
},
|
||||
{
|
||||
title: '预估流量(MB/s)',
|
||||
dataIndex: 'estimateUsed',
|
||||
key: 'estimateUsed',
|
||||
width: '10%',
|
||||
sorter: (a: IBrokersRegions, b: IBrokersRegions) => b.estimateUsed - a.estimateUsed,
|
||||
render: (t: number) => transBToMB(t),
|
||||
},
|
||||
{
|
||||
title: '修改时间',
|
||||
dataIndex: 'gmtModify',
|
||||
key: 'gmtModify',
|
||||
width: '10%',
|
||||
sorter: (a: IBrokersRegions, b: IBrokersRegions) => b.gmtModify - a.gmtModify,
|
||||
render: (t: number) => moment(t).format(timeFormat),
|
||||
},
|
||||
status,
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
width: '10%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 200,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
render: (text: string, r: IBrokersRegions) => (
|
||||
<Tooltip placement="bottomLeft" title={text}>
|
||||
{text}
|
||||
</Tooltip>),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: '10%',
|
||||
render: (text: string, record: IBrokersRegions) => {
|
||||
return (
|
||||
<span className="table-operation">
|
||||
<a onClick={() => this.addOrModifyRegion(record)}>编辑</a>
|
||||
<Popconfirm
|
||||
title="确定删除?"
|
||||
onConfirm={() => this.handleDeleteRegion(record)}
|
||||
>
|
||||
<a>删除</a>
|
||||
</Popconfirm>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public handleDeleteRegion = (record: IBrokersRegions) => {
|
||||
deleteRegions(record.id).then(() => {
|
||||
notification.success({ message: '删除成功' });
|
||||
admin.getBrokersRegions(this.clusterId);
|
||||
});
|
||||
}
|
||||
|
||||
public addOrModifyRegion(record?: IBrokersRegions) {
|
||||
const content = this.props.basicInfo as IMetaData;
|
||||
|
||||
this.xFormModal = {
|
||||
formMap: [
|
||||
{
|
||||
key: 'name',
|
||||
label: 'Region名称',
|
||||
rules: [{ required: true, message: '请输入Region名称' }],
|
||||
attrs: { placeholder: '请输入Region名称' },
|
||||
},
|
||||
{
|
||||
key: 'clusterName',
|
||||
label: '集群名称',
|
||||
rules: [{ required: true, message: '请输入集群名称' }],
|
||||
defaultValue: content.clusterName,
|
||||
attrs: {
|
||||
disabled: true,
|
||||
placeholder: '请输入集群名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'brokerIdList',
|
||||
label: 'Broker列表',
|
||||
defaultValue: record ? record.brokerIdList.join(',') : [],
|
||||
rules: [{ required: true, message: '请输入BrokerIdList' }],
|
||||
attrs: {
|
||||
placeholder: '请输入BrokerIdList',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: '状态',
|
||||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
label: '正常',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: '容量已满',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
defaultValue: 0,
|
||||
rules: [{ required: true, message: '请选择状态' }],
|
||||
attrs: {
|
||||
placeholder: '请选择状态',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
label: '备注',
|
||||
type: 'text_area',
|
||||
rules: [{
|
||||
required: false,
|
||||
}],
|
||||
attrs: {
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
},
|
||||
],
|
||||
formData: record,
|
||||
visible: true,
|
||||
title: `${record ? '编辑' : '新增Region'}`,
|
||||
onSubmit: (value: INewRegions) => {
|
||||
value.clusterId = this.clusterId;
|
||||
value.brokerIdList = value.brokerIdList && Array.isArray(value.brokerIdList) ?
|
||||
value.brokerIdList : value.brokerIdList.split(',');
|
||||
if (record) {
|
||||
value.id = record.id;
|
||||
}
|
||||
delete value.clusterName;
|
||||
if (record) {
|
||||
return admin.editRegions(this.clusterId, value).then(data => {
|
||||
notification.success({ message: '编辑Region成功' });
|
||||
});
|
||||
}
|
||||
return admin.addNewRegions(this.clusterId, value).then(data => {
|
||||
notification.success({ message: '新建Region成功' });
|
||||
});
|
||||
},
|
||||
};
|
||||
wrapper.open(this.xFormModal);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
admin.getBrokersRegions(this.clusterId);
|
||||
admin.getBrokersMetadata(this.clusterId);
|
||||
}
|
||||
|
||||
public getData<T extends IBrokersRegions>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: IBrokersRegions) =>
|
||||
(item.name !== undefined && item.name !== null) && item.name.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public renderRegion() {
|
||||
return (
|
||||
<Table
|
||||
columns={this.renderColumns()}
|
||||
dataSource={this.getData(admin.brokersRegions)}
|
||||
pagination={pagination}
|
||||
rowKey="id"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="k-row">
|
||||
<ul className="k-tab">
|
||||
<li>{this.props.tab}</li>
|
||||
<li className="k-add" onClick={() => this.addOrModifyRegion()}>
|
||||
<i className="k-icon-xinjian didi-theme" />
|
||||
<span>新增Region</span>
|
||||
</li>
|
||||
{this.renderSearch('', '请输入Region名称')}
|
||||
</ul>
|
||||
{this.renderRegion()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
.table-operation-bar {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
z-index: 100;
|
||||
li {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
.ant-select {
|
||||
width: 150px;
|
||||
}
|
||||
.ant-input-search {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.traffic-table {
|
||||
margin: 10px 0;
|
||||
min-height: 450px;
|
||||
.traffic-header {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
font-weight: bold;
|
||||
background: rgb(245, 245, 245);
|
||||
border: 1px solid #e8e8e8;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
span {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 13px;
|
||||
line-height: 44px;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
.k-abs {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.implement-button {
|
||||
float: right;
|
||||
margin-right: -120px;
|
||||
}
|
||||
|
||||
.diagram {
|
||||
min-width: 900px;
|
||||
background: white;
|
||||
height: 400px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
.diagram-box {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
width: 46%;
|
||||
height: 100%;
|
||||
h2 {
|
||||
line-height: 30px;
|
||||
font-size: 13px;
|
||||
margin-left: 15px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.descriptions {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.leader-seacrh {
|
||||
z-index: 9999999;
|
||||
margin-top: 10px;
|
||||
.search-top {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Tabs, PageHeader } from 'antd';
|
||||
import { IMetaData } from 'types/base-type';
|
||||
import { ClusterOverview } from './cluster-overview';
|
||||
import { ClusterTopic } from './cluster-topic';
|
||||
import { ClusterBroker } from './cluster-broker';
|
||||
import { ClusterConsumer } from './cluster-consumer';
|
||||
import { ExclusiveCluster } from './exclusive-cluster';
|
||||
import { LogicalCluster } from './logical-cluster';
|
||||
import { ClusterController } from './cluster-controller';
|
||||
import { CurrentLimiting } from './current-limiting';
|
||||
import { handleTabKey } from 'lib/utils';
|
||||
import { admin } from 'store/admin';
|
||||
import { handlePageBack } from 'lib/utils';
|
||||
import Url from 'lib/url-parser';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
@observer
|
||||
export class ClusterDetail extends React.Component {
|
||||
public clusterId: number;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
admin.getBasicInfo(this.clusterId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
let content = {} as IMetaData;
|
||||
content = admin.basicInfo ? admin.basicInfo : content;
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
className="detail topic-detail-header"
|
||||
onBack={() => handlePageBack('/admin')}
|
||||
title={`集群列表/${content.clusterName || ''}`}
|
||||
/>
|
||||
<Tabs activeKey={location.hash.substr(1) || '1'} type="card" onChange={handleTabKey}>
|
||||
<TabPane tab="集群概览" key="1">
|
||||
<ClusterOverview basicInfo={content} />
|
||||
</TabPane>
|
||||
<TabPane tab="Topic信息" key="2">
|
||||
<ClusterTopic tab={'Topic信息'}/>
|
||||
</TabPane>
|
||||
<TabPane tab="Broker信息" key="3">
|
||||
<ClusterBroker tab={'Broker信息'} basicInfo={content} />
|
||||
</TabPane>
|
||||
<TabPane tab="消费组信息" key="4">
|
||||
<ClusterConsumer tab={'消费组信息'} />
|
||||
</TabPane>
|
||||
<TabPane tab="Region信息" key="5">
|
||||
<ExclusiveCluster tab={'Region信息'} basicInfo={content} />
|
||||
</TabPane>
|
||||
<TabPane tab="逻辑集群信息" key="6">
|
||||
<LogicalCluster tab={'逻辑集群信息'} basicInfo={content} />
|
||||
</TabPane>
|
||||
<TabPane tab="Controller变更历史" key="7">
|
||||
<ClusterController tab={'Controller变更历史'} />
|
||||
</TabPane>
|
||||
<TabPane tab="限流信息" key="8">
|
||||
<CurrentLimiting tab={'限流信息'}/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Table, notification, Popconfirm } from 'component/antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { pagination } from 'constants/table';
|
||||
import Url from 'lib/url-parser';
|
||||
import moment from 'moment';
|
||||
import { admin } from 'store/admin';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { ILogicalCluster } from 'types/base-type';
|
||||
import './index.less';
|
||||
import { app } from 'store/app';
|
||||
import { showLogicalClusterOpModal } from 'container/modal';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
|
||||
@observer
|
||||
export class LogicalCluster extends SearchAndFilterContainer {
|
||||
public clusterId: number;
|
||||
|
||||
public state = {
|
||||
searchKey: '',
|
||||
filterStatus: false,
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
}
|
||||
|
||||
public renderColumns = () => {
|
||||
return [
|
||||
{
|
||||
title: '逻辑集群ID',
|
||||
dataIndex: 'logicalClusterId',
|
||||
key: 'logicalClusterId',
|
||||
},
|
||||
{
|
||||
title: '逻辑集群名称',
|
||||
dataIndex: 'logicalClusterName',
|
||||
key: 'logicalClusterName',
|
||||
},
|
||||
{
|
||||
title: '应用ID',
|
||||
dataIndex: 'appId',
|
||||
key: 'appId',
|
||||
},
|
||||
{
|
||||
title: 'RegionIdList',
|
||||
dataIndex: 'regionIdList',
|
||||
key: 'regionIdList',
|
||||
render: (value: number[]) => {
|
||||
const num = value ? `[${value.join(',')}]` : '';
|
||||
return(
|
||||
<span>{num}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '集群模式',
|
||||
dataIndex: 'mode',
|
||||
key: 'mode',
|
||||
render: (value: number) => {
|
||||
let val = '';
|
||||
cluster.clusterModes.forEach((ele: any) => {
|
||||
if (value === ele.code) {
|
||||
val = ele.message;
|
||||
}
|
||||
});
|
||||
return(<span>{val}</span>);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '修改时间',
|
||||
dataIndex: 'gmtModify',
|
||||
key: 'gmtModify',
|
||||
render: (t: number) => moment(t).format(timeFormat),
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
render: (text: string, record: ILogicalCluster) => {
|
||||
return (
|
||||
<span className="table-operation">
|
||||
<a onClick={() => this.editRegion(record)}>编辑</a>
|
||||
<Popconfirm
|
||||
title="确定删除?"
|
||||
onConfirm={() => this.handleDeleteRegion(record)}
|
||||
>
|
||||
<a>删除</a>
|
||||
</Popconfirm>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public handleDeleteRegion = (record: ILogicalCluster) => {
|
||||
admin.deteleLogicalClusters(this.clusterId, record.logicalClusterId).then(() => {
|
||||
notification.success({ message: '删除成功' });
|
||||
});
|
||||
}
|
||||
|
||||
public async editRegion(record: ILogicalCluster) {
|
||||
await admin.queryLogicalClusters(record.logicalClusterId);
|
||||
await this.addOrEditLogicalCluster(admin.queryLogical);
|
||||
}
|
||||
|
||||
public addOrEditLogicalCluster(record?: ILogicalCluster) {
|
||||
showLogicalClusterOpModal(this.clusterId, record);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
admin.getLogicalClusters(this.clusterId);
|
||||
cluster.getClusterModes();
|
||||
|
||||
admin.getBrokersRegions(this.clusterId);
|
||||
if (!app.adminAppData.length) {
|
||||
app.getAdminAppList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public getData<T extends ILogicalCluster>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: ILogicalCluster) =>
|
||||
(item.logicalClusterName !== undefined && item.logicalClusterName !== null)
|
||||
&& item.logicalClusterName.toLowerCase().includes(searchKey as string)
|
||||
|| (item.appId !== undefined && item.appId !== null) && item.appId.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public renderLogicalCluster() {
|
||||
return (
|
||||
<Table
|
||||
columns={this.renderColumns()}
|
||||
dataSource={this.getData(admin.logicalClusters)}
|
||||
pagination={pagination}
|
||||
rowKey="key"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="k-row">
|
||||
<ul className="k-tab">
|
||||
<li>{this.props.tab}</li>
|
||||
<li className="k-add" onClick={() => this.addOrEditLogicalCluster()}>
|
||||
<i className="k-icon-xinjian didi-theme" />
|
||||
<span>新增逻辑集群</span>
|
||||
</li>
|
||||
{this.renderSearch('', '请输入逻辑集群名称或AppId')}
|
||||
</ul>
|
||||
{this.renderLogicalCluster()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
318
kafka-manager-console/src/container/admin/cluster-list/index.tsx
Normal file
318
kafka-manager-console/src/container/admin/cluster-list/index.tsx
Normal file
@@ -0,0 +1,318 @@
|
||||
import * as React from 'react';
|
||||
import { Modal, Table, Button, notification, message, Tooltip, Icon, Popconfirm } from 'component/antd';
|
||||
import { wrapper } from 'store';
|
||||
import { observer } from 'mobx-react';
|
||||
import { IXFormWrapper, IMetaData, IRegister } from 'types/base-type';
|
||||
import { admin } from 'store/admin';
|
||||
import { registerCluster, createCluster, pauseMonitoring } from 'lib/api';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { customPagination } from 'constants/table';
|
||||
import { urlPrefix } from 'constants/left-menu';
|
||||
import { region } from 'store';
|
||||
import './index.less';
|
||||
import { getAdminClusterColumns } from '../config';
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
||||
@observer
|
||||
export class ClusterList extends SearchAndFilterContainer {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
private xFormModal: IXFormWrapper;
|
||||
|
||||
// TODO: 公共化
|
||||
public renderClusterHref(value: number | string, item: IMetaData, key: number) {
|
||||
return ( // 0 暂停监控--不可点击 1 监控中---可正常点击
|
||||
<>
|
||||
{item.status === 1 ? <a href={`${urlPrefix}/admin/cluster-detail?clusterId=${item.clusterId}#${key}`}>{value}</a>
|
||||
: <a style={{ cursor: 'not-allowed', color: '#999' }}>{value}</a>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public createOrRegisterCluster(item: IMetaData) {
|
||||
this.xFormModal = {
|
||||
formMap: [
|
||||
{
|
||||
key: 'clusterName',
|
||||
label: '集群名称',
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入集群名称',
|
||||
}],
|
||||
attrs: {
|
||||
placeholder: '请输入集群名称',
|
||||
disabled: item ? true : false,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'zookeeper',
|
||||
label: 'zookeeper地址',
|
||||
type: 'text_area',
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入zookeeper地址',
|
||||
}],
|
||||
attrs: {
|
||||
placeholder: '请输入zookeeper地址',
|
||||
rows: 2,
|
||||
disabled: item ? true : false,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'bootstrapServers',
|
||||
label: 'bootstrapServers',
|
||||
type: 'text_area',
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入bootstrapServers',
|
||||
}],
|
||||
attrs: {
|
||||
placeholder: '请输入bootstrapServers',
|
||||
rows: 2,
|
||||
disabled: item ? true : false,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'idc',
|
||||
label: '数据中心',
|
||||
defaultValue: region.regionName,
|
||||
rules: [{ required: true, message: '请输入数据中心' }],
|
||||
attrs: {
|
||||
placeholder: '请输入数据中心',
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'mode',
|
||||
label: '集群类型',
|
||||
type: 'select',
|
||||
options: cluster.clusterModes.map(ele => {
|
||||
return {
|
||||
label: ele.message,
|
||||
value: ele.code,
|
||||
};
|
||||
}),
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请选择集群类型',
|
||||
}],
|
||||
attrs: {
|
||||
placeholder: '请选择集群类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'kafkaVersion',
|
||||
label: 'kafka版本',
|
||||
invisible: item ? false : true,
|
||||
rules: [{
|
||||
required: false,
|
||||
message: '请输入kafka版本',
|
||||
}],
|
||||
attrs: {
|
||||
placeholder: '请输入kafka版本',
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'securityProperties',
|
||||
label: '安全协议',
|
||||
type: 'text_area',
|
||||
rules: [{
|
||||
required: false,
|
||||
message: '请输入安全协议',
|
||||
}],
|
||||
attrs: {
|
||||
placeholder: '请输入安全协议',
|
||||
rows: 6,
|
||||
},
|
||||
},
|
||||
],
|
||||
formData: item ? item : {},
|
||||
visible: true,
|
||||
width: 590,
|
||||
title: '注册集群',
|
||||
onSubmit: (value: IRegister) => {
|
||||
value.idc = region.currentRegion;
|
||||
if (item) {
|
||||
value.clusterId = item.clusterId;
|
||||
registerCluster(value).then(data => {
|
||||
admin.getMetaData(true);
|
||||
notification.success({ message: '修改集群成功' });
|
||||
});
|
||||
} else {
|
||||
createCluster(value).then(data => {
|
||||
admin.getMetaData(true);
|
||||
notification.success({ message: '注册集群成功' });
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
};
|
||||
wrapper.open(this.xFormModal);
|
||||
}
|
||||
|
||||
public pauseMonitor(item: IMetaData) {
|
||||
const info = item.status === 1 ? '暂停监控' : '开始监控';
|
||||
const status = item.status === 1 ? 0 : 1;
|
||||
pauseMonitoring(item.clusterId, status).then(data => {
|
||||
admin.getMetaData(true);
|
||||
notification.success({ message: `${info}成功` });
|
||||
});
|
||||
}
|
||||
|
||||
public showMonitor = (record: IMetaData) => {
|
||||
admin.getBrokersRegions(record.clusterId).then((data) => {
|
||||
confirm({
|
||||
// tslint:disable-next-line:jsx-wrap-multiline
|
||||
title: <>
|
||||
<span className="offline_span">
|
||||
删除集群
|
||||
<a>
|
||||
<Tooltip placement="right" title={'当前集群存在逻辑集群,无法申请下线'} >
|
||||
<Icon type="question-circle" />
|
||||
</Tooltip>
|
||||
</a>
|
||||
</span>
|
||||
</>,
|
||||
icon: 'none',
|
||||
content: this.deleteMonitorModal(data),
|
||||
width: 500,
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk() {
|
||||
if (data.length) {
|
||||
return message.warning('存在逻辑集群,无法申请下线!');
|
||||
}
|
||||
admin.deleteCluster(record.clusterId).then(data => {
|
||||
notification.success({ message: '删除成功' });
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public deleteMonitorModal = (source: any) => {
|
||||
const cellStyle = {
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
const monitorColumns = [
|
||||
{
|
||||
title: '集群名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 250,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
render: (t: string) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={t} >{t}</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<div className="render_offline">
|
||||
<Table
|
||||
rowKey="key"
|
||||
dataSource={source}
|
||||
columns={monitorColumns}
|
||||
scroll={{ x: 300, y: 200 }}
|
||||
pagination={false}
|
||||
bordered={true}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public getData<T extends IMetaData>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: IMetaData) =>
|
||||
(item.clusterName !== undefined && item.clusterName !== null) && item.clusterName.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public getColumns = () => {
|
||||
const cols = getAdminClusterColumns();
|
||||
const col = {
|
||||
title: '操作',
|
||||
render: (value: string, item: IMetaData) => (
|
||||
<>
|
||||
<a
|
||||
onClick={this.createOrRegisterCluster.bind(this, item)}
|
||||
className="action-button"
|
||||
>修改
|
||||
</a>
|
||||
<Popconfirm
|
||||
title={`确定${item.status === 1 ? '暂停' : '开始'}${item.clusterName}监控?`}
|
||||
onConfirm={() => this.pauseMonitor(item)}
|
||||
>
|
||||
<a
|
||||
className="action-button"
|
||||
>
|
||||
{item.status === 1 ? '暂停监控' : '开始监控'}
|
||||
</a>
|
||||
</Popconfirm>
|
||||
<a onClick={this.showMonitor.bind(this, item)}>
|
||||
删除
|
||||
</a>
|
||||
</>
|
||||
),
|
||||
};
|
||||
cols.push(col as any);
|
||||
return cols;
|
||||
}
|
||||
|
||||
public renderClusterList() {
|
||||
return (
|
||||
<>
|
||||
<div className="container">
|
||||
<div className="table-operation-panel">
|
||||
<ul>
|
||||
{this.renderSearch('', '请输入集群名称')}
|
||||
<li className="right-btn-1">
|
||||
<Button type="primary" onClick={this.createOrRegisterCluster.bind(this, null)}>注册集群</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="table-wrapper">
|
||||
<Table
|
||||
rowKey="key"
|
||||
loading={admin.loading}
|
||||
dataSource={this.getData(admin.metaList)}
|
||||
columns={this.getColumns()}
|
||||
pagination={customPagination}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
admin.getMetaData(true);
|
||||
cluster.getClusterModes();
|
||||
admin.getDataCenter();
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
admin.metaList ? <> {this.renderClusterList()} </> : null
|
||||
);
|
||||
}
|
||||
}
|
||||
319
kafka-manager-console/src/container/admin/config.tsx
Normal file
319
kafka-manager-console/src/container/admin/config.tsx
Normal file
@@ -0,0 +1,319 @@
|
||||
import * as React from 'react';
|
||||
import { IUser, IUploadFile, IConfigure, IMetaData, IBrokersPartitions } from 'types/base-type';
|
||||
import { users } from 'store/users';
|
||||
import { version } from 'store/version';
|
||||
import { showApplyModal, showModifyModal, showConfigureModal } from 'container/modal/admin';
|
||||
import { Popconfirm, Tooltip } from 'component/antd';
|
||||
import { admin } from 'store/admin';
|
||||
import { cellStyle } from 'constants/table';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
import { urlPrefix } from 'constants/left-menu';
|
||||
import moment = require('moment');
|
||||
|
||||
export const getUserColumns = () => {
|
||||
const columns = [
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
width: '35%',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
key: 'operation',
|
||||
width: '30%',
|
||||
render: (text: string, record: IUser) => {
|
||||
return (
|
||||
<span className="table-operation">
|
||||
<a onClick={() => showApplyModal(record)}>修改</a>
|
||||
<Popconfirm
|
||||
title="确定删除?"
|
||||
onConfirm={() => users.deleteUser(record.username)}
|
||||
>
|
||||
<a>删除</a>
|
||||
</Popconfirm>
|
||||
</span>);
|
||||
},
|
||||
},
|
||||
];
|
||||
return columns;
|
||||
};
|
||||
|
||||
export const getVersionColumns = () => {
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
},
|
||||
{
|
||||
title: '文件名称',
|
||||
dataIndex: 'fileName',
|
||||
key: 'fileName',
|
||||
render: (text: string, record: IUploadFile) => {
|
||||
return (
|
||||
<Tooltip placement="topLeft" title={text} >
|
||||
<a href={`${urlPrefix}/info?fileId=${record.id}`} target="_blank">{text}</a>
|
||||
</Tooltip>);
|
||||
},
|
||||
}, {
|
||||
title: 'MD5',
|
||||
dataIndex: 'fileMd5',
|
||||
key: 'fileMd5',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 120,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
render: (text: string) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={text} >
|
||||
{text.substring(0, 8)}
|
||||
</Tooltip>);
|
||||
},
|
||||
}, {
|
||||
title: '更新时间',
|
||||
dataIndex: 'gmtModify',
|
||||
key: 'gmtModify',
|
||||
render: (t: number) => moment(t).format(timeFormat),
|
||||
}, {
|
||||
title: '更新人',
|
||||
dataIndex: 'operator',
|
||||
key: 'operator',
|
||||
}, {
|
||||
title: '备注',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 200,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
render: (text: string) => {
|
||||
return (
|
||||
<Tooltip placement="topLeft" title={text} >
|
||||
{text}
|
||||
</Tooltip>);
|
||||
},
|
||||
}, {
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
key: 'operation',
|
||||
render: (text: string, record: IUploadFile) => {
|
||||
return (
|
||||
<span className="table-operation">
|
||||
<a onClick={() => showModifyModal(record)}>修改</a>
|
||||
<Popconfirm
|
||||
title="确定删除?"
|
||||
onConfirm={() => version.deleteFile(record.id)}
|
||||
>
|
||||
<a>删除</a>
|
||||
</Popconfirm>
|
||||
</span>);
|
||||
},
|
||||
},
|
||||
];
|
||||
return columns;
|
||||
};
|
||||
|
||||
export const getConfigureColumns = () => {
|
||||
const columns = [
|
||||
{
|
||||
title: '配置键',
|
||||
dataIndex: 'configKey',
|
||||
key: 'configKey',
|
||||
width: '20%',
|
||||
sorter: (a: IConfigure, b: IConfigure) => a.configKey.charCodeAt(0) - b.configKey.charCodeAt(0),
|
||||
},
|
||||
{
|
||||
title: '配置值',
|
||||
dataIndex: 'configValue',
|
||||
key: 'configValue',
|
||||
width: '30%',
|
||||
sorter: (a: IConfigure, b: IConfigure) => a.configValue.charCodeAt(0) - b.configValue.charCodeAt(0),
|
||||
render: (t: string) => {
|
||||
return t.substr(0, 1) === '{' && t.substr(0, -1) === '}' ? JSON.stringify(JSON.parse(t), null, 4) : t;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '修改时间',
|
||||
dataIndex: 'gmtModify',
|
||||
key: 'gmtModify',
|
||||
width: '20%',
|
||||
sorter: (a: IConfigure, b: IConfigure) => b.gmtModify - a.gmtModify,
|
||||
render: (t: number) => moment(t).format(timeFormat),
|
||||
},
|
||||
{
|
||||
title: '描述信息',
|
||||
dataIndex: 'configDescription',
|
||||
key: 'configDescription',
|
||||
width: '20%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 180,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: '10%',
|
||||
render: (text: string, record: IConfigure) => {
|
||||
return (
|
||||
<span className="table-operation">
|
||||
<a onClick={() => showConfigureModal(record)}>修改</a>
|
||||
<Popconfirm
|
||||
title="确定删除?"
|
||||
onConfirm={() => admin.deleteConfigure(record.configKey)}
|
||||
>
|
||||
<a>删除</a>
|
||||
</Popconfirm>
|
||||
</span>);
|
||||
},
|
||||
},
|
||||
];
|
||||
return columns;
|
||||
};
|
||||
|
||||
const renderClusterHref = (value: number | string, item: IMetaData, key: number) => {
|
||||
return ( // 0 暂停监控--不可点击 1 监控中---可正常点击
|
||||
<>
|
||||
{item.status === 1 ? <a href={`${urlPrefix}/admin/cluster-detail?clusterId=${item.clusterId}#${key}`}>{value}</a>
|
||||
: <a style={{ cursor: 'not-allowed', color: '#999' }}>{value}</a>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const getAdminClusterColumns = () => {
|
||||
return [
|
||||
{
|
||||
title: '集群ID',
|
||||
dataIndex: 'clusterId',
|
||||
key: 'clusterId',
|
||||
sorter: (a: IMetaData, b: IMetaData) => b.clusterId - a.clusterId,
|
||||
},
|
||||
{
|
||||
title: '集群名称',
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
sorter: (a: IMetaData, b: IMetaData) => a.clusterName.charCodeAt(0) - b.clusterName.charCodeAt(0),
|
||||
render: (text: string, item: IMetaData) => renderClusterHref(text, item, 1),
|
||||
},
|
||||
{
|
||||
title: 'Topic数',
|
||||
dataIndex: 'topicNum',
|
||||
key: 'topicNum',
|
||||
sorter: (a: any, b: IMetaData) => b.topicNum - a.topicNum,
|
||||
render: (text: number, item: IMetaData) => renderClusterHref(text, item, 2),
|
||||
},
|
||||
{
|
||||
title: 'Broker数',
|
||||
dataIndex: 'brokerNum',
|
||||
key: 'brokerNum',
|
||||
sorter: (a: IMetaData, b: IMetaData) => b.brokerNum - a.brokerNum,
|
||||
render: (text: number, item: IMetaData) => renderClusterHref(text, item, 3),
|
||||
},
|
||||
{
|
||||
title: 'Consumer数',
|
||||
dataIndex: 'consumerGroupNum',
|
||||
key: 'consumerGroupNum',
|
||||
sorter: (a: IMetaData, b: IMetaData) => b.consumerGroupNum - a.consumerGroupNum,
|
||||
render: (text: number, item: IMetaData) => renderClusterHref(text, item, 4),
|
||||
},
|
||||
{
|
||||
title: 'Region数',
|
||||
dataIndex: 'regionNum',
|
||||
key: 'regionNum',
|
||||
sorter: (a: IMetaData, b: IMetaData) => b.regionNum - a.regionNum,
|
||||
render: (text: number, item: IMetaData) => renderClusterHref(text, item, 5),
|
||||
},
|
||||
{
|
||||
title: 'Controllerld',
|
||||
dataIndex: 'controllerId',
|
||||
key: 'controllerId',
|
||||
sorter: (a: IMetaData, b: IMetaData) => b.controllerId - a.controllerId,
|
||||
render: (text: number, item: IMetaData) => renderClusterHref(text, item, 7),
|
||||
},
|
||||
{
|
||||
title: '监控中',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
sorter: (a: IMetaData, b: IMetaData) => b.key - a.key,
|
||||
render: (value: number) => value === 1 ?
|
||||
<span className="success">是</span > : <span className="fail">否</span>,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const getPartitionInfoColumns = () => {
|
||||
return [{
|
||||
title: 'Topic',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
width: '21%',
|
||||
render: (val: string) => <Tooltip placement="bottomLeft" title={val}> {val} </Tooltip>,
|
||||
}, {
|
||||
title: 'Leader',
|
||||
dataIndex: 'leaderPartitionList',
|
||||
width: '20%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 250,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
render: (value: number[]) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={value.join('、')}>
|
||||
{value.map(i => <span key={i} className="p-params">{i}</span>)}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
title: '副本',
|
||||
dataIndex: 'followerPartitionIdList',
|
||||
width: '22%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 250,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
render: (value: number[]) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={value.join('、')}>
|
||||
{value.map(i => <span key={i} className="p-params">{i}</span>)}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
title: '未同步副本',
|
||||
dataIndex: 'notUnderReplicatedPartitionIdList',
|
||||
width: '22%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 250,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
render: (value: number[]) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={value.join('、')}>
|
||||
{value.map(i => <span key={i} className="p-params p-params-unFinished">{i}</span>)}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
}];
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import * as React from 'react';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { Table, Button, Spin } from 'component/antd';
|
||||
import { admin } from 'store/admin';
|
||||
import { observer } from 'mobx-react';
|
||||
import { IConfigure } from 'types/base-type';
|
||||
import { users } from 'store/users';
|
||||
import { pagination } from 'constants/table';
|
||||
import { getConfigureColumns } from './config';
|
||||
import { showConfigureModal } from 'container/modal/admin';
|
||||
|
||||
@observer
|
||||
export class ConfigureManagement extends SearchAndFilterContainer {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
filterRole: '',
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
admin.getConfigure();
|
||||
}
|
||||
|
||||
public getData<T extends IConfigure>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: IConfigure) =>
|
||||
((item.configKey !== undefined && item.configKey !== null) && item.configKey.toLowerCase().includes(searchKey as string))
|
||||
|| ((item.configValue !== undefined && item.configValue !== null) && item.configValue.toLowerCase().includes(searchKey as string))
|
||||
|| ((item.configDescription !== undefined && item.configDescription !== null) &&
|
||||
item.configDescription.toLowerCase().includes(searchKey as string)),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public renderTable() {
|
||||
return (
|
||||
<Spin spinning={users.loading}>
|
||||
<Table
|
||||
rowKey="key"
|
||||
columns={getConfigureColumns()}
|
||||
dataSource={this.getData(admin.configureList)}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</Spin>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
public renderOperationPanel() {
|
||||
return (
|
||||
<ul>
|
||||
{this.renderSearch('', '请输入配置键、值或描述')}
|
||||
<li className="right-btn-1">
|
||||
<Button type="primary" onClick={() => showConfigureModal()}>增加配置</Button>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="table-operation-panel">
|
||||
{this.renderOperationPanel()}
|
||||
</div>
|
||||
<div className="table-wrapper">
|
||||
{this.renderTable()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
210
kafka-manager-console/src/container/admin/data-curve/config.ts
Normal file
210
kafka-manager-console/src/container/admin/data-curve/config.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { EChartOption } from 'echarts/lib/echarts';
|
||||
import moment from 'moment';
|
||||
import { ICurve } from 'container/common-curve/config';
|
||||
import { adminMonitor } from 'store/admin-monitor';
|
||||
import { parseBrokerMetricOption } from './parser';
|
||||
|
||||
export interface IPeriod {
|
||||
label: string;
|
||||
key: string;
|
||||
dateRange: [moment.Moment, moment.Moment];
|
||||
}
|
||||
|
||||
export const getMoment = () => {
|
||||
return moment();
|
||||
};
|
||||
export const baseColors = ['#F28E61', '#7082A6', '#5AD2A2', '#E96A72', '#59AEE9', '#65A8BF', '#9D7ECF'];
|
||||
|
||||
export enum curveKeys {
|
||||
'byteIn/byteOut' = 'byteIn/byteOut',
|
||||
bytesRejectedPerSec = 'bytesRejectedPerSec',
|
||||
failFetchRequestPerSec = 'failFetchRequestPerSec',
|
||||
failProduceRequestPerSec = 'failProduceRequestPerSec',
|
||||
fetchConsumerRequestPerSec = 'fetchConsumerRequestPerSec',
|
||||
healthScore = 'healthScore',
|
||||
messagesInPerSec = 'messagesInPerSec',
|
||||
networkProcessorIdlPercent = 'networkProcessorIdlPercent',
|
||||
produceRequestPerSec = 'produceRequestPerSec',
|
||||
requestHandlerIdlPercent = 'requestHandlerIdlPercent',
|
||||
requestQueueSize = 'requestQueueSize',
|
||||
responseQueueSize = 'responseQueueSize',
|
||||
totalTimeFetchConsumer99Th = 'totalTimeFetchConsumer99Th',
|
||||
totalTimeProduce99Th = 'totalTimeProduce99Th',
|
||||
}
|
||||
|
||||
export const byteCurves: ICurve[] = [
|
||||
{
|
||||
title: 'byteIn/byteOut',
|
||||
path: curveKeys['byteIn/byteOut'],
|
||||
api: adminMonitor.getBrokersChartsData,
|
||||
colors: baseColors,
|
||||
}, {
|
||||
title: 'bytesRejectedPerSec',
|
||||
path: curveKeys.bytesRejectedPerSec,
|
||||
api: adminMonitor.getBrokersChartsData,
|
||||
colors: ['#E96A72'],
|
||||
},
|
||||
];
|
||||
|
||||
export const perSecCurves: ICurve[] = [
|
||||
{
|
||||
title: 'failFetchRequestPerSec',
|
||||
path: curveKeys.failFetchRequestPerSec,
|
||||
api: adminMonitor.getBrokersChartsData,
|
||||
colors: baseColors,
|
||||
}, {
|
||||
title: 'failProduceRequestPerSec',
|
||||
path: curveKeys.failProduceRequestPerSec,
|
||||
api: adminMonitor.getBrokersChartsData,
|
||||
colors: baseColors,
|
||||
}, {
|
||||
title: 'fetchConsumerRequestPerSec',
|
||||
path: curveKeys.fetchConsumerRequestPerSec,
|
||||
api: adminMonitor.getBrokersChartsData,
|
||||
colors: baseColors,
|
||||
}, {
|
||||
title: 'produceRequestPerSec',
|
||||
path: curveKeys.produceRequestPerSec,
|
||||
api: adminMonitor.getBrokersChartsData,
|
||||
colors: baseColors,
|
||||
},
|
||||
];
|
||||
|
||||
export const otherCurves: ICurve[] = [
|
||||
{
|
||||
title: 'healthScore',
|
||||
path: curveKeys.healthScore,
|
||||
api: adminMonitor.getBrokersChartsData,
|
||||
colors: baseColors,
|
||||
}, {
|
||||
title: 'messagesInPerSec',
|
||||
path: curveKeys.messagesInPerSec,
|
||||
api: adminMonitor.getBrokersChartsData,
|
||||
colors: baseColors,
|
||||
}, {
|
||||
title: 'networkProcessorIdlPercent',
|
||||
path: curveKeys.networkProcessorIdlPercent,
|
||||
api: adminMonitor.getBrokersChartsData,
|
||||
colors: baseColors,
|
||||
}, {
|
||||
title: 'requestHandlerIdlPercent',
|
||||
path: curveKeys.requestHandlerIdlPercent,
|
||||
api: adminMonitor.getBrokersChartsData,
|
||||
colors: baseColors,
|
||||
}, {
|
||||
title: 'requestQueueSize',
|
||||
path: curveKeys.requestQueueSize,
|
||||
api: adminMonitor.getBrokersChartsData,
|
||||
colors: baseColors,
|
||||
}, {
|
||||
title: 'responseQueueSize',
|
||||
path: curveKeys.responseQueueSize,
|
||||
api: adminMonitor.getBrokersChartsData,
|
||||
colors: baseColors,
|
||||
}, {
|
||||
title: 'totalTimeFetchConsumer99Th',
|
||||
path: curveKeys.totalTimeFetchConsumer99Th,
|
||||
api: adminMonitor.getBrokersChartsData,
|
||||
colors: baseColors,
|
||||
}, {
|
||||
title: 'totalTimeProduce99Th',
|
||||
path: curveKeys.totalTimeProduce99Th,
|
||||
api: adminMonitor.getBrokersChartsData,
|
||||
colors: baseColors,
|
||||
},
|
||||
];
|
||||
|
||||
export enum curveType {
|
||||
byteCurves = 'byteCurves',
|
||||
perSecCurves = 'perSecCurves',
|
||||
other = 'other',
|
||||
}
|
||||
|
||||
export interface ICurveType {
|
||||
type: curveType;
|
||||
title: string;
|
||||
curves: ICurve[];
|
||||
parser: (option: ICurve, data: any[]) => EChartOption;
|
||||
}
|
||||
|
||||
export const byteTypeCurves: ICurveType[] = [
|
||||
{
|
||||
type: curveType.byteCurves,
|
||||
title: 'byte',
|
||||
curves: byteCurves.concat(perSecCurves, otherCurves),
|
||||
parser: parseBrokerMetricOption,
|
||||
},
|
||||
];
|
||||
export const perSecTypeCurves: ICurveType[] = [
|
||||
{
|
||||
type: curveType.perSecCurves,
|
||||
title: 'perSec',
|
||||
curves: perSecCurves,
|
||||
parser: parseBrokerMetricOption,
|
||||
},
|
||||
];
|
||||
export const otherTypeCurves: ICurveType[] = [
|
||||
{
|
||||
type: curveType.other,
|
||||
title: 'other',
|
||||
curves: otherCurves,
|
||||
parser: parseBrokerMetricOption,
|
||||
},
|
||||
];
|
||||
|
||||
export const allCurves: ICurveType[] = [].concat(byteTypeCurves);
|
||||
|
||||
const curveKeyMap = new Map<string, {typeInfo: ICurveType, curveInfo: ICurve}>();
|
||||
allCurves.forEach(t => {
|
||||
t.curves.forEach(c => {
|
||||
curveKeyMap.set(c.path, {
|
||||
typeInfo: t,
|
||||
curveInfo: c,
|
||||
});
|
||||
});
|
||||
});
|
||||
export const CURVE_KEY_MAP = curveKeyMap;
|
||||
|
||||
export const PERIOD_RADIO = [
|
||||
{
|
||||
label: '10分钟',
|
||||
key: 'tenMin',
|
||||
get dateRange() {
|
||||
return [getMoment().subtract(10, 'minute'), getMoment()];
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '1小时',
|
||||
key: 'oneHour',
|
||||
get dateRange() {
|
||||
return [getMoment().subtract(1, 'hour'), getMoment()];
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '6小时',
|
||||
key: 'sixHour',
|
||||
get dateRange() {
|
||||
return [getMoment().subtract(6, 'hour'), getMoment()];
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '近1天',
|
||||
key: 'oneDay',
|
||||
get dateRange() {
|
||||
return [getMoment().subtract(1, 'day'), getMoment()];
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '近1周',
|
||||
key: 'oneWeek',
|
||||
get dateRange() {
|
||||
return [getMoment().subtract(7, 'day'), getMoment()];
|
||||
},
|
||||
},
|
||||
] as IPeriod[];
|
||||
|
||||
const periodRadioMap = new Map<string, IPeriod>();
|
||||
PERIOD_RADIO.forEach(p => {
|
||||
periodRadioMap.set(p.key, p);
|
||||
});
|
||||
export const PERIOD_RADIO_MAP = periodRadioMap;
|
||||
@@ -0,0 +1,14 @@
|
||||
.curve-wrapper {
|
||||
background-color: #fff;
|
||||
padding: 24px;
|
||||
position: relative;
|
||||
}
|
||||
.right-btn {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
top: 24px;
|
||||
}
|
||||
.operator-select {
|
||||
position: absolute;
|
||||
top: 34px;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import './index.less';
|
||||
import { Radio, DatePicker, RadioChangeEvent, RangePickerValue, Button, Icon } from 'component/antd';
|
||||
import { curveInfo } from 'store/curve-info';
|
||||
import { curveKeys, CURVE_KEY_MAP, PERIOD_RADIO_MAP, PERIOD_RADIO } from './config';
|
||||
import moment = require('moment');
|
||||
import { observer } from 'mobx-react';
|
||||
import { timeStampStr } from 'constants/strategy';
|
||||
|
||||
@observer
|
||||
export class DataCurveFilter extends React.Component {
|
||||
public handleRangeChange = (dates: RangePickerValue, dateStrings: [string, string]) => {
|
||||
curveInfo.setTimeRange(dates as [moment.Moment, moment.Moment]);
|
||||
this.refreshAll();
|
||||
}
|
||||
|
||||
public radioChange = (e: RadioChangeEvent) => {
|
||||
const { value } = e.target;
|
||||
curveInfo.setTimeRange(PERIOD_RADIO_MAP.get(value).dateRange);
|
||||
this.refreshAll();
|
||||
}
|
||||
|
||||
public refreshAll = () => {
|
||||
Object.keys(curveKeys).forEach((c: curveKeys) => {
|
||||
const { typeInfo, curveInfo: option } = CURVE_KEY_MAP.get(c);
|
||||
const { parser } = typeInfo;
|
||||
curveInfo.getCommonCurveData(option, parser, true);
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<Radio.Group onChange={this.radioChange} defaultValue={curveInfo.periodKey}>
|
||||
{PERIOD_RADIO.map(p => <Radio.Button key={p.key} value={p.key}>{p.label}</Radio.Button>)}
|
||||
</Radio.Group>
|
||||
<DatePicker.RangePicker
|
||||
format={timeStampStr}
|
||||
onChange={this.handleRangeChange}
|
||||
className="ml-10"
|
||||
value={curveInfo.timeRange}
|
||||
/>
|
||||
<div className="right-btn">
|
||||
<Button onClick={this.refreshAll}><Icon className="dsui-icon-shuaxin1 mr-4" type="reload" />刷新</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
147
kafka-manager-console/src/container/admin/data-curve/parser.ts
Normal file
147
kafka-manager-console/src/container/admin/data-curve/parser.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import moment from 'moment';
|
||||
import { EChartOption } from 'echarts';
|
||||
import { ICurve, ILineData, baseLineLegend, baseLineGrid, baseAxisStyle, noAxis, UNIT_HEIGHT } from 'container/common-curve/config';
|
||||
import { IClusterMetrics, ISeriesOption } from 'types/base-type';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
import { getFilterSeries } from 'lib/chart-utils';
|
||||
import { dealFlowData } from 'lib/chart-utils';
|
||||
|
||||
export const getBaseOptions = (option: ICurve, data: ILineData[]) => {
|
||||
const date = (data || []).map(i => moment(i.timeStamp).format(timeFormat));
|
||||
|
||||
return {
|
||||
animationDuration: 200,
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: {},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
...baseLineLegend,
|
||||
bottom: '0',
|
||||
align: 'auto',
|
||||
},
|
||||
grid: {
|
||||
...baseLineGrid,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: date,
|
||||
...baseAxisStyle,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
...baseAxisStyle,
|
||||
...noAxis,
|
||||
name: option.unit || '',
|
||||
nameTextStyle: {
|
||||
lineHeight: UNIT_HEIGHT,
|
||||
},
|
||||
},
|
||||
series: [{
|
||||
type: 'line',
|
||||
data: data.map(i => {
|
||||
return Number(i.value);
|
||||
}),
|
||||
}],
|
||||
} as EChartOption;
|
||||
};
|
||||
|
||||
export const parseLine = (option: ICurve, data: ILineData[]): EChartOption => {
|
||||
return Object.assign({}, getBaseOptions(option, data), {
|
||||
legend: {
|
||||
...baseLineLegend,
|
||||
bottom: '0',
|
||||
align: 'auto',
|
||||
},
|
||||
}) as EChartOption;
|
||||
};
|
||||
|
||||
export const parseBrokerMetricOption = (option: ICurve, data: IClusterMetrics[]): EChartOption => {
|
||||
let name;
|
||||
let series: ISeriesOption[];
|
||||
data = data || [];
|
||||
data = data.map(item => {
|
||||
return {
|
||||
time: moment(item.gmtCreate).format(timeFormat),
|
||||
...item,
|
||||
};
|
||||
});
|
||||
data = data.sort((a, b) => a.gmtCreate - b.gmtCreate);
|
||||
const legend = option.path === 'byteIn/byteOut' ? ['bytesInPerSec', 'bytesOutPerSec'] : [option.path];
|
||||
series = Array.from(legend, (item: string) => ({
|
||||
name: item,
|
||||
id: item,
|
||||
type: 'line',
|
||||
symbol: 'circle',
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
encode: {
|
||||
x: 'time',
|
||||
y: item,
|
||||
tooltip: [
|
||||
item,
|
||||
],
|
||||
},
|
||||
data: data.map(row => row[item] !== null ? Number(row[item]) : null),
|
||||
}));
|
||||
|
||||
const filterSeries = getFilterSeries(series);
|
||||
const { name: unitName, data: xData } = dealFlowData(legend, data);
|
||||
name = unitName;
|
||||
data = xData;
|
||||
|
||||
return {
|
||||
animationDuration: 200,
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: {},
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
...baseLineGrid,
|
||||
},
|
||||
xAxis: {
|
||||
splitLine: null,
|
||||
type: 'time',
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
...baseAxisStyle,
|
||||
...noAxis,
|
||||
name,
|
||||
nameTextStyle: {
|
||||
lineHeight: UNIT_HEIGHT,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: legend,
|
||||
...baseLineLegend,
|
||||
bottom: '0',
|
||||
align: 'auto',
|
||||
},
|
||||
dataset: {
|
||||
source: data,
|
||||
},
|
||||
series: filterSeries,
|
||||
};
|
||||
};
|
||||
|
||||
export function isM(arr: number[]) {
|
||||
const filterData = arr.filter(i => i !== 0);
|
||||
if (filterData.length) return filterData.reduce((cur, pre) => cur + pre) / filterData.length >= 1000000;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isK(arr: number[]) {
|
||||
const filterData = arr.filter(i => i !== 0);
|
||||
if (filterData.length) return filterData.reduce((cur, pre) => cur + pre) / filterData.length >= 1000;
|
||||
return false;
|
||||
}
|
||||
13
kafka-manager-console/src/container/admin/index.tsx
Normal file
13
kafka-manager-console/src/container/admin/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
export * from './cluster-list';
|
||||
export * from './cluster-detail';
|
||||
export * from './broker-detail';
|
||||
export * from './user-management';
|
||||
export * from './version-management';
|
||||
export * from './operation-management';
|
||||
export * from './operation-detail';
|
||||
export * from './bill-management';
|
||||
export * from './admin-app-list';
|
||||
export * from './operation-management/migration-detail';
|
||||
export * from './configure-management';
|
||||
export * from './individual-bill';
|
||||
export * from './bill-detail';
|
||||
159
kafka-manager-console/src/container/admin/individual-bill.tsx
Normal file
159
kafka-manager-console/src/container/admin/individual-bill.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import * as React from 'react';
|
||||
import { Table, Tabs, DatePicker, notification, Icon } from 'component/antd';
|
||||
import { pagination } from 'constants/table';
|
||||
import moment, { Moment } from 'moment';
|
||||
import { BarChartComponet } from 'component/chart';
|
||||
import { observer } from 'mobx-react';
|
||||
import { getBillListColumns } from '../user-center/config';
|
||||
import { urlPrefix } from 'constants/left-menu';
|
||||
import { timeMonthStr } from 'constants/strategy';
|
||||
import { users } from 'store/users';
|
||||
import { admin } from 'store/admin';
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
@observer
|
||||
export class IndividualBill extends React.Component {
|
||||
public username: 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;
|
||||
|
||||
public getData() {
|
||||
const { startTime, endTime } = this;
|
||||
return admin.getBillStaffList(this.username, startTime, endTime);
|
||||
}
|
||||
|
||||
public handleDownLoad() {
|
||||
const tableData = admin.billStaff.map(item => {
|
||||
return {
|
||||
// tslint:disable
|
||||
'月份': item.gmtMonth,
|
||||
'Topic数量': item.topicNum,
|
||||
'quota数量': item.quota,
|
||||
'金额': item.cost,
|
||||
};
|
||||
});
|
||||
const data = [].concat(tableData);
|
||||
const wb = XLSX.utils.book_new();
|
||||
// json转sheet
|
||||
const ws = XLSX.utils.json_to_sheet(data, {
|
||||
header: ['月份', 'Topic数量', 'quota数量', '金额'],
|
||||
});
|
||||
// XLSX.utils.
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'bill');
|
||||
// 输出
|
||||
XLSX.writeFile(wb, 'bill' + '.xlsx');
|
||||
}
|
||||
|
||||
public disabledDateTime = (current: Moment) => {
|
||||
return current && current > moment().endOf('day');
|
||||
}
|
||||
|
||||
public handleChartSearch = (date: moment.Moment[]) => {
|
||||
this.setState({
|
||||
value: date,
|
||||
mode: ['month', 'month'] as any,
|
||||
});
|
||||
|
||||
this.startTime = date[0].valueOf();
|
||||
this.endTime = date[1].valueOf();
|
||||
|
||||
if (this.startTime >= this.endTime) {
|
||||
return notification.error({ message: '开始时间不能大于或等于结束时间' });
|
||||
}
|
||||
this.getData();
|
||||
this.handleRefreshChart();
|
||||
}
|
||||
|
||||
public handleRefreshChart = () => {
|
||||
this.chart.handleRefreshChart();
|
||||
}
|
||||
|
||||
public renderTableList() {
|
||||
const adminUrl=`${urlPrefix}/admin/bill-detail`
|
||||
return (
|
||||
<Table
|
||||
rowKey="key"
|
||||
columns={getBillListColumns(adminUrl)}
|
||||
dataSource={admin.billStaff}
|
||||
pagination={pagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public renderChart() {
|
||||
return (
|
||||
<div className="chart-box">
|
||||
<BarChartComponet ref={(ref) => this.chart = ref } getChartData={this.getData.bind(this, null)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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')],
|
||||
}}
|
||||
defaultValue={[moment(new Date()).subtract(6, 'months'), moment()]}
|
||||
value={value}
|
||||
mode={mode}
|
||||
format={timeMonthStr}
|
||||
onChange={this.handleChartSearch}
|
||||
onPanelChange={this.handleChartSearch}
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<Icon type="download" onClick={this.handleDownLoad.bind(this, null)} />
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
this.username = users.currentUser.username;
|
||||
return (
|
||||
<>
|
||||
<div className="container">
|
||||
<Tabs defaultActiveKey="1" type="card">
|
||||
<TabPane
|
||||
tab={<>
|
||||
<span>账单趋势</span>
|
||||
<a
|
||||
// tslint:disable-next-line:max-line-length
|
||||
href="https://github.com/didi/kafka-manager"
|
||||
target="_blank"
|
||||
>
|
||||
<Icon type="question-circle" />
|
||||
</a>
|
||||
</>}
|
||||
key="1"
|
||||
>
|
||||
{this.renderDatePick()}
|
||||
{this.username ? this.renderChart() : null}
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
<div className="table-content">
|
||||
{this.renderTableList()}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import * as React from 'react';
|
||||
import { ILabelValue, ITasksMetaData } from 'types/base-type';
|
||||
import { Descriptions } from 'antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
import { urlPrefix } from 'constants/left-menu';
|
||||
import { Table } from 'component/antd';
|
||||
import { pagination } from 'constants/table';
|
||||
import moment from 'moment';
|
||||
|
||||
interface IEassProps {
|
||||
tasksMetaData?: ITasksMetaData;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class EassentialInfo extends React.Component<IEassProps> {
|
||||
public render() {
|
||||
const { tasksMetaData } = this.props;
|
||||
let tasks = {} as ITasksMetaData;
|
||||
tasks = tasksMetaData ? tasksMetaData : tasks;
|
||||
const gmtCreate = moment(tasks.gmtCreate).format(timeFormat);
|
||||
const options = [{
|
||||
value: tasks.taskId,
|
||||
label: '任务ID',
|
||||
}, {
|
||||
value: tasks.clusterId,
|
||||
label: '集群ID',
|
||||
}, {
|
||||
value: tasks.clusterName,
|
||||
label: '集群名称',
|
||||
}, {
|
||||
value: gmtCreate,
|
||||
label: '创建时间',
|
||||
}, {
|
||||
value: tasks.kafkaPackageName,
|
||||
label: 'kafka包',
|
||||
}, {
|
||||
value: tasks.kafkaPackageMd5,
|
||||
label: 'kafka包 MD5',
|
||||
}, {
|
||||
value: tasks.operator,
|
||||
label: '操作人',
|
||||
}];
|
||||
const optionsHost = [{
|
||||
value: tasks.hostList,
|
||||
label: '升级主机列表',
|
||||
}, {
|
||||
value: tasks.pauseHostList,
|
||||
label: '升级的主机暂停点',
|
||||
}];
|
||||
return(
|
||||
<>
|
||||
<Descriptions column={3}>
|
||||
{options.map((item: ILabelValue, index) => (
|
||||
<Descriptions.Item key={item.label || index} label={item.label}>{item.value}</Descriptions.Item>
|
||||
))}
|
||||
<Descriptions.Item key="server" label="server配置名">
|
||||
<a href={`${urlPrefix}/info?fileId=${tasks.serverPropertiesFileId || ''}`} target="_blank">{tasks.serverPropertiesName}</a>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item key="server" label="server配置 MD5">{tasks.serverPropertiesMd5}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Descriptions column={1}>
|
||||
{optionsHost.map((item: any, index) => (
|
||||
<Descriptions.Item key={item.label || index} label="">
|
||||
<Table
|
||||
columns={[{
|
||||
title: item.label,
|
||||
dataIndex: '',
|
||||
key: '',
|
||||
}]}
|
||||
dataSource={item.value}
|
||||
pagination={pagination}
|
||||
rowKey="key"
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
))}
|
||||
</Descriptions>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
.task-status li{
|
||||
margin-left: -10px;
|
||||
font-size: 12px;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
.complete{
|
||||
color: #76a8ca;
|
||||
}
|
||||
|
||||
.executing {
|
||||
color: #66c84c;
|
||||
}
|
||||
|
||||
.pending {
|
||||
color: #de9845;
|
||||
}
|
||||
|
||||
.modal-body{
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.btn-position{
|
||||
margin-right: 10px;
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Tabs, PageHeader, Button, notification, Popconfirm, Spin } from 'antd';
|
||||
import { handleTabKey } from 'lib/utils';
|
||||
import { EassentialInfo } from './essential-info';
|
||||
import { TaskStatusDetails } from './taskStatus-details';
|
||||
import { ITasksMetaData, ITrigger } from 'types/base-type';
|
||||
import { triggerClusterTask } from 'lib/api';
|
||||
import { handlePageBack } from 'lib/utils';
|
||||
import { admin } from 'store/admin';
|
||||
import Url from 'lib/url-parser';
|
||||
import './index.less';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
let showStatus: boolean = false;
|
||||
let showContinue: boolean = false;
|
||||
|
||||
@observer
|
||||
export class OperationDetail extends React.Component {
|
||||
public taskId: number;
|
||||
public taskName: string;
|
||||
|
||||
public state = {
|
||||
showContinue: false,
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.taskId = Number(url.search.taskId);
|
||||
}
|
||||
|
||||
public bindClick() {
|
||||
const type = showContinue ? 'start' : 'pause';
|
||||
const params = {
|
||||
taskId: this.taskId,
|
||||
action: type,
|
||||
hostname: '',
|
||||
} as ITrigger;
|
||||
triggerClusterTask(params).then(data => {
|
||||
admin.getSubtasksStatus(this.taskId);
|
||||
notification.success({ message: `${showContinue ? '继续部署' : '暂停'}成功!` });
|
||||
});
|
||||
}
|
||||
|
||||
public callBackOrCancel(type: string) {
|
||||
const params = {
|
||||
taskId: this.taskId,
|
||||
action: type,
|
||||
hostname: '',
|
||||
} as ITrigger;
|
||||
triggerClusterTask(params).then(data => {
|
||||
admin.getSubtasksStatus(this.taskId);
|
||||
notification.success({ message: `${type === 'rollback' ? '回滚任务' : '取消'}成功` });
|
||||
});
|
||||
}
|
||||
public handleVal(value: number) {
|
||||
showStatus = (value + '').includes('100') ? true : false;
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
admin.getTasksMetadata(this.taskId);
|
||||
admin.getSubtasksStatus(this.taskId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
// 任务状态: 30:运行中(展示暂停), 40:暂停(展示开始), 100:完成(都置灰)
|
||||
showStatus = admin.taskStatusDetails && admin.taskStatusDetails.status === 100 ? true : false;
|
||||
showContinue = admin.taskStatusDetails && admin.taskStatusDetails.status === 40 ? true : false;
|
||||
const showRollBack = admin.taskStatusDetails && admin.taskStatusDetails.rollback;
|
||||
let tasks = {} as ITasksMetaData;
|
||||
tasks = admin.tasksMetaData ? admin.tasksMetaData : tasks;
|
||||
return (
|
||||
<>
|
||||
<Spin spinning={admin.loading}>
|
||||
<PageHeader
|
||||
className="detail hotspot-header"
|
||||
onBack={() => handlePageBack('/admin/operation#1')}
|
||||
title={`任务名称${tasks.taskName ? '/' + tasks.taskName : ''}`}
|
||||
extra={[
|
||||
<Button key="1" type="primary" disabled={showStatus} >
|
||||
<Popconfirm
|
||||
title={`确定${showContinue ? '开始' : '暂停'}?`}
|
||||
onConfirm={() => this.bindClick()}
|
||||
>
|
||||
<a>{showContinue ? '开始' : '暂停'}</a>
|
||||
</Popconfirm>
|
||||
</Button>,
|
||||
<Button
|
||||
key="2"
|
||||
type="primary"
|
||||
disabled={showRollBack || showStatus}
|
||||
>
|
||||
<Popconfirm
|
||||
title={`确定回滚?`}
|
||||
onConfirm={() => this.callBackOrCancel('rollback')}
|
||||
>
|
||||
<a>回滚</a>
|
||||
</Popconfirm>
|
||||
</Button>,
|
||||
<Button
|
||||
key="3"
|
||||
type="primary"
|
||||
disabled={showStatus}
|
||||
>
|
||||
<Popconfirm
|
||||
title={`确定回滚?`}
|
||||
onConfirm={() => this.callBackOrCancel('cancel')}
|
||||
>
|
||||
<a>取消</a>
|
||||
</Popconfirm>
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Tabs activeKey={location.hash.substr(1) || '1'} type="card" onChange={handleTabKey}>
|
||||
<TabPane tab="基本信息" key="1">
|
||||
<EassentialInfo tasksMetaData={tasks} />
|
||||
</TabPane>
|
||||
<TabPane tab="任务状态详情" key="2">
|
||||
<TaskStatusDetails handleVal={(value: number) => this.handleVal(value)} />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</PageHeader>
|
||||
</Spin>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import * as React from 'react';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { pagination } from 'constants/table';
|
||||
import { admin } from 'store/admin';
|
||||
import { Table, Popconfirm, notification } from 'component/antd';
|
||||
import { IEnumsMap, ITaskStatusDetails, ISubtasksStatus, ITrigger, IXFormWrapper } from 'types/base-type';
|
||||
import { tableFilter } from 'lib/utils';
|
||||
import { triggerClusterTask } from 'lib/api';
|
||||
import { wrapper } from 'store';
|
||||
import { observer } from 'mobx-react';
|
||||
import Url from 'lib/url-parser';
|
||||
import './index.less';
|
||||
|
||||
let taskStatus = [] as IEnumsMap[];
|
||||
let task = {} as ITaskStatusDetails;
|
||||
let subTaskStatusList = [] as ISubtasksStatus[];
|
||||
let statusNum: number;
|
||||
|
||||
@observer
|
||||
export class TaskStatusDetails extends SearchAndFilterContainer {
|
||||
public taskId: number;
|
||||
public timer = null as any;
|
||||
|
||||
public state = {
|
||||
searchKey: '',
|
||||
filterGroupVisible: false,
|
||||
filterStatusVisible: false,
|
||||
};
|
||||
|
||||
private xFormWrapper: IXFormWrapper;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.taskId = Number(url.search.taskId);
|
||||
}
|
||||
|
||||
public renderDataMigrationTasks(subTaskStatusList: ISubtasksStatus[]) {
|
||||
const groupId = Object.assign({
|
||||
title: '分组ID',
|
||||
dataIndex: 'groupId',
|
||||
key: 'groupId',
|
||||
width: '20%',
|
||||
filters: tableFilter<any>(subTaskStatusList, 'groupId'),
|
||||
onFilter: (value: number, record: ISubtasksStatus) => record.groupId === value,
|
||||
}, this.renderColumnsFilter('filterGroupVisible'));
|
||||
const status = Object.assign({
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: '20%',
|
||||
filters: taskStatus.map(ele => ({ text: ele.message, value: ele.code + '' })),
|
||||
onFilter: (value: number, record: ISubtasksStatus) => record.status === +value,
|
||||
render: (t: number) => {
|
||||
let messgae: string;
|
||||
taskStatus.map(ele => {
|
||||
if (ele.code === t) {
|
||||
messgae = ele.message;
|
||||
}
|
||||
});
|
||||
return(
|
||||
<span className={t === 102 ? 'fail' : t === 101 ? 'succee' : ''}>{messgae}</span>
|
||||
);
|
||||
},
|
||||
}, this.renderColumnsFilter('filterStatusVisible'));
|
||||
const columns = [{
|
||||
title: '主机名',
|
||||
dataIndex: 'hostname',
|
||||
key: 'hostname',
|
||||
width: '20%',
|
||||
sorter: (a: ISubtasksStatus, b: ISubtasksStatus) => a.hostname.charCodeAt(0) - b.hostname.charCodeAt(0),
|
||||
}, {
|
||||
title: '机器角色',
|
||||
dataIndex: 'kafkaRoles',
|
||||
key: 'kafkaRoles',
|
||||
width: '20%',
|
||||
},
|
||||
groupId,
|
||||
status,
|
||||
{
|
||||
title: '操作',
|
||||
width: '20%',
|
||||
render: (value: any, record: ISubtasksStatus) => {
|
||||
return (
|
||||
<>
|
||||
<span className="btn-position">
|
||||
<Popconfirm
|
||||
title={`确定忽略?`}
|
||||
onConfirm={() => this.bindClick(record, 'ignore')}
|
||||
>
|
||||
<a>忽略</a>
|
||||
</Popconfirm>
|
||||
</span>
|
||||
<a onClick={() => this.handleViewLog(record)}>查看日志</a>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={subTaskStatusList}
|
||||
pagination={pagination}
|
||||
rowKey="hostname"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public handleViewLog = async (record: ISubtasksStatus) => {
|
||||
await admin.getClusterTaskLog(this.taskId, record.hostname);
|
||||
this.xFormWrapper = {
|
||||
type: 'drawer',
|
||||
visible: true,
|
||||
width: 600,
|
||||
title: '查看日志',
|
||||
customRenderElement: this.showLog(),
|
||||
nofooter: true,
|
||||
noform: true,
|
||||
onSubmit: (value: any) => {
|
||||
// TODO:
|
||||
},
|
||||
};
|
||||
await wrapper.open(this.xFormWrapper);
|
||||
}
|
||||
|
||||
public showLog() {
|
||||
return (
|
||||
<>
|
||||
<div className="config-info">
|
||||
{admin.clusterTaskLog}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public bindClick(record: ISubtasksStatus, type: string) {
|
||||
const params = {
|
||||
taskId: this.taskId,
|
||||
action: type,
|
||||
hostname: record.hostname,
|
||||
} as ITrigger;
|
||||
triggerClusterTask(params).then(data => {
|
||||
admin.getSubtasksStatus(this.taskId);
|
||||
notification.success({ message: `${type === 'cancel' ? '取消' : '忽略'}成功!` });
|
||||
});
|
||||
}
|
||||
|
||||
public iTimer = () => {
|
||||
this.timer = setInterval(() => {
|
||||
admin.getSubtasksStatus(this.taskId);
|
||||
}, 3 * 1 * 1000);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
setTimeout(this.iTimer, 0);
|
||||
admin.getConfigsTaskStatus();
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
|
||||
public getData<T extends ISubtasksStatus>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: ISubtasksStatus) =>
|
||||
(item.hostname !== undefined && item.hostname !== null) && item.hostname.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public render() {
|
||||
let message = '';
|
||||
if (admin.taskStatusDetails) {
|
||||
task = admin.taskStatusDetails;
|
||||
subTaskStatusList = admin.taskStatusDetails.subTaskStatusList;
|
||||
statusNum = admin.taskStatusDetails.status;
|
||||
this.props.handleVal(statusNum);
|
||||
}
|
||||
taskStatus = admin.configsTaskStatus ? admin.configsTaskStatus : [] as IEnumsMap[];
|
||||
taskStatus.forEach(ele => {
|
||||
if (ele.code === task.status) {
|
||||
message = ele.message;
|
||||
}
|
||||
});
|
||||
return(
|
||||
<>
|
||||
<div className="k-row" >
|
||||
<ul className="k-tab task-status">
|
||||
<li>
|
||||
状态:<span className="complete">{message}</span>
|
||||
| 总数:<span className="complete">{task.sumCount}</span>
|
||||
| 成功:<span className="success">{task.successCount}</span>
|
||||
| 失败:<span className="fail">{task.failedCount}</span>
|
||||
| 执行中:<span className="executing">{task.runningCount}</span>
|
||||
| 待执行:<span className="pending">{task.waitingCount}</span>
|
||||
</li>
|
||||
{this.renderSearch('', '请输入主机名')}
|
||||
</ul>
|
||||
{this.renderDataMigrationTasks(this.getData(subTaskStatusList))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Table, Tabs, Button } from 'component/antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import { ITaskManage, IEnumsMap, ITasksEnums } from 'types/base-type';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { tableFilter } from 'lib/utils';
|
||||
import { pagination } from 'constants/table';
|
||||
import { addMigrationTask } from 'container/modal';
|
||||
import { admin } from 'store/admin';
|
||||
import moment from 'moment';
|
||||
import './index.less';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
import { region } from 'store';
|
||||
|
||||
@observer
|
||||
export class ClusterTask extends SearchAndFilterContainer {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
filterClusterVisible: false,
|
||||
filterStatusVisible: false,
|
||||
filterTaskVisible: false,
|
||||
};
|
||||
|
||||
public renderColumns = (data: ITaskManage[]) => {
|
||||
const taskStatus = admin.configsTaskStatus ? admin.configsTaskStatus : [] as IEnumsMap[];
|
||||
const cluster = Object.assign({
|
||||
title: '集群名称',
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
width: '15%',
|
||||
filters: tableFilter<any>(data, 'clusterName'),
|
||||
onFilter: (value: string, record: ITaskManage) => record.clusterName.indexOf(value) === 0,
|
||||
}, this.renderColumnsFilter('filterClusterVisible'));
|
||||
|
||||
const status = Object.assign({
|
||||
title: '任务状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: '15%',
|
||||
filters: taskStatus.map(ele => ({ text: ele.message, value: ele.code + '' })),
|
||||
onFilter: (value: number, record: ITaskManage) => record.status === +value,
|
||||
render: (t: number) => {
|
||||
let messgae: string;
|
||||
taskStatus.map(ele => {
|
||||
if (ele.code === t) {
|
||||
messgae = ele.message;
|
||||
}
|
||||
});
|
||||
return (
|
||||
<span>{messgae}</span>
|
||||
);
|
||||
},
|
||||
}, this.renderColumnsFilter('filterStatusVisible'));
|
||||
|
||||
const taskType = Object.assign({
|
||||
title: '任务类型',
|
||||
dataIndex: 'taskType',
|
||||
key: 'taskType',
|
||||
width: '15%',
|
||||
filters: admin.tasksEnums && admin.tasksEnums.map(ele => ({ text: ele.message, value: ele.name })),
|
||||
onFilter: (value: string, record: ITaskManage) => record.taskType === value,
|
||||
render: (text: string) => {
|
||||
const task = admin.tasksEnums && admin.tasksEnums.filter(ele => ele.name === text);
|
||||
return (<>{task && task[0].message}</>);
|
||||
},
|
||||
}, this.renderColumnsFilter('filterTaskVisible'));
|
||||
|
||||
return [
|
||||
{
|
||||
title: '任务ID',
|
||||
dataIndex: 'taskId',
|
||||
key: 'taskId',
|
||||
width: '15%',
|
||||
sorter: (a: ITaskManage, b: ITaskManage) => b.taskId - a.taskId,
|
||||
},
|
||||
taskType,
|
||||
cluster,
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'gmtCreate',
|
||||
key: 'gmtCreate',
|
||||
width: '15%',
|
||||
sorter: (a: ITaskManage, b: ITaskManage) => b.gmtCreate - a.gmtCreate,
|
||||
render: (t: number) => moment(t).format(timeFormat),
|
||||
},
|
||||
{
|
||||
title: '操作人',
|
||||
dataIndex: 'operator',
|
||||
key: 'operator',
|
||||
width: '15%',
|
||||
sorter: (a: ITaskManage, b: ITaskManage) => a.operator.charCodeAt(0) - b.operator.charCodeAt(0),
|
||||
},
|
||||
status,
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
key: 'operation',
|
||||
width: '10%',
|
||||
render: (text: string, record: ITaskManage) => {
|
||||
return (
|
||||
<span className="table-operation">
|
||||
<a href={`${this.urlPrefix}/admin/operation-detail?taskId=${record.taskId}®ion=${region.currentRegion}`}>详情</a>
|
||||
<a href={`${this.urlPrefix}/admin/operation-detail?taskId=${record.taskId}®ion=${region.currentRegion}#2`}>状态</a>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public getLabelValueData(data: any[]) {
|
||||
return data.map(item => {
|
||||
return {
|
||||
label: item,
|
||||
value: item,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public getPackages() {
|
||||
admin.packageList.map(item => {
|
||||
return {
|
||||
label: item,
|
||||
value: item,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
admin.getTaskManagement();
|
||||
admin.getMetaData(false);
|
||||
admin.getClusterTasksEnums();
|
||||
admin.getConfigsTaskStatus();
|
||||
admin.getConfigsKafkaRoles();
|
||||
}
|
||||
|
||||
public renderOperationPanel() {
|
||||
return (
|
||||
<ul>
|
||||
{this.renderSearch('', '请输入任务ID')}
|
||||
<li className="right-btn-1">
|
||||
<Button type="primary" onClick={() => addMigrationTask()}>新建集群任务</Button>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { searchKey } = this.state;
|
||||
const taskManage: ITaskManage[] = admin.taskManagement && searchKey ?
|
||||
admin.taskManagement.filter((d: { taskId: number; }) => d.taskId === +searchKey) : admin.taskManagement;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="container">
|
||||
<div className="table-operation-panel">
|
||||
{this.renderOperationPanel()}
|
||||
</div>
|
||||
<div className="table-wrapper">
|
||||
<Table
|
||||
rowKey="taskId"
|
||||
columns={this.renderColumns(taskManage)}
|
||||
dataSource={taskManage}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import { IReassignTasks } from 'types/base-type';
|
||||
import { Popconfirm } from 'component/antd';
|
||||
import { urlPrefix } from 'constants/left-menu';
|
||||
import { startMigrationTask, modifyMigrationTask, cancelMigrationTask } from 'container/modal';
|
||||
import moment = require('moment');
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
|
||||
export const migrationTaskColumns = (migrationUrl: string) => {
|
||||
const columns = [{
|
||||
title: '迁移任务名称',
|
||||
dataIndex: 'taskName',
|
||||
render: (text: string, item: IReassignTasks) =>
|
||||
<a href={`${urlPrefix}/${migrationUrl}?taskId=${item.taskId}`}>{text}</a>,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'gmtCreate',
|
||||
render: (t: number) => moment(t).format(timeFormat),
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
dataIndex: 'operator',
|
||||
},
|
||||
{
|
||||
title: 'Topic数量',
|
||||
dataIndex: 'totalTopicNum',
|
||||
},
|
||||
{
|
||||
title: '进度',
|
||||
dataIndex: 'completedTopicNum',
|
||||
render: (value: number, item: IReassignTasks) => <span>{item.completedTopicNum}/{item.totalTopicNum}</span>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
render: (value: string, item: IReassignTasks) => (
|
||||
<>
|
||||
{item.status === 0 &&
|
||||
<Popconfirm
|
||||
title="确定开始?"
|
||||
onConfirm={() => startMigrationTask(item, 'start')}
|
||||
>
|
||||
<a style={{ marginRight: 16 }}>开始</a>
|
||||
</Popconfirm>}
|
||||
{[0, 1].indexOf(item.status) > -1 &&
|
||||
<a onClick={() => modifyMigrationTask(item, 'modify')} style={{ marginRight: 16 }}>修改</a>}
|
||||
{item.status === 0 &&
|
||||
<Popconfirm
|
||||
title="确定取消?"
|
||||
onConfirm={() => cancelMigrationTask(item, 'cancel')}
|
||||
><a>取消</a>
|
||||
</Popconfirm>}
|
||||
</>
|
||||
),
|
||||
}];
|
||||
return columns;
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
.hotspot-header {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.hotspot-divider {
|
||||
margin-top: 5px;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Tabs } from 'antd';
|
||||
import { handleTabKey } from 'lib/utils';
|
||||
import { ClusterTask } from './cluster-task';
|
||||
import { MigrationTask } from './migration-task';
|
||||
import { VersionManagement } from '../version-management';
|
||||
import { users } from 'store/users';
|
||||
import { expert } from 'store/expert';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
@observer
|
||||
export class OperationManagement extends React.Component {
|
||||
public tabs = [{
|
||||
title: '迁移任务',
|
||||
component: <MigrationTask />,
|
||||
}, {
|
||||
title: '集群任务',
|
||||
component: <ClusterTask />,
|
||||
}, {
|
||||
title: '版本管理',
|
||||
component: <VersionManagement />,
|
||||
}];
|
||||
|
||||
public render() {
|
||||
let tabs = [].concat(this.tabs);
|
||||
if (users.currentUser.role !== 2) {
|
||||
tabs = tabs.splice(2);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Tabs activeKey={location.hash.substr(1) || '0'} type="card" onChange={handleTabKey}>
|
||||
{
|
||||
tabs.map((item, index) => {
|
||||
return (
|
||||
<TabPane tab={item.title} key={'' + index}>
|
||||
{item.component}
|
||||
</TabPane>);
|
||||
})
|
||||
}
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
import * as React from 'react';
|
||||
import './index.less';
|
||||
import { Table, PageHeader, Descriptions, Divider, Tooltip } from 'component/antd';
|
||||
import { wrapper } from 'store';
|
||||
import Url from 'lib/url-parser';
|
||||
import { expert } from 'store/expert';
|
||||
import { classStatusMap } from 'constants/status-map';
|
||||
import { admin } from 'store/admin';
|
||||
import { observer } from 'mobx-react';
|
||||
import { IXFormWrapper, IReassign, IDetailVO, ILabelValue, IEnumsMap } from 'types/base-type';
|
||||
import { modifyTransferTask } from 'container/modal';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { handlePageBack } from 'lib/utils';
|
||||
import moment from 'moment';
|
||||
import './index.less';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
|
||||
@observer
|
||||
export class MigrationDetail extends SearchAndFilterContainer {
|
||||
public taskId: number;
|
||||
|
||||
public state = {
|
||||
filterStatusVisible: false,
|
||||
};
|
||||
private xFormModal: IXFormWrapper;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.taskId = Number(url.search.taskId);
|
||||
}
|
||||
|
||||
public showDetails() {
|
||||
const isUrl = window.location.href.includes('/expert') ? '/expert#2' : '/admin/operation';
|
||||
const detail = expert.tasksDetail;
|
||||
const gmtCreate = moment(detail.gmtCreate).format(timeFormat);
|
||||
const startTime = moment(detail.beginTime).format(timeFormat);
|
||||
const endTime = moment(detail.endTime).format(timeFormat);
|
||||
const options = [{
|
||||
value: detail.taskName,
|
||||
label: '任务名称',
|
||||
}, {
|
||||
value: gmtCreate,
|
||||
label: '创建时间',
|
||||
}, {
|
||||
value: detail.operator,
|
||||
label: '创建人',
|
||||
}, {
|
||||
value: startTime,
|
||||
label: '计划开始时间',
|
||||
}, {
|
||||
value: endTime,
|
||||
label: '完成时间',
|
||||
}];
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
className="detail hotspot-header"
|
||||
onBack={() => handlePageBack(isUrl)}
|
||||
title={`Topic数据迁移任务/${detail.taskName || ''}`}
|
||||
>
|
||||
<Divider className="hotspot-divider" />
|
||||
<Descriptions column={3}>
|
||||
{options.map((item: ILabelValue, index) => (
|
||||
<Descriptions.Item key={index} label={item.label}>{item.value}</Descriptions.Item>
|
||||
))}
|
||||
<Descriptions.Item label="任务说明">
|
||||
<Tooltip placement="bottomLeft" title={detail.description}>
|
||||
<span className="overview">
|
||||
{detail.description}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</PageHeader>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public detailsTable() {
|
||||
let taskList = [] as IReassign[];
|
||||
taskList = expert.tasksStatus ? expert.tasksStatus : taskList;
|
||||
const taskStatus = admin.configsTaskStatus as IEnumsMap[];
|
||||
const status = Object.assign({
|
||||
title: '任务状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
filters: taskStatus.map(ele => ({ text: ele.message, value: ele.code + '' })),
|
||||
onFilter: (value: number, record: IReassign) => record.status === +value,
|
||||
render: (t: number) => {
|
||||
let message = '';
|
||||
taskStatus.forEach((ele: any) => {
|
||||
if (ele.code === t) {
|
||||
message = ele.message;
|
||||
}
|
||||
});
|
||||
let statusName = '';
|
||||
if (t === 100 || t === 101) {
|
||||
statusName = 'success';
|
||||
} else if ( t === 40 || t === 99 || t === 102 || t === 103 || t === 104 || t === 105 || t === 106) {
|
||||
statusName = 'fail';
|
||||
}
|
||||
return <span className={statusName}>{message}</span>;
|
||||
},
|
||||
}, this.renderColumnsFilter('filterStatusVisible'));
|
||||
const columns = [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
sorter: (a: IReassign, b: IReassign) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
render: (val: string) => <Tooltip placement="bottomLeft" title={val}> {val} </Tooltip>,
|
||||
},
|
||||
{
|
||||
title: '所在集群',
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
},
|
||||
{
|
||||
title: '迁移进度',
|
||||
dataIndex: 'PartitionNum',
|
||||
key: 'PartitionNum',
|
||||
render: (text: string, item: IReassign) => <span>{item.completedPartitionNum}/{item.totalPartitionNum}</span>,
|
||||
},
|
||||
status,
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
render: (text: string, item: IReassign) => (
|
||||
<>
|
||||
<a onClick={() => this.renderRessignDetail(item)} style={{ marginRight: 16 }}>详情</a>
|
||||
<a onClick={() => modifyTransferTask(item, 'modify', this.taskId)}>修改</a>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<Table rowKey="key" dataSource={taskList} columns={columns} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public renderRessignDetail(item: IReassign) {
|
||||
let statusList = [] as IDetailVO[];
|
||||
statusList = item.reassignList ? item.reassignList : statusList;
|
||||
this.xFormModal = {
|
||||
type: 'drawer',
|
||||
noform: true,
|
||||
nofooter: true,
|
||||
visible: true,
|
||||
title: '查看任务状态',
|
||||
customRenderElement: this.renderInfo(statusList),
|
||||
width: 500,
|
||||
onSubmit: () => {
|
||||
// TODO:
|
||||
},
|
||||
};
|
||||
wrapper.open(this.xFormModal);
|
||||
}
|
||||
|
||||
public renderInfo(statusList: IDetailVO[]) {
|
||||
const statusColumns = [
|
||||
{
|
||||
title: '分区ID',
|
||||
dataIndex: 'partitionId',
|
||||
key: 'partitionId',
|
||||
},
|
||||
{
|
||||
title: '目标BrokerID',
|
||||
dataIndex: 'destReplicaIdList',
|
||||
key: 'destReplicaIdList',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 180,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
render: (t: []) => {
|
||||
return t.map(i => <span key={i} className="p-params">{i}</span>);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (t: number) => {
|
||||
let message = '';
|
||||
const taskStatus = admin.configsTaskStatus ? admin.configsTaskStatus : [] as IEnumsMap[];
|
||||
taskStatus.forEach((ele: any) => {
|
||||
if (ele.code === t) {
|
||||
message = ele.message;
|
||||
}
|
||||
});
|
||||
return <span className={`${classStatusMap[t]} p-params`}>{message}</span>;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table rowKey="key" dataSource={statusList} columns={statusColumns} />
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
expert.getReassignTasksDetail(this.taskId);
|
||||
expert.getReassignTasksStatus(this.taskId);
|
||||
admin.getConfigsTaskStatus();
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
expert.tasksDetail ?
|
||||
(
|
||||
<>
|
||||
{this.showDetails()}
|
||||
{admin.configsTaskStatus ? this.detailsTable() : null}
|
||||
</>
|
||||
) : null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import * as React from 'react';
|
||||
import { IReassignTasks, IEnumsMap } from 'types/base-type';
|
||||
import { Table, Button } from 'component/antd';
|
||||
import { expert } from 'store/expert';
|
||||
import { pagination } from 'constants/table';
|
||||
import { observer } from 'mobx-react';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { createMigrationTasks } from 'container/modal';
|
||||
import { admin } from 'store/admin';
|
||||
import { migrationTaskColumns } from './config';
|
||||
import './index.less';
|
||||
|
||||
@observer
|
||||
export class MigrationTask extends SearchAndFilterContainer {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
filterStatusVisible: false,
|
||||
};
|
||||
|
||||
public getColumns = () => {
|
||||
const columns = migrationTaskColumns(window.location.href.includes('/expert') ? 'expert/hotspot-detail' : 'admin/migration-detail');
|
||||
const taskStatus = admin.configsTaskStatus as IEnumsMap[];
|
||||
const status = Object.assign({
|
||||
title: '任务状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
filters: taskStatus.map(ele => ({ text: ele.message, value: ele.code + '' })),
|
||||
onFilter: (value: number, record: IReassignTasks) => record.status === +value,
|
||||
render: (t: number) => {
|
||||
let message = '';
|
||||
taskStatus.forEach((ele: any) => {
|
||||
if (ele.code === t) {
|
||||
message = ele.message;
|
||||
}
|
||||
});
|
||||
let statusName = '';
|
||||
if (t === 100 || t === 101) {
|
||||
statusName = 'success';
|
||||
} else if (t === 40 || t === 99 || t === 102 || t === 103 || t === 104 || t === 105 || t === 106) {
|
||||
statusName = 'fail';
|
||||
}
|
||||
return <span className={statusName}>{message}</span>;
|
||||
},
|
||||
}, this.renderColumnsFilter('filterStatusVisible'));
|
||||
const col = columns.splice(4, 0, status);
|
||||
return columns;
|
||||
}
|
||||
|
||||
public getMigrationTask() {
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
columns={this.getColumns()}
|
||||
dataSource={expert.reassignTasks}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public getData(data: IReassignTasks[]) {
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
const reassignTasks: IReassignTasks[] = data.filter(d =>
|
||||
(d.taskName !== undefined && d.taskName !== null) && d.taskName.toLowerCase().includes(searchKey as string));
|
||||
return reassignTasks;
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
expert.getReassignTasks();
|
||||
if (!expert.metaData.length) {
|
||||
expert.getMetaData(false);
|
||||
}
|
||||
admin.getConfigsTaskStatus();
|
||||
}
|
||||
|
||||
public renderOperationPanel() {
|
||||
return (
|
||||
<ul>
|
||||
{this.renderSearch('', '请输入任务名称')}
|
||||
{location.pathname.includes('expert') ? null : <li className="right-btn-1">
|
||||
<Button type="primary" onClick={() => createMigrationTasks()}>新建迁移任务</Button>
|
||||
</li>}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (!admin.configsTaskStatus) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="container">
|
||||
<div className="table-operation-panel">
|
||||
{this.renderOperationPanel()}
|
||||
</div>
|
||||
<div className="table-wrapper">
|
||||
<Table
|
||||
columns={this.getColumns()}
|
||||
dataSource={this.getData(expert.reassignTasks)}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Tabs } from 'antd';
|
||||
import { handleTabKey } from 'lib/utils';
|
||||
import { AdminAppList } from './admin-app-list';
|
||||
import { UserManagement } from './user-management';
|
||||
import { ConfigureManagement } from './configure-management';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
@observer
|
||||
export class PlatformManagement extends React.Component {
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<Tabs activeKey={location.hash.substr(1) || '1'} type="card" onChange={handleTabKey}>
|
||||
<TabPane tab="应用管理" key="1">
|
||||
<AdminAppList />
|
||||
</TabPane>
|
||||
<TabPane tab="用户管理" key="2">
|
||||
<UserManagement />
|
||||
</TabPane>
|
||||
<TabPane tab="配置管理" key="3">
|
||||
<ConfigureManagement />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
import { Table, Button, Spin } from 'component/antd';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { IUser } from 'types/base-type';
|
||||
import { users } from 'store/users';
|
||||
import { pagination } from 'constants/table';
|
||||
import { getUserColumns } from './config';
|
||||
import { showApplyModal } from 'container/modal/admin';
|
||||
import { roleMap } from 'constants/status-map';
|
||||
import { tableFilter } from 'lib/utils';
|
||||
|
||||
@observer
|
||||
export class UserManagement extends SearchAndFilterContainer {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
filterRole: false,
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
if (!users.userData.length) {
|
||||
users.getUserList();
|
||||
}
|
||||
}
|
||||
|
||||
public getData<T extends IUser>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: IUser) =>
|
||||
(item.username !== undefined && item.username !== null) && item.username.toLowerCase().includes(searchKey as string)) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public renderTable() {
|
||||
const roleColumn = Object.assign({
|
||||
title: '角色权限',
|
||||
dataIndex: 'role',
|
||||
key: 'role',
|
||||
width: '35%',
|
||||
filters: tableFilter<IUser>(users.userData, 'role', roleMap),
|
||||
onFilter: (text: number, record: IUser) => record.role === text,
|
||||
render: (text: number) => roleMap[text] || '',
|
||||
|
||||
}, this.renderColumnsFilter('filterRole')) as any;
|
||||
|
||||
const userColumns = getUserColumns();
|
||||
|
||||
userColumns.splice(1, 0, roleColumn);
|
||||
|
||||
return (
|
||||
<Spin spinning={users.loading}>
|
||||
<Table
|
||||
rowKey="key"
|
||||
columns={userColumns}
|
||||
dataSource={this.getData(users.userData)}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</Spin>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
public renderOperationPanel() {
|
||||
return (
|
||||
<ul>
|
||||
{this.renderSearch('', '请输入用户名或应用名称')}
|
||||
<li className="right-btn-1">
|
||||
<Button type="primary" onClick={() => showApplyModal()}>添加用户</Button>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="table-operation-panel">
|
||||
{this.renderOperationPanel()}
|
||||
</div>
|
||||
<div className="table-wrapper">
|
||||
{this.renderTable()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
105
kafka-manager-console/src/container/admin/version-management.tsx
Normal file
105
kafka-manager-console/src/container/admin/version-management.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
import { Table, Button, Spin } from 'component/antd';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { IUploadFile } from 'types/base-type';
|
||||
import { version } from 'store/version';
|
||||
import { pagination } from 'constants/table';
|
||||
import { getVersionColumns } from './config';
|
||||
import { showUploadModal } from 'container/modal/admin';
|
||||
import { tableFilter } from 'lib/utils';
|
||||
import { admin } from 'store/admin';
|
||||
|
||||
@observer
|
||||
export class VersionManagement extends SearchAndFilterContainer {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
filterClusterNameVisible: false,
|
||||
filterConfigTypeVisible: false,
|
||||
};
|
||||
|
||||
public async componentDidMount() {
|
||||
if (!version.fileTypeList.length) {
|
||||
await version.getFileTypeList();
|
||||
}
|
||||
|
||||
if (!version.fileList.length) {
|
||||
version.getFileList();
|
||||
}
|
||||
|
||||
if (!admin.metaList.length) {
|
||||
admin.getMetaData(false);
|
||||
}
|
||||
}
|
||||
public getColumns = () => {
|
||||
const columns = getVersionColumns();
|
||||
const clusterName = Object.assign({
|
||||
title: '集群名称',
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
filters: tableFilter<any>(this.getData(version.fileList), 'clusterName'),
|
||||
onFilter: (value: string, record: IUploadFile) => record.clusterName === value,
|
||||
}, this.renderColumnsFilter('filterClusterNameVisible'));
|
||||
const configType = Object.assign({
|
||||
title: '配置类型',
|
||||
dataIndex: 'configType',
|
||||
key: 'configType',
|
||||
filters: tableFilter<any>(this.getData(version.fileList), 'configType'),
|
||||
onFilter: (value: string, record: IUploadFile) => record.configType === value,
|
||||
}, this.renderColumnsFilter('filterConfigTypeVisible'));
|
||||
const col = columns.splice(1, 0, clusterName, configType);
|
||||
return columns;
|
||||
}
|
||||
|
||||
public getData<T extends IUploadFile>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
if (searchKey) {
|
||||
data = origin.filter((item: IUploadFile) => item.id + '' === searchKey
|
||||
|| ((item.fileName !== undefined && item.fileName !== null) && item.fileName.toLowerCase().includes(searchKey as string)));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
public renderTable() {
|
||||
return (
|
||||
<Spin spinning={version.loading}>
|
||||
<Table
|
||||
rowKey="key"
|
||||
columns={this.getColumns()}
|
||||
dataSource={this.getData(version.fileList)}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</Spin>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
public renderOperationPanel() {
|
||||
return (
|
||||
<ul>
|
||||
{this.renderSearch('', '请输入ID或文件名')}
|
||||
<li className="right-btn-1">
|
||||
<Button type="primary" onClick={() => showUploadModal()}>上传配置</Button>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const currentFileType = version.currentFileType;
|
||||
const acceptFileMap = version.acceptFileMap;
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="table-operation-panel">
|
||||
{this.renderOperationPanel()}
|
||||
</div>
|
||||
<div className="table-wrapper">
|
||||
{this.renderTable()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { XFormComponent } from 'component/x-form';
|
||||
import { xActionFormMap } from './config';
|
||||
import * as React from 'react';
|
||||
import { IRequestParams, IStrategyAction, IConfigForm } from 'types/alarm';
|
||||
|
||||
export class ActionForm extends React.Component {
|
||||
public $form: any = null;
|
||||
|
||||
public getFormData() {
|
||||
let configValue = null as IConfigForm;
|
||||
this.$form.validateFields((error: Error, result: IConfigForm) => {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
configValue = result;
|
||||
});
|
||||
return configValue;
|
||||
}
|
||||
|
||||
public resetFormData() {
|
||||
this.$form.resetFields();
|
||||
}
|
||||
|
||||
public updateFormData(monitorRule: IRequestParams) {
|
||||
const strategyAction = monitorRule.strategyActionList[0] || {} as IStrategyAction;
|
||||
this.$form.setFieldsValue({
|
||||
level: monitorRule.priority,
|
||||
alarmPeriod: strategyAction.converge.split(',')[0],
|
||||
alarmTimes: strategyAction.converge.split(',')[1],
|
||||
acceptGroup: strategyAction.notifyGroup,
|
||||
callback: strategyAction.callback,
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const formData = {};
|
||||
const formLayout = {
|
||||
labelCol: { span: 3 },
|
||||
wrapperCol: { span: 12 },
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="config-wrapper">
|
||||
<span className="span-tag">配置发送信息</span>
|
||||
<div className="alarm-x-form action-form">
|
||||
<XFormComponent
|
||||
ref={form => this.$form = form}
|
||||
formData={formData}
|
||||
formMap={xActionFormMap}
|
||||
formLayout={formLayout}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import * as React from 'react';
|
||||
import { alarm } from 'store/alarm';
|
||||
import { IMonitorGroups } from 'types/base-type';
|
||||
import { getValueFromLocalStorage, setValueToLocalStorage } from 'lib/local-storage';
|
||||
import { VirtualScrollSelect } from '../../../component/virtual-scroll-select';
|
||||
|
||||
interface IAlarmSelectProps {
|
||||
onChange?: (result: string[]) => any;
|
||||
value?: string[];
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
export class AlarmSelect extends React.Component<IAlarmSelectProps> {
|
||||
public getData = async () => {
|
||||
const originMonitorList = getValueFromLocalStorage('monitorGroups');
|
||||
if (originMonitorList) return originMonitorList;
|
||||
return await this.fetchMonitor();
|
||||
}
|
||||
|
||||
public fetchMonitor = async () => {
|
||||
let data = await alarm.getMonitorGroups();
|
||||
data = (data || []).map((item: IMonitorGroups) => {
|
||||
return {
|
||||
...item,
|
||||
label: item.name,
|
||||
value: item.name,
|
||||
};
|
||||
});
|
||||
setValueToLocalStorage('monitorGroups', data);
|
||||
return data;
|
||||
}
|
||||
public handleChange = (params: string[]) => {
|
||||
const { onChange } = this.props;
|
||||
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
onChange && onChange(params);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { value, isDisabled } = this.props;
|
||||
return (
|
||||
<>
|
||||
<VirtualScrollSelect
|
||||
attrs={{ mode: 'multiple', placeholder: '请选择报警接收组' }}
|
||||
value={value}
|
||||
isDisabled={isDisabled}
|
||||
getData={this.getData}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<a
|
||||
className="icon-color"
|
||||
target="_blank"
|
||||
href="https://github.com/didi/kafka-manager"
|
||||
>
|
||||
新建告警组?
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
246
kafka-manager-console/src/container/alarm/add-alarm/config.tsx
Normal file
246
kafka-manager-console/src/container/alarm/add-alarm/config.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
import * as React from 'react';
|
||||
import { Tooltip, notification, Radio, Icon, Popconfirm, RadioChangeEvent } from 'component/antd';
|
||||
import { IMonitorStrategies, ILabelValue } from 'types/base-type';
|
||||
import { IFormItem, IFormSelect } from 'component/x-form';
|
||||
import { AlarmSelect } from 'container/alarm/add-alarm/alarm-select';
|
||||
import { weekOptions } from 'constants/status-map';
|
||||
import { alarm } from 'store/alarm';
|
||||
import { app } from 'store/app';
|
||||
import moment from 'moment';
|
||||
import { cellStyle } from 'constants/table';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
import { region } from 'store/region';
|
||||
|
||||
export const getAlarmColumns = (urlPrefix: string) => {
|
||||
const columns = [
|
||||
{
|
||||
title: '告警名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: '25%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 250,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
sorter: (a: IMonitorStrategies, b: IMonitorStrategies) => a.name.charCodeAt(0) - b.name.charCodeAt(0),
|
||||
render: (text: string, record: IMonitorStrategies) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={record.name} >
|
||||
<a href={`${urlPrefix}/alarm/alarm-detail?id=${record.id}®ion=${region.currentRegion}`}> {text} </a>
|
||||
</Tooltip>);
|
||||
},
|
||||
}, {
|
||||
title: '应用名称',
|
||||
dataIndex: 'appName',
|
||||
key: 'appName',
|
||||
width: '25%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 250,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
sorter: (a: IMonitorStrategies, b: IMonitorStrategies) => a.appName.charCodeAt(0) - b.appName.charCodeAt(0),
|
||||
render: (text: string, record: IMonitorStrategies) =>
|
||||
<Tooltip placement="bottomLeft" title={record.principals} >{text}</Tooltip>,
|
||||
}, {
|
||||
title: '操作人',
|
||||
dataIndex: 'operator',
|
||||
key: 'operator',
|
||||
width: '20%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 100,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
sorter: (a: IMonitorStrategies, b: IMonitorStrategies) => a.operator.charCodeAt(0) - b.operator.charCodeAt(0),
|
||||
render: (text: string) => <Tooltip placement="bottomLeft" title={text} >{text}</Tooltip>,
|
||||
}, {
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
width: '20%',
|
||||
sorter: (a: IMonitorStrategies, b: IMonitorStrategies) => b.createTime - a.createTime,
|
||||
render: (time: number) => moment(time).format(timeFormat),
|
||||
}, {
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
key: 'operation',
|
||||
width: '10%',
|
||||
render: (text: string, item: IMonitorStrategies) => (
|
||||
<>
|
||||
<a href={`${urlPrefix}/alarm/modify?id=${item.id}`} className="action-button">修改</a>
|
||||
<Popconfirm
|
||||
title="确定删除?"
|
||||
onConfirm={() => deteleMonitor(item)}
|
||||
>
|
||||
<a>删除</a>
|
||||
</Popconfirm>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
return columns;
|
||||
};
|
||||
|
||||
export const getRandomKey = () => {
|
||||
return (new Date()).getTime();
|
||||
};
|
||||
|
||||
export const deteleMonitor = (item: IMonitorStrategies) => {
|
||||
alarm.deteleMonitorStrategies(item.id).then(data => {
|
||||
notification.success({ message: '删除成功' });
|
||||
});
|
||||
};
|
||||
|
||||
export const getAlarmTime = () => {
|
||||
const timeOptions = [] as ILabelValue[];
|
||||
const defaultTime = [] as number[];
|
||||
|
||||
for (let i = 0; i < 24; i++) {
|
||||
timeOptions.push({
|
||||
label: `${i}点`,
|
||||
value: i,
|
||||
});
|
||||
defaultTime.push(i);
|
||||
}
|
||||
return { timeOptions, defaultTime };
|
||||
};
|
||||
export const getAlarmWeek = () => {
|
||||
const defWeek = [] as number[];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
defWeek.push(i);
|
||||
}
|
||||
return { defWeek, weekOptions };
|
||||
};
|
||||
|
||||
interface IRadioProps {
|
||||
onChange?: (result: number) => any;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
const isDetailPage = window.location.pathname.includes('/alarm-detail'); // 判断是否为详情
|
||||
|
||||
class RadioIcon extends React.Component<IRadioProps> {
|
||||
public onRadioChange = (e: RadioChangeEvent) => {
|
||||
const { onChange } = this.props;
|
||||
if (onChange) {
|
||||
onChange(e.target.value);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { value } = this.props;
|
||||
return (
|
||||
<Radio.Group
|
||||
name="radiogroup"
|
||||
value={value || 3}
|
||||
disabled={isDetailPage}
|
||||
onChange={this.onRadioChange}
|
||||
>
|
||||
<Radio value={1} key={1}>
|
||||
一级告警
|
||||
<Icon type="phone" />
|
||||
<Icon type="message" />
|
||||
<Icon type="mail" />
|
||||
<Icon type="dingding" />
|
||||
</Radio>
|
||||
<Radio value={2} key={2}>
|
||||
二级告警
|
||||
<Icon type="message" />
|
||||
<Icon type="mail" />
|
||||
<Icon type="dingding" />
|
||||
</Radio>
|
||||
<Radio value={3} key={3}>
|
||||
三级告警
|
||||
<Icon type="mail" />
|
||||
<Icon type="dingding" />
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const xActionFormMap = [{
|
||||
key: 'level',
|
||||
label: '报警级别',
|
||||
type: 'custom',
|
||||
defaultValue: 3,
|
||||
customFormItem: <RadioIcon />,
|
||||
rules: [{ required: true, message: '请输入报警接收组' }],
|
||||
}, {
|
||||
key: 'alarmPeriod',
|
||||
label: '报警周期(分钟)',
|
||||
type: 'input_number',
|
||||
attrs: {
|
||||
min: 0,
|
||||
disabled: isDetailPage,
|
||||
},
|
||||
rules: [{ required: true, message: '请输入报警周期' }],
|
||||
}, {
|
||||
key: 'alarmTimes',
|
||||
label: '周期内报警次数',
|
||||
type: 'input_number',
|
||||
attrs: {
|
||||
min: 0,
|
||||
disabled: isDetailPage,
|
||||
},
|
||||
rules: [{ required: true, message: '请输入周期内报警次数' }],
|
||||
}, {
|
||||
key: 'acceptGroup',
|
||||
label: '报警接收组',
|
||||
type: 'custom',
|
||||
customFormItem: <AlarmSelect isDisabled={isDetailPage}/>,
|
||||
rules: [{ required: true, message: '请输入报警接收组' }],
|
||||
},
|
||||
{
|
||||
key: 'callback',
|
||||
label: '回调地址',
|
||||
rules: [{ required: false, message: '请输入回调地址' }],
|
||||
attrs: {disabled: isDetailPage},
|
||||
}] as unknown as IFormSelect[]; // as IFormItem[];
|
||||
|
||||
export const xTypeFormMap = [{
|
||||
key: 'alarmName',
|
||||
label: '告警名称',
|
||||
rules: [{ required: true, message: '请输入告警名称' }],
|
||||
attrs: {placeholder: '请输入', disabled: isDetailPage},
|
||||
}, {
|
||||
key: 'app',
|
||||
label: '所属应用',
|
||||
type: 'select',
|
||||
attrs: {
|
||||
placeholder: '请选择',
|
||||
optionFilterProp: 'children',
|
||||
showSearch: true,
|
||||
filterOption: (input: any, option: any) => {
|
||||
if ( typeof option.props.children === 'object' ) {
|
||||
const { props } = option.props.children as any;
|
||||
return (props.children + '').toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
}
|
||||
return (option.props.children + '').toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
},
|
||||
onChange: (e: string) => app.changeActiveApp(e),
|
||||
disabled: isDetailPage,
|
||||
},
|
||||
rules: [{ required: true, message: '请输入报警接收组' }],
|
||||
}] as unknown as IFormSelect[];
|
||||
|
||||
export const xTimeFormMap = [{
|
||||
key: 'weeks',
|
||||
label: '每周',
|
||||
type: 'check_box',
|
||||
defaultValue: getAlarmWeek().defWeek,
|
||||
options: getAlarmWeek().weekOptions,
|
||||
rules: [{ required: true, message: '请选择' }],
|
||||
}, {
|
||||
key: 'hours',
|
||||
label: '每天',
|
||||
type: 'check_box',
|
||||
defaultValue: getAlarmTime().defaultTime,
|
||||
options: getAlarmTime().timeOptions,
|
||||
rules: [{ required: true, message: '请选择' }],
|
||||
}] as unknown as IFormSelect[];
|
||||
@@ -0,0 +1,391 @@
|
||||
import * as React from 'react';
|
||||
import { Select, Spin, Form, Tooltip } from 'component/antd';
|
||||
import { message } from 'antd';
|
||||
import { IFormItem } from 'component/x-form';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { alarm } from 'store/alarm';
|
||||
import { topic } from 'store/topic';
|
||||
import { observer } from 'mobx-react';
|
||||
import { IRequestParams, IStrategyFilter } from 'types/alarm';
|
||||
import { filterKeys } from 'constants/strategy';
|
||||
import { VirtualScrollSelect } from 'component/virtual-scroll-select';
|
||||
import { IsNotNaN } from 'lib/utils';
|
||||
import { searchProps } from 'constants/table';
|
||||
|
||||
interface IDynamicProps {
|
||||
form?: any;
|
||||
formData?: any;
|
||||
}
|
||||
|
||||
interface IFormSelect extends IFormItem {
|
||||
options: Array<{ key?: string | number, value: string | number, label: string }>;
|
||||
}
|
||||
|
||||
interface IVritualScrollSelect extends IFormSelect {
|
||||
getData: () => any;
|
||||
isDisabled: boolean;
|
||||
refetchData?: boolean;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class DynamicSetFilter extends React.Component<IDynamicProps> {
|
||||
public isDetailPage = window.location.pathname.includes('/alarm-detail'); // 判断是否为详情
|
||||
public monitorType: string = null;
|
||||
public clusterId: number = null;
|
||||
public clusterName: string = null;
|
||||
public topicName: string = null;
|
||||
public consumerGroup: string = null;
|
||||
public location: string = null;
|
||||
|
||||
public getFormValidateData() {
|
||||
const filterList = [] as IStrategyFilter[];
|
||||
let monitorType = '' as string;
|
||||
let filterObj = {} as any;
|
||||
|
||||
this.props.form.validateFields((err: Error, values: any) => {
|
||||
if (!err) {
|
||||
monitorType = values.monitorType;
|
||||
const index = cluster.clusterData.findIndex(item => item.clusterId === values.cluster);
|
||||
if (index > -1) {
|
||||
values.clusterName = cluster.clusterData[index].clusterName;
|
||||
}
|
||||
for (const key of Object.keys(values)) {
|
||||
if (filterKeys.indexOf(key) > -1) { // 只有这几种值可以设置
|
||||
filterList.push({
|
||||
tkey: key === 'clusterName' ? 'cluster' : key, // 传参需要将clusterName转成cluster
|
||||
topt: '=',
|
||||
tval: [values[key]],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return filterObj = {
|
||||
monitorType,
|
||||
filterList,
|
||||
};
|
||||
}
|
||||
|
||||
public resetForm() {
|
||||
const { resetFields } = this.props.form;
|
||||
this.clearFormData();
|
||||
resetFields();
|
||||
}
|
||||
|
||||
public resetFormValue(
|
||||
monitorType: string = null,
|
||||
clusterId: number = null,
|
||||
topicName: string = null,
|
||||
consumerGroup: string = null,
|
||||
location: string = null) {
|
||||
const { setFieldsValue } = this.props.form;
|
||||
setFieldsValue({
|
||||
cluster: clusterId,
|
||||
topic: topicName,
|
||||
consumerGroup,
|
||||
location,
|
||||
monitorType,
|
||||
});
|
||||
}
|
||||
|
||||
public getClusterId = (clusterName: string) => {
|
||||
let clusterId = null;
|
||||
const index = cluster.clusterData.findIndex(item => item.clusterName === clusterName);
|
||||
if (index > -1) {
|
||||
clusterId = cluster.clusterData[index].clusterId;
|
||||
}
|
||||
if (clusterId) {
|
||||
cluster.getClusterMetaTopics(clusterId);
|
||||
this.clusterId = clusterId;
|
||||
return this.clusterId;
|
||||
}
|
||||
return this.clusterId = clusterName as any;
|
||||
}
|
||||
|
||||
public async initFormValue(monitorRule: IRequestParams) {
|
||||
const strategyFilterList = monitorRule.strategyFilterList;
|
||||
const clusterFilter = strategyFilterList.filter(item => item.tkey === 'cluster')[0];
|
||||
const topicFilter = strategyFilterList.filter(item => item.tkey === 'topic')[0];
|
||||
const consumerFilter = strategyFilterList.filter(item => item.tkey === 'consumerGroup')[0];
|
||||
|
||||
const clusterName = clusterFilter ? clusterFilter.tval[0] : null;
|
||||
const topic = topicFilter ? topicFilter.tval[0] : null;
|
||||
const consumerGroup = consumerFilter ? consumerFilter.tval[0] : null;
|
||||
const location: string = null;
|
||||
const monitorType = monitorRule.strategyExpressionList[0].metric;
|
||||
alarm.changeMonitorStrategyType(monitorType);
|
||||
|
||||
await this.getClusterId(clusterName);
|
||||
await this.handleSelectChange(topic, 'topic');
|
||||
await this.handleSelectChange(consumerGroup, 'consumerGroup');
|
||||
this.resetFormValue(monitorType, this.clusterId, topic, consumerGroup, location);
|
||||
}
|
||||
|
||||
public clearFormData() {
|
||||
this.monitorType = null;
|
||||
this.topicName = null;
|
||||
this.clusterId = null;
|
||||
this.consumerGroup = null;
|
||||
this.location = null;
|
||||
this.resetFormValue();
|
||||
}
|
||||
|
||||
public async handleClusterChange(e: number) {
|
||||
this.clusterId = e;
|
||||
this.topicName = null;
|
||||
topic.setLoading(true);
|
||||
await cluster.getClusterMetaTopics(e);
|
||||
this.resetFormValue(this.monitorType, e, null, this.consumerGroup, this.location);
|
||||
topic.setLoading(false);
|
||||
}
|
||||
|
||||
public handleSelectChange = (e: string, type: 'topic' | 'consumerGroup' | 'location') => {
|
||||
switch (type) {
|
||||
case 'topic':
|
||||
if (!this.clusterId) {
|
||||
return message.info('请选择集群');
|
||||
}
|
||||
this.topicName = e;
|
||||
const type = this.dealMonitorType();
|
||||
if (['kafka-consumer-maxLag', 'kafka-consumer-maxDelayTime', 'kafka-consumer-lag'].indexOf(type) > -1) {
|
||||
this.getConsumerInfo();
|
||||
}
|
||||
break;
|
||||
case 'consumerGroup':
|
||||
this.consumerGroup = e;
|
||||
break;
|
||||
case 'location':
|
||||
this.location = e;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public getConsumerInfo = () => {
|
||||
if (!this.clusterId || !this.topicName) {
|
||||
return;
|
||||
}
|
||||
topic.setLoading(true);
|
||||
if (IsNotNaN(this.clusterId)) {
|
||||
topic.getConsumerGroups(this.clusterId, this.topicName);
|
||||
}
|
||||
this.consumerGroup = null;
|
||||
this.location = null;
|
||||
this.resetFormValue(this.monitorType, this.clusterId, this.topicName);
|
||||
topic.setLoading(false);
|
||||
}
|
||||
|
||||
public dealMonitorType() {
|
||||
const index = alarm.monitorType.indexOf('-');
|
||||
let type = alarm.monitorType;
|
||||
if (index > -1) {
|
||||
type = type.substring(index + 1);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
public getRenderItem() {
|
||||
const type = this.dealMonitorType();
|
||||
const showMore = ['kafka-consumer-maxLag', 'kafka-consumer-maxDelayTime', 'kafka-consumer-lag'].indexOf(type) > -1;
|
||||
this.monitorType = alarm.monitorType;
|
||||
|
||||
const monitorType = {
|
||||
key: 'monitorType',
|
||||
label: '监控指标',
|
||||
type: 'select',
|
||||
options: alarm.monitorTypeList.map(item => ({
|
||||
label: item.metricName,
|
||||
value: item.metricName,
|
||||
})),
|
||||
attrs: {
|
||||
placeholder: '请选择',
|
||||
className: 'large-size',
|
||||
disabled: this.isDetailPage,
|
||||
optionFilterProp: 'children',
|
||||
showSearch: true,
|
||||
filterOption: (input: any, option: any) => {
|
||||
if (typeof option.props.children === 'object') {
|
||||
const { props } = option.props.children as any;
|
||||
return (props.children + '').toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
}
|
||||
return (option.props.children + '').toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
},
|
||||
onChange: (e: string) => this.handleTypeChange(e),
|
||||
},
|
||||
rules: [{ required: true, message: '请选择监控指标' }],
|
||||
} as IVritualScrollSelect;
|
||||
|
||||
const clusterItem = {
|
||||
label: '集群',
|
||||
options: cluster.clusterData,
|
||||
defaultValue: this.clusterId,
|
||||
rules: [{ required: true, message: '请选择集群' }],
|
||||
attrs: {
|
||||
placeholder: '请选择集群',
|
||||
className: 'middle-size',
|
||||
disabled: this.isDetailPage,
|
||||
onChange: (e: number) => this.handleClusterChange(e),
|
||||
},
|
||||
key: 'cluster',
|
||||
} as unknown as IVritualScrollSelect;
|
||||
|
||||
const topicItem = {
|
||||
label: 'Topic',
|
||||
defaultValue: this.topicName,
|
||||
rules: [{ required: true, message: '请选择Topic' }],
|
||||
isDisabled: this.isDetailPage,
|
||||
options: cluster.clusterMetaTopics.map(item => {
|
||||
return {
|
||||
label: item.topicName,
|
||||
value: item.topicName,
|
||||
};
|
||||
}),
|
||||
attrs: {
|
||||
placeholder: '请选择Topic',
|
||||
className: 'middle-size',
|
||||
disabled: this.isDetailPage,
|
||||
onChange: (e: string) => this.handleSelectChange(e, 'topic'),
|
||||
},
|
||||
key: 'topic',
|
||||
} as IVritualScrollSelect;
|
||||
|
||||
const consumerGroupItem = {
|
||||
label: '消费组',
|
||||
options: topic.consumerGroups.map(item => {
|
||||
return {
|
||||
label: item.consumerGroup,
|
||||
value: item.consumerGroup,
|
||||
};
|
||||
}),
|
||||
defaultValue: this.consumerGroup,
|
||||
rules: [{ required: showMore, message: '请选择消费组' }],
|
||||
attrs: {
|
||||
placeholder: '请选择消费组',
|
||||
className: 'middle-size',
|
||||
disabled: this.isDetailPage,
|
||||
onChange: (e: string) => this.handleSelectChange(e, 'consumerGroup'),
|
||||
},
|
||||
key: 'consumerGroup',
|
||||
} as IVritualScrollSelect;
|
||||
|
||||
const locationItem = {
|
||||
label: 'location',
|
||||
options: topic.filterGroups.map(item => {
|
||||
return {
|
||||
label: item.location,
|
||||
value: item.location,
|
||||
};
|
||||
}),
|
||||
defaultValue: this.location,
|
||||
rules: [{ required: showMore, message: '请选择location' }],
|
||||
attrs: {
|
||||
placeholder: '请选择location',
|
||||
optionFilterProp: 'children',
|
||||
showSearch: true,
|
||||
className: 'middle-size',
|
||||
disabled: this.isDetailPage,
|
||||
onChange: (e: string) => this.handleSelectChange(e, 'location'),
|
||||
},
|
||||
key: 'location',
|
||||
} as IVritualScrollSelect;
|
||||
|
||||
const common = (
|
||||
<>
|
||||
{this.renderFormItem(clusterItem)}
|
||||
{this.renderFormItem(topicItem)}
|
||||
</>
|
||||
);
|
||||
|
||||
const more = showMore ? (
|
||||
<>
|
||||
{this.renderFormItem(consumerGroupItem)}
|
||||
{/* {this.renderFormItem(locationItem)} */}
|
||||
</>
|
||||
) : null;
|
||||
return (
|
||||
<>
|
||||
<div className="dynamic-set">
|
||||
{this.renderFormItem(monitorType)}
|
||||
<ul>{common}{more}</ul>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public handleTypeChange = (e: string) => {
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
this.clearFormData();
|
||||
alarm.changeMonitorStrategyType(e);
|
||||
}
|
||||
|
||||
public getSelectFormItem(item: IFormItem) {
|
||||
return (
|
||||
<Select
|
||||
key={item.key}
|
||||
{...item.attrs}
|
||||
{...searchProps}
|
||||
>
|
||||
{(item as IFormSelect).options && (item as IFormSelect).options.map((v, index) => (
|
||||
<Select.Option
|
||||
key={v.value || v.key || index}
|
||||
value={v.value}
|
||||
>
|
||||
{v.label.length > 25 ? <Tooltip placement="bottomLeft" title={v.label}>
|
||||
{v.label}
|
||||
</Tooltip> : v.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
public renderFormItem(item: IVritualScrollSelect, virtualScroll: boolean = false) {
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
const { formData = {} } = this.props;
|
||||
const initialValue = formData[item.key] === 0 ? 0 : (formData[item.key] || item.defaultValue || '');
|
||||
const getFieldValue = {
|
||||
initialValue,
|
||||
rules: item.rules || [{ required: true, message: '请填写' }],
|
||||
};
|
||||
const formItemLayout = {
|
||||
labelCol: { span: 6 },
|
||||
wrapperCol: { span: 10 },
|
||||
};
|
||||
return (
|
||||
<Form.Item
|
||||
label={item.label}
|
||||
key={item.key}
|
||||
{...formItemLayout}
|
||||
>
|
||||
{getFieldDecorator(item.key, getFieldValue)(
|
||||
virtualScroll ?
|
||||
<VirtualScrollSelect
|
||||
attrs={item.attrs}
|
||||
isDisabled={item.isDisabled}
|
||||
onChange={item.attrs.onChange}
|
||||
getData={item.getData}
|
||||
refetchData={item.refetchData}
|
||||
/>
|
||||
: this.getSelectFormItem(item),
|
||||
)}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
cluster.getClusters();
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Spin spinning={cluster.filterLoading}>
|
||||
<Form>
|
||||
<div className="form-list">
|
||||
{this.getRenderItem()}
|
||||
</div>
|
||||
</Form>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const WrappedDynamicSetFilter = Form.create({ name: 'dynamic_filter_form_item' })(DynamicSetFilter);
|
||||
224
kafka-manager-console/src/container/alarm/add-alarm/index.less
Normal file
224
kafka-manager-console/src/container/alarm/add-alarm/index.less
Normal file
@@ -0,0 +1,224 @@
|
||||
.btn-group {
|
||||
background: white;
|
||||
width: calc(100% - 215px);
|
||||
position: fixed;
|
||||
top: 75px;
|
||||
right: 22px;
|
||||
z-index: 999999;
|
||||
box-shadow: 0px 12px 8px -14px #c5c2c2;
|
||||
}
|
||||
|
||||
.container_box{
|
||||
width: 100%;
|
||||
margin-top: 65px;
|
||||
}
|
||||
|
||||
.config-wrapper {
|
||||
background: white;
|
||||
height: 100%;
|
||||
padding-left: 20px;
|
||||
|
||||
.alarm-time-form{
|
||||
border-top: 1px solid #E8E8E8;
|
||||
height: 80px;
|
||||
padding: 10px 0 20px;
|
||||
margin-bottom: 20px;
|
||||
.form-item{
|
||||
float: left;
|
||||
}
|
||||
b{
|
||||
float: left;
|
||||
font-size: 13px;
|
||||
margin: 0 5px;
|
||||
font-weight: 100;
|
||||
line-height: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
.alarm-x-form {
|
||||
border-top: 1px solid #E8E8E8;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
Icon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
&.type-form {
|
||||
padding-top: 10px;
|
||||
|
||||
.ant-form-item {
|
||||
width: 30%
|
||||
}
|
||||
.ant-form-item-label {
|
||||
padding-left: 10px;
|
||||
}
|
||||
.ant-form-item-control {
|
||||
width: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
&.action-form {
|
||||
.ant-col-3 {
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.anticon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.span-tag {
|
||||
border-left: 2px solid @primary-color;
|
||||
padding-left: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 40px;
|
||||
font-family: PingFangSC-Regular;
|
||||
}
|
||||
|
||||
.info-wrapper {
|
||||
border-top: 1px solid #E8E8E8;
|
||||
margin-bottom: 20px;
|
||||
padding: 15px 10px;
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
|
||||
li {
|
||||
flex: 1;
|
||||
vertical-align: middle;
|
||||
margin-right: 15px;
|
||||
|
||||
.ant-select {
|
||||
margin-left: 15px;
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-form {
|
||||
border: 1px dashed #e29864;
|
||||
padding: 20px 0px 20px 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-list {
|
||||
line-height: 40px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ant-form-item {
|
||||
display: inline-block;
|
||||
margin: 0px 5px 10px 5px;
|
||||
|
||||
.ant-select {
|
||||
width: 150px;
|
||||
margin-right: 5px;
|
||||
|
||||
&.small-size {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
&.middle-size {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
&.large-size {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-input-number {
|
||||
width: 100px;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.dynamic-button {
|
||||
font-size: 16px;
|
||||
transition: all 0.3s;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.dynamic-button:hover {
|
||||
&.delete {
|
||||
color: red;
|
||||
}
|
||||
|
||||
&.plus {
|
||||
color: green;
|
||||
}
|
||||
}
|
||||
|
||||
.dynamic-button[disabled] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dynamic-set {
|
||||
padding: 15px 10px;
|
||||
|
||||
ul{
|
||||
li{
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.form-list {
|
||||
line-height: 40px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ant-form-item {
|
||||
display: inline-block;
|
||||
margin: 0px 5px 10px 5px;
|
||||
|
||||
.ant-select {
|
||||
width: 150px;
|
||||
margin-right: 5px;
|
||||
|
||||
&.small-size {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
&.middle-size {
|
||||
width: 190px;
|
||||
}
|
||||
|
||||
&.large-size {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.strategy {
|
||||
display: inline-block;
|
||||
width: 90%;
|
||||
border: 1px dashed #dcc4af;
|
||||
padding: 15px 15px;
|
||||
margin: 0px 15px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: inline-block;
|
||||
width: 80%;
|
||||
|
||||
.time-select {
|
||||
width: 50%;
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-show{
|
||||
display: none;
|
||||
}
|
||||
171
kafka-manager-console/src/container/alarm/add-alarm/index.tsx
Normal file
171
kafka-manager-console/src/container/alarm/add-alarm/index.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import * as React from 'react';
|
||||
import './index.less';
|
||||
import { WrappedDynamicSetStrategy } from './strategy-form';
|
||||
import { Button, PageHeader, Spin, message } from 'component/antd';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { WrappedTimeForm } from './time-form';
|
||||
import { ActionForm } from './action-form';
|
||||
import { TypeForm } from './type-form';
|
||||
import { handlePageBack } from 'lib/utils';
|
||||
import { observer } from 'mobx-react';
|
||||
import { alarm } from 'store/alarm';
|
||||
import { app } from 'store/app';
|
||||
import Url from 'lib/url-parser';
|
||||
import { IStrategyExpression, IRequestParams } from 'types/alarm';
|
||||
|
||||
@observer
|
||||
export class AddAlarm extends SearchAndFilterContainer {
|
||||
public isDetailPage = window.location.pathname.includes('/alarm-detail'); // 判断是否为详情
|
||||
public strategyForm: any = null;
|
||||
public actionForm: any = null;
|
||||
public timeForm: any = null;
|
||||
public typeForm: any = null;
|
||||
|
||||
public id: number = null;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.id = Number(url.search.id);
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
alarm.getMonitorType();
|
||||
alarm.setLoading(true);
|
||||
app.getAppList();
|
||||
if (this.id || this.id === 0) {
|
||||
await alarm.getMonitorDetail(this.id);
|
||||
this.initMonitorDetailData();
|
||||
}
|
||||
alarm.setLoading(false);
|
||||
}
|
||||
|
||||
public initMonitorDetailData() {
|
||||
if (alarm.monitorStrategyDetail.monitorRule) {
|
||||
if (!this.strategyForm || !this.actionForm || !this.typeForm || !this.timeForm) {
|
||||
return;
|
||||
}
|
||||
const monitorRule = alarm.monitorStrategyDetail.monitorRule || {} as IRequestParams;
|
||||
|
||||
this.timeForm.updateFormData(monitorRule);
|
||||
this.typeForm.updateFormData(monitorRule);
|
||||
this.actionForm.updateFormData(monitorRule);
|
||||
this.strategyForm.updateFormValue(monitorRule);
|
||||
}
|
||||
}
|
||||
|
||||
public handleSubmit = () => {
|
||||
if (!this.strategyForm || !this.actionForm || !this.typeForm || !this.timeForm) {
|
||||
return;
|
||||
}
|
||||
|
||||
const params = this.generateRequestParams() as IRequestParams;
|
||||
if (!params) return;
|
||||
|
||||
(this.id || this.id === 0) ?
|
||||
alarm.modifyMonitorStrategy({ id: this.id, ...params }) : alarm.addMonitorStategy(params);
|
||||
}
|
||||
|
||||
public handleResetForm = (id?: number) => {
|
||||
if (id || id === 0) {
|
||||
alarm.getMonitorDetail(this.id);
|
||||
this.initMonitorDetailData();
|
||||
} else {
|
||||
if (!this.strategyForm || !this.actionForm || !this.typeForm || !this.timeForm) {
|
||||
return;
|
||||
}
|
||||
this.typeForm.resetFormData();
|
||||
this.timeForm.resetFormData();
|
||||
this.actionForm.resetFormData();
|
||||
|
||||
this.strategyForm.resetForm();
|
||||
}
|
||||
}
|
||||
|
||||
public generateRequestParams() {
|
||||
const actionValue = this.actionForm.getFormData();
|
||||
const timeValue = this.timeForm.getFormData();
|
||||
const typeValue = this.typeForm.getFormData().typeValue;
|
||||
let strategyList = this.strategyForm.getFormValidateData();
|
||||
const filterObj = this.typeForm.getFormData().filterObj;
|
||||
// tslint:disable-next-line:max-line-length
|
||||
if (!actionValue || !timeValue || !typeValue || !strategyList.length || !filterObj || !filterObj.filterList.length) {
|
||||
message.error('请正确填写必填项');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (filterObj.monitorType === 'online-kafka-topic-throttled') {
|
||||
filterObj.filterList.push({
|
||||
tkey: 'app',
|
||||
topt: '=',
|
||||
tval: [typeValue.app],
|
||||
});
|
||||
}
|
||||
strategyList = strategyList.map((row: IStrategyExpression) => {
|
||||
return {
|
||||
...row,
|
||||
metric: filterObj.monitorType,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
appId: typeValue.app,
|
||||
name: typeValue.alarmName,
|
||||
periodDaysOfWeek: timeValue.weeks.join(','),
|
||||
periodHoursOfDay: timeValue.hours.join(','),
|
||||
priority: actionValue.level,
|
||||
strategyActionList: [{
|
||||
callback: actionValue.callback,
|
||||
notifyGroup: actionValue.acceptGroup,
|
||||
converge: actionValue.alarmPeriod + ',' + actionValue.alarmTimes,
|
||||
type: 'notify',
|
||||
sendRecovery: 1,
|
||||
}],
|
||||
strategyExpressionList: strategyList,
|
||||
strategyFilterList: filterObj.filterList,
|
||||
} as IRequestParams;
|
||||
}
|
||||
|
||||
public renderAlarmStrategy() {
|
||||
return (
|
||||
<div className="config-wrapper">
|
||||
<span className="span-tag">报警策略</span>
|
||||
<div className="info-wrapper">
|
||||
<WrappedDynamicSetStrategy wrappedComponentRef={(form: any) => this.strategyForm = form} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public renderTimeForm() {
|
||||
return (
|
||||
<>
|
||||
<WrappedTimeForm wrappedComponentRef={(form: any) => this.timeForm = form} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Spin spinning={alarm.loading}>
|
||||
<div className={this.isDetailPage ? '' : 'container_box'}>
|
||||
<PageHeader
|
||||
className={this.isDetailPage ? 'is-show' : 'btn-group'}
|
||||
onBack={() => handlePageBack('/alarm')}
|
||||
title={(this.id || this.id === 0) ? '修改告警配置' : '新建告警配置'}
|
||||
extra={[
|
||||
<Button key="1" type="primary" onClick={() => this.handleSubmit()}>提交</Button>,
|
||||
<Button key="2" onClick={() => this.handleResetForm(this.id)}>重置</Button>,
|
||||
]}
|
||||
/>
|
||||
<TypeForm
|
||||
ref={(form) => this.typeForm = form}
|
||||
/>
|
||||
{this.renderAlarmStrategy()}
|
||||
{this.renderTimeForm()}
|
||||
<ActionForm ref={(actionForm) => this.actionForm = actionForm} />
|
||||
</div>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
import * as React from 'react';
|
||||
import { Icon, InputNumber, Select, message, Form, Tooltip } from 'component/antd';
|
||||
import { equalList, funcKeyMap, funcList } from 'constants/strategy';
|
||||
import { IStringMap } from 'types/base-type';
|
||||
import { IRequestParams } from 'types/alarm';
|
||||
import { IFormSelect, IFormItem, FormItemType } from 'component/x-form';
|
||||
import { searchProps } from 'constants/table';
|
||||
|
||||
interface IDynamicProps {
|
||||
form: any;
|
||||
formData?: any;
|
||||
maxLimit?: number;
|
||||
}
|
||||
interface ICRUDItem {
|
||||
id: string;
|
||||
func: string;
|
||||
eopt?: string;
|
||||
threshold?: number;
|
||||
period?: number;
|
||||
count?: number;
|
||||
day?: number;
|
||||
}
|
||||
const commonKeys = ['eopt', 'threshold', 'func'];
|
||||
|
||||
class DynamicSetStrategy extends React.Component<IDynamicProps> {
|
||||
public isDetailPage = window.location.pathname.includes('/alarm-detail'); // 判断是否为详情
|
||||
public crudList = [] as ICRUDItem[];
|
||||
public state = {
|
||||
shouldUpdate: false,
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
if (!this.crudList.length) {
|
||||
const id = `0_`;
|
||||
this.crudList.push({
|
||||
id,
|
||||
func: 'happen',
|
||||
});
|
||||
}
|
||||
this.updateRender();
|
||||
}
|
||||
|
||||
public updateRender() {
|
||||
this.setState({
|
||||
shouldUpdate: !this.state.shouldUpdate,
|
||||
});
|
||||
}
|
||||
|
||||
public resetForm() {
|
||||
const { resetFields } = this.props.form;
|
||||
resetFields();
|
||||
}
|
||||
|
||||
public dealFormParams(monitorRule: IRequestParams) {
|
||||
const initialCrudList = [] as ICRUDItem[];
|
||||
|
||||
if (monitorRule.strategyExpressionList) {
|
||||
const expressionList = monitorRule.strategyExpressionList;
|
||||
expressionList.map((row: any, index) => {
|
||||
const obj = {} as any;
|
||||
for (const key of commonKeys) {
|
||||
obj[key] = row[key];
|
||||
}
|
||||
const otherKeys = funcKeyMap[row.func] as string[];
|
||||
// 除去commonKeys中的key 其他值在提交时全塞到params中 故在编辑详情渲染时再拆回来
|
||||
const parmas = row.params ? row.params.split(',').map((row: string) => +row) : [];
|
||||
otherKeys.forEach((line: string, i: number) => {
|
||||
obj[line] = parmas[i] || 0;
|
||||
});
|
||||
obj.id = `${index}_`;
|
||||
|
||||
initialCrudList.push(obj);
|
||||
});
|
||||
}
|
||||
return initialCrudList;
|
||||
}
|
||||
|
||||
public updateFormValue(monitorRule: IRequestParams) {
|
||||
const { setFieldsValue } = this.props.form;
|
||||
const initialCrudList = this.dealFormParams(monitorRule);
|
||||
if (!initialCrudList.length) return;
|
||||
|
||||
const filledKeys = ['period'].concat(commonKeys);
|
||||
const formKeyMap = {
|
||||
happen: ['count'].concat(filledKeys),
|
||||
ndiff: ['count'].concat(filledKeys),
|
||||
all: [].concat(filledKeys),
|
||||
pdiff: [].concat(filledKeys),
|
||||
sum: [].concat(filledKeys),
|
||||
c_avg_rate_abs: ['day'].concat(filledKeys),
|
||||
} as {
|
||||
[key: string]: string[],
|
||||
};
|
||||
const feildValue = {
|
||||
|
||||
} as any;
|
||||
|
||||
for (const item of initialCrudList) {
|
||||
for (const key of formKeyMap[item.func]) {
|
||||
feildValue[item.id + '-' + key] = (item as any)[key];
|
||||
}
|
||||
}
|
||||
setFieldsValue(feildValue);
|
||||
this.crudList = initialCrudList;
|
||||
this.updateRender();
|
||||
}
|
||||
|
||||
public getFormValidateData() {
|
||||
let value = [] as IStringMap[];
|
||||
const { crudList } = this;
|
||||
|
||||
this.props.form.validateFields((err: Error, values: any) => {
|
||||
if (!err) {
|
||||
let strategyList = [];
|
||||
for (const item of crudList) {
|
||||
const lineValue = {} as IStringMap;
|
||||
const paramsArray = [] as number[];
|
||||
|
||||
// 不在commonKeys里的塞到params
|
||||
for (const key of Object.keys(values)) {
|
||||
if (key.indexOf(item.id) > -1) {
|
||||
const finalKey = key.substring(key.indexOf('-') + 1);
|
||||
if (commonKeys.indexOf(finalKey) < 0) { // 不在commonKeys里的塞到params 奇奇怪怪的接口
|
||||
paramsArray.push(finalKey === 'day' ? values[key] * 24 * 60 * 60 : values[key]); // 按接口单位天的时候需要换算成秒
|
||||
} else { // 在commonKeys里直接赋值
|
||||
lineValue[finalKey] = values[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lineValue.func === 'happen' && paramsArray.length > 1 && paramsArray[0] < paramsArray[1]) {
|
||||
strategyList = []; // 清空赋值
|
||||
return message.error('周期值应大于次数') ;
|
||||
}
|
||||
|
||||
lineValue.params = paramsArray.join(',');
|
||||
strategyList.push(lineValue);
|
||||
}
|
||||
value = strategyList;
|
||||
}
|
||||
});
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public remove = (curr: string) => {
|
||||
const { crudList } = this;
|
||||
if (crudList.length <= 1) {
|
||||
return message.info('至少保留一项');
|
||||
}
|
||||
|
||||
const index = crudList.findIndex(item => item.id === curr);
|
||||
|
||||
crudList.splice(index, 1);
|
||||
this.updateRender();
|
||||
}
|
||||
|
||||
public add = () => {
|
||||
const { maxLimit = 5 } = this.props;
|
||||
const { crudList } = this;
|
||||
if (crudList.length >= maxLimit) {
|
||||
return message.info('已达最大数量');
|
||||
}
|
||||
|
||||
const id = `${crudList.length}_`;
|
||||
crudList.push({
|
||||
id,
|
||||
func: 'happen',
|
||||
});
|
||||
this.updateRender();
|
||||
}
|
||||
|
||||
public onFuncTypeChange = (e: string, key: string) => {
|
||||
const { crudList } = this;
|
||||
const index = crudList.findIndex(row => row.id === key);
|
||||
|
||||
if (index > -1) {
|
||||
crudList[index].func = e;
|
||||
}
|
||||
this.updateRender();
|
||||
}
|
||||
|
||||
public getFormItem(item: IFormItem) {
|
||||
switch (item.type) {
|
||||
default:
|
||||
case FormItemType.input:
|
||||
return <InputNumber min={0} key={item.key} {...item.attrs} disabled={this.isDetailPage} />;
|
||||
case FormItemType.select:
|
||||
return (
|
||||
<Select
|
||||
key={item.key}
|
||||
{...item.attrs}
|
||||
disabled={this.isDetailPage}
|
||||
{...searchProps}
|
||||
>
|
||||
{(item as IFormSelect).options && (item as IFormSelect).options.map((v, index) => (
|
||||
<Select.Option
|
||||
key={v.value || v.key || index}
|
||||
value={v.value}
|
||||
>
|
||||
{v.label.length > 15 ? <Tooltip placement="bottomLeft" title={v.label}>
|
||||
{v.label}
|
||||
</Tooltip> : v.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public getFuncItem(row: ICRUDItem) {
|
||||
const key = row.id;
|
||||
const funcType = row.func;
|
||||
let element = null;
|
||||
const common = (
|
||||
<>
|
||||
在最近
|
||||
{this.renderFormItem({ type: 'input', key: key + '-period', defaultValue: row.period } as IFormItem)}
|
||||
个周期内
|
||||
</>
|
||||
);
|
||||
const equalItem = {
|
||||
type: 'select',
|
||||
attrs: { className: 'small-size' },
|
||||
defaultValue: row.eopt || '=',
|
||||
options: equalList,
|
||||
key: key + '-eopt',
|
||||
} as IFormSelect;
|
||||
|
||||
switch (funcType) {
|
||||
case 'happen':
|
||||
case 'ndiff':
|
||||
element = (
|
||||
<>
|
||||
{common}
|
||||
{this.renderFormItem({ type: 'input', key: key + '-count', defaultValue: row.count } as IFormItem)}
|
||||
次
|
||||
{this.renderFormItem(equalItem)}
|
||||
{this.renderFormItem({ type: 'input', key: key + '-threshold', defaultValue: row.threshold } as IFormItem)}
|
||||
</>
|
||||
);
|
||||
break;
|
||||
case 'all':
|
||||
case 'diff':
|
||||
case 'max':
|
||||
case 'min':
|
||||
case 'sum':
|
||||
case 'avg':
|
||||
element = (
|
||||
<>
|
||||
{common}
|
||||
{this.renderFormItem(equalItem)}
|
||||
{this.renderFormItem({ type: 'input', key: key + '-threshold', defaultValue: row.threshold } as IFormItem)}
|
||||
</>
|
||||
);
|
||||
break;
|
||||
case 'c_avg_rate_abs':
|
||||
case 'c_avg_rate':
|
||||
element = (
|
||||
<>
|
||||
{common},平均值相对
|
||||
{this.renderFormItem({ type: 'input', key: key + '-day', defaultValue: row.day } as IFormItem)}
|
||||
天
|
||||
{this.renderFormItem(equalItem)}
|
||||
{this.renderFormItem({ type: 'input', key: key + '-threshold', defaultValue: row.threshold } as IFormItem)}
|
||||
%
|
||||
</>
|
||||
);
|
||||
break;
|
||||
case 'c_avg_abs':
|
||||
case 'c_avg':
|
||||
element = (
|
||||
<>
|
||||
{common},平均值相对
|
||||
{this.renderFormItem({ type: 'input', key: key + '-day', defaultValue: row.day } as IFormItem)}
|
||||
天
|
||||
{this.renderFormItem(equalItem)}
|
||||
{this.renderFormItem({ type: 'input', key: key + '-threshold', defaultValue: row.threshold } as IFormItem)}
|
||||
</>
|
||||
);
|
||||
break;
|
||||
case 'pdiff':
|
||||
element = (
|
||||
<>
|
||||
{common}
|
||||
{this.renderFormItem(equalItem)}
|
||||
{this.renderFormItem({ type: 'input', key: key + '-threshold', defaultValue: row.threshold } as IFormItem)}
|
||||
%
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
public renderFormList(row: ICRUDItem) {
|
||||
const key = row.id;
|
||||
const funcType = row.func;
|
||||
|
||||
return (
|
||||
<div key={key} className="form-list">
|
||||
{this.renderFormItem({
|
||||
type: 'select',
|
||||
defaultValue: funcType,
|
||||
attrs: {
|
||||
onChange: (e: string) => this.onFuncTypeChange(e, key),
|
||||
},
|
||||
options: funcList,
|
||||
key: key + '-func',
|
||||
} as IFormSelect)}
|
||||
{this.getFuncItem(row)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public renderFormItem(item: IFormItem) {
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
const initialValue = item.defaultValue || '';
|
||||
const getFieldValue = {
|
||||
initialValue,
|
||||
rules: item.rules || [{ required: true, message: '请填写' }],
|
||||
};
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
key={item.key}
|
||||
>
|
||||
{getFieldDecorator(item.key, getFieldValue)(
|
||||
this.getFormItem(item),
|
||||
)}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { crudList } = this;
|
||||
const { maxLimit = 5 } = this.props;
|
||||
|
||||
return (
|
||||
<Form>
|
||||
{crudList.map((row, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
{this.renderFormList(row)}
|
||||
{
|
||||
crudList.length > 1 ? (
|
||||
<Icon
|
||||
className={this.isDetailPage ? 'is-show' : 'dynamic-button delete'}
|
||||
type="minus-circle-o"
|
||||
onClick={() => this.remove(row.id)}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
{index === crudList.length - 1 && crudList.length < maxLimit ? (
|
||||
<Icon
|
||||
className={this.isDetailPage ? 'is-show' : 'dynamic-button plus'}
|
||||
type="plus-circle-o"
|
||||
onClick={() => this.add()}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const WrappedDynamicSetStrategy = Form.create({ name: 'dynamic_form_item' })(DynamicSetStrategy);
|
||||
@@ -0,0 +1,137 @@
|
||||
import { getAlarmTime, getAlarmWeek } from './config';
|
||||
import * as React from 'react';
|
||||
import { IRequestParams, IAlarmTime } from 'types/alarm';
|
||||
import { Checkbox, TimePicker, Form } from 'component/antd';
|
||||
import { weekOptions } from 'constants/status-map';
|
||||
|
||||
import moment = require('moment');
|
||||
|
||||
interface ITimeProps {
|
||||
form?: any;
|
||||
formData?: any;
|
||||
}
|
||||
|
||||
export class TimeForm extends React.Component<ITimeProps> {
|
||||
public isDetailPage = window.location.pathname.includes('/alarm-detail'); // 判断是否为详情
|
||||
public $form: any = null;
|
||||
public weeks: number[] = [0, 1, 2, 3, 4, 5, 6, 7];
|
||||
public startTime: number = 0;
|
||||
public endTime: number = 23;
|
||||
|
||||
public getFormData() {
|
||||
let value = null as IAlarmTime;
|
||||
this.props.form.validateFields((error: Error, result: any) => {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
const start = Number(moment(result.startTime).format('HH'));
|
||||
const end = Number(moment(result.endTime).format('HH'));
|
||||
const timeArr = getAlarmTime().defaultTime;
|
||||
const hours = timeArr.slice(start, end + 1);
|
||||
value = {
|
||||
weeks: result.weeks,
|
||||
hours,
|
||||
};
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
public resetFormData() {
|
||||
const { defaultTime } = getAlarmTime();
|
||||
const { defWeek } = getAlarmWeek();
|
||||
this.props.form.setFieldsValue({
|
||||
hours: defaultTime,
|
||||
weeks: defWeek,
|
||||
startTime: moment(0, 'HH'),
|
||||
endTime: moment(23, 'HH'),
|
||||
});
|
||||
}
|
||||
|
||||
public updateFormData = (monitorRule: IRequestParams) => {
|
||||
const selectHours = monitorRule.periodHoursOfDay.split(',').map(item => +item);
|
||||
const selectWeek = monitorRule.periodDaysOfWeek.split(',').map(item => +item);
|
||||
|
||||
this.props.form.setFieldsValue({
|
||||
// hours: selectHours,
|
||||
weeks: selectWeek,
|
||||
startTime: moment(selectHours[0], 'HH'),
|
||||
endTime: moment(selectHours[selectHours.length - 1], 'HH'),
|
||||
});
|
||||
this.startTime = selectHours[0];
|
||||
this.endTime = selectHours[selectHours.length - 1];
|
||||
}
|
||||
public onStartChange = (time: any, timeString: string) => {
|
||||
this.startTime = Number(timeString);
|
||||
}
|
||||
public disabledHours = () => {
|
||||
const hours = [] as number[];
|
||||
for (let i = 0; i < this.startTime; i++) {
|
||||
hours.push(i);
|
||||
}
|
||||
return hours;
|
||||
}
|
||||
|
||||
public render() {
|
||||
// const formData = {};
|
||||
// {/* <div className="alarm-x-form">
|
||||
// <XFormComponent
|
||||
// ref={form => this.$form = form}
|
||||
// formData={formData}
|
||||
// formMap={xTimeFormMap}
|
||||
// formLayout={formLayout}
|
||||
// />
|
||||
// </div> */}
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
const format = 'HH';
|
||||
return (
|
||||
<div className="config-wrapper">
|
||||
<span className="span-tag">生效时间</span>
|
||||
<div className="alarm-time-form">
|
||||
<Form name="basic" >
|
||||
<b>在每</b>
|
||||
<Form.Item label="" key={1} className="form-item">
|
||||
{getFieldDecorator('weeks', {
|
||||
initialValue: this.weeks,
|
||||
rules: [{ required: true, message: '请选择周期' }],
|
||||
})(
|
||||
<Checkbox.Group
|
||||
options={weekOptions}
|
||||
disabled={this.isDetailPage}
|
||||
/>)}
|
||||
</Form.Item>
|
||||
<b>的</b>
|
||||
<Form.Item label="" key={2} className="form-item">
|
||||
{getFieldDecorator('startTime', {
|
||||
initialValue: moment(this.startTime, format),
|
||||
rules: [{ required: true, message: '请选择开始时间' }],
|
||||
})(
|
||||
<TimePicker
|
||||
key={1}
|
||||
format={format}
|
||||
style={{width: 60}}
|
||||
onChange={this.onStartChange}
|
||||
disabled={this.isDetailPage}
|
||||
/>)}
|
||||
</Form.Item>
|
||||
<b>~</b>
|
||||
<Form.Item label="" key={3} className="form-item">
|
||||
{getFieldDecorator('endTime', {
|
||||
initialValue: moment(this.endTime, format),
|
||||
rules: [{ required: true, message: '请选择结束时间' }],
|
||||
})(
|
||||
<TimePicker
|
||||
key={2}
|
||||
format={format}
|
||||
disabledHours={this.disabledHours}
|
||||
style={{width: 60}}
|
||||
disabled={this.isDetailPage}
|
||||
/>)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const WrappedTimeForm = Form.create({ name: 'dynamic_time_form' })(TimeForm);
|
||||
@@ -0,0 +1,74 @@
|
||||
import { XFormComponent } from 'component/x-form';
|
||||
import { xTypeFormMap } from './config';
|
||||
import * as React from 'react';
|
||||
import { IRequestParams, ITypeForm } from 'types/alarm';
|
||||
import { app } from 'store/app';
|
||||
import { observer } from 'mobx-react';
|
||||
import { WrappedDynamicSetFilter } from './filter-form';
|
||||
|
||||
@observer
|
||||
export class TypeForm extends React.Component {
|
||||
|
||||
public $form: any = null;
|
||||
public filterForm: any = null;
|
||||
|
||||
public getFormData() {
|
||||
const filterObj = this.filterForm.getFormValidateData();
|
||||
let typeValue = null as ITypeForm;
|
||||
this.$form.validateFields((error: Error, result: ITypeForm) => {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
typeValue = result;
|
||||
});
|
||||
const valueObj = {
|
||||
typeValue,
|
||||
filterObj,
|
||||
};
|
||||
return valueObj;
|
||||
}
|
||||
|
||||
public resetFormData() {
|
||||
this.$form.resetFields();
|
||||
this.filterForm.resetForm();
|
||||
}
|
||||
|
||||
public updateFormData(monitorRule: IRequestParams) {
|
||||
this.$form.setFieldsValue({
|
||||
app: monitorRule.appId,
|
||||
alarmName: monitorRule.name,
|
||||
});
|
||||
this.filterForm.initFormValue(monitorRule);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const formData = {};
|
||||
xTypeFormMap[1].options = app.data.map(item => ({
|
||||
label: item.name,
|
||||
value: item.appId,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="config-wrapper">
|
||||
<span className="span-tag">基本信息</span>
|
||||
<div className="alarm-x-form type-form">
|
||||
<XFormComponent
|
||||
ref={form => this.$form = form}
|
||||
formData={formData}
|
||||
formMap={xTypeFormMap}
|
||||
layout="inline"
|
||||
/>
|
||||
</div>
|
||||
</div >
|
||||
<div className="config-wrapper">
|
||||
<span className="span-tag">选择指标</span>
|
||||
<div className="alarm-x-form type-form">
|
||||
<WrappedDynamicSetFilter wrappedComponentRef={(form: any) => this.filterForm = form} />
|
||||
</div>
|
||||
</div >
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import * as React from 'react';
|
||||
import { Table, Button } from 'component/antd';
|
||||
import { urlPrefix } from 'constants/left-menu';
|
||||
import moment from 'moment';
|
||||
import { alarm } from 'store/alarm';
|
||||
import Url from 'lib/url-parser';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { IMonitorAlerts } from 'types/base-type';
|
||||
import './index.less';
|
||||
import { observer } from 'mobx-react';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
|
||||
@observer
|
||||
export class AlarmHistory extends SearchAndFilterContainer {
|
||||
public id: number = null;
|
||||
public startTime: any = moment().subtract(3, 'day').format('x');
|
||||
public endTime: any = moment().endOf('day').format('x');
|
||||
|
||||
public state = {
|
||||
filterStatus: false,
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.id = Number(url.search.id);
|
||||
}
|
||||
|
||||
public historyCreateTime(value?: number) {
|
||||
this.startTime = value ? moment().subtract(7, 'day').format('x') : moment().subtract(3, 'day').format('x');
|
||||
this.endTime = moment().format('x');
|
||||
alarm.getMonitorAlerts(this.id, this.startTime, this.endTime);
|
||||
}
|
||||
|
||||
public historySelect() {
|
||||
return(
|
||||
<>
|
||||
<div className="alarm-history-day">
|
||||
<Button onClick={() => this.historyCreateTime()}>近三天</Button>
|
||||
<Button onClick={() => this.historyCreateTime(7)}>近一周</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public historyTable() {
|
||||
const monitorAlerts: IMonitorAlerts[] = alarm.monitorAlerts ? alarm.monitorAlerts : [];
|
||||
const alertStatus = Object.assign({
|
||||
title: '状态',
|
||||
dataIndex: 'alertStatus',
|
||||
key: 'alertStatus',
|
||||
filters: [{ text: '故障', value: '0' }, { text: '已恢复', value: '1' }],
|
||||
onFilter: (value: string, record: IMonitorAlerts) => record.alertStatus === Number(value),
|
||||
render: (t: number) => t === 0 ? '故障' : '已恢复',
|
||||
}, this.renderColumnsFilter('filterStatus'));
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '监控名称',
|
||||
dataIndex: 'monitorName',
|
||||
key: 'monitorName',
|
||||
render: (text: string, record: IMonitorAlerts) => (
|
||||
<a href={`${urlPrefix}/alarm/history-detail?alertId=${record.alertId}`}> {text} </a>),
|
||||
},
|
||||
{
|
||||
title: '开始时间',
|
||||
dataIndex: 'startTime',
|
||||
key: 'startTime',
|
||||
render: (time: number) => moment(time).format(timeFormat),
|
||||
},
|
||||
{
|
||||
title: '结束时间',
|
||||
dataIndex: 'endTime',
|
||||
key: 'endTime',
|
||||
render: (time: number) => moment(time).format(timeFormat),
|
||||
},
|
||||
alertStatus,
|
||||
{
|
||||
title: '监控级别',
|
||||
dataIndex: 'monitorPriority',
|
||||
key: 'monitorPriority',
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<Table rowKey="key" dataSource={monitorAlerts} columns={columns} loading={alarm.loading}/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
alarm.getMonitorAlerts(this.id, this.startTime, this.endTime);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return(
|
||||
<>
|
||||
{this.historySelect()}
|
||||
{this.historyTable()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import * as React from 'react';
|
||||
import { createMonitorSilences } from 'container/modal';
|
||||
import { IMonitorAlert, IMonitorMetric } from 'types/base-type';
|
||||
import { Divider, Table, Button, PageHeader, Spin, Tooltip } from 'component/antd';
|
||||
import { alarm } from 'store/alarm';
|
||||
import { observer } from 'mobx-react';
|
||||
import { handlePageBack } from 'lib/utils';
|
||||
import LineChart, { hasData } from 'component/chart/line-chart';
|
||||
import { EChartOption } from 'echarts';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
import Url from 'lib/url-parser';
|
||||
import moment = require('moment');
|
||||
import './index.less';
|
||||
|
||||
@observer
|
||||
export class HistoryDetail extends React.Component {
|
||||
public alertId: number;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.alertId = Number(url.search.alertId);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
alarm.getAlertsDetail(this.alertId);
|
||||
}
|
||||
|
||||
public getChartOption = () => {
|
||||
return alarm.getMetircHistoryChartOptions();
|
||||
}
|
||||
|
||||
public renderNoData = (height?: number) => {
|
||||
const style = { height: `${height}px`, lineHeight: `${height}px` };
|
||||
return <div className="no-data-info" style={{ ...style }} key="noData">暂无数据</div>;
|
||||
}
|
||||
|
||||
public renderLoading = (height?: number) => {
|
||||
const style = { height: `${height}px`, lineHeight: `${height}px` };
|
||||
return <div className="no-data-info" style={{ ...style }} key="loading"><Spin /></div>;
|
||||
}
|
||||
|
||||
public renderEchart = (options: EChartOption, loading = false) => {
|
||||
const data = hasData(options);
|
||||
if (loading) return this.renderLoading(400);
|
||||
if (!data) return this.renderNoData(400);
|
||||
return (
|
||||
<div className="chart">
|
||||
<LineChart height={400} options={options} key="chart" />
|
||||
</div>);
|
||||
}
|
||||
|
||||
public renderHistoricalTraffic(metric: IMonitorMetric) {
|
||||
const option = this.getChartOption() as EChartOption;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="history-left">
|
||||
<div className="chart-box-0">
|
||||
<div className="chart-title metric-head">
|
||||
<span>{metric.metric}</span>
|
||||
</div>
|
||||
<Divider />
|
||||
{this.renderEchart(option)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public renderAlarmEventDetails(alert: IMonitorAlert) {
|
||||
const pointsColumns = [
|
||||
{
|
||||
title: 'timestamp',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
render: (t: number) => moment(t * 1000).format(timeFormat),
|
||||
},
|
||||
{
|
||||
title: 'value',
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
}];
|
||||
return (
|
||||
<>
|
||||
<div className="history-right">
|
||||
<div className="history-right-header">
|
||||
<h2>报警事件详情</h2>
|
||||
<Button onClick={() => { createMonitorSilences(alert.monitorId, alert.monitorName); }}>快速屏蔽</Button>
|
||||
</div>
|
||||
<Divider className="history-right-divider" />
|
||||
<ul>
|
||||
<li><b>监控名称:</b>{alert.monitorName}</li>
|
||||
<li><b>告警状态:</b>{alert.alertStatus === 0 ? '故障' : '已恢复'}</li>
|
||||
<li><b>告警组:</b>{alert.groups ? alert.groups.join('、') : null}</li>
|
||||
<li><b>告警指标:</b>{alert.metric}</li>
|
||||
<li><b>告警开始时间:</b>{moment(alert.startTime).format(timeFormat)}</li>
|
||||
<li><b>告警结束时间:</b>{moment(alert.endTime).format(timeFormat)}</li>
|
||||
<li><b>监控级别:</b>{alert.monitorPriority}级告警</li>
|
||||
<li><b>触发值:</b>{alert.value}</li>
|
||||
<li>
|
||||
<b>表达式:</b>
|
||||
<Tooltip placement="bottomLeft" title={alert.info} >
|
||||
{alert.info}
|
||||
</Tooltip>
|
||||
</li>
|
||||
</ul>
|
||||
<h4>现场值:</h4>
|
||||
<Table
|
||||
rowKey="timestamp"
|
||||
dataSource={alert.points}
|
||||
columns={pointsColumns}
|
||||
showHeader={false}
|
||||
pagination={false}
|
||||
bordered={true}
|
||||
scroll={{ y: 260 }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
{alarm.alertsDetail &&
|
||||
<>
|
||||
<PageHeader
|
||||
className="detail topic-detail-header"
|
||||
onBack={() => handlePageBack('/alarm')}
|
||||
title={`${alarm.monitorAlert.monitorName || ''}`}
|
||||
/>
|
||||
<div className="alarm-history">
|
||||
{this.renderHistoricalTraffic(alarm.monitorMetric)}
|
||||
{this.renderAlarmEventDetails(alarm.monitorAlert)}
|
||||
</div>
|
||||
</>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
.alarm-history{
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
.history-left{
|
||||
width: 60%;
|
||||
}
|
||||
.history-right{
|
||||
width: 30%;
|
||||
padding: 15px 20px;
|
||||
background: #fff;
|
||||
.history-right-divider{
|
||||
margin-top: -5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.history-right-header{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
h2{
|
||||
font-size: 14px;
|
||||
line-height: 38px;
|
||||
}
|
||||
}
|
||||
ul{
|
||||
li{
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis; //超出部分以省略号显示
|
||||
white-space: nowrap;
|
||||
b{
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alarm-history-day{
|
||||
width: 160px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.monitor-detail{
|
||||
font-size: 13px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.metric-head{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 10px;
|
||||
span{
|
||||
font-size: 13px;
|
||||
}
|
||||
a{
|
||||
margin: 0 5px;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import * as React from 'react';
|
||||
import { Tabs, PageHeader, Button } from 'antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import { AlarmHistory } from './alarm-history';
|
||||
import { handleTabKey } from 'lib/utils';
|
||||
import { ShieldHistory } from './shield-history';
|
||||
import { IXFormWrapper } from 'types/base-type';
|
||||
import { alarm } from 'store/alarm';
|
||||
import { IMonitorStrategyDetail } from 'types/alarm';
|
||||
import { urlPrefix } from 'constants/left-menu';
|
||||
import { createMonitorSilences } from 'container/modal';
|
||||
import { AddAlarm } from '../add-alarm';
|
||||
import { handlePageBack } from 'lib/utils';
|
||||
import Url from 'lib/url-parser';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
@observer
|
||||
export class AlarmDetail extends React.Component {
|
||||
public id: number = null;
|
||||
public monitorName: any = null;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.id = Number(url.search.id);
|
||||
}
|
||||
|
||||
public render() {
|
||||
let baseInfo = {} as IMonitorStrategyDetail;
|
||||
if (alarm.monitorStrategyDetail) {
|
||||
baseInfo = alarm.monitorStrategyDetail;
|
||||
}
|
||||
this.monitorName = baseInfo.name;
|
||||
return(
|
||||
<>
|
||||
<PageHeader
|
||||
className="detail topic-detail-header"
|
||||
onBack={() => handlePageBack('/alarm')}
|
||||
title={`${baseInfo.name || ''}`}
|
||||
extra={[
|
||||
<Button key="1" type="primary">
|
||||
<a href={`${urlPrefix}/alarm/modify?id=${this.id}`}>编辑</a>
|
||||
</Button>,
|
||||
<Button onClick={() => {createMonitorSilences(this.id, this.monitorName); }} key="2" >
|
||||
屏蔽
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
<Tabs activeKey={location.hash.substr(1) || '1'} type="card" onChange={handleTabKey}>
|
||||
<TabPane tab="基本信息" key="1">
|
||||
<AddAlarm />
|
||||
</TabPane>
|
||||
<TabPane tab="告警历史" key="2">
|
||||
<AlarmHistory />
|
||||
</TabPane>
|
||||
<TabPane tab="屏蔽历史" key="3">
|
||||
<ShieldHistory />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import * as React from 'react';
|
||||
import { Table, notification, Modal, Popconfirm } from 'component/antd';
|
||||
import moment from 'moment';
|
||||
import { alarm } from 'store/alarm';
|
||||
import { wrapper } from 'store';
|
||||
import Url from 'lib/url-parser';
|
||||
import { IMonitorSilences, IXFormWrapper } from 'types/base-type';
|
||||
import { observer } from 'mobx-react';
|
||||
import './index.less';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
|
||||
@observer
|
||||
export class ShieldHistory extends React.Component {
|
||||
public id: number = null;
|
||||
|
||||
private xFormWrapper: IXFormWrapper;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.id = Number(url.search.id);
|
||||
}
|
||||
|
||||
public silencesDetail(record: IMonitorSilences) {
|
||||
alarm.getSilencesDetail(record.silenceId).then((data) => {
|
||||
if (alarm.silencesDetail) {
|
||||
this.modifyInfo(alarm.silencesDetail);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public modifyInfo(record: IMonitorSilences) {
|
||||
Modal.info({
|
||||
title: '详情',
|
||||
content: (
|
||||
<ul className="monitor-detail">
|
||||
<li><b>告警名称:</b>{record.monitorName}</li>
|
||||
<li><b>开始时间:</b>{moment(record.startTime).format(timeFormat)}</li>
|
||||
<li><b>结束时间:</b>{moment(record.endTime).format(timeFormat)}</li>
|
||||
<li><b>说明:</b>{record.description}</li>
|
||||
</ul>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
public modifyMonitor(record: IMonitorSilences) {
|
||||
this.xFormWrapper = {
|
||||
formMap: [
|
||||
{
|
||||
key: 'monitorName',
|
||||
label: '告警名称',
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入告警名称',
|
||||
}],
|
||||
attrs: {
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'beginEndTime',
|
||||
label: '开始~结束时间',
|
||||
type: 'range_picker',
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入开始~结束时间',
|
||||
}],
|
||||
attrs: {
|
||||
placeholder: ['开始时间', '结束时间'],
|
||||
format: timeFormat,
|
||||
showTime: true,
|
||||
disabled: false,
|
||||
ranges: {
|
||||
'1小时': [moment(), moment().add(1, 'hour')],
|
||||
'2小时': [moment(), moment().add(2, 'hour')],
|
||||
'6小时': [moment(), moment().add(6, 'hour')],
|
||||
'12小时': [moment(), moment().add(12, 'hour')],
|
||||
'1天': [moment(), moment().add(1, 'day')],
|
||||
'2天': [moment(), moment().add(7, 'day')],
|
||||
'7天': [moment(), moment().add(7, 'day')],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
label: '说明',
|
||||
type: 'text_area',
|
||||
rules: [{
|
||||
required: true,
|
||||
}],
|
||||
attrs: {
|
||||
disabled: false,
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
},
|
||||
],
|
||||
formData: {
|
||||
monitorName: record.monitorName,
|
||||
beginEndTime: [moment(record.startTime), moment(record.endTime)],
|
||||
description: record.description,
|
||||
},
|
||||
okText: '确认',
|
||||
visible: true,
|
||||
width: 600,
|
||||
title: '编辑',
|
||||
onSubmit: (value: any) => {
|
||||
const params = {
|
||||
description: value.description,
|
||||
startTime: +moment(value.beginEndTime[0]).format('x'),
|
||||
endTime: +moment(value.beginEndTime[1]).format('x'),
|
||||
id: record.silenceId,
|
||||
monitorId: record.monitorId,
|
||||
} as IMonitorSilences;
|
||||
alarm.modifyMask(params, this.id).then(data => {
|
||||
notification.success({ message: '修改成功' });
|
||||
});
|
||||
},
|
||||
};
|
||||
wrapper.open(this.xFormWrapper);
|
||||
}
|
||||
|
||||
public deleteSilences(record: IMonitorSilences) {
|
||||
alarm.deleteSilences(this.id, record.silenceId).then(data => {
|
||||
notification.success({ message: '删除成功' });
|
||||
});
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
alarm.getMonitorSilences(this.id);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const monitorSilences: IMonitorSilences[] = alarm.monitorSilences ? alarm.monitorSilences : [];
|
||||
const monitorColumns = [
|
||||
{
|
||||
title: '监控名称',
|
||||
dataIndex: 'monitorName',
|
||||
key: 'monitorName',
|
||||
render: (text: string) => <span>{text}</span>,
|
||||
}, {
|
||||
title: '开始时间',
|
||||
dataIndex: 'startTime',
|
||||
key: 'startTime',
|
||||
render: (t: number) => moment(t).format(timeFormat),
|
||||
}, {
|
||||
title: '结束时间',
|
||||
dataIndex: 'endTime',
|
||||
key: 'endTime',
|
||||
render: (t: number) => moment(t).format(timeFormat),
|
||||
}, {
|
||||
title: '备注',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
}, {
|
||||
title: '操作',
|
||||
dataIndex: 'option',
|
||||
key: 'option',
|
||||
render: (action: any, record: IMonitorSilences) => {
|
||||
return(
|
||||
<>
|
||||
<a onClick={() => this.modifyMonitor(record)} className="action-button">修改</a>
|
||||
<a onClick={() => this.silencesDetail(record)} className="action-button">详情</a>
|
||||
<Popconfirm
|
||||
title="确定删除?"
|
||||
onConfirm={() => this.deleteSilences(record)}
|
||||
>
|
||||
<a>删除</a>
|
||||
</Popconfirm>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return(
|
||||
<>
|
||||
<Table dataSource={monitorSilences} columns={monitorColumns} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
87
kafka-manager-console/src/container/alarm/alarm-list.tsx
Normal file
87
kafka-manager-console/src/container/alarm/alarm-list.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import * as React from 'react';
|
||||
import { Table, Button } from 'component/antd';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { observer } from 'mobx-react';
|
||||
import { app } from 'store/app';
|
||||
import { getAlarmColumns } from './add-alarm/config';
|
||||
import { IMonitorStrategies } from 'types/base-type';
|
||||
import { pagination } from 'constants/table';
|
||||
import { urlPrefix } from 'constants/left-menu';
|
||||
import { alarm } from 'store/alarm';
|
||||
import 'styles/table-filter.less';
|
||||
|
||||
@observer
|
||||
export class AlarmList extends SearchAndFilterContainer {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
public getData<T extends IMonitorStrategies>(origin: T[]) {
|
||||
let data: T[] = [];
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
if (app.active !== '-1' || searchKey !== '') {
|
||||
data = origin.filter(d =>
|
||||
((d.name !== undefined && d.name !== null) && d.name.toLowerCase().includes(searchKey as string)
|
||||
|| ((d.operator !== undefined && d.operator !== null) && d.operator.toLowerCase().includes(searchKey as string)))
|
||||
&& (app.active === '-1' || d.appId === (app.active + '')),
|
||||
);
|
||||
} else {
|
||||
data = origin;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
public renderTableList(data: IMonitorStrategies[]) {
|
||||
return (
|
||||
<Table
|
||||
rowKey="key"
|
||||
columns={getAlarmColumns(urlPrefix)}
|
||||
dataSource={data}
|
||||
pagination={pagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public renderTable() {
|
||||
return this.renderTableList(this.getData(alarm.monitorStrategies));
|
||||
}
|
||||
|
||||
public renderOperationPanel() {
|
||||
return (
|
||||
<>
|
||||
{this.renderApp('应用:')}
|
||||
{this.renderSearch('名称:', '请输入告警名称或者操作人')}
|
||||
<li className="right-btn-1">
|
||||
<Button type="primary">
|
||||
<a href={`${urlPrefix}/alarm/add`}>
|
||||
新建告警
|
||||
</a>
|
||||
</Button>
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
if (!alarm.monitorStrategies.length) {
|
||||
alarm.getMonitorStrategies();
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="table-operation-panel">
|
||||
<ul>
|
||||
{this.renderOperationPanel()}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="table-wrapper">
|
||||
{this.renderTable()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
2
kafka-manager-console/src/container/alarm/index.tsx
Normal file
2
kafka-manager-console/src/container/alarm/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './alarm-list';
|
||||
export * from './add-alarm';
|
||||
46
kafka-manager-console/src/container/app-select.tsx
Normal file
46
kafka-manager-console/src/container/app-select.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Select, Tooltip } from 'component/antd';
|
||||
import { urlPrefix } from 'constants/left-menu';
|
||||
import { searchProps } from 'constants/table';
|
||||
import * as React from 'react';
|
||||
|
||||
const Option = Select.Option;
|
||||
|
||||
interface IStaffSelectProps {
|
||||
selectData?: any[];
|
||||
onChange?: (result: string []) => any;
|
||||
value?: string[];
|
||||
}
|
||||
|
||||
export class AppSelect extends React.Component<IStaffSelectProps> {
|
||||
|
||||
public render() {
|
||||
const { value, selectData } = this.props;
|
||||
const query = `application=1`;
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
placeholder="请选择"
|
||||
value={value || []}
|
||||
onChange={(e: string []) => this.handleChange(e)}
|
||||
{...searchProps}
|
||||
>
|
||||
{selectData.map((d: any) =>
|
||||
<Option value={d.appId} key={d.appId}>
|
||||
{d.name.length > 25 ? <Tooltip placement="bottomLeft" title={d.name}>{d.name}</Tooltip> : d.name}
|
||||
</Option>)}
|
||||
</Select>
|
||||
{
|
||||
selectData.length ? null : <i>
|
||||
没有应用?
|
||||
<a href={`${urlPrefix}/topic/app-list?${query}`}>立刻创建</a>
|
||||
</i>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public handleChange(params: string []) {
|
||||
const { onChange } = this.props;
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
onChange && onChange(params);
|
||||
}
|
||||
}
|
||||
205
kafka-manager-console/src/container/app/app-detail.tsx
Normal file
205
kafka-manager-console/src/container/app/app-detail.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import * as React from 'react';
|
||||
import { Table, Tabs, PageHeader, Descriptions, Divider, Spin, Icon, Tooltip } from 'component/antd';
|
||||
import { ILabelValue, ITopic, IAppItem, IConnectionInfo } from 'types/base-type';
|
||||
import urlQuery from 'store/url-query';
|
||||
import { tableFilter } from 'lib/utils';
|
||||
import { app } from 'store/app';
|
||||
import { topicStatusMap } from 'constants/status-map';
|
||||
import { urlPrefix } from 'constants/left-menu';
|
||||
import { observer } from 'mobx-react';
|
||||
import { pagination } from 'constants/table';
|
||||
import { copyString } from 'lib/utils';
|
||||
import { region } from 'store/region';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
import { modal } from 'store/modal';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { handlePageBack } from 'lib/utils';
|
||||
import moment = require('moment');
|
||||
import './index.less';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
@observer
|
||||
export class AppDetail extends SearchAndFilterContainer {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
public getColumns(data: ITopic[]) {
|
||||
const statusColumn = Object.assign({
|
||||
title: '权限',
|
||||
dataIndex: 'access',
|
||||
key: 'access',
|
||||
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 { currentTab } = app;
|
||||
const columns = [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
sorter: (a: ITopic, b: ITopic) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
render: (text: string, r: ITopic) => (
|
||||
<Tooltip placement="bottomLeft" title={text}>
|
||||
<a
|
||||
// tslint:disable-next-line:max-line-length
|
||||
href={`${urlPrefix}/topic/topic-detail?clusterId=${r.clusterId}&topic=${r.topicName}®ion=${region.currentRegion}`}
|
||||
>{text}
|
||||
</a>
|
||||
</Tooltip>),
|
||||
}, {
|
||||
title: '集群名称',
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
}, {
|
||||
title: '申请时间',
|
||||
dataIndex: 'gmtCreate',
|
||||
key: 'gmtCreate',
|
||||
render: (t: number) => moment(t).format(timeFormat),
|
||||
},
|
||||
statusColumn,
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
key: 'operation',
|
||||
render: (text: string, record: ITopic) =>
|
||||
<a key={record.key} onClick={() => this.cancelPermission(record)}>取消权限</a>,
|
||||
},
|
||||
];
|
||||
const tableColumns = [].concat(columns);
|
||||
|
||||
if (currentTab === '1') {
|
||||
tableColumns.splice(4, 2);
|
||||
}
|
||||
|
||||
return tableColumns;
|
||||
}
|
||||
|
||||
public cancelPermission(record: ITopic) {
|
||||
modal.showCancelTopicPermission(record);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
if (urlQuery.appId) {
|
||||
app.getAppDetail(urlQuery.appId);
|
||||
app.getAppTopicList(urlQuery.appId);
|
||||
}
|
||||
}
|
||||
|
||||
public renderBaseInfo(baseInfo: IAppItem) {
|
||||
const infoList: ILabelValue[] = [{
|
||||
label: '应用名称',
|
||||
value: baseInfo.name,
|
||||
}, {
|
||||
label: '负责人',
|
||||
value: baseInfo.principals,
|
||||
}];
|
||||
const infoCopy: ILabelValue[] = [{
|
||||
label: 'AppID',
|
||||
value: baseInfo.appId,
|
||||
}, {
|
||||
label: '密钥',
|
||||
value: baseInfo.password,
|
||||
}];
|
||||
return (
|
||||
<PageHeader
|
||||
onBack={() => handlePageBack('/topic/app-list')}
|
||||
title={baseInfo.name || ''}
|
||||
>
|
||||
<Divider />
|
||||
<Descriptions column={2}>
|
||||
{infoList.map((item, key) => (
|
||||
<Descriptions.Item key={key} label={item.label}>
|
||||
<Tooltip placement="bottomLeft" title={item.value}>
|
||||
<span className="overview-bootstrap">
|
||||
<i className="overview-boot">{item.value}</i>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Descriptions.Item>
|
||||
))}
|
||||
</Descriptions>
|
||||
<Descriptions column={2}>
|
||||
{infoCopy.map((item, key) => (
|
||||
<Descriptions.Item key={key} label={item.label}>
|
||||
<Icon
|
||||
onClick={() => copyString(item.value)}
|
||||
type="copy"
|
||||
className="didi-theme"
|
||||
/> {item.value}
|
||||
</Descriptions.Item>
|
||||
))}
|
||||
</Descriptions>
|
||||
<Descriptions size="small" column={1}>
|
||||
<Descriptions.Item label="应用描述">
|
||||
<Tooltip placement="bottomLeft" title={baseInfo.description}>
|
||||
<span className="overview-bootstrap" style={{width: '600px'}}>
|
||||
<i className="overview-boot"> {baseInfo.description} </i>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</PageHeader>
|
||||
);
|
||||
}
|
||||
|
||||
public getData<T extends ITopic>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: ITopic) =>
|
||||
(item.topicName !== undefined && item.topicName !== null) && item.topicName.toLowerCase().includes(searchKey as string)
|
||||
|| (item.clusterName !== undefined && item.clusterName !== null) && item.clusterName.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public renderTable() {
|
||||
return (
|
||||
<Spin spinning={app.loading}>
|
||||
<Table
|
||||
columns={this.getColumns(app.topicList)}
|
||||
dataSource={this.getData(app.topicList)}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
|
||||
public onChangeTab(e: string) {
|
||||
app.setCurrentTab(e);
|
||||
if (urlQuery.appId) {
|
||||
app.getAppTopicList(urlQuery.appId);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { currentTab } = app;
|
||||
return (
|
||||
<>
|
||||
<div className="app-container">
|
||||
<div className="base-info">
|
||||
{this.renderBaseInfo(app.baseInfo)}
|
||||
</div>
|
||||
<div className="k-row">
|
||||
<Tabs defaultActiveKey="1" type="card" onChange={(e) => this.onChangeTab(e)}>
|
||||
<TabPane tab="创建的Topic" key="1" />
|
||||
<TabPane tab="有权限Topic" key="2" />
|
||||
</Tabs>
|
||||
<ul className="k-tab">
|
||||
<li>{currentTab === '1' ? '创建的Topic' : '有权限Topic'}</li>
|
||||
{this.renderSearch('', '请输入Topic名称/集群名称')}
|
||||
</ul>
|
||||
{this.renderTable()}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
136
kafka-manager-console/src/container/app/app-list.tsx
Normal file
136
kafka-manager-console/src/container/app/app-list.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import { Table, Tooltip, Spin } from 'component/antd';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { observer } from 'mobx-react';
|
||||
import 'styles/table-filter.less';
|
||||
import { IAppItem } from 'types/base-type';
|
||||
import { app } from 'store/app';
|
||||
import { pagination, cellStyle } from 'constants/table';
|
||||
import { showEditModal } from 'container/modal';
|
||||
import { modal } from 'store/modal';
|
||||
import * as React from 'react';
|
||||
|
||||
interface IProps {
|
||||
from: string;
|
||||
}
|
||||
@observer
|
||||
export class CommonAppList extends SearchAndFilterContainer {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
public from = 'topic';
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.from = props.from;
|
||||
}
|
||||
|
||||
public getColumns = (data: IAppItem[]) => {
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'AppID',
|
||||
dataIndex: 'appId',
|
||||
key: 'appId',
|
||||
width: '15%',
|
||||
sorter: (a: IAppItem, b: IAppItem) => a.appId.localeCompare(b.appId),
|
||||
render: (text: string, record: IAppItem) => {
|
||||
return (
|
||||
<a href={`${this.urlPrefix}/topic/app-detail?appId=${record.appId}`}>{text}</a>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '应用名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: '20%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 150,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
render: (text: string, record: IAppItem) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={record.name}>{text}</Tooltip>);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '应用描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
width: '25%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 150,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
render: (text: string, record: IAppItem) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={record.description} >{text}</Tooltip>);
|
||||
},
|
||||
}, {
|
||||
title: '负责人',
|
||||
dataIndex: 'principals',
|
||||
key: 'principals',
|
||||
width: '25%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 150,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
render: (text: string) => <Tooltip placement="bottomLeft" title={text} >{text}</Tooltip>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
key: 'operation',
|
||||
width: '15%',
|
||||
render: (text: any, record: IAppItem) => {
|
||||
return (
|
||||
<span className="table-operation">
|
||||
<a onClick={() => showEditModal(record, this.from)}>修改</a>
|
||||
<a onClick={() => showEditModal(record, this.from, true)}>详情</a>
|
||||
<a onClick={() => this.getOnlineConnect(record)}>申请下线</a>
|
||||
</span>);
|
||||
},
|
||||
},
|
||||
];
|
||||
return columns;
|
||||
}
|
||||
|
||||
public getOnlineConnect(record: IAppItem) {
|
||||
modal.showOfflineAppModal(record.appId);
|
||||
}
|
||||
|
||||
public getData<T extends IAppItem>(origin: T[]) {
|
||||
let data: T[] = [];
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: IAppItem) =>
|
||||
((item.name !== undefined && item.name !== null) && item.name.toLowerCase().includes(searchKey as string)) ||
|
||||
((item.principals !== undefined && item.principals !== null) && item.principals.toLowerCase().includes(searchKey as string)) ||
|
||||
((item.appId !== undefined && item.appId !== null) && item.appId.toLowerCase().includes(searchKey as string)) ) : origin;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public renderTableList(data: IAppItem[]) {
|
||||
return (
|
||||
<>
|
||||
<Spin spinning={app.loading}>
|
||||
<Table
|
||||
rowKey="key"
|
||||
columns={this.getColumns(data)}
|
||||
dataSource={data}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</Spin>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
38
kafka-manager-console/src/container/app/index.less
Normal file
38
kafka-manager-console/src/container/app/index.less
Normal file
@@ -0,0 +1,38 @@
|
||||
|
||||
.app-container {
|
||||
.base-info {
|
||||
background-color: white;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ant-divider-horizontal {
|
||||
margin: 0px 0px 24px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.app-detail-ul {
|
||||
border: 1px solid #000;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis; //超出部分以省略号显示
|
||||
white-space: nowrap;
|
||||
min-width: 600px;
|
||||
}
|
||||
|
||||
.custom-content {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.overview {
|
||||
width: 700px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.appitem-detail {
|
||||
margin-top: 30px;
|
||||
|
||||
p {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
2
kafka-manager-console/src/container/app/index.tsx
Normal file
2
kafka-manager-console/src/container/app/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './app-list';
|
||||
export * from './app-detail';
|
||||
@@ -0,0 +1,159 @@
|
||||
import * as React from 'react';
|
||||
import { Table } from 'component/antd';
|
||||
import Url from 'lib/url-parser';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { observer } from 'mobx-react';
|
||||
import { pagination } from 'constants/table';
|
||||
import { IBrokerData, IEnumsMap } from 'types/base-type';
|
||||
import { admin } from 'store/admin';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { transBToMB } from 'lib/utils';
|
||||
import moment from 'moment';
|
||||
import './index.less';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
|
||||
@observer
|
||||
export class ClusterBroker extends SearchAndFilterContainer {
|
||||
public clusterId: number;
|
||||
public clusterName: string;
|
||||
|
||||
public state = {
|
||||
filterPeakFlowVisible: false,
|
||||
filterReplicatedVisible: false,
|
||||
filterStatusVisible: false,
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
this.clusterName = decodeURI(url.search.clusterName);
|
||||
}
|
||||
|
||||
public getData<T extends IBrokerData>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: IBrokerData) =>
|
||||
(item.brokerId !== undefined && item.brokerId !== null) && (item.brokerId + '').toLowerCase().includes(searchKey as string)
|
||||
|| (item.host !== undefined && item.host !== null) && item.host.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public renderBrokerData() {
|
||||
let peakFlow = [] as IEnumsMap[];
|
||||
peakFlow = admin.peakFlowStatusList ? admin.peakFlowStatusList : peakFlow;
|
||||
const peakFlowStatus = Object.assign({
|
||||
title: '峰值状态',
|
||||
dataIndex: 'peakFlowStatus',
|
||||
key: 'peakFlowStatus',
|
||||
filters: peakFlow.map(ele => ({ text: ele.message, value: ele.code + '' })),
|
||||
onFilter: (value: string, record: IBrokerData) => record.peakFlowStatus === +value,
|
||||
render: (value: number) => {
|
||||
let messgae: string;
|
||||
peakFlow.map(ele => {
|
||||
if (ele.code === value) {
|
||||
messgae = ele.message;
|
||||
}
|
||||
});
|
||||
return(
|
||||
<span>{messgae}</span>
|
||||
);
|
||||
},
|
||||
}, this.renderColumnsFilter('filterPeakFlowVisible'));
|
||||
|
||||
const underReplicated = Object.assign({
|
||||
title: '副本状态',
|
||||
dataIndex: 'underReplicated',
|
||||
key: 'underReplicated',
|
||||
filters: [{ text: '同步', value: 'false' }, { text: '未同步', value: 'true' }],
|
||||
onFilter: (value: string, record: IBrokerData) => record.underReplicated === (value === 'true') ? true : false,
|
||||
render: (t: boolean) => <span className={t ? 'fail' : 'success'}>{t ? '未同步' : '同步'}</span>,
|
||||
}, this.renderColumnsFilter('filterReplicatedVisible'));
|
||||
|
||||
const status = Object.assign({
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
filters: [{ text: '未使用', value: '-1' }, { text: '使用中', value: '0' }],
|
||||
onFilter: (value: string, record: IBrokerData) => record.status === Number(value),
|
||||
render: (t: number) => t === 0 ? '使用中' : '未使用',
|
||||
}, this.renderColumnsFilter('filterStatusVisible'));
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'brokerId',
|
||||
key: 'brokerId',
|
||||
sorter: (a: IBrokerData, b: IBrokerData) => b.brokerId - a.brokerId,
|
||||
render: (text: number, record: IBrokerData) => <span>{text}</span>,
|
||||
},
|
||||
{
|
||||
title: '主机',
|
||||
dataIndex: 'host',
|
||||
key: 'host',
|
||||
sorter: (a: any, b: any) => a.host.charCodeAt(0) - b.host.charCodeAt(0),
|
||||
},
|
||||
{
|
||||
title: 'Port',
|
||||
dataIndex: 'port',
|
||||
key: 'port',
|
||||
sorter: (a: IBrokerData, b: IBrokerData) => b.port - a.port,
|
||||
},
|
||||
{
|
||||
title: 'JMX Port',
|
||||
dataIndex: 'jmxPort',
|
||||
key: 'jmxPort',
|
||||
sorter: (a: IBrokerData, b: IBrokerData) => b.jmxPort - a.jmxPort,
|
||||
},
|
||||
{
|
||||
title: '启动时间',
|
||||
dataIndex: 'startTime',
|
||||
key: 'startTime',
|
||||
sorter: (a: IBrokerData, b: IBrokerData) => b.startTime - a.startTime,
|
||||
render: (time: number) => moment(time).format(timeFormat),
|
||||
},
|
||||
{
|
||||
title: 'Bytes In(MB/s)',
|
||||
dataIndex: 'byteIn',
|
||||
key: 'byteIn',
|
||||
sorter: (a: IBrokerData, b: IBrokerData) => b.byteIn - a.byteIn,
|
||||
render: (t: number) => transBToMB(t),
|
||||
},
|
||||
{
|
||||
title: 'Bytes Out(MB/s)',
|
||||
dataIndex: 'byteOut',
|
||||
key: 'byteOut',
|
||||
sorter: (a: IBrokerData, b: IBrokerData) => b.byteOut - a.byteOut,
|
||||
render: (t: number) => transBToMB(t),
|
||||
},
|
||||
// peakFlowStatus,
|
||||
underReplicated,
|
||||
status,
|
||||
];
|
||||
return (
|
||||
<Table dataSource={this.getData(cluster.clusterBroker)} columns={columns} pagination={pagination} loading={cluster.loading} />
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
cluster.getClusterDetailBroker(this.clusterId);
|
||||
// admin.getBrokersMetadata(this.clusterId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<div className="k-row">
|
||||
<ul className="k-tab">
|
||||
<li>{this.props.tab}</li>
|
||||
{this.renderSearch('', '请输入ID或主机')}
|
||||
</ul>
|
||||
{this.renderBrokerData()}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import * as React from 'react';
|
||||
import { PageHeader, Descriptions, Tooltip, Icon, Spin } from 'component/antd';
|
||||
import { ILabelValue, IBasicInfo, IOptionType, IClusterReal } from 'types/base-type';
|
||||
import { selectOptionMap } from 'constants/status-map';
|
||||
import { observer } from 'mobx-react';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { clusterTypeMap } from 'constants/status-map';
|
||||
import { copyString } from 'lib/utils';
|
||||
import Url from 'lib/url-parser';
|
||||
import moment from 'moment';
|
||||
import './index.less';
|
||||
import { StatusGraghCom } from 'component/flow-table';
|
||||
import { renderTrafficTable, NetWorkFlow } from 'container/network-flow';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
|
||||
interface IOverview {
|
||||
basicInfo: IBasicInfo;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClusterOverview extends React.Component<IOverview> {
|
||||
public clusterId: number;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
}
|
||||
|
||||
public clusterContent() {
|
||||
const content = this.props.basicInfo as IBasicInfo;
|
||||
const clusterContent = [{
|
||||
value: content.clusterName,
|
||||
label: '集群名称',
|
||||
}, {
|
||||
value: clusterTypeMap[content.mode],
|
||||
label: '集群类型',
|
||||
}, {
|
||||
value: moment(content.gmtCreate).format(timeFormat),
|
||||
label: '接入时间',
|
||||
}, {
|
||||
value: content.physicalClusterId,
|
||||
label: '物理集群ID',
|
||||
}];
|
||||
const clusterInfo = [{
|
||||
value: content.clusterVersion,
|
||||
label: 'kafka版本',
|
||||
}, {
|
||||
value: content.bootstrapServers,
|
||||
label: 'Bootstrap Severs',
|
||||
}];
|
||||
return (
|
||||
<>
|
||||
<div className="chart-title">基本信息</div>
|
||||
<PageHeader className="detail" title="">
|
||||
<Descriptions size="small" column={3}>
|
||||
{clusterContent.map((item: ILabelValue, index: number) => (
|
||||
<Descriptions.Item key={index} label={item.label} >
|
||||
{item.value}
|
||||
</Descriptions.Item>
|
||||
))}
|
||||
{clusterInfo.map((item: ILabelValue, index: number) => (
|
||||
<Descriptions.Item key={index} label={item.label}>
|
||||
<Tooltip placement="bottomLeft" title={item.value}>
|
||||
<span className="overview-bootstrap">
|
||||
<Icon
|
||||
onClick={() => copyString(item.value)}
|
||||
type="copy"
|
||||
className="didi-theme overview-theme"
|
||||
/>
|
||||
<i className="overview-boot">{item.value}</i>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Descriptions.Item>
|
||||
))}
|
||||
</Descriptions>
|
||||
</PageHeader>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public updateRealStatus = () => {
|
||||
cluster.getClusterDetailRealTime(this.clusterId);
|
||||
}
|
||||
|
||||
public onSelectChange(e: IOptionType) {
|
||||
return cluster.changeType(e);
|
||||
}
|
||||
|
||||
public getOptionApi = () => {
|
||||
return cluster.getClusterDetailMetrice(this.clusterId);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
cluster.getClusterBasicInfo(this.clusterId);
|
||||
cluster.getClusterDetailRealTime(this.clusterId);
|
||||
}
|
||||
|
||||
public renderHistoryTraffic() {
|
||||
return (
|
||||
<NetWorkFlow
|
||||
key="1"
|
||||
selectArr={selectOptionMap}
|
||||
type={cluster.type}
|
||||
selectChange={(value: IOptionType) => this.onSelectChange(value)}
|
||||
getApi={() => this.getOptionApi()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public renderTrafficInfo = () => {
|
||||
return (
|
||||
<Spin spinning={cluster.realLoading}>
|
||||
{renderTrafficTable(this.updateRealStatus, StatusGragh)}
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<div className="base-info">
|
||||
{this.clusterContent()}
|
||||
{this.renderTrafficInfo()}
|
||||
{this.renderHistoryTraffic()}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export class StatusGragh extends StatusGraghCom<IClusterReal> {
|
||||
public getData = () => {
|
||||
return cluster.clusterRealData;
|
||||
}
|
||||
public getLoading = () => {
|
||||
return cluster.realLoading;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import * as React from 'react';
|
||||
import Url from 'lib/url-parser';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { Table, Tooltip } from 'antd';
|
||||
import { pagination, cellStyle } from 'constants/table';
|
||||
import { observer } from 'mobx-react';
|
||||
import { IClusterTopics } from 'types/base-type';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { urlPrefix } from 'constants/left-menu';
|
||||
import { transMSecondToHour } from 'lib/utils';
|
||||
import { region } from 'store/region';
|
||||
import './index.less';
|
||||
|
||||
import moment = require('moment');
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
|
||||
@observer
|
||||
export class ClusterTopic extends SearchAndFilterContainer {
|
||||
public clusterId: number;
|
||||
public clusterTopicsFrom: IClusterTopics;
|
||||
|
||||
public state = {
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
}
|
||||
|
||||
public getData<T extends IClusterTopics>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: IClusterTopics) =>
|
||||
(item.topicName !== undefined && item.topicName !== null) && item.topicName.toLowerCase().includes(searchKey as string)
|
||||
|| (item.appName !== undefined && item.appName !== null) && item.appName.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public clusterTopicList() {
|
||||
const columns = [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
width: '20%',
|
||||
sorter: (a: IClusterTopics, b: IClusterTopics) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
render: (text: string, record: IClusterTopics) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={record.topicName} >
|
||||
<a
|
||||
// tslint:disable-next-line:max-line-length
|
||||
href={`${urlPrefix}/topic/topic-detail?clusterId=${record.logicalClusterId}&topic=${record.topicName}®ion=${region.currentRegion}`}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
</Tooltip>);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'QPS',
|
||||
dataIndex: 'produceRequest',
|
||||
key: 'produceRequest',
|
||||
width: '10%',
|
||||
sorter: (a: IClusterTopics, b: IClusterTopics) => b.produceRequest - a.produceRequest,
|
||||
render: (t: number) => t === null ? '' : t.toFixed(2),
|
||||
},
|
||||
{
|
||||
title: 'Bytes In(KB/s)',
|
||||
dataIndex: 'byteIn',
|
||||
key: 'byteIn',
|
||||
width: '15%',
|
||||
sorter: (a: IClusterTopics, b: IClusterTopics) => b.byteIn - a.byteIn,
|
||||
render: (val: number) => val === null ? '' : (val / 1024).toFixed(2),
|
||||
},
|
||||
{
|
||||
title: '所属应用',
|
||||
dataIndex: 'appName',
|
||||
key: 'appName',
|
||||
width: '15%',
|
||||
render: (val: string, record: IClusterTopics) => (
|
||||
<Tooltip placement="bottomLeft" title={record.appId} >
|
||||
{val}
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '保存时间(h)',
|
||||
dataIndex: 'retentionTime',
|
||||
key: 'retentionTime',
|
||||
width: '15%',
|
||||
sorter: (a: IClusterTopics, b: IClusterTopics) => b.retentionTime - a.retentionTime,
|
||||
render: (time: any) => transMSecondToHour(time),
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updateTime',
|
||||
key: 'updateTime',
|
||||
width: '20%',
|
||||
render: (t: number) => moment(t).format(timeFormat),
|
||||
},
|
||||
{
|
||||
title: 'Topic说明',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
width: '30%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 200,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
render: (val: string) => (
|
||||
<Tooltip placement="topLeft" title={val} >
|
||||
{val}
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<div className="k-row">
|
||||
<ul className="k-tab">
|
||||
<li>{this.props.tab}</li>
|
||||
{this.renderSearch('', '请输入Topic名称或所属应用')}
|
||||
</ul>
|
||||
<Table
|
||||
loading={cluster.loading}
|
||||
rowKey="topicName"
|
||||
dataSource={this.getData(cluster.clusterTopics)}
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
cluster.getClusterDetailTopics(this.clusterId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
cluster.clusterTopics ? <> {this.clusterTopicList()} </> : null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { Table, Tooltip } from 'component/antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import { pagination } from 'constants/table';
|
||||
import Url from 'lib/url-parser';
|
||||
import { IThrottles } from 'types/base-type';
|
||||
import { cluster } from 'store/cluster';
|
||||
import './index.less';
|
||||
|
||||
@observer
|
||||
export class CurrentLimiting extends SearchAndFilterContainer {
|
||||
public clusterId: number;
|
||||
|
||||
public state = {
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
}
|
||||
|
||||
public getData<T extends IThrottles>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: IThrottles) =>
|
||||
(item.topicName !== undefined && item.topicName !== null) && item.topicName.toLowerCase().includes(searchKey as string)
|
||||
|| (item.appId !== undefined && item.appId !== null) && item.appId.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public renderController() {
|
||||
const clientType = Object.assign({
|
||||
title: '类型',
|
||||
dataIndex: 'throttleClientType',
|
||||
key: 'throttleClientType',
|
||||
filters: [{ text: 'fetch', value: 'FetchThrottleTime' }, { text: 'produce', value: 'ProduceThrottleTime' }],
|
||||
onFilter: (value: string, record: IThrottles) => record.throttleClientType === value,
|
||||
render: (t: string) => t,
|
||||
}, this.renderColumnsFilter('filterStatus'));
|
||||
const columns = [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
key: 'topicName',
|
||||
dataIndex: 'topicName',
|
||||
sorter: (a: IThrottles, b: IThrottles) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
render: (val: string) => <Tooltip placement="bottomLeft" title={val}> {val} </Tooltip>,
|
||||
},
|
||||
{
|
||||
title: '应用ID',
|
||||
dataIndex: 'appId',
|
||||
key: 'appId',
|
||||
sorter: (a: IThrottles, b: IThrottles) => a.appId.charCodeAt(0) - b.appId.charCodeAt(0),
|
||||
},
|
||||
clientType,
|
||||
{
|
||||
title: 'Broker',
|
||||
dataIndex: 'brokerIdList',
|
||||
key: 'brokerIdList',
|
||||
render: (value: number[]) => {
|
||||
const num = value ? `[${value.join(',')}]` : '';
|
||||
return(
|
||||
<span>{num}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={this.getData(cluster.clustersThrottles)}
|
||||
pagination={pagination}
|
||||
rowKey="key"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
cluster.getClusterDetailThrottles(this.clusterId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<div className="k-row">
|
||||
<ul className="k-tab">
|
||||
<li>{this.props.tab}</li>
|
||||
{this.renderSearch('', '请输入Topic名称或AppId')}
|
||||
</ul>
|
||||
{this.renderController()}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
|
||||
.table-operation-bar {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
z-index: 100;
|
||||
li {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
.ant-select {
|
||||
width: 150px;
|
||||
}
|
||||
.ant-input-search {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.traffic-table {
|
||||
margin: 10px 0;
|
||||
|
||||
.traffic-header {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
font-weight: bold;
|
||||
background: rgb(245, 245, 245);
|
||||
border: 1px solid #e8e8e8;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
span {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 13px;
|
||||
line-height: 44px;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
.k-abs {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.implement-button {
|
||||
float: right;
|
||||
margin-right: -120px;
|
||||
}
|
||||
|
||||
.overview-bootstrap {
|
||||
width: 200px;
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
line-height: 20px;
|
||||
.overview-theme {
|
||||
margin: 3px;
|
||||
}
|
||||
.overview-boot {
|
||||
font-style: normal;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Tabs, PageHeader } from 'antd';
|
||||
import { ClusterOverview } from './cluster-overview';
|
||||
import { ClusterTopic } from './cluster-topic';
|
||||
import { ClusterBroker } from './cluster-broker';
|
||||
import { CurrentLimiting } from './current-limiting';
|
||||
import { IBasicInfo } from 'types/base-type';
|
||||
import { handleTabKey } from 'lib/utils';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { handlePageBack } from 'lib/utils';
|
||||
import Url from 'lib/url-parser';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
@observer
|
||||
export class ClusterDetail extends React.Component {
|
||||
public clusterId: number;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
}
|
||||
public componentDidMount() {
|
||||
cluster.getClusterBasicInfo(this.clusterId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
let content = {} as IBasicInfo;
|
||||
content = cluster.basicInfo ? cluster.basicInfo : content;
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
className="detail topic-detail-header"
|
||||
onBack={() => handlePageBack('/cluster')}
|
||||
title={`集群列表/${content.clusterName || ''}`}
|
||||
/>
|
||||
<Tabs activeKey={location.hash.substr(1) || '1'} type="card" onChange={handleTabKey}>
|
||||
<TabPane tab="集群概览" key="1">
|
||||
<ClusterOverview basicInfo={content}/>
|
||||
</TabPane>
|
||||
<TabPane tab="Topic信息" key="2">
|
||||
<ClusterTopic tab={'Topic信息'} />
|
||||
</TabPane>
|
||||
<TabPane tab="Broker信息" key="3">
|
||||
<ClusterBroker tab={'Broker信息'} />
|
||||
</TabPane>
|
||||
<TabPane tab="限流信息" key="7">
|
||||
<CurrentLimiting tab={'限流信息'} />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
95
kafka-manager-console/src/container/cluster/config.tsx
Normal file
95
kafka-manager-console/src/container/cluster/config.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import * as React from 'react';
|
||||
import { clusterTypeMap } from 'constants/status-map';
|
||||
import { notification, Tooltip, Modal, Table, message, Icon } from 'component/antd';
|
||||
import { IClusterData } from 'types/base-type';
|
||||
import { showCpacityModal } from 'container/modal';
|
||||
import moment = require('moment');
|
||||
import { cellStyle } from 'constants/table';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
import { modal } from 'store/modal';
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
||||
export const getClusterColumns = (urlPrefix: string) => {
|
||||
return [
|
||||
{
|
||||
title: '集群名称',
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
width: '15%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 120,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
sorter: (a: IClusterData, b: IClusterData) => a.clusterName.charCodeAt(0) - b.clusterName.charCodeAt(0),
|
||||
render: (text: string, record: IClusterData) => (
|
||||
<Tooltip placement="bottomLeft" title={text} >
|
||||
<a href={`${urlPrefix}/cluster/cluster-detail?clusterId=${record.clusterId}`}> {text} </a>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Topic数量',
|
||||
dataIndex: 'topicNum',
|
||||
key: 'topicNum',
|
||||
width: '10%',
|
||||
sorter: (a: IClusterData, b: IClusterData) => b.topicNum - a.topicNum,
|
||||
},
|
||||
{
|
||||
title: '集群类型',
|
||||
dataIndex: 'mode',
|
||||
key: 'mode',
|
||||
width: '10%',
|
||||
render: (text: number) => (clusterTypeMap[text] || ''),
|
||||
},
|
||||
{
|
||||
title: '集群版本',
|
||||
dataIndex: 'clusterVersion',
|
||||
key: 'clusterVersion',
|
||||
width: '25%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 200,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
render: (text: string) => <Tooltip placement="bottomLeft" title={text} >{text}</Tooltip>,
|
||||
}, {
|
||||
title: '接入时间',
|
||||
dataIndex: 'gmtCreate',
|
||||
key: 'gmtCreate',
|
||||
width: '15%',
|
||||
sorter: (a: IClusterData, b: IClusterData) => b.gmtCreate - a.gmtCreate,
|
||||
render: (t: number) => moment(t).format(timeFormat),
|
||||
}, {
|
||||
title: '修改时间',
|
||||
dataIndex: 'gmtModify',
|
||||
key: 'gmtModify',
|
||||
width: '15%',
|
||||
sorter: (a: IClusterData, b: IClusterData) => b.gmtModify - a.gmtModify,
|
||||
render: (t: number) => moment(t).format(timeFormat),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
width: '10%',
|
||||
render: (val: string, record: IClusterData) => (
|
||||
<>
|
||||
{
|
||||
record.mode !== 0 ? <>
|
||||
<a onClick={() => showConfirm(record)} className="action-button">申请下线</a>
|
||||
<a onClick={() => showCpacityModal(record)}>扩缩容</a>
|
||||
</> : null
|
||||
}
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const showConfirm = (record: IClusterData) => {
|
||||
modal.showOfflineClusterModal(record.clusterId);
|
||||
};
|
||||
24
kafka-manager-console/src/container/cluster/index.less
Normal file
24
kafka-manager-console/src/container/cluster/index.less
Normal file
@@ -0,0 +1,24 @@
|
||||
.apply-button {
|
||||
Button {
|
||||
position: absolute;
|
||||
right : 25px;
|
||||
z-index: 999999;
|
||||
}
|
||||
Table {
|
||||
margin-top: 40px;
|
||||
}
|
||||
.new_task_but{
|
||||
right: 5px;
|
||||
}
|
||||
}
|
||||
.radio-and-input{
|
||||
.radio-and-input-inputNuber{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 251px;
|
||||
justify-content: space-between;
|
||||
.ant-input-number {
|
||||
width: 210px;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
kafka-manager-console/src/container/cluster/index.tsx
Normal file
2
kafka-manager-console/src/container/cluster/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './my-cluster';
|
||||
export * from './cluster-detail';
|
||||
254
kafka-manager-console/src/container/cluster/my-cluster.tsx
Normal file
254
kafka-manager-console/src/container/cluster/my-cluster.tsx
Normal file
@@ -0,0 +1,254 @@
|
||||
import * as React from 'react';
|
||||
import './index.less';
|
||||
import { wrapper } from 'store';
|
||||
import { users } from 'store/users';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
notification,
|
||||
Radio,
|
||||
RadioChangeEvent,
|
||||
InputNumber,
|
||||
} from 'component/antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import { pagination } from 'constants/table';
|
||||
import { urlPrefix } from 'constants/left-menu';
|
||||
import { getClusterColumns } from './config';
|
||||
import { app } from 'store/app';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { IOrderParams, IClusterData, IRadioItem } from 'types/base-type';
|
||||
import { region } from 'store';
|
||||
|
||||
@observer
|
||||
export class MyCluster extends SearchAndFilterContainer {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
public applyCluster() {
|
||||
const xFormModal = {
|
||||
formMap: [
|
||||
{
|
||||
key: 'idc',
|
||||
label: '数据中心',
|
||||
defaultValue: region.regionName,
|
||||
rules: [{ required: true, message: '请输入数据中心' }],
|
||||
attrs: {
|
||||
placeholder: '请输入数据中心',
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'appId',
|
||||
label: '所属应用',
|
||||
rules: [{ required: true, message: '请选择所属应用' }],
|
||||
type: 'select',
|
||||
options: app.data.map((item) => {
|
||||
return {
|
||||
label: item.name,
|
||||
value: item.appId,
|
||||
};
|
||||
}),
|
||||
attrs: {
|
||||
placeholder: '请选择所属应用',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'mode',
|
||||
label: '集群类型',
|
||||
type: 'radio_group',
|
||||
options: cluster.clusterMode,
|
||||
rules: [{ required: true, message: '请选择' }],
|
||||
attrs: {
|
||||
placeholder: '请选择集群',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'bytesIn',
|
||||
label: '峰值流量',
|
||||
type: 'custom',
|
||||
rules: [{ required: true, message: '请选择' }],
|
||||
customFormItem: <RadioAndInput />,
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
label: '申请原因',
|
||||
type: 'text_area',
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
pattern: /^.{5,}.$/,
|
||||
message: '请输入至少5个字符',
|
||||
},
|
||||
],
|
||||
attrs: {
|
||||
placeholder: `请大致说明集群申请的原因、用途,对稳定性SLA的要求等。
|
||||
例如:
|
||||
原因:xxx, 用途:xxx, 稳定性:xxx`,
|
||||
},
|
||||
},
|
||||
],
|
||||
formData: {},
|
||||
visible: true,
|
||||
title: '申请集群',
|
||||
okText: '确认',
|
||||
onSubmit: (value: any) => {
|
||||
value.idc = region.currentRegion;
|
||||
const params = JSON.parse(JSON.stringify(value));
|
||||
delete params.description;
|
||||
if (typeof params.bytesIn === 'number') {
|
||||
params.bytesIn = params.bytesIn * 1024 * 1024;
|
||||
}
|
||||
const clusterParams: IOrderParams = {
|
||||
type: 4,
|
||||
applicant: users.currentUser.username,
|
||||
description: value.description,
|
||||
extensions: JSON.stringify(params),
|
||||
};
|
||||
cluster.applyCluster(clusterParams).then((data) => {
|
||||
notification.success({
|
||||
message: '申请集群成功',
|
||||
// description: this.aHrefUrl(data.id),
|
||||
});
|
||||
window.location.href = `${urlPrefix}/user/order-detail/?orderId=${data.id}®ion=${region.currentRegion}`;
|
||||
});
|
||||
},
|
||||
};
|
||||
wrapper.open(xFormModal);
|
||||
}
|
||||
|
||||
public aHrefUrl(id: number) {
|
||||
return (
|
||||
<>
|
||||
<a href={urlPrefix + '/user/order-detail/?orderId=' + id}>
|
||||
是否跳转到集群审批页?
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
if (!cluster.clusterData.length) {
|
||||
cluster.getClusters();
|
||||
}
|
||||
if (!cluster.clusterModes.length) {
|
||||
cluster.getClusterModes();
|
||||
}
|
||||
if (!app.data.length) {
|
||||
app.getAppList();
|
||||
}
|
||||
}
|
||||
|
||||
public renderOperationPanel() {
|
||||
return (
|
||||
<ul>
|
||||
{this.renderSearch('', '请输入集群名称')}
|
||||
<li className="right-btn-1">
|
||||
<Button type="primary" onClick={() => this.applyCluster()}>
|
||||
申请集群
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
public getData<T extends IClusterData>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: IClusterData) =>
|
||||
(item.clusterName !== undefined && item.clusterName !== null) && item.clusterName.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public renderTable() {
|
||||
return (
|
||||
<Table
|
||||
loading={cluster.loading}
|
||||
rowKey="clusterId"
|
||||
dataSource={this.getData(cluster.clusterData)}
|
||||
columns={getClusterColumns(urlPrefix)}
|
||||
pagination={pagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="table-operation-panel">
|
||||
{this.renderOperationPanel()}
|
||||
</div>
|
||||
<div className="table-wrapper">{this.renderTable()}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface IRadioProps {
|
||||
onChange?: (result: any) => any;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
@observer
|
||||
class RadioAndInput extends React.Component<IRadioProps> {
|
||||
public state = {
|
||||
value: null as number,
|
||||
};
|
||||
|
||||
public onRadioChange = (e: RadioChangeEvent) => {
|
||||
const { onChange } = this.props;
|
||||
if (onChange) {
|
||||
onChange(e.target.value);
|
||||
this.setState({
|
||||
value: e.target.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onInputChange = (e: number) => {
|
||||
const { onChange } = this.props;
|
||||
if (onChange) {
|
||||
onChange(e);
|
||||
this.setState({
|
||||
value: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
if (!cluster.clusterComboList.length) {
|
||||
cluster.getClusterComboList();
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const options = cluster.clusterComboList;
|
||||
const attrs = {
|
||||
min: 0,
|
||||
placeholder: '没合适?手动输入试试。',
|
||||
};
|
||||
return (
|
||||
<div className="radio-and-input">
|
||||
<Radio.Group value={this.state.value} onChange={this.onRadioChange}>
|
||||
{options.map((v: IRadioItem, index: number) => (
|
||||
<Radio.Button key={v.value || index} value={parseInt(v.label)}>
|
||||
{v.label}
|
||||
</Radio.Button>
|
||||
))}
|
||||
</Radio.Group>
|
||||
<div className="radio-and-input-inputNuber">
|
||||
<InputNumber
|
||||
{...attrs}
|
||||
value={this.state.value}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
<span>MB/s</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
85
kafka-manager-console/src/container/common-curve/config.ts
Normal file
85
kafka-manager-console/src/container/common-curve/config.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { EChartOption } from 'echarts/lib/echarts';
|
||||
import moment from 'moment';
|
||||
|
||||
export interface ILineData {
|
||||
value: number;
|
||||
timeStamp: number;
|
||||
}
|
||||
export interface ICurve {
|
||||
title?: string;
|
||||
path: string;
|
||||
colors: string[];
|
||||
parser?: (option: ICurve, data: ILineData) => EChartOption;
|
||||
message?: string;
|
||||
unit?: string;
|
||||
api?: any;
|
||||
}
|
||||
|
||||
export const LEGEND_HEIGHT = 18;
|
||||
export const defaultLegendPadding = 10;
|
||||
export const GRID_HEIGHT = 192;
|
||||
export const EXPAND_GRID_HEIGHT = 250;
|
||||
export const TITLE_HEIGHT = 40;
|
||||
export const UNIT_HEIGHT = 20;
|
||||
export const LEGEND_PADDING = 10;
|
||||
export const OPERATOR_TITLE_HEIGHT = 92;
|
||||
|
||||
export const baseLineLegend = {
|
||||
itemWidth: 12,
|
||||
itemHeight: 2,
|
||||
icon: 'rect',
|
||||
textStyle: {
|
||||
lineHeight: LEGEND_HEIGHT,
|
||||
},
|
||||
};
|
||||
|
||||
export const baseLineGrid = {
|
||||
left: '0',
|
||||
right: '2%',
|
||||
top: TITLE_HEIGHT + UNIT_HEIGHT,
|
||||
height: GRID_HEIGHT,
|
||||
containLabel: true,
|
||||
};
|
||||
|
||||
export const baseAxisStyle = {
|
||||
nameTextStyle: {
|
||||
color: '#A0A4AA',
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#A0A4AA',
|
||||
},
|
||||
},
|
||||
splitline: {
|
||||
lineStyle: {
|
||||
color: '#A0A4AA',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const noAxis = {
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#A0A4AA',
|
||||
},
|
||||
show: false,
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const getHight = (options: EChartOption) => {
|
||||
let grid = options ? options.grid as EChartOption.Grid : null;
|
||||
if (!options || !grid) grid = baseLineGrid;
|
||||
return Number(grid.height) + getLegendHight(options) + Number(grid.top) + LEGEND_PADDING + UNIT_HEIGHT;
|
||||
};
|
||||
|
||||
export const getLegendHight = (options: EChartOption) => {
|
||||
if (!options) return 0;
|
||||
if (options.legend.show === false) return 0;
|
||||
const legendHight = options.legend.textStyle.lineHeight + defaultLegendPadding;
|
||||
if (options.legend.orient !== 'vertical') return legendHight;
|
||||
const legendLength = options.series.length;
|
||||
return legendHight * legendLength;
|
||||
};
|
||||
27
kafka-manager-console/src/container/common-curve/index.less
Normal file
27
kafka-manager-console/src/container/common-curve/index.less
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
.common-chart-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.charts-op {
|
||||
position: absolute;
|
||||
font-size: 18px;
|
||||
top: 0;
|
||||
right: 30px;
|
||||
i {
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
i:hover {
|
||||
color: #F27E49;
|
||||
}
|
||||
.ml-17 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.charts-title {
|
||||
position: absolute;
|
||||
color: #000000;
|
||||
font-family: PingFangSC-Medium;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
118
kafka-manager-console/src/container/common-curve/index.tsx
Normal file
118
kafka-manager-console/src/container/common-curve/index.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { EChartOption } from 'echarts';
|
||||
import { observer } from 'mobx-react';
|
||||
import React from 'react';
|
||||
import { curveInfo } from 'store/curve-info';
|
||||
import { fullScreen } from 'store/full-screen';
|
||||
import { getHight, EXPAND_GRID_HEIGHT, ICurve } from './config';
|
||||
import './index.less';
|
||||
import { Spin, Icon } from 'component/antd';
|
||||
import LineChart, { hasData } from 'component/chart/line-chart';
|
||||
|
||||
export interface ICommonCurveProps {
|
||||
options: ICurve;
|
||||
parser?: (option: ICurve, data: any[]) => EChartOption;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class CommonCurve extends React.Component<ICommonCurveProps> {
|
||||
|
||||
public componentDidMount() {
|
||||
this.refresh(false);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
const { path } = this.props.options;
|
||||
curveInfo.setCurveData(path, null);
|
||||
}
|
||||
|
||||
public refresh = (refresh?: boolean) => {
|
||||
curveInfo.getCommonCurveData(this.props.options, this.props.parser, refresh);
|
||||
}
|
||||
|
||||
public expand = () => {
|
||||
const curveOption = this.getCurveData();
|
||||
const options = Object.assign({}, curveOption, {
|
||||
grid: {
|
||||
...curveOption.grid,
|
||||
height: EXPAND_GRID_HEIGHT,
|
||||
},
|
||||
});
|
||||
const loading = this.getLoading();
|
||||
fullScreen.show(this.renderCurve(options, loading, true));
|
||||
}
|
||||
|
||||
public renderOpBtns = (options: EChartOption, expand = false) => {
|
||||
const data = hasData(options);
|
||||
return (
|
||||
<div className="charts-op" key="op">
|
||||
{!expand ? <Icon type="reload" onClick={() => this.refresh(true)} key="refresh" /> : null}
|
||||
{data ? this.renderExpand(expand) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public renderExpand = (expand = false) => {
|
||||
if (expand) return <Icon type="close" onClick={fullScreen.close} key="close" />;
|
||||
return <Icon type="fullscreen" className="ml-17" onClick={this.expand} key="full-screen" />;
|
||||
}
|
||||
|
||||
public renderTitle = () => {
|
||||
const { title } = this.props.options;
|
||||
return (
|
||||
<div className="charts-title" key="title">{title}</div>
|
||||
);
|
||||
}
|
||||
|
||||
public getCurveData = () => {
|
||||
const { path } = this.props.options;
|
||||
return curveInfo.curveData[path];
|
||||
}
|
||||
|
||||
public getLoading = () => {
|
||||
const { path } = this.props.options;
|
||||
return curveInfo.curveLoading[path] || false;
|
||||
}
|
||||
|
||||
public renderOthers = () => null as JSX.Element;
|
||||
|
||||
public renderNoData = (height?: number) => {
|
||||
const style = { height: `${height}px`, lineHeight: `${height}px` };
|
||||
return <div className="no-data-info" style={{ ...style }} key="noData">暂无数据</div>;
|
||||
}
|
||||
|
||||
public renderLoading = (height?: number) => {
|
||||
const style = { height: `${height}px`, lineHeight: `${height}px` };
|
||||
return <div className="no-data-info" style={{ ...style }} key="loading"><Spin /></div>;
|
||||
}
|
||||
|
||||
public renderEchart = (options: EChartOption, loading = false) => {
|
||||
const height = getHight(options);
|
||||
const data = hasData(options);
|
||||
|
||||
if (loading) return this.renderLoading(height);
|
||||
if (!data) return this.renderNoData(height);
|
||||
return <LineChart height={height} options={options} key="chart" />;
|
||||
}
|
||||
|
||||
public renderCurve = (options: EChartOption, loading: boolean, expand = false) => {
|
||||
const data = hasData(options);
|
||||
return (
|
||||
<div className="common-chart-wrapper" >
|
||||
{this.renderTitle()}
|
||||
{this.renderEchart(options, loading)}
|
||||
{this.renderOpBtns(options, expand)}
|
||||
{data ? this.renderOthers() : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const options = this.getCurveData();
|
||||
const loading = this.getLoading();
|
||||
return (
|
||||
<>
|
||||
{this.renderCurve(options, loading)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
30
kafka-manager-console/src/container/configure-info.tsx
Normal file
30
kafka-manager-console/src/container/configure-info.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { version } from 'store/version';
|
||||
import Url from 'lib/url-parser';
|
||||
import './error/index.less';
|
||||
|
||||
@observer
|
||||
export class ConfigureInfoPage extends React.Component {
|
||||
public fileId: number;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.fileId = Number(url.search.fileId);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
version.getConfigFiles(this.fileId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<div className="config-info">
|
||||
{version.configFiles}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
33
kafka-manager-console/src/container/custom-component.tsx
Normal file
33
kafka-manager-console/src/container/custom-component.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import 'styles/custom-component.less';
|
||||
import { IBtn } from 'types/base-type';
|
||||
import { Dropdown } from 'component/antd';
|
||||
|
||||
interface IMoreBtnsProps {
|
||||
btns: IBtn[];
|
||||
data: object;
|
||||
}
|
||||
|
||||
export const MoreBtns = (props: IMoreBtnsProps) => {
|
||||
const { btns, data } = props;
|
||||
const btnsMenu = (
|
||||
<ul className="dropdown-menu">
|
||||
{btns.map((v, index) => (
|
||||
v.clickFunc ? <li key={index} onClick={() => v.clickFunc(data)} className="didi-theme">{v.label}</li>
|
||||
: <li key={index} className="didi-theme">{v.label}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
return (
|
||||
<Dropdown
|
||||
key="2"
|
||||
overlay={btnsMenu}
|
||||
trigger={['click', 'hover']}
|
||||
placement="bottomLeft"
|
||||
>
|
||||
<span className="didi-theme ml-10">
|
||||
更多
|
||||
</span>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
10
kafka-manager-console/src/container/drawer/add-alarm.tsx
Normal file
10
kafka-manager-console/src/container/drawer/add-alarm.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export class AddAlarm extends React.Component {
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
329
kafka-manager-console/src/container/drawer/data-migration.tsx
Normal file
329
kafka-manager-console/src/container/drawer/data-migration.tsx
Normal file
@@ -0,0 +1,329 @@
|
||||
import * as React from 'react';
|
||||
import { Form, Table, InputNumber, Input, Tooltip, DatePicker, PaginationConfig, Button, notification, message } from 'component/antd';
|
||||
import { IRenderData } from 'container/modal/expert';
|
||||
import { IMigration } from 'types/base-type';
|
||||
import { createMigrationTask } from 'lib/api';
|
||||
import { expert } from 'store/expert';
|
||||
import moment from 'moment';
|
||||
import { transMBToB, transMSecondToHour, transHourToMSecond } from 'lib/utils';
|
||||
import { wrapper } from 'store';
|
||||
import { cellStyle } from 'constants/table';
|
||||
import { urlPrefix } from 'constants/left-menu';
|
||||
import { timeMinute } from 'constants/strategy';
|
||||
|
||||
const EditableContext = React.createContext(null);
|
||||
interface IFormTableProps {
|
||||
data: IRenderData[];
|
||||
form: any;
|
||||
}
|
||||
|
||||
interface IEditTableCellProps {
|
||||
dataIndex: string;
|
||||
title: string;
|
||||
inputType: string;
|
||||
record: IRenderData;
|
||||
}
|
||||
|
||||
class EditableCell extends React.Component<IEditTableCellProps> {
|
||||
|
||||
public renderCell = ({ getFieldDecorator }: any) => {
|
||||
const {
|
||||
dataIndex,
|
||||
title,
|
||||
inputType,
|
||||
record,
|
||||
children,
|
||||
...restProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<td {...restProps}>
|
||||
{record ? (
|
||||
<Form.Item style={{ margin: 0 }}>
|
||||
{getFieldDecorator(`${record.key}-${dataIndex}`, {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: `请输入 ${title}!`,
|
||||
},
|
||||
],
|
||||
initialValue: (record as any)[dataIndex],
|
||||
})(
|
||||
<InputNumber min={0} style={{ width: 80 }} />,
|
||||
)}
|
||||
</Form.Item>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return <EditableContext.Consumer>{this.renderCell}</EditableContext.Consumer>;
|
||||
}
|
||||
}
|
||||
|
||||
class DataMigrationFormTable extends React.Component<IFormTableProps> {
|
||||
public columns = [
|
||||
{
|
||||
title: '集群名称',
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 100,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
render: (text: string) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={text}>
|
||||
{text}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 120,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
sorter: (a: IRenderData, b: IRenderData) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
render: (text: string) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={text}>
|
||||
{text}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
title: '限流下限(MB/s)',
|
||||
dataIndex: 'minThrottle',
|
||||
key: 'minThrottle',
|
||||
editable: true,
|
||||
}, {
|
||||
title: '初始限流(MB/s)',
|
||||
dataIndex: 'throttle',
|
||||
key: 'throttle',
|
||||
editable: true,
|
||||
}, {
|
||||
title: '限流上限(MB/s)',
|
||||
dataIndex: 'maxThrottle',
|
||||
key: 'maxThrottle',
|
||||
editable: true,
|
||||
}, {
|
||||
title: '迁移保存时间(h)',
|
||||
dataIndex: 'reassignRetentionTime',
|
||||
key: 'reassignRetentionTime',
|
||||
editable: true,
|
||||
}, {
|
||||
title: '原本保存时间(h)',
|
||||
dataIndex: 'retentionTime',
|
||||
key: 'retentionTime', // originalRetentionTime
|
||||
width: '132px',
|
||||
sorter: (a: IRenderData, b: IRenderData) => b.retentionTime - a.retentionTime,
|
||||
render: (time: any) => transMSecondToHour(time),
|
||||
}, {
|
||||
title: 'BrokerID',
|
||||
dataIndex: 'brokerIdList',
|
||||
key: 'brokerIdList',
|
||||
render: (t: []) => {
|
||||
return (
|
||||
<Tooltip placement="bottom" title={t.join('、')}>
|
||||
{t.join('、')}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
public infoForm: any = null;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = {
|
||||
data: [],
|
||||
editingKey: '',
|
||||
};
|
||||
}
|
||||
|
||||
public cancel = () => {
|
||||
this.setState({ editingKey: '' });
|
||||
}
|
||||
|
||||
public onSubmit = () => {
|
||||
let tableResult = null as any;
|
||||
this.props.form.validateFields((error: Error, result: any) => {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
tableResult = result;
|
||||
});
|
||||
|
||||
const infoData = this.infoForm.getSubmitData();
|
||||
if (tableResult && infoData) {
|
||||
// 处理参数
|
||||
const paramsData = [] as IMigration[];
|
||||
const { data } = this.props;
|
||||
const throttleArr = [] as any[];
|
||||
infoData.beginTime = +moment(infoData.beginTime).format('x');
|
||||
Object.getOwnPropertyNames(tableResult).forEach(key => {
|
||||
const throttleIndex = Number(key.slice(0, 1));
|
||||
const throttleKey = key.slice(2);
|
||||
const throttleName = tableResult[key];
|
||||
if (!throttleArr[throttleIndex]) {
|
||||
throttleArr[throttleIndex] = {};
|
||||
}
|
||||
throttleArr[throttleIndex][throttleKey] = throttleName;
|
||||
});
|
||||
data.forEach((ele, index) => {
|
||||
throttleArr.forEach((record, i) => {
|
||||
if (index === i) {
|
||||
if (Number(record.minThrottle) >= Number(record.throttle)) {
|
||||
message.warning('限流下限小于初始限流');
|
||||
return null;
|
||||
}
|
||||
if (Number(record.throttle) >= Number(record.maxThrottle)) {
|
||||
message.warning('初始限流小于限流上限');
|
||||
return null;
|
||||
}
|
||||
const obj = {
|
||||
clusterId: ele.clusterId,
|
||||
topicName: ele.topicName,
|
||||
originalRetentionTime: ele.retentionTime,
|
||||
partitionIdList: ele.partitionIdList,
|
||||
brokerIdList: ele.brokerIdList,
|
||||
throttle: transMBToB(record.throttle),
|
||||
maxThrottle: transMBToB(record.maxThrottle),
|
||||
minThrottle: transMBToB(record.minThrottle),
|
||||
reassignRetentionTime: transHourToMSecond(record.reassignRetentionTime),
|
||||
beginTime: infoData.beginTime,
|
||||
description: infoData.description,
|
||||
} as IMigration;
|
||||
paramsData.push(obj);
|
||||
}
|
||||
});
|
||||
});
|
||||
if (paramsData.length === data.length) {
|
||||
createMigrationTask(paramsData).then(data => {
|
||||
notification.success({ message: '新建迁移任务成功' });
|
||||
expert.getHotTopics();
|
||||
window.location.href = `${urlPrefix}/expert#2`;
|
||||
wrapper.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public onCancel() {
|
||||
wrapper.close();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const components = {
|
||||
body: {
|
||||
cell: EditableCell,
|
||||
},
|
||||
};
|
||||
|
||||
const columns = this.columns.map(col => {
|
||||
if (!col.editable) {
|
||||
return col;
|
||||
}
|
||||
|
||||
return {
|
||||
...col,
|
||||
onCell: (record: IRenderData) => ({
|
||||
record,
|
||||
inputType: 'number',
|
||||
dataIndex: col.dataIndex,
|
||||
title: col.title,
|
||||
}),
|
||||
};
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<EditableContext.Provider value={this.props.form}>
|
||||
<Table
|
||||
components={components}
|
||||
dataSource={this.props.data}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
scroll={{y: 520}}
|
||||
className="migration-table"
|
||||
/>
|
||||
</EditableContext.Provider>
|
||||
<WrappedInfoForm wrappedComponentRef={(form: any) => this.infoForm = form} />
|
||||
<div className="transfer-button">
|
||||
<Button type="primary" onClick={this.onSubmit}>确定</Button>
|
||||
<Button onClick={this.onCancel}>取消</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const WrappedDataMigrationFormTable = Form.create<IFormTableProps>()(DataMigrationFormTable);
|
||||
|
||||
export class InfoForm extends React.Component<IFormTableProps> {
|
||||
|
||||
public getSubmitData() {
|
||||
let value = null as any;
|
||||
this.props.form.validateFields((error: Error, result: any) => {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
value = result;
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const formItemLayout = {
|
||||
labelCol: {
|
||||
xs: { span: 24 },
|
||||
sm: { span: 3 },
|
||||
},
|
||||
wrapperCol: {
|
||||
xs: { span: 24 },
|
||||
sm: { span: 16 },
|
||||
},
|
||||
};
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
const datePickerAttrs = {
|
||||
placeholder: '请输入计划开始时间',
|
||||
format: timeMinute,
|
||||
showTime: true,
|
||||
};
|
||||
return (
|
||||
<Form name="basic" {...formItemLayout} >
|
||||
<Form.Item
|
||||
key={1}
|
||||
className="form-item"
|
||||
label="计划开始时间"
|
||||
>
|
||||
{getFieldDecorator('beginTime', {
|
||||
initialValue: moment(),
|
||||
rules: [{ required: true, message: '请输入计划开始时间' }],
|
||||
})(
|
||||
<DatePicker {...datePickerAttrs} />)}
|
||||
</Form.Item>
|
||||
<Form.Item label="迁移说明" key={2} className="form-item">
|
||||
{getFieldDecorator('description', {
|
||||
initialValue: '',
|
||||
rules: [{ required: true, message: '请输入至少5个字符', pattern: /^.{5,}.$/ }],
|
||||
})(
|
||||
<Input.TextArea rows={5} placeholder="请输入至少5个字符" />,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const WrappedInfoForm = Form.create({ name: 'migration_form' })(InfoForm);
|
||||
14
kafka-manager-console/src/container/error/forbidden.tsx
Normal file
14
kafka-manager-console/src/container/error/forbidden.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import * as React from 'react';
|
||||
import './index.less';
|
||||
|
||||
export class ForbiddenPage extends React.Component {
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<div className="forbidden">
|
||||
<span>您暂无权限查看当前页面</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
7
kafka-manager-console/src/container/error/index.less
Normal file
7
kafka-manager-console/src/container/error/index.less
Normal file
@@ -0,0 +1,7 @@
|
||||
.forbidden{
|
||||
width: 100%;
|
||||
height: 60%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
1
kafka-manager-console/src/container/error/index.tsx
Normal file
1
kafka-manager-console/src/container/error/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from './forbidden';
|
||||
@@ -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} />
|
||||
);
|
||||
}
|
||||
}
|
||||
18
kafka-manager-console/src/container/full-screen/index.less
Normal file
18
kafka-manager-console/src/container/full-screen/index.less
Normal file
@@ -0,0 +1,18 @@
|
||||
.full-screen-mark {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1050;
|
||||
.full-screen-content {
|
||||
width: 80%;
|
||||
box-shadow: 0 4px 12px 0;
|
||||
border-radius: 4px;
|
||||
margin: 10% auto;
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
23
kafka-manager-console/src/container/full-screen/index.tsx
Normal file
23
kafka-manager-console/src/container/full-screen/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import './index.less';
|
||||
import { fullScreen } from 'store/full-screen';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
@observer
|
||||
export class FullScreen extends React.Component {
|
||||
|
||||
public handleClose = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
if ((event.target as any).nodeName === 'SECTION') fullScreen.close();
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (!fullScreen.content) return null;
|
||||
return (
|
||||
<section className="full-screen-mark" onClick={this.handleClose}>
|
||||
<div className="full-screen-content">
|
||||
{fullScreen.content}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
162
kafka-manager-console/src/container/header/index.less
Normal file
162
kafka-manager-console/src/container/header/index.less
Normal file
@@ -0,0 +1,162 @@
|
||||
.kafka-header-container {
|
||||
min-width: 1200px;
|
||||
height: 64px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, .1);
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
||||
.left-content {
|
||||
width: 20%;
|
||||
display: inline-block;
|
||||
margin-left: 16px;
|
||||
|
||||
& > span {
|
||||
display: inline-block;
|
||||
line-height: 64px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.mid-content {
|
||||
width: 53%;
|
||||
display: inline-block;
|
||||
|
||||
a {
|
||||
color: #4A4A4A;
|
||||
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
line-height: 64px;
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
|
||||
&.k-active {
|
||||
a {
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
&:after {
|
||||
width: 100%;
|
||||
content: '';
|
||||
height: 4px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-content {
|
||||
width: 25%;
|
||||
text-align: right;
|
||||
display: inline-block;
|
||||
padding-right: 10px;
|
||||
height: 64px;
|
||||
|
||||
.content-region-select{
|
||||
width: 120px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
& > span {
|
||||
display: inline-block;
|
||||
line-height: 64px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.kafka-user-text {
|
||||
margin-left: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 200;
|
||||
color: #4A4A4A;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.kafka-header-icon {
|
||||
height: 45px;
|
||||
width: 45px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.kafka-header-text {
|
||||
margin-left: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
font-family: PingFangSC-Medium;
|
||||
color: rgba(25, 24, 24, 1);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.kafka-header-menu {
|
||||
background-color: #fff;
|
||||
box-shadow: 0px 0px 4px 0px rgba(217, 217, 217);
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
|
||||
li {
|
||||
text-align: center;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
padding: 0px 20px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(236, 111, 38, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.region-user-text{
|
||||
width: 70px;
|
||||
margin-right: 20px;
|
||||
.region-text{
|
||||
font-size: 14px;
|
||||
font-weight: 200;
|
||||
color: #4A4A4A;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
line-height: 50px;
|
||||
.region-text-icon{
|
||||
line-height: 54px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.kafka-avatar-box{
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
margin-right: -12px;
|
||||
.kafka-avatar-icon{
|
||||
float: left;
|
||||
margin: 8px 34px 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.kafka-user-span{
|
||||
float: left;
|
||||
width: 100%;
|
||||
font-size: 10px;
|
||||
line-height: 24px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user