mirror of
https://github.com/didi/KnowStreaming.git
synced 2026-01-03 19:38:20 +08:00
kafka-manager 2.0
This commit is contained in:
337
kafka-manager-console/src/container/topic/config.tsx
Normal file
337
kafka-manager-console/src/container/topic/config.tsx
Normal file
@@ -0,0 +1,337 @@
|
||||
import * as React from 'react';
|
||||
import { IBtn, ITopic, IQuotaQuery, IConnectionInfo } from 'types/base-type';
|
||||
import { showAllPermissionModal, showPermissionModal, showTopicEditModal, showApplyQuatoModal, applyExpandModal, showTopicApplyQuatoModal } from 'container/modal';
|
||||
import { applyOnlineModal } from 'container/modal';
|
||||
import { Tooltip } from 'component/antd';
|
||||
import { topic } from 'store/topic';
|
||||
import { region } from 'store/region';
|
||||
import { cellStyle } from 'constants/table';
|
||||
import { MoreBtns } from 'container/custom-component';
|
||||
import { app } from 'store/app';
|
||||
import './index.less';
|
||||
|
||||
/**
|
||||
* 0: 无权限 申请权限
|
||||
* 1: 可消费 申请配额,只能申请消费配额, 申请权限(申请发送)
|
||||
* 2: 可发送 申请配额,只能申请发送配额, 申请权限(申请消费)
|
||||
* 3: 可消费发送 申请配额,可以申请发送及消费配额
|
||||
* 4: 可管理 可编辑,可以申请发送及消费配额
|
||||
*/
|
||||
|
||||
export const renderMyTopicOperation = (record: ITopic) => {
|
||||
const twoBtns = getTopicBtn(record).splice(0, 2);
|
||||
const leftBtns = getTopicBtn(record).splice(2);
|
||||
return (
|
||||
<>
|
||||
<span className="table-operation">
|
||||
{twoBtns.map((item, index) => (
|
||||
item.clickFunc ? <a type="javascript;" key={index} onClick={() => item.clickFunc(record)}>{item.label}</a> :
|
||||
<span key={index} className="mr-10">{item.label}</span>
|
||||
))}
|
||||
{getTopicBtn(record).length > 2 && <MoreBtns btns={leftBtns} data={record}/>}
|
||||
</span>
|
||||
</>);
|
||||
};
|
||||
|
||||
const getTopicBtn = (record: ITopic) => {
|
||||
const btnList: IBtn[] = [{
|
||||
label: '申请权限',
|
||||
show: [0, 1, 2].indexOf(record.access) > -1,
|
||||
clickFunc: (item: ITopic) => {
|
||||
showPermissionModal(item);
|
||||
},
|
||||
}, {
|
||||
label: '申请配额',
|
||||
show: [1, 2, 3, 4].indexOf(record.access) > -1,
|
||||
clickFunc: (item: ITopic) => {
|
||||
applyQuotaQuery(item);
|
||||
},
|
||||
}, {
|
||||
label: '申请分区',
|
||||
show: true,
|
||||
clickFunc: (item: ITopic) => {
|
||||
applyExpandModal(item);
|
||||
},
|
||||
}, {
|
||||
label: '申请下线',
|
||||
show: record.access === 4,
|
||||
clickFunc: (item: ITopic) => {
|
||||
applyOnlineModal(item);
|
||||
},
|
||||
}, {
|
||||
label: '编辑',
|
||||
show: record.access === 4,
|
||||
clickFunc: (item: ITopic) => {
|
||||
showTopicEditModal(item);
|
||||
},
|
||||
}];
|
||||
const origin = btnList.filter((item: IBtn) => item.show);
|
||||
return origin;
|
||||
};
|
||||
|
||||
export const renderAllTopicOperation = (item: ITopic) => {
|
||||
return (
|
||||
<>
|
||||
{item.needAuth && <a className="mr-10" onClick={() => showAllPermissionModal(item)}>申请权限</a>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const applyQuotaQuery = (item: ITopic) => {
|
||||
topic.getQuotaQuery(item.appId, item.clusterId, item.topicName).then((data) => {
|
||||
const record = data && data.length ? data[0] : {} as IQuotaQuery;
|
||||
showApplyQuatoModal(item, record);
|
||||
});
|
||||
};
|
||||
|
||||
export const applyTopicQuotaQuery = async (item: ITopic) => {
|
||||
await app.getTopicAppQuota(item.clusterId, item.topicName);
|
||||
await showTopicApplyQuatoModal(item);
|
||||
};
|
||||
|
||||
export const getOnlineColumns = () => {
|
||||
const columns = [
|
||||
{
|
||||
title: 'AppID',
|
||||
dataIndex: 'appId',
|
||||
key: 'appId',
|
||||
},
|
||||
{
|
||||
title: 'HostName',
|
||||
dataIndex: 'hostname',
|
||||
key: 'hostname',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 120,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
render: (text: string) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={text} >
|
||||
{text}
|
||||
</Tooltip>);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'ClientType',
|
||||
dataIndex: 'clientType',
|
||||
key: 'clientType',
|
||||
},
|
||||
];
|
||||
return columns;
|
||||
};
|
||||
|
||||
export const getAllTopicColumns = (urlPrefix: string) => {
|
||||
const columns = [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
width: '25%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 250,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
sorter: (a: ITopic, b: ITopic) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
render: (text: string, record: ITopic) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={record.topicName} >
|
||||
<a
|
||||
// tslint:disable-next-line:max-line-length
|
||||
href={`${urlPrefix}/topic/topic-detail?clusterId=${record.clusterId}&topic=${record.topicName}®ion=${region.currentRegion}`}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
</Tooltip>);
|
||||
},
|
||||
}, {
|
||||
title: '集群名称',
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
width: '20%',
|
||||
}, {
|
||||
title: 'Topic描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
width: '25%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 250,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
render: (text: string) => <Tooltip placement="bottomLeft" title={text} >{text}</Tooltip>,
|
||||
}, {
|
||||
title: '负责人',
|
||||
dataIndex: 'appPrincipals',
|
||||
key: 'appPrincipals',
|
||||
width: '20%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 100,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
render: (text: string) => <Tooltip placement="bottomLeft" title={text} >{text}</Tooltip>,
|
||||
}, {
|
||||
title: '操作',
|
||||
dataIndex: 'operation',
|
||||
key: 'operation',
|
||||
width: '10%',
|
||||
render: (text: string, item: ITopic) => (
|
||||
renderAllTopicOperation(item)
|
||||
),
|
||||
},
|
||||
];
|
||||
return columns;
|
||||
};
|
||||
|
||||
export const getExpireColumns = (urlPrefix: string) => {
|
||||
return [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
width: '35%',
|
||||
sorter: (a: ITopic, b: ITopic) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
render: (t: string, r: ITopic) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={r.topicName} >
|
||||
<a
|
||||
// tslint:disable-next-line:max-line-length
|
||||
href={`${urlPrefix}/topic/topic-detail?clusterId=${r.clusterId}&topic=${r.topicName}®ion=${region.currentRegion}`}
|
||||
>
|
||||
{t}
|
||||
</a>
|
||||
</Tooltip>);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '所属集群',
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
title: '关联应用',
|
||||
dataIndex: 'appName',
|
||||
key: 'appName',
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
title: '消费者个数',
|
||||
dataIndex: 'fetchConnectionNum',
|
||||
key: 'fetchConnectionNum',
|
||||
width: '10%',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const getMyTopicColumns = (urlPrefix: string) => {
|
||||
return [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
width: '21%',
|
||||
sorter: (a: ITopic, b: ITopic) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
render: (t: string, r: ITopic) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={r.topicName} >
|
||||
<a
|
||||
// tslint:disable-next-line:max-line-length
|
||||
href={`${urlPrefix}/topic/topic-detail?clusterId=${r.clusterId}&topic=${r.topicName}®ion=${region.currentRegion}`}
|
||||
>
|
||||
{t}
|
||||
</a>
|
||||
</Tooltip>);
|
||||
},
|
||||
}, {
|
||||
title: 'Bytes in(KB/s)',
|
||||
dataIndex: 'bytesIn',
|
||||
key: 'bytesIn',
|
||||
width: '12%',
|
||||
sorter: (a: ITopic, b: ITopic) => b.bytesIn - a.bytesIn,
|
||||
render: (t: number) => t === null ? '' : (t / 1024).toFixed(2),
|
||||
}, {
|
||||
title: 'Bytes out(KB/s)',
|
||||
dataIndex: 'bytesOut',
|
||||
key: 'bytesOut',
|
||||
width: '12%',
|
||||
sorter: (a: ITopic, b: ITopic) => b.bytesOut - a.bytesOut,
|
||||
render: (t: number) => t === null ? '' : (t / 1024).toFixed(2),
|
||||
}, {
|
||||
title: '所属集群',
|
||||
dataIndex: 'clusterName',
|
||||
key: 'clusterName',
|
||||
width: '15%',
|
||||
}, {
|
||||
title: '关联应用',
|
||||
dataIndex: 'appName',
|
||||
key: 'appName',
|
||||
width: '12%',
|
||||
render: (text: string, record: ITopic) => (
|
||||
<>
|
||||
<Tooltip placement="bottomLeft" title={record.appId} >
|
||||
{text}
|
||||
</Tooltip>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
width: '35%',
|
||||
render: (val: string, item: ITopic, index: number) => (
|
||||
renderMyTopicOperation(item)
|
||||
),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const getApplyOnlineColumns = () => {
|
||||
const columns = [
|
||||
{
|
||||
title: 'AppID',
|
||||
dataIndex: 'appId',
|
||||
key: 'appId',
|
||||
width: '22%',
|
||||
sorter: (a: IConnectionInfo, b: IConnectionInfo) => a.appId.charCodeAt(0) - b.appId.charCodeAt(0),
|
||||
},
|
||||
{
|
||||
title: '主机名',
|
||||
dataIndex: 'hostname',
|
||||
key: 'hostname',
|
||||
width: '40%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 250,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
render: (t: string) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={t} >{t}</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '客户端版本',
|
||||
dataIndex: 'clientVersion',
|
||||
key: 'clientVersion',
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
title: '客户端类型',
|
||||
dataIndex: 'clientType',
|
||||
key: 'clientType',
|
||||
width: '18%',
|
||||
render: (t: string) => <span>{t === 'consumer' ? '消费' : '生产'}</span>,
|
||||
},
|
||||
];
|
||||
return columns;
|
||||
};
|
||||
58
kafka-manager-console/src/container/topic/index.less
Normal file
58
kafka-manager-console/src/container/topic/index.less
Normal file
@@ -0,0 +1,58 @@
|
||||
.commonbox {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
ul {
|
||||
min-width: 720px;
|
||||
|
||||
li {
|
||||
margin: 0 5px;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.expand-text{
|
||||
float: right;
|
||||
color: #F5222D;
|
||||
}
|
||||
|
||||
.offline_span{
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.render_offline{
|
||||
padding: 20px 0;
|
||||
margin-left: -35px;
|
||||
}
|
||||
|
||||
.min-width{
|
||||
min-width: 900px;
|
||||
}
|
||||
|
||||
.table-operation {
|
||||
a {
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
a+a {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.mr--10{
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.mr-10 {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.mb-30{
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.form-tip{
|
||||
color: #F5222D;
|
||||
}
|
||||
4
kafka-manager-console/src/container/topic/index.tsx
Normal file
4
kafka-manager-console/src/container/topic/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './topic-all';
|
||||
export * from './topic-mine';
|
||||
export * from './topic-detail';
|
||||
export * from './topic-app-list';
|
||||
39
kafka-manager-console/src/container/topic/peak-flow.tsx
Normal file
39
kafka-manager-console/src/container/topic/peak-flow.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Input } from 'component/antd';
|
||||
import { region } from 'store';
|
||||
import * as React from 'react';
|
||||
|
||||
interface IPeakFlowProps {
|
||||
value?: any;
|
||||
onChange?: (result: any) => any;
|
||||
}
|
||||
|
||||
export class PeakFlowInput extends React.Component<IPeakFlowProps> {
|
||||
|
||||
public render() {
|
||||
const { value } = this.props;
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
placeholder="请输入峰值流量"
|
||||
suffix="MB/s"
|
||||
value={value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.handleChange(e)}
|
||||
/>
|
||||
<span>
|
||||
预估费用:{region.currentRegion === 'cn' ? value * 40 : value * 45}元/月,
|
||||
<a
|
||||
// tslint:disable-next-line:max-line-length
|
||||
href="https://github.com/didi/kafka-manager"
|
||||
target="_blank"
|
||||
>kafka计价方式
|
||||
</a>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
public handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const { onChange } = this.props;
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
onChange && onChange(e.target.value);
|
||||
}
|
||||
}
|
||||
111
kafka-manager-console/src/container/topic/topic-all.tsx
Normal file
111
kafka-manager-console/src/container/topic/topic-all.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { Table } from 'component/antd';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { tableFilter } from 'lib/utils';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { topic } from 'store/topic';
|
||||
import { app } from 'store/app';
|
||||
import { getAllTopicColumns } from './config';
|
||||
import { ITopic } from 'types/base-type';
|
||||
import { pagination } from 'constants/table';
|
||||
import { topicStatusMap } from 'constants/status-map';
|
||||
import 'styles/table-filter.less';
|
||||
|
||||
@observer
|
||||
export class AllTopic extends SearchAndFilterContainer {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
if (!cluster.allData.length) {
|
||||
cluster.getAllClusters();
|
||||
}
|
||||
if (!topic.allTopicData.length) {
|
||||
topic.getAllTopic();
|
||||
}
|
||||
if (!app.data.length) {
|
||||
app.getAppList();
|
||||
}
|
||||
}
|
||||
|
||||
public getColumns = (data: ITopic[]) => {
|
||||
const statusColumn = Object.assign({
|
||||
title: '状态',
|
||||
dataIndex: 'access',
|
||||
key: 'access',
|
||||
width: '10%',
|
||||
filters: tableFilter<ITopic>(data, 'access', topicStatusMap),
|
||||
onFilter: (text: number, record: ITopic) => record.access === text,
|
||||
render: (val: number) => (
|
||||
<div className={val === 0 ? '' : 'success'}>
|
||||
{topicStatusMap[val] || ''}
|
||||
</div>
|
||||
),
|
||||
}, this.renderColumnsFilter('filterStatus')) as any;
|
||||
|
||||
const columns = getAllTopicColumns(this.urlPrefix);
|
||||
|
||||
// columns.splice(-2, 0, statusColumn);
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
public getData<T extends ITopic>(origin: T[]) {
|
||||
let data: T[] = [];
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
if (cluster.allActive !== -1 || searchKey !== '') {
|
||||
data = origin.filter(d =>
|
||||
((d.topicName !== undefined && d.topicName !== null) && d.topicName.toLowerCase().includes(searchKey as string)
|
||||
|| ((d.appPrincipals !== undefined && d.appPrincipals !== null) && d.appPrincipals.toLowerCase().includes(searchKey as string)))
|
||||
&& (cluster.allActive === -1 || d.clusterId === cluster.allActive),
|
||||
);
|
||||
} else {
|
||||
data = origin;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
public renderTableList(data: ITopic[]) {
|
||||
return (
|
||||
<Table
|
||||
rowKey="key"
|
||||
columns={this.getColumns(data)}
|
||||
dataSource={data}
|
||||
pagination={pagination}
|
||||
loading={topic.loading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public renderTable() {
|
||||
return this.renderTableList(this.getData(topic.allTopicData));
|
||||
}
|
||||
|
||||
public renderOperationPanel() {
|
||||
return (
|
||||
<>
|
||||
{this.renderAllCluster('集群:')}
|
||||
{this.renderSearch('名称:', '请输入Topic名称或负责人')}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="table-operation-panel">
|
||||
<ul>
|
||||
{this.renderOperationPanel()}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="table-wrapper">
|
||||
{this.renderTable()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
61
kafka-manager-console/src/container/topic/topic-app-list.tsx
Normal file
61
kafka-manager-console/src/container/topic/topic-app-list.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Button } from 'component/antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
import 'styles/table-filter.less';
|
||||
import { app } from 'store/app';
|
||||
import { CommonAppList } from 'container/app/app-list';
|
||||
import { showEditModal } from 'container/modal';
|
||||
import Url from 'lib/url-parser';
|
||||
|
||||
@observer
|
||||
export class TopicAppList extends CommonAppList {
|
||||
public static defaultProps = {
|
||||
from: 'topic',
|
||||
};
|
||||
|
||||
constructor(defaultProps: any) {
|
||||
super(defaultProps);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
if (!app.data.length) {
|
||||
app.getAppList();
|
||||
}
|
||||
|
||||
if (Url().search.hasOwnProperty('application')) {
|
||||
showEditModal();
|
||||
}
|
||||
}
|
||||
|
||||
public renderTable() {
|
||||
return this.renderTableList(this.getData(app.data));
|
||||
}
|
||||
|
||||
public renderOperationPanel() {
|
||||
return (
|
||||
<ul>
|
||||
{this.renderSearch('', '请输入应用名称或者负责人')}
|
||||
<li className="right-btn-1">
|
||||
<Button type="primary" onClick={() => showEditModal()}>应用申请</Button>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
public applyApp() {
|
||||
showEditModal();
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="table-operation-panel">
|
||||
{this.renderOperationPanel()}
|
||||
</div>
|
||||
<div className="table-wrapper">
|
||||
{this.renderTable()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Select, Tooltip } from 'component/antd';
|
||||
import { urlPrefix } from 'constants/left-menu';
|
||||
import { ITopic } from 'types/base-type';
|
||||
import { topic } from 'store/topic';
|
||||
import { updateAllTopicFormModal } from '../modal/topic';
|
||||
import { searchProps } from 'constants/table';
|
||||
import * as React from 'react';
|
||||
|
||||
const Option = Select.Option;
|
||||
|
||||
interface IStaffSelectProps {
|
||||
parameter?: ITopic;
|
||||
selectData?: any[];
|
||||
onChange?: (result: string) => any;
|
||||
value?: string;
|
||||
|
||||
}
|
||||
|
||||
export class TopicAppSelect extends React.Component<IStaffSelectProps> {
|
||||
|
||||
public render() {
|
||||
const { value, selectData } = this.props;
|
||||
const query = `application=1`;
|
||||
|
||||
let appId: string = null;
|
||||
const index = Array.isArray(selectData) ? selectData.findIndex(row => row.appId === value) : -1;
|
||||
appId = index > -1 ? value : selectData && selectData.length ? selectData[0].appId : '' ;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
placeholder="请选择"
|
||||
defaultValue={appId}
|
||||
onChange={(e: string) => this.handleChange(e)}
|
||||
{...searchProps}
|
||||
>
|
||||
{selectData.map((d: any) => {
|
||||
const label = `${d.name}(${d.appId})`;
|
||||
return (<Option value={d.appId} key={d.appId}>
|
||||
{label.length > 25 ? <Tooltip placement="bottomLeft" title={label}>{label}</Tooltip> : label}
|
||||
</Option>);
|
||||
})}
|
||||
</Select>
|
||||
{
|
||||
selectData.length ? null : <i>
|
||||
没有应用?
|
||||
<a href={`${urlPrefix}/topic/app-list?${query}`}>立刻创建</a>
|
||||
</i>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public handleChange(params: string) {
|
||||
const { onChange, parameter } = this.props;
|
||||
topic.getAuthorities(params, parameter.clusterId, parameter.topicName).then(() => {
|
||||
updateAllTopicFormModal();
|
||||
});
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
onChange && onChange(params);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import * as React from 'react';
|
||||
import './index.less';
|
||||
import Url from 'lib/url-parser';
|
||||
import { observer } from 'mobx-react';
|
||||
import { topic, IAppsIdInfo } from 'store/topic';
|
||||
import { Table, Tooltip } from 'component/antd';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { IQuotaQuery } from 'types/base-type';
|
||||
import { showApplyQuatoModal } from 'container/modal';
|
||||
import { pagination, cellStyle } from 'constants/table';
|
||||
import { transBToMB } from 'lib/utils';
|
||||
|
||||
@observer
|
||||
export class AppIdInformation extends SearchAndFilterContainer {
|
||||
public clusterId: number;
|
||||
public topicName: string;
|
||||
|
||||
public state = {
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
this.topicName = url.search.topic;
|
||||
}
|
||||
|
||||
public renderColumns = () => {
|
||||
return [{
|
||||
title: '应用Id',
|
||||
key: 'appId',
|
||||
dataIndex: 'appId',
|
||||
sorter: (a: IAppsIdInfo, b: IAppsIdInfo) => a.appId.charCodeAt(0) - b.appId.charCodeAt(0),
|
||||
}, {
|
||||
title: '应用名称',
|
||||
key: 'appName',
|
||||
dataIndex: 'appName',
|
||||
sorter: (a: IAppsIdInfo, b: IAppsIdInfo) => a.appName.charCodeAt(0) - b.appName.charCodeAt(0),
|
||||
}, {
|
||||
title: '负责人',
|
||||
key: 'appPrincipals',
|
||||
dataIndex: 'appPrincipals',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 120,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
render: (text: string) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={text} >
|
||||
{text}
|
||||
</Tooltip>);
|
||||
},
|
||||
}, {
|
||||
title: '生产配额(MB/s)',
|
||||
key: 'produceQuota',
|
||||
dataIndex: 'produceQuota',
|
||||
render: (val: number) => transBToMB(val),
|
||||
}, {
|
||||
title: '生产是否限流',
|
||||
key: 'produceThrottled',
|
||||
dataIndex: 'produceThrottled',
|
||||
render: (t: boolean) => <span className={t ? 'fail' : 'success'}>{t ? '是' : '否'}</span>,
|
||||
}, {
|
||||
title: '消费配额(MB/s)',
|
||||
key: 'consumerQuota',
|
||||
dataIndex: 'consumerQuota',
|
||||
render: (val: number) => transBToMB(val),
|
||||
}, {
|
||||
title: '消费是否限流',
|
||||
key: 'fetchThrottled',
|
||||
dataIndex: 'fetchThrottled',
|
||||
render: (t: boolean) => <span className={t ? 'fail' : 'success'}>{t ? '是' : '否'}</span>,
|
||||
}, {
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
dataIndex: 'action',
|
||||
render: (val: string, item: IAppsIdInfo) =>
|
||||
<a onClick={() => this.applyQuotaQuery(item)}>申请配额</a>,
|
||||
}];
|
||||
}
|
||||
|
||||
public applyQuotaQuery = (item: IAppsIdInfo) => {
|
||||
const isPhysicalClusterId = location.search.indexOf('isPhysicalClusterId') > -1;
|
||||
topic.getQuotaQuery(item.appId, this.clusterId, this.topicName).then((data) => {
|
||||
const record = data && data.length ? data[0] : {} as IQuotaQuery;
|
||||
item.clusterId = this.clusterId;
|
||||
item.isPhysicalClusterId = isPhysicalClusterId;
|
||||
showApplyQuatoModal(item, record);
|
||||
});
|
||||
}
|
||||
|
||||
public getData(data: IAppsIdInfo[]) {
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
const filterData = searchKey ?
|
||||
(data || []).filter(d => ((d.appId !== undefined && d.appId !== null) && d.appId.toLowerCase().includes(searchKey as string))
|
||||
|| ((d.appName !== undefined && d.appName !== null) && d.appName.toLowerCase().includes(searchKey as string)),
|
||||
) : topic.appsIdInfo;
|
||||
return filterData;
|
||||
}
|
||||
|
||||
public renderAppList() {
|
||||
const { searchKey } = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="k-row" >
|
||||
<ul className="k-tab">
|
||||
<li>应用信息</li>
|
||||
{this.renderSearch('', '请输入所属应用信息')}
|
||||
</ul>
|
||||
<div style={searchKey ? { minHeight: 700 } : null}>
|
||||
<Table
|
||||
loading={topic.loading}
|
||||
columns={this.renderColumns()}
|
||||
table-Layout="fixed"
|
||||
dataSource={this.getData(topic.appsIdInfo)}
|
||||
rowKey="key"
|
||||
pagination={pagination}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
topic.getAppsIdInfo(this.clusterId, this.topicName);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>{this.renderAppList()}</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
import * as React from 'react';
|
||||
import './index.less';
|
||||
import Url from 'lib/url-parser';
|
||||
import { observer } from 'mobx-react';
|
||||
import { topic, IRealConsumeDetail, ITopicBaseInfo, IRealTimeTraffic } from 'store/topic';
|
||||
import { Table, Tooltip, Icon, PageHeader, Descriptions, Spin } from 'component/antd';
|
||||
import { ILabelValue } from 'types/base-type';
|
||||
import { copyString, transMSecondToHour } from 'lib/utils';
|
||||
import moment from 'moment';
|
||||
import { StatusGraghCom } from 'component/flow-table';
|
||||
import { renderTrafficTable } from 'container/network-flow';
|
||||
import { timeFormat, indexUrl } from 'constants/strategy';
|
||||
|
||||
interface IInfoProps {
|
||||
baseInfo: ITopicBaseInfo;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class BaseInformation extends React.Component<IInfoProps> {
|
||||
public clusterId: number;
|
||||
public topicName: string;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
this.topicName = url.search.topic;
|
||||
}
|
||||
|
||||
public updateRealStatus = () => {
|
||||
topic.getRealTimeTraffic(this.clusterId, this.topicName);
|
||||
}
|
||||
|
||||
public updateConsumeStatus = () => {
|
||||
topic.getRealConsume(this.clusterId, this.topicName);
|
||||
}
|
||||
|
||||
public fillBaseInfo() {
|
||||
const { baseInfo } = this.props;
|
||||
const createTime = moment(baseInfo.createTime).format(timeFormat);
|
||||
const modifyTime = moment(baseInfo.modifyTime).format(timeFormat);
|
||||
const retentionTime = transMSecondToHour(baseInfo.retentionTime);
|
||||
if (baseInfo) {
|
||||
const infoList: ILabelValue[] = [{
|
||||
label: '健康分',
|
||||
value: baseInfo.score,
|
||||
}, {
|
||||
label: '分区数',
|
||||
value: baseInfo.partitionNum,
|
||||
}, {
|
||||
label: '副本数',
|
||||
value: baseInfo.replicaNum,
|
||||
}, {
|
||||
label: '存储时间',
|
||||
value: `${retentionTime} 小时`,
|
||||
}, {
|
||||
label: '创建时间',
|
||||
value: createTime,
|
||||
}, {
|
||||
label: '更改时间',
|
||||
value: modifyTime,
|
||||
}, {
|
||||
label: '压缩格式',
|
||||
value: baseInfo.topicCodeC,
|
||||
}, {
|
||||
label: '集群ID',
|
||||
value: baseInfo.clusterId,
|
||||
}];
|
||||
const infoHide: ILabelValue[] = [{
|
||||
label: 'Bootstrap Severs',
|
||||
value: baseInfo.bootstrapServers,
|
||||
}, {
|
||||
label: 'Topic说明',
|
||||
value: baseInfo.description,
|
||||
}];
|
||||
return (
|
||||
<>
|
||||
<div className="chart-title">基本信息</div>
|
||||
<PageHeader className="detail" title="">
|
||||
<Descriptions size="small" column={3}>
|
||||
<Descriptions.Item key={baseInfo.appName} label="所属应用">{baseInfo.appName}</Descriptions.Item>
|
||||
<Descriptions.Item key={baseInfo.principals} label="应用负责人">
|
||||
<Tooltip placement="bottomLeft" title={baseInfo.principals}>
|
||||
<span className="overview-bootstrap">
|
||||
<Icon
|
||||
onClick={() => copyString(baseInfo.principals)}
|
||||
type="copy"
|
||||
className="didi-theme overview-theme"
|
||||
/>
|
||||
<i className="overview-boot">{baseInfo.principals}</i>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Descriptions.Item>
|
||||
{infoList.map((item: ILabelValue, index: number) => (
|
||||
<Descriptions.Item
|
||||
key={index}
|
||||
label={item.label}
|
||||
>{item.value}
|
||||
</Descriptions.Item>
|
||||
))}
|
||||
{infoHide.map((item: ILabelValue, index: number) => (
|
||||
<Descriptions.Item key={index} label={item.label}>
|
||||
<Tooltip placement="bottomLeft" title={item.value}>
|
||||
<span className="overview-bootstrap">
|
||||
<Icon
|
||||
onClick={() => copyString(item.value)}
|
||||
type="copy"
|
||||
className="didi-theme overview-theme"
|
||||
/>
|
||||
<i className="overview-boot">{item.value}</i>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Descriptions.Item>
|
||||
))}
|
||||
</Descriptions>
|
||||
</PageHeader>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public realTimeTraffic() {
|
||||
const realTraffic = topic.realTraffic;
|
||||
if (realTraffic) {
|
||||
return (
|
||||
<>
|
||||
<Spin spinning={topic.realLoading}>
|
||||
{renderTrafficTable(this.updateRealStatus, StatusGragh)}
|
||||
</Spin>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public realTimeConsume() {
|
||||
const consumeColumns = [{
|
||||
title: ' ',
|
||||
dataIndex: 'metricsName',
|
||||
key: 'metricsName',
|
||||
},
|
||||
{
|
||||
title: 'RequestQueueTime',
|
||||
dataIndex: 'requestQueueTimeMs',
|
||||
key: 'requestQueueTimeMs',
|
||||
render: (t: number) => t === null ? '' : (t ? t.toFixed(2) : 0),
|
||||
},
|
||||
{
|
||||
title: 'LocalTime',
|
||||
dataIndex: 'localTimeMs',
|
||||
key: 'localTimeMs',
|
||||
render: (t: number) => t === null ? '' : (t ? t.toFixed(2) : 0),
|
||||
},
|
||||
{
|
||||
title: 'RemoteTime',
|
||||
dataIndex: 'remoteTimeMs',
|
||||
key: 'remoteTimeMs',
|
||||
render: (t: number) => t === null ? '' : (t ? t.toFixed(2) : 0),
|
||||
},
|
||||
{
|
||||
title: 'ThrottleTime',
|
||||
dataIndex: 'throttleTimeMs',
|
||||
key: 'throttleTimeMs',
|
||||
render: (t: number) => t === null ? '' : (t ? t.toFixed(2) : 0),
|
||||
},
|
||||
{
|
||||
title: 'ResponseQueueTime',
|
||||
dataIndex: 'responseQueueTimeMs',
|
||||
key: 'responseQueueTimeMs',
|
||||
render: (t: number) => t === null ? '' : (t ? t.toFixed(2) : 0),
|
||||
},
|
||||
{
|
||||
title: 'ResponseSendTime',
|
||||
dataIndex: 'responseSendTimeMs',
|
||||
key: 'responseSendTimeMs',
|
||||
render: (t: number) => t === null ? '' : (t ? t.toFixed(2) : 0),
|
||||
},
|
||||
{
|
||||
title: 'TotalTime',
|
||||
dataIndex: 'totalTimeMs',
|
||||
key: 'totalTimeMs',
|
||||
render: (t: number) => t === null ? '' : (t ? t.toFixed(2) : 0),
|
||||
}];
|
||||
const realConsume = topic.realConsume;
|
||||
if (realConsume) {
|
||||
const consumeData: IRealConsumeDetail[] = [];
|
||||
Object.keys(realConsume).map((key: string) => {
|
||||
if (realConsume[key]) {
|
||||
realConsume[key].metricsName = (key === '0') ? 'Produce' : 'Fetch';
|
||||
consumeData.push(realConsume[key]);
|
||||
}
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<div className="traffic-table">
|
||||
<div className="traffic-header">
|
||||
<span>
|
||||
<span className="action-button">实时耗时</span>
|
||||
<a href={indexUrl} target="_blank">指标说明</a>
|
||||
</span>
|
||||
<span className="k-abs" onClick={this.updateConsumeStatus}>
|
||||
<i className="k-icon-shuaxin didi-theme mr-5" />
|
||||
<a>刷新</a>
|
||||
</span>
|
||||
</div>
|
||||
<Table
|
||||
columns={consumeColumns}
|
||||
dataSource={consumeData}
|
||||
pagination={false}
|
||||
loading={topic.consumeLoading}
|
||||
rowKey="metricsName"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
topic.getRealTimeTraffic(this.clusterId, this.topicName);
|
||||
topic.getRealConsume(this.clusterId, this.topicName);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<div className="base-info">
|
||||
{this.fillBaseInfo()}
|
||||
{this.realTimeTraffic()}
|
||||
{this.realTimeConsume()}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export class StatusGragh extends StatusGraghCom<IRealTimeTraffic> {
|
||||
public getData = () => {
|
||||
return topic.realTraffic;
|
||||
}
|
||||
|
||||
public getLoading = () => {
|
||||
return topic.realLoading;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import * as React from 'react';
|
||||
import Url from 'lib/url-parser';
|
||||
import { DatePicker, notification, Icon } from 'component/antd';
|
||||
import { BarChartComponet } from 'component/chart';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Moment } from 'moment';
|
||||
import { topic } from 'store/topic';
|
||||
import { timeMonth } from 'constants/strategy';
|
||||
import './index.less';
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
import moment = require('moment');
|
||||
|
||||
@observer
|
||||
export class BillInformation extends SearchAndFilterContainer {
|
||||
public clusterId: number;
|
||||
public topicName: string;
|
||||
|
||||
public state = {
|
||||
mode: ['month', 'month'] as any,
|
||||
value: [moment(new Date()).subtract(6, 'months'), moment()] as any,
|
||||
};
|
||||
|
||||
private startTime: number = moment(new Date()).subtract(6, 'months').valueOf();
|
||||
private endTime: number = moment().valueOf();
|
||||
private chart: any = null;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
this.topicName = url.search.topic;
|
||||
}
|
||||
|
||||
public getData() {
|
||||
const { clusterId, topicName, startTime, endTime } = this;
|
||||
topic.setLoading(true);
|
||||
return topic.getBillInfo(clusterId, topicName, startTime, endTime);
|
||||
}
|
||||
|
||||
public renderDatePick() {
|
||||
const { value, mode } = this.state;
|
||||
return (
|
||||
<>
|
||||
<div className="op-panel">
|
||||
<span>
|
||||
<RangePicker
|
||||
ranges={{
|
||||
近半年: [moment(new Date()).subtract(6, 'months'), moment()],
|
||||
近一年: [moment().startOf('year'), moment().endOf('year')],
|
||||
}}
|
||||
value={value}
|
||||
mode={mode}
|
||||
format={timeMonth}
|
||||
onChange={this.handlePanelChange}
|
||||
onPanelChange={this.handlePanelChange}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public disabledDateTime = (current: Moment) => {
|
||||
return current && current > moment().endOf('day');
|
||||
}
|
||||
|
||||
public handleRefreshChart = () => {
|
||||
this.chart.handleRefreshChart();
|
||||
}
|
||||
|
||||
public handlePanelChange = (value: any, mode: any) => {
|
||||
this.setState({
|
||||
value,
|
||||
mode: ['month', 'month'] as any,
|
||||
});
|
||||
|
||||
this.startTime = value[0].valueOf();
|
||||
this.endTime = value[1].valueOf();
|
||||
|
||||
if (this.startTime >= this.endTime) {
|
||||
return notification.error({ message: '开始时间不能大于或等于结束时间' });
|
||||
}
|
||||
this.getData();
|
||||
this.handleRefreshChart();
|
||||
}
|
||||
|
||||
public renderChart() {
|
||||
return (
|
||||
<div className="chart-box">
|
||||
<BarChartComponet ref={(ref) => this.chart = ref} getChartData={this.getData.bind(this, null)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return(
|
||||
<>
|
||||
<div className="k-row" >
|
||||
<ul className="k-tab">
|
||||
<li>账单信息
|
||||
<a
|
||||
// tslint:disable-next-line:max-line-length
|
||||
href="https://github.com/didi/kafka-manager"
|
||||
target="_blank"
|
||||
>
|
||||
<Icon type="question-circle" />
|
||||
</a>
|
||||
</li>
|
||||
{this.renderDatePick()}
|
||||
</ul>
|
||||
{this.renderChart()}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import * as React from 'react';
|
||||
import './index.less';
|
||||
import { topic, ITopicBroker, IBrokerInfo } from 'store/topic';
|
||||
import { Table, Tooltip } from 'component/antd';
|
||||
import Url from 'lib/url-parser';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { observer } from 'mobx-react';
|
||||
import { pagination } from 'constants/table';
|
||||
|
||||
@observer
|
||||
export class BrokersInformation extends SearchAndFilterContainer {
|
||||
public clusterId: number;
|
||||
public topicName: string;
|
||||
public from: string;
|
||||
public isPhysical: boolean;
|
||||
|
||||
public state = {
|
||||
searchKey: '',
|
||||
};
|
||||
|
||||
public brokerColumns = [{
|
||||
title: 'BrokerID',
|
||||
key: 'brokerId',
|
||||
dataIndex: 'brokerId',
|
||||
width: '10%',
|
||||
sorter: (a: ITopicBroker, b: ITopicBroker) => b.brokerId - a.brokerId,
|
||||
render: (t: string, record: ITopicBroker) => {
|
||||
return ( // alive true==可点击,false==不可点击
|
||||
<>
|
||||
{record.alive ?
|
||||
<a href={`${this.urlPrefix}/admin/broker-detail?clusterId=${record.clusterId}&brokerId=${t}`}>{t}</a>
|
||||
: <a style={{ cursor: 'not-allowed', color: '#999' }}>{t}</a>}
|
||||
</>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
title: 'Host',
|
||||
key: 'host',
|
||||
dataIndex: 'host',
|
||||
width: '20%',
|
||||
render: (text: string) => <Tooltip placement="bottomLeft" title={text}>{text}</Tooltip>,
|
||||
}, {
|
||||
title: 'Leader个数',
|
||||
key: 'leaderPartitionIdListLength',
|
||||
dataIndex: 'leaderPartitionIdList',
|
||||
width: '10%',
|
||||
sorter: (a: ITopicBroker, b: ITopicBroker) => b.leaderPartitionIdList.length - a.leaderPartitionIdList.length,
|
||||
render: (t: []) => t.length,
|
||||
}, {
|
||||
title: '分区LeaderID',
|
||||
key: 'leaderPartitionIdList',
|
||||
dataIndex: 'leaderPartitionIdList',
|
||||
width: '25%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 180,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
render: (t: []) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={t.join('、')}>
|
||||
{t.map(i => <span key={i} className="p-params">{i}</span>)}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
title: '分区个数',
|
||||
key: 'partitionNum',
|
||||
dataIndex: 'partitionNum',
|
||||
width: '10%',
|
||||
sorter: (a: ITopicBroker, b: ITopicBroker) => b.partitionNum - a.partitionNum,
|
||||
}, {
|
||||
title: '分区ID',
|
||||
key: 'partitionIdList',
|
||||
dataIndex: 'partitionIdList',
|
||||
width: '25%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 180,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
render: (t: []) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={t.join('、')}>
|
||||
{t.map(i => <span key={i} className="p-params">{i}</span>)}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
}];
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
this.topicName = url.search.topic;
|
||||
this.from = decodeURIComponent(url.search.from);
|
||||
this.isPhysical = this.from.includes('expert');
|
||||
}
|
||||
|
||||
public getMoreDetail = (record: ITopicBroker) => {
|
||||
return (
|
||||
<div className="p-description">
|
||||
<p><span>BrokerID: </span>{record.brokerId}</p>
|
||||
<p><span>Host:</span>{record.host}</p>
|
||||
<p><span>LeaderID: </span>{record.leaderPartitionIdList.join(',')}(共{record.leaderPartitionIdList.length}个)</p>
|
||||
<p><span>分区ID:</span>{record.partitionIdList.join(',')}(共{record.partitionIdList.length}个)</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public getData<T extends IBrokerInfo>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: IBrokerInfo) =>
|
||||
(item.host !== undefined && item.host !== null) && item.host.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public renderMore() {
|
||||
const { searchKey } = this.state;
|
||||
return (
|
||||
<>
|
||||
<div className="k-row">
|
||||
<ul className="k-tab">
|
||||
<li>Broker信息</li>
|
||||
{this.renderSearch('', '请输入Host')}
|
||||
</ul>
|
||||
<div style={searchKey ? { minHeight: 370 } : null}>
|
||||
<Table
|
||||
columns={this.brokerColumns}
|
||||
dataSource={this.getData(topic.brokerInfo)}
|
||||
rowKey="key"
|
||||
pagination={pagination}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
topic.getBrokerInfo(this.clusterId, this.topicName);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
{this.renderMore()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import * as React from 'react';
|
||||
import { Select, Spin, Tooltip } from 'component/antd';
|
||||
import { topic, IAppsIdInfo } from 'store/topic';
|
||||
import { ILabelValue } from 'types/base-type';
|
||||
import { observer } from 'mobx-react';
|
||||
import { selectOptionMap } from 'constants/status-map';
|
||||
import { ChartWithDatePicker } from 'component/chart';
|
||||
import { searchProps } from 'constants/table';
|
||||
import moment from 'moment';
|
||||
|
||||
@observer
|
||||
export class NetWorkFlow extends React.Component<any> {
|
||||
|
||||
public state = {
|
||||
loading: false,
|
||||
};
|
||||
private $chart: any;
|
||||
|
||||
public getChartData = (startTime: moment.Moment, endTime: moment.Moment) => {
|
||||
const { getApi } = this.props;
|
||||
topic.changeStartTime(startTime);
|
||||
topic.changeEndTime(endTime);
|
||||
return getApi();
|
||||
}
|
||||
|
||||
public handleChange = (value: any) => {
|
||||
const { selectChange } = this.props;
|
||||
this.$chart.changeChartOptions(selectChange(value));
|
||||
}
|
||||
|
||||
public handleAppChange = (value: string) => {
|
||||
const {clusterId, topicName} = this.props;
|
||||
topic.appId = value;
|
||||
this.setState({ loading: true });
|
||||
topic.getMetriceInfo(clusterId, topicName).then(data => {
|
||||
this.$chart.changeChartOptions(data);
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
}
|
||||
|
||||
public renderSelect() {
|
||||
const isTrue = this.props.selectArr === selectOptionMap;
|
||||
return (
|
||||
<>
|
||||
<li>
|
||||
<span className="label">统计类型</span>
|
||||
<Select
|
||||
defaultValue={this.props.type}
|
||||
style={{ width: 230 }}
|
||||
onChange={this.handleChange}
|
||||
{...searchProps}
|
||||
>
|
||||
{this.props.selectArr.map((item: ILabelValue) => (
|
||||
<Select.Option key={item.value} value={item.value}>
|
||||
{item.label.length > 16 ?
|
||||
<Tooltip placement="bottomLeft" title={item.label}>{item.label}</Tooltip>
|
||||
: item.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</li>
|
||||
<li className={!isTrue ? 'is-show' : ''}>
|
||||
<span className="label">应用</span>
|
||||
<Select
|
||||
placeholder="请选择应用"
|
||||
style={{ width: 180 }}
|
||||
onChange={this.handleAppChange}
|
||||
{...searchProps}
|
||||
>
|
||||
{topic.appInfo.map((item: IAppsIdInfo) => (
|
||||
<Select.Option key={item.appId} value={item.appId}>
|
||||
{item.appName.length > 16 ?
|
||||
<Tooltip placement="bottomLeft" title={item.appName}> {item.appName} </Tooltip>
|
||||
: item.appName}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
topic.initTime();
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Spin spinning={this.state.loading} className="chart-content">
|
||||
<ChartWithDatePicker
|
||||
customerNode={this.renderSelect()}
|
||||
getChartData={this.getChartData}
|
||||
ref={chart => this.$chart = chart}
|
||||
/>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import * as React from 'react';
|
||||
import './index.less';
|
||||
import { observer } from 'mobx-react';
|
||||
import { topic, IConnectionInfo } from 'store/topic';
|
||||
import { Table, Tooltip } from 'component/antd';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import Url from 'lib/url-parser';
|
||||
import { pagination, cellStyle } from 'constants/table';
|
||||
|
||||
@observer
|
||||
export class ConnectInformation extends SearchAndFilterContainer {
|
||||
public clusterId: number;
|
||||
public topicName: string;
|
||||
|
||||
public state = {
|
||||
searchKey: '',
|
||||
};
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
this.topicName = url.search.topic;
|
||||
}
|
||||
|
||||
public renderConnectionInfo(connectInfo: IConnectionInfo[]) {
|
||||
const clientType = Object.assign({
|
||||
title: '客户端类型',
|
||||
dataIndex: 'clientType',
|
||||
key: 'clientType',
|
||||
width: '20%',
|
||||
filters: [{ text: '消费', value: 'consumer' }, { text: '生产', value: 'produce' }],
|
||||
onFilter: (value: string, record: IConnectionInfo) => record.clientType.indexOf(value) === 0,
|
||||
render: (t: string) =>
|
||||
<span>{t === 'consumer' ? '消费' : '生产'}</span>,
|
||||
}, this.renderColumnsFilter('filterVisible'));
|
||||
|
||||
const columns = [{
|
||||
title: 'AppID',
|
||||
dataIndex: 'appId',
|
||||
key: 'appId',
|
||||
width: '20%',
|
||||
sorter: (a: IConnectionInfo, b: IConnectionInfo) => a.appId.charCodeAt(0) - b.appId.charCodeAt(0),
|
||||
},
|
||||
{
|
||||
title: '主机名',
|
||||
dataIndex: 'hostname',
|
||||
key: 'hostname',
|
||||
width: '40%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 250,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
render: (t: string) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={t} >{t}</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '客户端版本',
|
||||
dataIndex: 'clientVersion',
|
||||
key: 'clientVersion',
|
||||
width: '20%',
|
||||
},
|
||||
clientType,
|
||||
];
|
||||
if (connectInfo) {
|
||||
return (
|
||||
<>
|
||||
<Table dataSource={connectInfo} columns={columns} pagination={pagination} loading={topic.loading} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public getData<T extends IConnectionInfo>(origin: T[]) {
|
||||
let data: T[] = [];
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
if (searchKey !== '') {
|
||||
data = origin.filter(d =>
|
||||
((d.appId !== undefined && d.appId !== null) && d.appId.toLowerCase().includes(searchKey as string))
|
||||
|| ((d.hostname !== undefined && d.hostname !== null) && d.hostname.toLowerCase().includes(searchKey as string)),
|
||||
);
|
||||
} else {
|
||||
data = origin;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const appId = this.props.baseInfo.appId;
|
||||
topic.getConnectionInfo(this.clusterId, this.topicName, appId);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<div className="k-row" >
|
||||
<ul className="k-tab">
|
||||
<li>连接信息</li>
|
||||
{this.renderSearch('', '请输入连接信息', 'searchKey')}
|
||||
</ul>
|
||||
{this.renderConnectionInfo(this.getData(topic.connectionInfo))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
import * as React from 'react';
|
||||
import { wrapper, region } from 'store';
|
||||
import './index.less';
|
||||
import { topic, IConsumerGroups, IConsumeDetails } from 'store/topic';
|
||||
import { observer } from 'mobx-react';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import Url from 'lib/url-parser';
|
||||
import { pagination } from 'constants/table';
|
||||
import { IXFormWrapper, IOffset } from 'types/base-type';
|
||||
import { Table, Button, Tooltip } from 'component/antd';
|
||||
import ResetOffset from './reset-offset';
|
||||
import { urlPrefix } from 'constants/left-menu';
|
||||
import { cellStyle } from 'constants/table';
|
||||
import './index.less';
|
||||
|
||||
@observer
|
||||
export class GroupID extends SearchAndFilterContainer {
|
||||
|
||||
public static getDerivedStateFromProps(nextProps: any, prevState: any) {
|
||||
const url = Url();
|
||||
return {
|
||||
...prevState,
|
||||
isDetailPage: url.search.consumerGroup && url.search.location,
|
||||
};
|
||||
}
|
||||
public clusterId: number;
|
||||
public topicName: string;
|
||||
public consumerGroup: string;
|
||||
public location: string;
|
||||
public isPhysicalTrue: string;
|
||||
|
||||
public state = {
|
||||
searchKey: '',
|
||||
updateRender: false,
|
||||
isDetailPage: false,
|
||||
};
|
||||
|
||||
private xFormWrapper: IXFormWrapper;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.handleUrlSearch();
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
if (!topic.showConsumeDetail) {
|
||||
return topic.getConsumerGroups(this.clusterId, this.topicName);
|
||||
}
|
||||
return topic.getConsumeDetails(this.clusterId, this.topicName, this.consumerGroup, this.location);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: any, prevState: any) {
|
||||
if (prevState.isDetailPage !== this.state.isDetailPage) {
|
||||
this.handleUrlSearch();
|
||||
if (!topic.showConsumeDetail && !topic.consumerGroups.length) {
|
||||
topic.getConsumerGroups(this.clusterId, this.topicName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public handleUrlSearch = () => {
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
this.topicName = url.search.topic;
|
||||
const isPhysical = Url().search.hasOwnProperty('isPhysicalClusterId');
|
||||
this.isPhysicalTrue = isPhysical ? '&isPhysicalClusterId=true' : '';
|
||||
topic.setConsumeDetail(false);
|
||||
if (url.search.consumerGroup && url.search.location) {
|
||||
topic.setConsumeDetail(true);
|
||||
this.consumerGroup = url.search.consumerGroup;
|
||||
this.location = url.search.location;
|
||||
}
|
||||
}
|
||||
|
||||
public showConsumeDetail = (record: IConsumerGroups) => {
|
||||
// tslint:disable-next-line:max-line-length
|
||||
const url = `${urlPrefix}/topic/topic-detail?clusterId=${this.clusterId}&topic=${this.topicName}&consumerGroup=${record.consumerGroup}&location=${record.location}${this.isPhysicalTrue}®ion=${region.currentRegion}#4`;
|
||||
history.pushState({ url }, '', url);
|
||||
this.handleUrlSearch();
|
||||
topic.setConsumeDetail(true);
|
||||
topic.getConsumeDetails(this.clusterId, this.topicName, record.consumerGroup, record.location);
|
||||
this.setState({
|
||||
updateRender: !this.state.updateRender,
|
||||
});
|
||||
}
|
||||
|
||||
public updateDetailsStatus = () => {
|
||||
topic.getConsumeDetails(this.clusterId,
|
||||
this.topicName, this.consumerGroup, this.location);
|
||||
}
|
||||
|
||||
public backToPage = () => {
|
||||
// tslint:disable-next-line:max-line-length
|
||||
const url = `${urlPrefix}/topic/topic-detail?clusterId=${this.clusterId}&topic=${this.topicName}${this.isPhysicalTrue}®ion=${region.currentRegion}#4`;
|
||||
history.pushState({ url }, '', url);
|
||||
topic.setConsumeDetail(false);
|
||||
topic.getConsumerGroups(this.clusterId, this.topicName);
|
||||
this.setState({
|
||||
updateRender: !this.state.updateRender,
|
||||
});
|
||||
}
|
||||
|
||||
public showResetOffset() {
|
||||
this.xFormWrapper = {
|
||||
type: 'drawer',
|
||||
formMap: [
|
||||
],
|
||||
formData: {
|
||||
},
|
||||
visible: true,
|
||||
width: 600,
|
||||
title: '重置消费偏移',
|
||||
customRenderElement: this.renderDrawerInfo(),
|
||||
noform: true,
|
||||
nofooter: true,
|
||||
onSubmit: (value: any) => {
|
||||
//
|
||||
},
|
||||
};
|
||||
wrapper.open(this.xFormWrapper);
|
||||
}
|
||||
|
||||
public renderDrawerInfo() {
|
||||
const OffsetReset = {
|
||||
clusterId: this.clusterId,
|
||||
topicName: this.topicName,
|
||||
consumerGroup: this.consumerGroup,
|
||||
location: this.location,
|
||||
offsetList: [],
|
||||
timestamp: 0,
|
||||
} as IOffset;
|
||||
return (
|
||||
<div>
|
||||
<ResetOffset OffsetReset={OffsetReset} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public renderConsumerDetails() {
|
||||
const consumerGroup = this.consumerGroup;
|
||||
const columns = [{
|
||||
title: 'Partition ID',
|
||||
dataIndex: 'partitionId',
|
||||
key: 'partitionId',
|
||||
width: '10%',
|
||||
sorter: (a: IConsumeDetails, b: IConsumeDetails) => +b.partitionId - +a.partitionId,
|
||||
}, {
|
||||
title: 'Consumer ID',
|
||||
dataIndex: 'clientId',
|
||||
key: 'clientId',
|
||||
width: '40%',
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 300,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
render: (t: IConsumeDetails) => <Tooltip placement="bottomLeft" title={t}> {t} </Tooltip>,
|
||||
}, {
|
||||
title: 'Consume Offset',
|
||||
dataIndex: 'consumeOffset',
|
||||
key: 'consumeOffset',
|
||||
width: '20%',
|
||||
sorter: (a: IConsumeDetails, b: IConsumeDetails) => +b.consumeOffset - +a.consumeOffset,
|
||||
}, {
|
||||
title: 'Partition Offset',
|
||||
dataIndex: 'partitionOffset',
|
||||
key: 'partitionOffset',
|
||||
width: '20%',
|
||||
sorter: (a: IConsumeDetails, b: IConsumeDetails) => +b.partitionOffset - +a.partitionOffset,
|
||||
}, {
|
||||
title: 'Lag',
|
||||
dataIndex: 'lag',
|
||||
key: 'lag',
|
||||
width: '10%',
|
||||
sorter: (a: IConsumeDetails, b: IConsumeDetails) => +b.lag - +a.lag,
|
||||
}];
|
||||
return (
|
||||
<>
|
||||
<div className="details-box">
|
||||
<b>{consumerGroup}</b>
|
||||
<div>
|
||||
<Button onClick={this.backToPage}>返回</Button>
|
||||
<Button onClick={this.updateDetailsStatus}>刷新</Button>
|
||||
<Button onClick={() => this.showResetOffset()}>重置Offset</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={topic.consumeDetails}
|
||||
rowKey="key"
|
||||
pagination={pagination}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public renderConsumerTable(consumerData: IConsumerGroups[]) {
|
||||
const columns = [{
|
||||
title: '消费组名称',
|
||||
dataIndex: 'consumerGroup',
|
||||
key: 'consumerGroup',
|
||||
width: '36%',
|
||||
render: (t: string, r: IConsumerGroups) => (
|
||||
<> <a onClick={() => this.showConsumeDetail(r)}>{t}</a> </>
|
||||
),
|
||||
}, {
|
||||
title: 'AppIds',
|
||||
dataIndex: 'appIds',
|
||||
key: 'appIds',
|
||||
width: '36%',
|
||||
}, {
|
||||
title: 'Location',
|
||||
dataIndex: 'location',
|
||||
key: 'location',
|
||||
width: '34%',
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={consumerData}
|
||||
pagination={pagination}
|
||||
rowKey="key"
|
||||
scroll={{ y: 400 }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public getData<T extends IConsumerGroups>(origin: T[]) {
|
||||
let data: T[] = origin;
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
data = searchKey ? origin.filter((item: IConsumerGroups) =>
|
||||
(item.consumerGroup !== undefined && item.consumerGroup !== null) && item.consumerGroup.toLowerCase().includes(searchKey as string),
|
||||
) : origin ;
|
||||
return data;
|
||||
}
|
||||
|
||||
public renderConsumerGroups() {
|
||||
return (
|
||||
<>
|
||||
<div className="k-row" >
|
||||
<ul className="k-tab">
|
||||
<li>消费组信息</li>
|
||||
{this.renderSearch('', '请输入消费组名称')}
|
||||
</ul>
|
||||
{this.renderConsumerTable(this.getData(topic.consumerGroups))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
{topic.showConsumeDetail ? this.renderConsumerDetails() : this.renderConsumerGroups()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
|
||||
.topic-detail-header {
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
.base-info {
|
||||
width: 100%;
|
||||
|
||||
.base-info-header {
|
||||
background: #fff;
|
||||
min-width: 960px;
|
||||
height: 200px;
|
||||
padding: 20px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
ul {
|
||||
width: 320px;
|
||||
position: relative;
|
||||
|
||||
li {
|
||||
float: left;
|
||||
font-size: 13px;
|
||||
width: 320px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
/**超出部分省略号**/
|
||||
overflow: hidden;
|
||||
/** 隐藏超出的内容 **/
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
|
||||
b {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.special-li {
|
||||
min-width: 960px;
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.traffic-table {
|
||||
margin: 10px 0;
|
||||
|
||||
.traffic-header {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
font-weight: bold;
|
||||
background: rgb(245, 245, 245);
|
||||
border: 1px solid #e8e8e8;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
span {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 13px;
|
||||
line-height: 44px;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
.k-abs {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.p-params {
|
||||
display: inline-block;
|
||||
padding: 0px 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(217, 217, 217, 1);
|
||||
margin: 0px 8px 8px 0px;
|
||||
|
||||
&-unFinished {
|
||||
background: rgba(245, 34, 45, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.p-description {
|
||||
margin-left: 20px;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
width: 150px;
|
||||
text-align: right;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.o-container {
|
||||
width: 100%;
|
||||
padding: 40px 0 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.partionButton {
|
||||
float: right;
|
||||
margin: 30px 60px;
|
||||
}
|
||||
|
||||
.b-list {
|
||||
button {
|
||||
margin: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.title-con {
|
||||
font-size: 14px;
|
||||
color: @primary-color;
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 13px;
|
||||
line-height: 44px;
|
||||
font-weight: 100;
|
||||
background: rgb(245, 245, 245);
|
||||
border: 1px solid #e8e8e8;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.chart-box-0 {
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
min-height: 520px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.chart-title {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 13px;
|
||||
line-height: 44px;
|
||||
font-weight: 100;
|
||||
background: rgb(245, 245, 245);
|
||||
border: 1px solid #e8e8e8;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.chart-divider {
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
|
||||
.ant-divider-horizontal {
|
||||
margin: 0px 0px 24px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.details-box {
|
||||
padding: 0 5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 5px;
|
||||
|
||||
b {
|
||||
font-weight: 100;
|
||||
line-height: 32px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
Button {
|
||||
margin: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.sample-button {
|
||||
margin-right: 100px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.topic-detail-sample {
|
||||
margin-top: 60px;
|
||||
|
||||
h2 {
|
||||
line-height: 35px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.detail-sample-box {
|
||||
width: 560px;
|
||||
border: 1px solid #d9d9dd;
|
||||
border-radius: 3px;
|
||||
|
||||
h3 {
|
||||
font-weight: bold;
|
||||
height: 45px;
|
||||
line-height: 45px;
|
||||
border-bottom: 1px solid #e3e3e4;
|
||||
background: #f5f7f9;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.detail-sample-span {
|
||||
height: 500px;
|
||||
overflow-y: auto;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid #e3e3e4;
|
||||
padding: 5px 10px;
|
||||
line-height: 24px;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-show {
|
||||
display: none;
|
||||
}
|
||||
380
kafka-manager-console/src/container/topic/topic-detail/index.tsx
Normal file
380
kafka-manager-console/src/container/topic/topic-detail/index.tsx
Normal file
@@ -0,0 +1,380 @@
|
||||
import * as React from 'react';
|
||||
import './index.less';
|
||||
import { wrapper, region } from 'store';
|
||||
import { Tabs, PageHeader, Button, notification, Drawer, message, Icon } from 'antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import { BaseInformation } from './base-information';
|
||||
import { StatusChart } from './status-chart';
|
||||
import { ConnectInformation } from './connect-information';
|
||||
import { GroupID } from './group-id';
|
||||
import { PartitionInformation } from './partition-information';
|
||||
import { BrokersInformation } from './brokers-information';
|
||||
import { AppIdInformation } from './appid-information';
|
||||
import { BillInformation } from './bill-information';
|
||||
import { IXFormWrapper, ITopic } from 'types/base-type';
|
||||
import { getTopicCompile, getTopicSampling } from 'lib/api';
|
||||
import { copyString } from 'lib/utils';
|
||||
import { topic, ITopicBaseInfo } from 'store/topic';
|
||||
import { XFormComponent, IFormItem } from 'component/x-form';
|
||||
import { applyExpandModal } from 'container/modal';
|
||||
import { applyTopicQuotaQuery } from '../config';
|
||||
import { users } from 'store/users';
|
||||
import { urlPrefix } from 'constants/left-menu';
|
||||
import { handlePageBack } from 'lib/utils';
|
||||
import Url from 'lib/url-parser';
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
interface IInfoData {
|
||||
value: string;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class TopicDetail extends React.Component {
|
||||
public clusterId: number;
|
||||
public topicName: string;
|
||||
public isPhysicalTrue: string;
|
||||
|
||||
public state = {
|
||||
drawerVisible: false,
|
||||
infoVisible: false,
|
||||
infoTopicList: [] as IInfoData[],
|
||||
};
|
||||
private $formRef: any;
|
||||
|
||||
private xFormWrapper: IXFormWrapper;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
this.topicName = url.search.topic;
|
||||
const isPhysical = Url().search.hasOwnProperty('isPhysicalClusterId');
|
||||
this.isPhysicalTrue = isPhysical ? '&isPhysicalClusterId=true' : '';
|
||||
}
|
||||
|
||||
public compileDetails() {
|
||||
this.xFormWrapper = {
|
||||
formMap: [
|
||||
{
|
||||
key: 'topicName',
|
||||
label: 'Topic名称',
|
||||
attrs: {
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'appName',
|
||||
label: '应用名称',
|
||||
attrs: {
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
label: '备注',
|
||||
attrs: {
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入备注',
|
||||
}],
|
||||
},
|
||||
],
|
||||
formData: {
|
||||
topicName: this.topicName,
|
||||
appName: topic.baseInfo.appName,
|
||||
description: topic.baseInfo.description,
|
||||
},
|
||||
okText: '确认',
|
||||
visible: true,
|
||||
width: 600,
|
||||
title: '编辑',
|
||||
onSubmit: (value: any) => {
|
||||
const compile = {
|
||||
appId: topic.baseInfo.appId,
|
||||
clusterId: this.clusterId,
|
||||
description: value.description,
|
||||
topicName: this.topicName,
|
||||
} as ITopic;
|
||||
getTopicCompile(compile).then(data => {
|
||||
notification.success({ message: '编辑成功' });
|
||||
topic.getTopicBasicInfo(this.clusterId, this.topicName);
|
||||
});
|
||||
},
|
||||
};
|
||||
wrapper.open(this.xFormWrapper);
|
||||
}
|
||||
|
||||
public drawerRender() {
|
||||
const formMap = [
|
||||
{
|
||||
key: 'maxMsgNum',
|
||||
label: '最大采样数据条数',
|
||||
type: 'input_number',
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入最大采样数据条数',
|
||||
}],
|
||||
attrs: {
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'timeout',
|
||||
label: '最大采样时间',
|
||||
type: 'input_number',
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入最大采样时间',
|
||||
}],
|
||||
attrs: {
|
||||
max: 300000,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'partitionId',
|
||||
label: '分区号',
|
||||
type: 'input_number',
|
||||
rules: [{
|
||||
required: false,
|
||||
message: '请输入分区号',
|
||||
}],
|
||||
},
|
||||
{
|
||||
key: 'offset',
|
||||
label: '偏移量',
|
||||
type: 'input_number',
|
||||
rules: [{
|
||||
required: false,
|
||||
message: '请输入偏移量',
|
||||
}],
|
||||
},
|
||||
{
|
||||
key: 'truncate',
|
||||
label: '是否截断',
|
||||
type: 'radio_group',
|
||||
defaultValue: 'true',
|
||||
options: [{
|
||||
label: '是',
|
||||
value: 'true',
|
||||
}, {
|
||||
label: '否',
|
||||
value: 'false',
|
||||
}],
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请选择是否截断',
|
||||
}],
|
||||
},
|
||||
] as IFormItem [];
|
||||
const formData = {
|
||||
maxMsgNum: 1,
|
||||
timeout: 3000,
|
||||
};
|
||||
const { infoVisible } = this.state;
|
||||
return(
|
||||
<>
|
||||
<Drawer
|
||||
title="Topic 采样"
|
||||
placement="right"
|
||||
closable={false}
|
||||
onClose={this.drawerClose}
|
||||
visible={this.state.drawerVisible}
|
||||
width={600}
|
||||
key="1"
|
||||
>
|
||||
<XFormComponent
|
||||
ref={form => this.$formRef = form}
|
||||
formData={formData}
|
||||
formMap={formMap}
|
||||
/>
|
||||
<Button type="primary" onClick={this.drawerSubmit} className="sample-button">采样</Button>
|
||||
{infoVisible ? this.renderInfo() : null}
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public getAllValue = () => {
|
||||
const { infoTopicList } = this.state;
|
||||
const text = infoTopicList.map(ele => ele.value );
|
||||
return text.join('\n\n');
|
||||
}
|
||||
|
||||
public renderInfo() {
|
||||
const { infoTopicList } = this.state;
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="topic-detail-sample">
|
||||
<h2>采样</h2>
|
||||
<div className="detail-sample-box">
|
||||
<h3>
|
||||
{this.topicName} <Icon
|
||||
onClick={() => copyString(this.getAllValue())}
|
||||
type="copy"
|
||||
className="didi-theme"
|
||||
/>
|
||||
</h3>
|
||||
<div className="detail-sample-span">
|
||||
{infoTopicList.map((v, index) => (
|
||||
<li key={index}>
|
||||
<Icon
|
||||
onClick={() => copyString(v.value)}
|
||||
type="copy"
|
||||
className="didi-theme"
|
||||
/> {v.value}
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public drawerSubmit = (value: any) => {
|
||||
this.$formRef.validateFields((error: Error, result: any) => {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
result.truncate = result.truncate === 'true';
|
||||
getTopicSampling(result, this.clusterId, this.topicName).then(data => {
|
||||
this.setState({
|
||||
infoTopicList: data,
|
||||
infoVisible: true,
|
||||
});
|
||||
message.success('采样成功');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public showDrawer = () => {
|
||||
this.setState({
|
||||
drawerVisible: true,
|
||||
});
|
||||
}
|
||||
|
||||
public drawerClose = () => {
|
||||
this.setState({
|
||||
drawerVisible: false,
|
||||
infoVisible: false,
|
||||
});
|
||||
this.resetForm();
|
||||
}
|
||||
|
||||
public resetForm = (resetFields?: string[]) => {
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
this.$formRef && this.$formRef.resetFields(resetFields || '');
|
||||
}
|
||||
|
||||
public cusstr(str: string, findStr: string, num: number) {
|
||||
let idx = str.indexOf(findStr);
|
||||
let count = 1;
|
||||
while (idx >= 0 && count < num) {
|
||||
idx = str.indexOf(findStr, idx + 1);
|
||||
count++;
|
||||
}
|
||||
if (idx < 0) {
|
||||
return '';
|
||||
}
|
||||
return str.substring(0, idx);
|
||||
}
|
||||
|
||||
public setConsumeUrl = (key: string) => {
|
||||
// tslint:disable-next-line:max-line-length
|
||||
const url = `${urlPrefix}/topic/topic-detail?clusterId=${this.clusterId}&topic=${this.topicName}${this.isPhysicalTrue}®ion=${region.currentRegion}#${key}`;
|
||||
history.pushState({ url }, '', url);
|
||||
}
|
||||
|
||||
public handleTabKey = (key: string) => {
|
||||
location.hash = key;
|
||||
if (location.search.match(RegExp(/&consumerGroup=/))) {
|
||||
this.setConsumeUrl(key);
|
||||
}
|
||||
}
|
||||
|
||||
public onTabClick = (key: string) => {
|
||||
location.hash = key;
|
||||
if (key === '4') {
|
||||
topic.setConsumeDetail(false);
|
||||
this.setConsumeUrl(key);
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
topic.getTopicBasicInfo(this.clusterId, this.topicName);
|
||||
topic.getTopicBusiness(this.clusterId, this.topicName);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const role = users.currentUser.role;
|
||||
const baseInfo = topic.baseInfo as ITopicBaseInfo;
|
||||
const showEditBtn = topic.topicBusiness && topic.topicBusiness.principals.includes(users.currentUser.username);
|
||||
const topicRecord = {
|
||||
clusterId: this.clusterId,
|
||||
topicName: this.topicName,
|
||||
} as ITopic;
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
baseInfo ?
|
||||
<>
|
||||
<PageHeader
|
||||
className="detail topic-detail-header"
|
||||
onBack={() => handlePageBack(`/topic`)}
|
||||
title={this.topicName || ''}
|
||||
extra={
|
||||
<>
|
||||
<Button key="1" type="primary" onClick={() => applyTopicQuotaQuery(topicRecord)} >申请配额</Button>
|
||||
<Button key="2" type="primary" onClick={() => applyExpandModal(topicRecord)} >申请分区</Button>
|
||||
<Button key="3" type="primary" onClick={this.showDrawer.bind(this)} >采样</Button>
|
||||
{showEditBtn && <Button key="4" onClick={() => this.compileDetails()} type="primary">编辑</Button>}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Tabs
|
||||
activeKey={location.hash.substr(1) || '1'}
|
||||
type="card"
|
||||
onChange={this.handleTabKey}
|
||||
onTabClick={this.onTabClick}
|
||||
>
|
||||
<TabPane tab="基本信息" key="1">
|
||||
<BaseInformation baseInfo={topic.baseInfo} />
|
||||
</TabPane>
|
||||
<TabPane tab="状态图" key="2">
|
||||
<StatusChart />
|
||||
</TabPane>
|
||||
<TabPane tab="连接信息" key="3">
|
||||
<ConnectInformation baseInfo={topic.baseInfo} />
|
||||
</TabPane>
|
||||
<TabPane tab="消费组信息" key="4">
|
||||
<GroupID />
|
||||
</TabPane>
|
||||
<TabPane tab="分区信息" key="5">
|
||||
<PartitionInformation />
|
||||
</TabPane>
|
||||
{
|
||||
role === 0 ? null :
|
||||
<TabPane tab="Broker信息" key="6">
|
||||
<BrokersInformation />
|
||||
</TabPane>
|
||||
}
|
||||
<TabPane tab="应用信息" key="7">
|
||||
<AppIdInformation />
|
||||
</TabPane>
|
||||
<TabPane tab="账单信息" key="8">
|
||||
<BillInformation />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</>
|
||||
: null
|
||||
}
|
||||
{this.drawerRender()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import * as React from 'react';
|
||||
import './index.less';
|
||||
import { observer } from 'mobx-react';
|
||||
import { topic, IPartitionsInfo } from 'store/topic';
|
||||
import { Table, Tooltip } from 'component/antd';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import Url from 'lib/url-parser';
|
||||
import { transBToMB } from 'lib/utils';
|
||||
import { pagination } from 'constants/table';
|
||||
|
||||
@observer
|
||||
export class PartitionInformation extends SearchAndFilterContainer {
|
||||
public clusterId: number;
|
||||
public topicName: string;
|
||||
|
||||
public state = {
|
||||
brokerKey: '',
|
||||
searchKey: '',
|
||||
};
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
this.topicName = url.search.topic;
|
||||
}
|
||||
|
||||
public renderColumns = () => {
|
||||
const underReplicated = Object.assign({
|
||||
title: '是否同步',
|
||||
key: 'underReplicated',
|
||||
dataIndex: 'underReplicated',
|
||||
filters: [{ text: '是', value: 'true' }, { text: '否', value: 'false' }],
|
||||
onFilter: (value: string, record: IPartitionsInfo) => record.underReplicated + '' === value,
|
||||
render: (t: any) => <span className={t ? 'success' : 'fail'}>{t ? '是' : '否'}</span>,
|
||||
}, this.renderColumnsFilter('filterVisible'));
|
||||
return [{
|
||||
title: '分区ID',
|
||||
key: 'partitionId',
|
||||
dataIndex: 'partitionId',
|
||||
sorter: (a: IPartitionsInfo, b: IPartitionsInfo) => b.partitionId - a.partitionId,
|
||||
}, {
|
||||
title: 'beginningOffset',
|
||||
key: 'beginningOffset',
|
||||
dataIndex: 'beginningOffset',
|
||||
sorter: (a: IPartitionsInfo, b: IPartitionsInfo) => b.beginningOffset - a.beginningOffset,
|
||||
}, {
|
||||
title: 'endOffset',
|
||||
key: 'endOffset',
|
||||
dataIndex: 'endOffset',
|
||||
sorter: (a: IPartitionsInfo, b: IPartitionsInfo) => b.endOffset - a.endOffset,
|
||||
}, {
|
||||
title: 'msgNum',
|
||||
key: 'msgNum',
|
||||
dataIndex: 'msgNum',
|
||||
sorter: (a: IPartitionsInfo, b: IPartitionsInfo) => b.msgNum - a.msgNum,
|
||||
}, {
|
||||
title: 'Leader Broker',
|
||||
key: 'leaderBrokerId',
|
||||
dataIndex: 'leaderBrokerId',
|
||||
sorter: (a: IPartitionsInfo, b: IPartitionsInfo) => b.leaderBrokerId - a.leaderBrokerId,
|
||||
}, {
|
||||
title: 'LogSize(MB)',
|
||||
key: 'logSize',
|
||||
dataIndex: 'logSize',
|
||||
sorter: (a: IPartitionsInfo, b: IPartitionsInfo) => b.logSize - a.logSize,
|
||||
render: (t: number) => transBToMB(t),
|
||||
}, {
|
||||
title: '优选副本',
|
||||
key: 'preferredBrokerId',
|
||||
dataIndex: 'preferredBrokerId',
|
||||
sorter: (a: IPartitionsInfo, b: IPartitionsInfo) => b.preferredBrokerId - a.preferredBrokerId,
|
||||
}, {
|
||||
title: 'AR',
|
||||
key: 'replicaBrokerIdList',
|
||||
dataIndex: 'replicaBrokerIdList',
|
||||
render: (t: []) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={t.join('、')}>
|
||||
{t ? t.map(i => <span key={i} className="p-params">{i}</span>) : null}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
title: 'ISR',
|
||||
key: 'isrBrokerIdList',
|
||||
dataIndex: 'isrBrokerIdList',
|
||||
render: (t: []) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={t.join('、')}>
|
||||
{t ? t.map(i => <span key={i} className="p-params">{i}</span>) : null}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
underReplicated];
|
||||
}
|
||||
|
||||
public renderPartitionsInfo() {
|
||||
const { searchKey } = this.state;
|
||||
const data = searchKey ?
|
||||
topic.partitionsInfo.filter((d) => d.partitionId + '' === searchKey) : topic.partitionsInfo;
|
||||
return (
|
||||
<>
|
||||
<div className="k-row" >
|
||||
<ul className="k-tab">
|
||||
<li>分区信息</li>
|
||||
{this.renderSearch('', '请输入分区号')}
|
||||
</ul>
|
||||
<div style={searchKey ? { minHeight: 700 } : null}>
|
||||
<Table
|
||||
columns={this.renderColumns()}
|
||||
table-Layout="fixed"
|
||||
dataSource={data}
|
||||
rowKey="key"
|
||||
pagination={pagination}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
topic.getPartitionsInfo(this.clusterId, this.topicName);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>{this.renderPartitionsInfo()}</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import * as React from 'react';
|
||||
import { Form, Row, Button, Input, DatePicker, Col, Select, Radio, Alert, notification, Tooltip } from 'component/antd';
|
||||
import { topic } from 'store/topic';
|
||||
import { consume } from 'store/consume';
|
||||
import { observer } from 'mobx-react';
|
||||
import { resetOffset } from 'lib/api';
|
||||
import { timeMinute } from 'constants/strategy';
|
||||
import { searchProps } from 'constants/table';
|
||||
import { disabledDate, disabledDateTime } from 'lib/utils';
|
||||
import moment, { Moment } from 'moment';
|
||||
import './index.less';
|
||||
|
||||
@observer
|
||||
class ResetOffset extends React.Component<any> {
|
||||
|
||||
public state = {
|
||||
typeValue: 'time',
|
||||
offsetValue: 'offset',
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public handleSubmit = (e: React.MouseEvent<any, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
const { typeValue, offsetValue } = this.state;
|
||||
if (typeValue === 'time') {
|
||||
this.props.form.validateFields((err: Error, values: any) => {
|
||||
if (err) return;
|
||||
const { timestamp } = values;
|
||||
this.props.OffsetReset.offsetList = [];
|
||||
if (offsetValue === 'offset') {
|
||||
this.props.OffsetReset.offsetPos = 2;
|
||||
this.props.OffsetReset.timestamp = 0;
|
||||
} else {
|
||||
this.props.OffsetReset.offsetPos = 0;
|
||||
this.props.OffsetReset.timestamp = +moment(timestamp).format('x');
|
||||
}
|
||||
resetOffset(this.props.OffsetReset).then(data => {
|
||||
notification.success({ message: '重置时间成功' });
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const offsetData = consume.offsetList.filter(ele => ele.offset || ele.partitionId);
|
||||
if (offsetData && offsetData.length) {
|
||||
this.props.OffsetReset.offsetPos = 0;
|
||||
this.props.OffsetReset.timestamp = 0;
|
||||
this.props.OffsetReset.offsetList = offsetData;
|
||||
return resetOffset(this.props.OffsetReset).then(data => {
|
||||
notification.success({ message: '重置分区及偏移成功' });
|
||||
});
|
||||
}
|
||||
return notification.warning({ message: '请先填写分区及偏移!!!' });
|
||||
}
|
||||
}
|
||||
|
||||
public onChangeType = (e: any) => {
|
||||
this.setState({
|
||||
typeValue: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
public onChangeOffset = (e: any) => {
|
||||
this.setState({
|
||||
offsetValue: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
const { typeValue, offsetValue } = this.state;
|
||||
return (
|
||||
<>
|
||||
<Alert message="重置之前一定要关闭消费客户端!!!" type="warning" showIcon={true} />
|
||||
<Alert message="重置之前一定要关闭消费客户端!!!" type="warning" showIcon={true} />
|
||||
<Alert message="重置之前一定要关闭消费客户端!!!" type="warning" showIcon={true} className="mb-30"/>
|
||||
<div className="o-container">
|
||||
<Form labelAlign="left" onSubmit={this.handleSubmit} >
|
||||
<Radio.Group onChange={this.onChangeType} value={typeValue}>
|
||||
<Radio value="time"><span className="title-con">重置到指定时间</span></Radio>
|
||||
<Row>
|
||||
<Col span={26}>
|
||||
<Form.Item label="" >
|
||||
<Radio.Group
|
||||
onChange={this.onChangeOffset}
|
||||
value={offsetValue}
|
||||
disabled={typeValue === 'partition'}
|
||||
defaultValue="offset"
|
||||
className="mr-10"
|
||||
>
|
||||
<Radio.Button value="offset">最新offset</Radio.Button>
|
||||
<Radio.Button value="custom">自定义</Radio.Button>
|
||||
</Radio.Group>
|
||||
{typeValue === 'time' && offsetValue === 'custom' &&
|
||||
getFieldDecorator('timestamp', {
|
||||
rules: [{ required: false, message: '' }],
|
||||
initialValue: moment(),
|
||||
})(
|
||||
<DatePicker
|
||||
showTime={true}
|
||||
format={timeMinute}
|
||||
disabledDate={disabledDate}
|
||||
disabledTime={disabledDateTime}
|
||||
style={{ width: '50%' }}
|
||||
/>,
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Radio value="partition"><span className="title-con">重置指定分区及偏移</span></Radio>
|
||||
</Radio.Group>
|
||||
<Row>
|
||||
<Form.Item>
|
||||
<Row>
|
||||
<Col span={8}>分区ID</Col>
|
||||
<Col span={16}>偏移</Col>
|
||||
</Row>
|
||||
{consume.offsetList.map((ele, index) => {
|
||||
return (
|
||||
<Row key={index} gutter={16}>
|
||||
<Col span={8}>
|
||||
<Select
|
||||
placeholder="请选择"
|
||||
onChange={consume.selectChange.bind(consume, index)}
|
||||
disabled={typeValue === 'time'}
|
||||
{...searchProps}
|
||||
>
|
||||
{topic.consumeDetails.map((r, k) => {
|
||||
return <Select.Option key={k} value={r.partitionId}>
|
||||
{(r.partitionId + '').length > 16 ?
|
||||
<Tooltip placement="bottomLeft" title={r.partitionId}>{r.partitionId}</Tooltip>
|
||||
: r.partitionId}
|
||||
</Select.Option>;
|
||||
})}
|
||||
</Select></Col>
|
||||
<Col span={10}>
|
||||
<Input
|
||||
placeholder="请输入partition offset"
|
||||
disabled={typeValue === 'time'}
|
||||
onChange={consume.inputChange.bind(consume, index)}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6} className="b-list">
|
||||
<Button
|
||||
type="dashed"
|
||||
icon="plus"
|
||||
disabled={typeValue === 'time'}
|
||||
onClick={consume.handleList.bind(null, null)}
|
||||
/>
|
||||
<Button
|
||||
type="dashed"
|
||||
icon="minus"
|
||||
disabled={index === 0 || typeValue === 'time'}
|
||||
onClick={consume.handleList.bind(null, index)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
<Row className="partionButton">
|
||||
<Button type="primary" htmlType="submit" onClick={this.handleSubmit}>重置</Button>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
</Row>
|
||||
</Form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create<any>({ name: 'topicSample' })(ResetOffset);
|
||||
@@ -0,0 +1,78 @@
|
||||
import * as React from 'react';
|
||||
import './index.less';
|
||||
import Url from 'lib/url-parser';
|
||||
import { topic } from 'store/topic';
|
||||
import { NetWorkFlow } from './common-detail';
|
||||
import { IOptionType, ITakeType } from 'types/base-type';
|
||||
import { selectOptionMap, selectTakeMap } from 'constants/status-map';
|
||||
import { Divider } from 'component/antd';
|
||||
import { indexUrl } from 'constants/strategy';
|
||||
|
||||
export class StatusChart extends React.Component {
|
||||
public clusterId: number;
|
||||
public topicName: string;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const url = Url();
|
||||
this.clusterId = Number(url.search.clusterId);
|
||||
this.topicName = url.search.topic;
|
||||
}
|
||||
|
||||
public onSelectChange(e: IOptionType) {
|
||||
return topic.changeType(e);
|
||||
}
|
||||
|
||||
public onSelectTakeChange(e: ITakeType) {
|
||||
return topic.changeTakeType(e);
|
||||
}
|
||||
|
||||
public getOptionApi = () => {
|
||||
return topic.getMetriceInfo(this.clusterId, this.topicName);
|
||||
}
|
||||
|
||||
public getTakeApi = () => {
|
||||
return topic.getMetriceTake(this.clusterId, this.topicName);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
topic.getAppsIdInfo(this.clusterId, this.topicName);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<div className="chart-box-0">
|
||||
<div className="chart-title">
|
||||
<span className="action-button">历史流量</span>
|
||||
<a href={indexUrl} target="_blank">指标说明</a>
|
||||
</div>
|
||||
<Divider className="chart-divider" />
|
||||
<NetWorkFlow
|
||||
key="1"
|
||||
selectArr={selectOptionMap}
|
||||
type={topic.type}
|
||||
selectChange={(value: IOptionType) => this.onSelectChange(value)}
|
||||
getApi={() => this.getOptionApi()}
|
||||
clusterId={this.clusterId}
|
||||
topicName={this.topicName}
|
||||
/>
|
||||
</div>
|
||||
<div className="chart-box-0">
|
||||
<div className="chart-title">
|
||||
<span className="action-button">历史耗时信息</span>
|
||||
<a href={indexUrl} target="_blank">指标说明</a>
|
||||
</div>
|
||||
<Divider className="chart-divider" />
|
||||
<NetWorkFlow
|
||||
key="2"
|
||||
selectArr={selectTakeMap}
|
||||
type={topic.takeType}
|
||||
selectChange={(value: ITakeType) => this.onSelectTakeChange(value)}
|
||||
getApi={() => this.getTakeApi()}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
170
kafka-manager-console/src/container/topic/topic-mine.tsx
Normal file
170
kafka-manager-console/src/container/topic/topic-mine.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import * as React from 'react';
|
||||
import { Tabs, Table, Button } from 'component/antd';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { observer } from 'mobx-react';
|
||||
import { topic } from 'store/topic';
|
||||
import { app } from 'store/app';
|
||||
import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { ITopic } from 'types/base-type';
|
||||
import { getExpireColumns, getMyTopicColumns } from './config';
|
||||
import { applyTopic, deferTopic, applyOnlineModal } from 'container/modal';
|
||||
import { pagination } from 'constants/table';
|
||||
import { tableFilter } from 'lib/utils';
|
||||
import { topicStatusMap } from 'constants/status-map';
|
||||
import './index.less';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
@observer
|
||||
export class MineTopic extends SearchAndFilterContainer {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
filterAccess: false,
|
||||
};
|
||||
|
||||
public getData<T extends ITopic>(origin: T[]) {
|
||||
let data: T[] = [];
|
||||
let { searchKey } = this.state;
|
||||
searchKey = (searchKey + '').trim().toLowerCase();
|
||||
|
||||
if (cluster.active !== -1 || app.active !== '-1' || searchKey !== '') {
|
||||
data = origin.filter(d =>
|
||||
((d.topicName !== undefined && d.topicName !== null) && d.topicName.toLowerCase().includes(searchKey as string)
|
||||
|| ((d.appName !== undefined && d.appName !== null) && d.appName.toLowerCase().includes(searchKey as string)))
|
||||
&& (cluster.active === -1 || d.clusterId === cluster.active)
|
||||
&& (app.active === '-1' || d.appId === (app.active + '')),
|
||||
);
|
||||
} else {
|
||||
data = origin;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
topic.getTopic();
|
||||
topic.getExpired();
|
||||
|
||||
if (!cluster.clusterData.length) {
|
||||
cluster.getClusters();
|
||||
}
|
||||
if (!app.data.length) {
|
||||
app.getAppList();
|
||||
}
|
||||
}
|
||||
|
||||
public renderOperationPanel(key: number) {
|
||||
return (
|
||||
<div className="table-operation-panel">
|
||||
<ul>
|
||||
{this.renderApp('关联应用:')}
|
||||
{this.renderCluster('集群:')}
|
||||
{this.renderSearch('名称:', '请输入Topic名称或者应用')}
|
||||
{
|
||||
key === 1 && <li className="right-btn-1">
|
||||
<Button type="primary" onClick={() => { applyTopic(); }}>
|
||||
申请Topic
|
||||
</Button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public getColumns = (mytopicData: ITopic[]) => {
|
||||
const access = Object.assign({
|
||||
title: '权限',
|
||||
dataIndex: 'access',
|
||||
key: 'access',
|
||||
width: '10%',
|
||||
filters: tableFilter<ITopic>(mytopicData, 'access', topicStatusMap),
|
||||
onFilter: (text: number, record: ITopic) => record.access === text,
|
||||
render: (val: number) => (
|
||||
<div className={val === 0 ? '' : 'success'}>
|
||||
{topicStatusMap[val] || ''}
|
||||
</div>
|
||||
),
|
||||
}, this.renderColumnsFilter('filterAccess')) as any;
|
||||
|
||||
const columns = getMyTopicColumns(this.urlPrefix);
|
||||
|
||||
columns.splice(-2, 0, access);
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
public renderMyTopicTable(mytopicData: ITopic[]) {
|
||||
return (
|
||||
<div>
|
||||
<Table
|
||||
rowKey="key"
|
||||
loading={topic.loading}
|
||||
dataSource={mytopicData}
|
||||
columns={this.getColumns(mytopicData)}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public renderDeprecatedTopicTable(expireData: ITopic[]) {
|
||||
const operationCol = {
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
width: '15%',
|
||||
render: (val: string, item: ITopic, index: number) => (
|
||||
<>
|
||||
<span>
|
||||
<a style={{ marginRight: 16 }} onClick={() => deferTopic(item)}>申请延期</a>
|
||||
<a style={{ marginRight: 16 }} onClick={() => applyOnlineModal(item)}>申请下线</a>
|
||||
</span>
|
||||
</>
|
||||
),
|
||||
} as any;
|
||||
const expireColumns = getExpireColumns(this.urlPrefix);
|
||||
expireColumns.push(operationCol);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table
|
||||
rowKey="key"
|
||||
dataSource={expireData}
|
||||
columns={expireColumns}
|
||||
pagination={pagination}
|
||||
loading={topic.expiredLoading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public handleTabKey(key: string) {
|
||||
location.hash = key;
|
||||
cluster.changeCluster(-1);
|
||||
app.changeActiveApp('-1');
|
||||
this.setState({
|
||||
searchKey: '',
|
||||
});
|
||||
topic.setLoading(false);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<div className="min-width">
|
||||
<Tabs activeKey={location.hash.substr(1) || '1'} type="card" onChange={(key) => this.handleTabKey(key)}>
|
||||
<TabPane tab="有效Topic" key="1" >
|
||||
{this.renderOperationPanel(1)}
|
||||
{this.renderMyTopicTable(this.getData(topic.mytopicData))}
|
||||
</TabPane>
|
||||
<TabPane tab="已过期Topic" key="2">
|
||||
{this.renderOperationPanel(2)}
|
||||
{this.renderDeprecatedTopicTable(this.getData(topic.expireData))}
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user