mirror of
https://github.com/didi/KnowStreaming.git
synced 2025-12-24 11:52:08 +08:00
v2.8.0_e初始化
1、测试代码,开源用户尽量不要使用; 2、包含Kafka-HA的相关功能; 3、并非基于2.6.0拉的分支,是基于master分支的 commit-id:462303fca0拉的2.8.0_e的分支。出现这个情况的原因是v2.6.0的代码并不是最新的,2.x最新的代码是462303fca0这个commit对应的代码;
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "logi-kafka",
|
||||
"version": "2.6.1",
|
||||
"version": "2.8.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"prestart": "npm install --save-dev webpack-dev-server",
|
||||
@@ -58,4 +58,4 @@
|
||||
"dependencies": {
|
||||
"format-to-json": "^1.0.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ export class XFormWrapper extends React.Component<IXFormWrapper> {
|
||||
public state = {
|
||||
confirmLoading: false,
|
||||
formMap: this.props.formMap || [] as any,
|
||||
formData: this.props.formData || {}
|
||||
formData: this.props.formData || {},
|
||||
};
|
||||
|
||||
private $formRef: any;
|
||||
@@ -121,7 +121,8 @@ export class XFormWrapper extends React.Component<IXFormWrapper> {
|
||||
this.closeModalWrapper();
|
||||
}).catch((err: any) => {
|
||||
const { formMap, formData } = wrapper.xFormWrapper;
|
||||
onSubmitFaild(err, this.$formRef, formData, formMap);
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
onSubmitFaild && onSubmitFaild(err, this.$formRef, formData, formMap);
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
confirmLoading: false,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.ant-input-number, .ant-form-item-children .ant-select {
|
||||
.ant-input-number,
|
||||
.ant-form-item-children .ant-select {
|
||||
width: 314px
|
||||
}
|
||||
|
||||
@@ -8,4 +9,36 @@
|
||||
Button:first-child {
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.x-form {
|
||||
.ant-form-item-label {
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.ant-form-item-control {
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.prompt-info {
|
||||
color: #ccc;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
display: block;
|
||||
|
||||
&.inline {
|
||||
margin-left: 16px;
|
||||
display: inline-block;
|
||||
|
||||
font-family: PingFangSC-Regular;
|
||||
font-size: 12px;
|
||||
color: #042866;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
|
||||
.anticon {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,10 @@ class XForm extends React.Component<IXFormProps> {
|
||||
initialValue = false;
|
||||
}
|
||||
|
||||
if (formItem.type === FormItemType.select) {
|
||||
initialValue = initialValue || undefined;
|
||||
}
|
||||
|
||||
// if (formItem.type === FormItemType.select && formItem.attrs
|
||||
// && ['tags'].includes(formItem.attrs.mode)) {
|
||||
// initialValue = formItem.defaultValue ? [formItem.defaultValue] : [];
|
||||
@@ -105,7 +109,7 @@ class XForm extends React.Component<IXFormProps> {
|
||||
const { form, formData, formMap, formLayout, layout } = this.props;
|
||||
const { getFieldDecorator } = form;
|
||||
return (
|
||||
<Form layout={layout || 'horizontal'} onSubmit={() => ({})}>
|
||||
<Form className="x-form" layout={layout || 'horizontal'} onSubmit={() => ({})}>
|
||||
{formMap.map(formItem => {
|
||||
const { initialValue, valuePropName } = this.handleFormItem(formItem, formData);
|
||||
const getFieldValue = {
|
||||
@@ -131,7 +135,13 @@ class XForm extends React.Component<IXFormProps> {
|
||||
)}
|
||||
{formItem.renderExtraElement ? formItem.renderExtraElement() : null}
|
||||
{/* 添加保存时间提示文案 */}
|
||||
{formItem.attrs?.prompttype ? <span style={{ color: "#cccccc", fontSize: '12px', lineHeight: '20px', display: 'block' }}>{formItem.attrs.prompttype}</span> : null}
|
||||
{formItem.attrs?.prompttype ?
|
||||
<span className={`prompt-info ${formItem.attrs?.promptclass || ''}`}>
|
||||
{formItem.attrs?.prompticon ?
|
||||
<Icon type="info-circle" theme="twoTone" twoToneColor="#0A70F5" className={formItem.attrs?.prompticomclass} /> : null}
|
||||
{formItem.attrs.prompttype}
|
||||
</span>
|
||||
: null}
|
||||
</Form.Item>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -30,13 +30,13 @@ export class ClusterOverview extends React.Component<IOverview> {
|
||||
const content = this.props.basicInfo as IMetaData;
|
||||
const gmtCreate = moment(content.gmtCreate).format(timeFormat);
|
||||
const clusterContent = [{
|
||||
value: content.clusterName,
|
||||
value: `${content.clusterName}${content.haRelation === 0 ? '(备)' : content.haRelation === 1 ? '(主)' : content.haRelation === 2 ? '(主&备)' : ''}`,
|
||||
label: '集群名称',
|
||||
},
|
||||
},
|
||||
// {
|
||||
// value: clusterTypeMap[content.mode],
|
||||
// label: '集群类型',
|
||||
// },
|
||||
// },
|
||||
{
|
||||
value: gmtCreate,
|
||||
label: '接入时间',
|
||||
@@ -50,6 +50,9 @@ export class ClusterOverview extends React.Component<IOverview> {
|
||||
}, {
|
||||
value: content.zookeeper,
|
||||
label: 'Zookeeper',
|
||||
}, {
|
||||
value: `${content.mutualBackupClusterName || '-'}${content.haRelation === 0 ? '(主)' : content.haRelation === 1 ? '(备)' : content.haRelation === 2 ? '(主&备)' : ''}`,
|
||||
label: '互备集群',
|
||||
}];
|
||||
return (
|
||||
<>
|
||||
@@ -64,18 +67,18 @@ export class ClusterOverview extends React.Component<IOverview> {
|
||||
</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.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>
|
||||
|
||||
@@ -118,10 +118,10 @@ export class ClusterTopic extends SearchAndFilterContainer {
|
||||
public renderClusterTopicList() {
|
||||
const clusterColumns = [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
title: `Topic名称`,
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
width: '120px',
|
||||
width: '140px',
|
||||
sorter: (a: IClusterTopics, b: IClusterTopics) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
|
||||
render: (text: string, record: IClusterTopics) => {
|
||||
return (
|
||||
@@ -130,7 +130,7 @@ export class ClusterTopic extends SearchAndFilterContainer {
|
||||
// tslint:disable-next-line:max-line-length
|
||||
href={`${urlPrefix}/topic/topic-detail?clusterId=${record.clusterId || ''}&topic=${record.topicName || ''}&isPhysicalClusterId=true®ion=${region.currentRegion}`}
|
||||
>
|
||||
{text}
|
||||
{text}{record.haRelation === 0 ? '(备)' : record.haRelation === 1 ? '(主)' : record.haRelation === 2 ? '(主&备)' : ''}
|
||||
</a>
|
||||
</Tooltip>);
|
||||
},
|
||||
@@ -208,23 +208,27 @@ export class ClusterTopic extends SearchAndFilterContainer {
|
||||
{
|
||||
title: '操作',
|
||||
width: '120px',
|
||||
render: (value: string, item: IClusterTopics) => (
|
||||
<>
|
||||
<a onClick={() => this.getBaseInfo(item)} className="action-button">编辑</a>
|
||||
<a onClick={() => this.expandPartition(item)} className="action-button">扩分区</a>
|
||||
{/* <a onClick={() => this.expandPartition(item)} className="action-button">删除</a> */}
|
||||
<Popconfirm
|
||||
title="确定删除?"
|
||||
// 运维管控-集群列表-Topic列表修改删除业务逻辑
|
||||
onConfirm={() => this.confirmDetailTopic(item)}
|
||||
// onConfirm={() => this.deleteTopic(item)}
|
||||
cancelText="取消"
|
||||
okText="确认"
|
||||
>
|
||||
<a>删除</a>
|
||||
</Popconfirm>
|
||||
</>
|
||||
),
|
||||
render: (value: string, item: IClusterTopics) => {
|
||||
if (item.haRelation === 0) return '-';
|
||||
|
||||
return (
|
||||
<>
|
||||
<a onClick={() => this.getBaseInfo(item)} className="action-button">编辑</a>
|
||||
<a onClick={() => this.expandPartition(item)} className="action-button">扩分区</a>
|
||||
{/* <a onClick={() => this.expandPartition(item)} className="action-button">删除</a> */}
|
||||
<Popconfirm
|
||||
title="确定删除?"
|
||||
// 运维管控-集群列表-Topic列表修改删除业务逻辑
|
||||
onConfirm={() => this.confirmDetailTopic(item)}
|
||||
// onConfirm={() => this.deleteTopic(item)}
|
||||
cancelText="取消"
|
||||
okText="确认"
|
||||
>
|
||||
<a>删除</a>
|
||||
</Popconfirm>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
if (users.currentUser.role !== 2) {
|
||||
|
||||
@@ -73,6 +73,7 @@ export class LogicalCluster extends SearchAndFilterContainer {
|
||||
key: 'mode',
|
||||
render: (value: number) => {
|
||||
let val = '';
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
cluster.clusterModes && cluster.clusterModes.forEach((ele: any) => {
|
||||
if (value === ele.code) {
|
||||
val = ele.message;
|
||||
@@ -206,6 +207,7 @@ export class LogicalCluster extends SearchAndFilterContainer {
|
||||
}
|
||||
|
||||
public render() {
|
||||
const clusterModes = cluster.clusterModes;
|
||||
return (
|
||||
<div className="k-row">
|
||||
<ul className="k-tab">
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
.switch-style {
|
||||
&.ant-switch {
|
||||
min-width: 32px;
|
||||
height: 20px;
|
||||
line-height: 18px;
|
||||
|
||||
::after {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-switch-loading-icon,
|
||||
&.ant-switch::after {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.expanded-table {
|
||||
width: auto ! important;
|
||||
|
||||
.ant-table-thead {
|
||||
// visibility: hidden;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-table-tbody>tr>td {
|
||||
background-color: #FAFAFA;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
tr.ant-table-expanded-row td>.expanded-table {
|
||||
padding: 10px;
|
||||
// margin: -13px 0px -14px ! important;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cluster-tag {
|
||||
background: #27D687;
|
||||
border-radius: 2px;
|
||||
font-family: PingFangSC-Medium;
|
||||
color: #FFFFFF;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
-webkit-transform: scale(0.5);
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.no-padding {
|
||||
.ant-modal-body {
|
||||
padding: 0;
|
||||
|
||||
.attribute-content {
|
||||
.tag-gray {
|
||||
font-family: PingFangSC-Regular;
|
||||
font-size: 12px;
|
||||
color: #575757;
|
||||
text-align: center;
|
||||
line-height: 18px;
|
||||
padding: 0 4px;
|
||||
margin: 3px;
|
||||
height: 20px;
|
||||
background: #EEEEEE;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
zoom: 0.8;
|
||||
}
|
||||
|
||||
.tag-num {
|
||||
font-family: PingFangSC-Medium;
|
||||
text-align: right;
|
||||
line-height: 13px;
|
||||
margin-left: 6px;
|
||||
transform: scale(0.8333);
|
||||
}
|
||||
}
|
||||
|
||||
.attribute-tag {
|
||||
.ant-popover-inner-content {
|
||||
padding: 12px;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.ant-popover-arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-popover-placement-bottom,
|
||||
.ant-popover-placement-bottomLeft,
|
||||
.ant-popover-placement-bottomRight {
|
||||
top: 23px !important;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.tag-gray {
|
||||
font-family: PingFangSC-Regular;
|
||||
font-size: 12px;
|
||||
color: #575757;
|
||||
text-align: center;
|
||||
line-height: 12px;
|
||||
padding: 0 4px;
|
||||
margin: 3px;
|
||||
height: 20px;
|
||||
background: #EEEEEE;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.col-status {
|
||||
font-family: PingFangSC-Regular;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
|
||||
&.green {
|
||||
.ant-badge-status-text {
|
||||
color: #2FC25B;
|
||||
}
|
||||
}
|
||||
|
||||
&.black {
|
||||
.ant-badge-status-text {
|
||||
color: #575757;
|
||||
}
|
||||
}
|
||||
|
||||
&.red {
|
||||
.ant-badge-status-text {
|
||||
color: #F5202E;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-alert-message {
|
||||
font-family: PingFangSC-Regular;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.ant-alert-warning {
|
||||
border: none;
|
||||
color: #592D00;
|
||||
padding: 7px 15px 7px 41px;
|
||||
background: #FFFAE0;
|
||||
|
||||
.ant-alert-message {
|
||||
color: #592D00
|
||||
}
|
||||
}
|
||||
|
||||
.ant-alert-info {
|
||||
border: none;
|
||||
padding: 7px 15px 7px 41px;
|
||||
color: #042866;
|
||||
background: #EFF8FF;
|
||||
|
||||
.ant-alert-message {
|
||||
color: #042866;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-alert-icon {
|
||||
left: 24px;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.switch-warning {
|
||||
.btn {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
right: 24px;
|
||||
height: 22px;
|
||||
width: 64px;
|
||||
padding: 0px;
|
||||
|
||||
&.disabled {
|
||||
top: 77px;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 22px;
|
||||
width: 64px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
&.loading {
|
||||
width: 80px;
|
||||
|
||||
button {
|
||||
height: 22px;
|
||||
width: 88px;
|
||||
padding: 0px 0px 0px 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-table-content {
|
||||
padding: 0px 24px 16px;
|
||||
|
||||
.ant-table-small {
|
||||
border: none;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
|
||||
.ant-table-thead {
|
||||
background: #FAFAFA;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-table-download {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.ant-form {
|
||||
padding: 18px 24px 0px;
|
||||
|
||||
.ant-col-3 {
|
||||
width: 9.5%;
|
||||
}
|
||||
|
||||
.ant-form-item-label {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.no-label {
|
||||
.ant-col-21 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.transfe-list {
|
||||
.ant-transfer-list {
|
||||
height: 359px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-transfer-list {
|
||||
width: 249px;
|
||||
border: 1px solid #E8E8E8;
|
||||
border-radius: 8px;
|
||||
|
||||
.ant-transfer-list-header-title {
|
||||
font-family: PingFangSC-Regular;
|
||||
font-size: 12px;
|
||||
color: #252525;
|
||||
letter-spacing: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ant-transfer-list-body-search-wrapper {
|
||||
padding: 19px 16px 6px;
|
||||
|
||||
input {
|
||||
height: 27px;
|
||||
background: #FAFAFA;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ant-transfer-list-search-action {
|
||||
line-height: 27px;
|
||||
height: 27px;
|
||||
top: 19px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-transfer-list-header {
|
||||
border-radius: 8px 8px 0px 0px;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-transfer-customize-list .ant-transfer-list-body-customize-wrapper {
|
||||
padding: 0px;
|
||||
margin: 0px 16px;
|
||||
background: #FAFAFA;
|
||||
border-radius: 8px;
|
||||
|
||||
.ant-table-header-column {
|
||||
font-family: PingFangSC-Regular;
|
||||
font-size: 12px;
|
||||
color: #575757;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.ant-table-thead>tr {
|
||||
border: none;
|
||||
background: #FAFAFA;
|
||||
}
|
||||
|
||||
.ant-table-tbody>tr>td {
|
||||
border: none;
|
||||
background: #FAFAFA;
|
||||
}
|
||||
|
||||
.ant-table-body {
|
||||
background: #FAFAFA;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-selection-column {
|
||||
|
||||
.ant-table-header-column {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.log-process {
|
||||
height: 56px;
|
||||
background: #FAFAFA;
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.name {
|
||||
display: flex;
|
||||
color: #575757;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.log-panel {
|
||||
padding: 24px;
|
||||
font-family: PingFangSC-Regular;
|
||||
font-size: 12px;
|
||||
|
||||
.title {
|
||||
color: #252525;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.divider {
|
||||
display: inline-block;
|
||||
border-left: 2px solid #F38031;
|
||||
height: 9px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.log-info {
|
||||
color: #575757;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.text-num {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.warning-num {
|
||||
color: #F38031;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.log-table {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.ant-table-small {
|
||||
border: none;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
|
||||
.ant-table-thead {
|
||||
background: #FAFAFA;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { Modal, Table, Button, notification, message, Tooltip, Icon, Popconfirm, Alert, Popover } from 'component/antd';
|
||||
import { Modal, Table, Button, notification, message, Tooltip, Icon, Popconfirm, Alert, Dropdown } from 'component/antd';
|
||||
import { wrapper } from 'store';
|
||||
import { observer } from 'mobx-react';
|
||||
import { IXFormWrapper, IMetaData, IRegister } from 'types/base-type';
|
||||
import { IXFormWrapper, IMetaData, IRegister, ILabelValue } from 'types/base-type';
|
||||
import { admin } from 'store/admin';
|
||||
import { users } from 'store/users';
|
||||
import { registerCluster, createCluster, pauseMonitoring } from 'lib/api';
|
||||
@@ -10,11 +10,14 @@ import { SearchAndFilterContainer } from 'container/search-filter';
|
||||
import { cluster } from 'store/cluster';
|
||||
import { customPagination } from 'constants/table';
|
||||
import { urlPrefix } from 'constants/left-menu';
|
||||
import { indexUrl } from 'constants/strategy'
|
||||
import { indexUrl } from 'constants/strategy';
|
||||
import { region } from 'store';
|
||||
import './index.less';
|
||||
import Monacoeditor from 'component/editor/monacoEditor';
|
||||
import { getAdminClusterColumns } from '../config';
|
||||
import { FormItemType } from 'component/x-form';
|
||||
import { TopicHaRelationWrapper } from 'container/modal/admin/TopicHaRelation';
|
||||
import { TopicSwitchWrapper } from 'container/modal/admin/TopicHaSwitch';
|
||||
import { TopicSwitchLog } from 'container/modal/admin/SwitchTaskLog';
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
||||
@@ -22,6 +25,10 @@ const { confirm } = Modal;
|
||||
export class ClusterList extends SearchAndFilterContainer {
|
||||
public state = {
|
||||
searchKey: '',
|
||||
haVisible: false,
|
||||
switchVisible: false,
|
||||
logVisible: false,
|
||||
currentCluster: {} as IMetaData,
|
||||
};
|
||||
|
||||
private xFormModal: IXFormWrapper;
|
||||
@@ -36,7 +43,26 @@ export class ClusterList extends SearchAndFilterContainer {
|
||||
);
|
||||
}
|
||||
|
||||
public updateFormModal(value: boolean, metaList: ILabelValue[]) {
|
||||
const formMap = wrapper.xFormWrapper.formMap;
|
||||
formMap[1].attrs.prompttype = !value ? '' : metaList.length ? '已设置为高可用集群,请选择所关联的主集群' : '当前暂无可用集群进行关联高可用关系,请先添加集群';
|
||||
formMap[1].attrs.prompticon = 'true';
|
||||
formMap[2].invisible = !value;
|
||||
formMap[2].attrs.disabled = !metaList.length;
|
||||
formMap[6].rules[0].required = value;
|
||||
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
wrapper.ref && wrapper.ref.updateFormMap$(formMap, wrapper.xFormWrapper.formData);
|
||||
|
||||
}
|
||||
|
||||
public createOrRegisterCluster(item: IMetaData) {
|
||||
const self = this;
|
||||
const metaList = Array.from(admin.metaList).filter(item => item.haRelation === null).map(item => ({
|
||||
label: item.clusterName,
|
||||
value: item.clusterId,
|
||||
}));
|
||||
|
||||
this.xFormModal = {
|
||||
formMap: [
|
||||
{
|
||||
@@ -51,6 +77,38 @@ export class ClusterList extends SearchAndFilterContainer {
|
||||
disabled: item ? true : false,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'ha',
|
||||
label: '高可用',
|
||||
type: FormItemType._switch,
|
||||
invisible: item ? true : false,
|
||||
rules: [{
|
||||
required: false,
|
||||
}],
|
||||
attrs: {
|
||||
className: 'switch-style',
|
||||
prompttype: '',
|
||||
prompticon: '',
|
||||
prompticomclass: '',
|
||||
promptclass: 'inline',
|
||||
onChange(value: boolean) {
|
||||
self.updateFormModal(value, metaList);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'activeClusterId',
|
||||
label: '主集群',
|
||||
type: FormItemType.select,
|
||||
options: metaList,
|
||||
invisible: true,
|
||||
rules: [{
|
||||
required: false,
|
||||
}],
|
||||
attrs: {
|
||||
placeholder: '请选择主集群',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'zookeeper',
|
||||
label: 'zookeeper地址',
|
||||
@@ -130,9 +188,9 @@ export class ClusterList extends SearchAndFilterContainer {
|
||||
}],
|
||||
attrs: {
|
||||
placeholder: `请输入安全协议,例如:
|
||||
{
|
||||
"security.protocol": "SASL_PLAINTEXT",
|
||||
"sasl.mechanism": "PLAIN",
|
||||
{
|
||||
"security.protocol": "SASL_PLAINTEXT",
|
||||
"sasl.mechanism": "PLAIN",
|
||||
"sasl.jaas.config": "org.apache.kafka.common.security.plain.PlainLoginModule required username=\\"xxxxxx\\" password=\\"xxxxxx\\";"
|
||||
}`,
|
||||
rows: 8,
|
||||
@@ -162,17 +220,18 @@ export class ClusterList extends SearchAndFilterContainer {
|
||||
visible: true,
|
||||
width: 590,
|
||||
title: item ? '编辑' : '接入集群',
|
||||
isWaitting: true,
|
||||
onSubmit: (value: IRegister) => {
|
||||
value.idc = region.currentRegion;
|
||||
if (item) {
|
||||
value.clusterId = item.clusterId;
|
||||
registerCluster(value).then(data => {
|
||||
admin.getMetaData(true);
|
||||
return registerCluster(value).then(data => {
|
||||
admin.getHaMetaData();
|
||||
notification.success({ message: '编辑集群成功' });
|
||||
});
|
||||
} else {
|
||||
createCluster(value).then(data => {
|
||||
admin.getMetaData(true);
|
||||
return createCluster(value).then(data => {
|
||||
admin.getHaMetaData();
|
||||
notification.success({ message: '接入集群成功' });
|
||||
});
|
||||
}
|
||||
@@ -186,7 +245,7 @@ export class ClusterList extends SearchAndFilterContainer {
|
||||
const info = item.status === 1 ? '暂停监控' : '开始监控';
|
||||
const status = item.status === 1 ? 0 : 1;
|
||||
pauseMonitoring(item.clusterId, status).then(data => {
|
||||
admin.getMetaData(true);
|
||||
admin.getHaMetaData();
|
||||
notification.success({ message: `${info}成功` });
|
||||
});
|
||||
}
|
||||
@@ -198,7 +257,7 @@ export class ClusterList extends SearchAndFilterContainer {
|
||||
title: <>
|
||||
<span className="offline_span">
|
||||
删除集群
|
||||
<a>
|
||||
<a>
|
||||
<Tooltip placement="right" title={'若当前集群存在逻辑集群,则无法删除'} >
|
||||
<Icon type="question-circle" />
|
||||
</Tooltip>
|
||||
@@ -216,12 +275,34 @@ export class ClusterList extends SearchAndFilterContainer {
|
||||
}
|
||||
admin.deleteCluster(record.clusterId).then(data => {
|
||||
notification.success({ message: '删除成功' });
|
||||
admin.getHaMetaData();
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public showDelStandModal = (record: IMetaData) => {
|
||||
confirm({
|
||||
// tslint:disable-next-line:jsx-wrap-multiline
|
||||
title: '删除集群',
|
||||
// icon: 'none',
|
||||
content: <>{record.activeTopicCount ? `当前集群含有主topic,无法删除!` : record.haStatus !== 0 ? `当前集群正在进行主备切换,无法删除!` : `确认删除集群${record.clusterName}吗?`}</>,
|
||||
width: 500,
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk() {
|
||||
if (record.activeTopicCount || record.haStatus !== 0) {
|
||||
return;
|
||||
}
|
||||
admin.deleteCluster(record.clusterId).then(data => {
|
||||
notification.success({ message: '删除成功' });
|
||||
admin.getHaMetaData();
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public deleteMonitorModal = (source: any) => {
|
||||
const cellStyle = {
|
||||
overflow: 'hidden',
|
||||
@@ -275,11 +356,105 @@ export class ClusterList extends SearchAndFilterContainer {
|
||||
return data;
|
||||
}
|
||||
|
||||
public expandedRowRender = (record: IMetaData) => {
|
||||
const dataSource: any = record.haClusterVO ? [record.haClusterVO] : [];
|
||||
const cols = getAdminClusterColumns(false);
|
||||
const role = users.currentUser.role;
|
||||
|
||||
if (!record.haClusterVO) return null;
|
||||
|
||||
const haRecord = record.haClusterVO;
|
||||
|
||||
const btnsMenu = (
|
||||
<>
|
||||
<ul className="dropdown-menu">
|
||||
<li>
|
||||
<a onClick={this.createOrRegisterCluster.bind(this, haRecord)} className="action-button">
|
||||
编辑
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<Popconfirm
|
||||
title={`确定${haRecord.status === 1 ? '暂停' : '开始'}${haRecord.clusterName}监控?`}
|
||||
onConfirm={() => this.pauseMonitor(haRecord)}
|
||||
cancelText="取消"
|
||||
okText="确认"
|
||||
>
|
||||
<Tooltip placement="left" title="暂停监控将无法正常监控指标信息,建议开启监控">
|
||||
<a
|
||||
className="action-button"
|
||||
>
|
||||
{haRecord.status === 1 ? '暂停监控' : '开始监控'}
|
||||
</a>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</li>
|
||||
<li>
|
||||
<a onClick={this.showDelStandModal.bind(this, haRecord)}>
|
||||
删除
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</>);
|
||||
|
||||
const noAuthMenu = (
|
||||
<ul className="dropdown-menu">
|
||||
<Tooltip placement="left" title="该功能只对运维人员开放">
|
||||
<li><a style={{ color: '#a0a0a0' }} className="action-button">编辑</a></li>
|
||||
<li><a className="action-button" style={{ color: '#a0a0a0' }}>{record.status === 1 ? '暂停监控' : '开始监控'}</a></li>
|
||||
<li><a style={{ color: '#a0a0a0' }}>删除</a></li>
|
||||
</Tooltip>
|
||||
</ul>
|
||||
);
|
||||
|
||||
const col = {
|
||||
title: '操作',
|
||||
width: 270,
|
||||
render: (value: string, item: IMetaData) => (
|
||||
<>
|
||||
<a
|
||||
onClick={this.openModal.bind(this, 'haVisible', record)}
|
||||
className="action-button"
|
||||
>
|
||||
Topic高可用关联
|
||||
</a>
|
||||
{item.haStatus !== 0 ? null : <a onClick={this.openModal.bind(this, 'switchVisible', record)} className="action-button">
|
||||
Topic主备切换
|
||||
</a>}
|
||||
{item.haASSwitchJobId ? <a className="action-button" onClick={this.openModal.bind(this, 'logVisible', record)}>
|
||||
查看日志
|
||||
</a> : null}
|
||||
<Dropdown
|
||||
overlay={role === 2 ? btnsMenu : noAuthMenu}
|
||||
trigger={['click', 'hover']}
|
||||
placement="bottomLeft"
|
||||
>
|
||||
<span className="didi-theme ml-10">
|
||||
···
|
||||
</span>
|
||||
</Dropdown>
|
||||
</>
|
||||
),
|
||||
};
|
||||
cols.push(col as any);
|
||||
return (
|
||||
<Table
|
||||
className="expanded-table"
|
||||
rowKey="clusterId"
|
||||
style={{ width: '500px' }}
|
||||
columns={cols}
|
||||
dataSource={dataSource}
|
||||
pagination={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public getColumns = () => {
|
||||
const cols = getAdminClusterColumns();
|
||||
const role = users.currentUser.role;
|
||||
const col = {
|
||||
title: '操作',
|
||||
width: 270,
|
||||
render: (value: string, item: IMetaData) => (
|
||||
<>
|
||||
{
|
||||
@@ -307,10 +482,10 @@ export class ClusterList extends SearchAndFilterContainer {
|
||||
删除
|
||||
</a>
|
||||
</> : <Tooltip placement="left" title="该功能只对运维人员开放">
|
||||
<a style={{ color: '#a0a0a0' }} className="action-button">编辑</a>
|
||||
<a className="action-button" style={{ color: '#a0a0a0' }}>{item.status === 1 ? '暂停监控' : '开始监控'}</a>
|
||||
<a style={{ color: '#a0a0a0' }}>删除</a>
|
||||
</Tooltip>
|
||||
<a style={{ color: '#a0a0a0' }} className="action-button">编辑</a>
|
||||
<a className="action-button" style={{ color: '#a0a0a0' }}>{item.status === 1 ? '暂停监控' : '开始监控'}</a>
|
||||
<a style={{ color: '#a0a0a0' }}>删除</a>
|
||||
</Tooltip>
|
||||
}
|
||||
</>
|
||||
),
|
||||
@@ -319,6 +494,20 @@ export class ClusterList extends SearchAndFilterContainer {
|
||||
return cols;
|
||||
}
|
||||
|
||||
public openModal(type: string, record: IMetaData) {
|
||||
this.setState({
|
||||
currentCluster: record,
|
||||
}, () => {
|
||||
this.handleVisible(type, true);
|
||||
});
|
||||
}
|
||||
|
||||
public handleVisible(type: string, visible: boolean) {
|
||||
this.setState({
|
||||
[type]: visible,
|
||||
});
|
||||
}
|
||||
|
||||
public renderClusterList() {
|
||||
const role = users.currentUser.role;
|
||||
return (
|
||||
@@ -333,8 +522,8 @@ export class ClusterList extends SearchAndFilterContainer {
|
||||
role && role === 2 ?
|
||||
<Button type="primary" onClick={this.createOrRegisterCluster.bind(this, null)}>接入集群</Button>
|
||||
:
|
||||
<Tooltip placement="left" title="该功能只对运维人员开放" trigger='hover'>
|
||||
<Button disabled type="primary">接入集群</Button>
|
||||
<Tooltip placement="left" title="该功能只对运维人员开放" trigger="hover">
|
||||
<Button disabled={true} type="primary">接入集群</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
</li>
|
||||
@@ -343,26 +532,63 @@ export class ClusterList extends SearchAndFilterContainer {
|
||||
<div className="table-wrapper">
|
||||
<Table
|
||||
rowKey="key"
|
||||
expandIcon={({ expanded, onExpand, record }) => (
|
||||
record.haClusterVO ?
|
||||
<Icon style={{ fontSize: 10 }} type={expanded ? 'down' : 'right'} onClick={e => onExpand(record, e)} />
|
||||
: null
|
||||
)}
|
||||
loading={admin.loading}
|
||||
dataSource={this.getData(admin.metaList)}
|
||||
expandedRowRender={this.expandedRowRender}
|
||||
dataSource={this.getData(admin.haMetaList)}
|
||||
columns={this.getColumns()}
|
||||
pagination={customPagination}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{this.state.haVisible && <TopicHaRelationWrapper
|
||||
handleVisible={(val: boolean) => this.handleVisible('haVisible', val)}
|
||||
visible={this.state.haVisible}
|
||||
currentCluster={this.state.currentCluster}
|
||||
reload={() => admin.getHaMetaData()}
|
||||
formData={{}}
|
||||
/>}
|
||||
{this.state.switchVisible &&
|
||||
<TopicSwitchWrapper
|
||||
reload={(jobId: number) => {
|
||||
admin.getHaMetaData().then((res) => {
|
||||
const currentRecord = res.find(item => item.clusterId === this.state.currentCluster.clusterId);
|
||||
currentRecord.haClusterVO.haASSwitchJobId = jobId;
|
||||
this.openModal('logVisible', currentRecord);
|
||||
});
|
||||
}}
|
||||
handleVisible={(val: boolean) => this.handleVisible('switchVisible', val)}
|
||||
visible={this.state.switchVisible}
|
||||
currentCluster={this.state.currentCluster}
|
||||
formData={{}}
|
||||
/>
|
||||
}
|
||||
{this.state.logVisible &&
|
||||
<TopicSwitchLog
|
||||
reload={() => admin.getHaMetaData()}
|
||||
handleVisible={(val: boolean) => this.handleVisible('logVisible', val)}
|
||||
visible={this.state.logVisible}
|
||||
currentCluster={this.state.currentCluster}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
admin.getMetaData(true);
|
||||
admin.getHaMetaData();
|
||||
cluster.getClusterModes();
|
||||
admin.getDataCenter();
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
admin.metaList ? <> {this.renderClusterList()} </> : null
|
||||
admin.haMetaList ? <> {this.renderClusterList()} </> : null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ import { IUser, IUploadFile, IConfigure, IConfigGateway, IMetaData } from 'types
|
||||
import { users } from 'store/users';
|
||||
import { version } from 'store/version';
|
||||
import { showApplyModal, showApplyModalModifyPassword, showModifyModal, showConfigureModal, showConfigGatewayModal } from 'container/modal/admin';
|
||||
import { Popconfirm, Tooltip } from 'component/antd';
|
||||
import { Icon, 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');
|
||||
import { Tag } from 'antd';
|
||||
|
||||
export const getUserColumns = () => {
|
||||
const columns = [
|
||||
@@ -28,15 +29,15 @@ export const getUserColumns = () => {
|
||||
<span className="table-operation">
|
||||
<a onClick={() => showApplyModal(record)}>编辑</a>
|
||||
<a onClick={() => showApplyModalModifyPassword(record)}>修改密码</a>
|
||||
{record.username == users.currentUser.username ? "" :
|
||||
<Popconfirm
|
||||
title="确定删除?"
|
||||
onConfirm={() => users.deleteUser(record.username)}
|
||||
cancelText="取消"
|
||||
okText="确认"
|
||||
>
|
||||
<a>删除</a>
|
||||
</Popconfirm>
|
||||
{record.username === users.currentUser.username ? '' :
|
||||
<Popconfirm
|
||||
title="确定删除?"
|
||||
onConfirm={() => users.deleteUser(record.username)}
|
||||
cancelText="取消"
|
||||
okText="确认"
|
||||
>
|
||||
<a>删除</a>
|
||||
</Popconfirm>
|
||||
}
|
||||
</span>);
|
||||
},
|
||||
@@ -271,33 +272,82 @@ export const getConfigColumns = () => {
|
||||
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>}
|
||||
{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 = () => {
|
||||
const renderTopicNum = (value: number | string, item: IMetaData, key: number, active?: boolean) => {
|
||||
const show = item.haClusterVO || (!item.haClusterVO && !active);
|
||||
|
||||
if (!show) {
|
||||
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>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return ( // 0 暂停监控--不可点击 1 监控中---可正常点击
|
||||
<>
|
||||
{item.status === 1 ? <a href={`${urlPrefix}/admin/cluster-detail?clusterId=${item.clusterId}#${key}`}>
|
||||
{value}
|
||||
<>(主{item.activeTopicCount ?? '-'}/备{item.standbyTopicCount ?? '-'})</>
|
||||
</a> :
|
||||
<a style={{ cursor: 'not-allowed', color: '#999' }}>
|
||||
{value}
|
||||
<>(主{item.activeTopicCount ?? '-'}/备{item.standbyTopicCount ?? '-'})</>
|
||||
</a>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderClusterName = (value: number | string, item: IMetaData, key: number, active: boolean) => {
|
||||
const show = item.haClusterVO || (!item.haClusterVO && !active);
|
||||
|
||||
return ( // 0 暂停监控--不可点击 1 监控中---可正常点击
|
||||
<>
|
||||
{item.status === 1 ?
|
||||
<a href={`${urlPrefix}/admin/cluster-detail?clusterId=${item.clusterId}`}>{value}</a> :
|
||||
<a style={{ cursor: 'not-allowed', color: '#999' }}>{value}</a>}
|
||||
{active ? <>
|
||||
{item.haClusterVO ? <Tooltip title="高可用集群"><Tag className="cluster-tag">HA</Tag></Tooltip> : null}
|
||||
{item.haClusterVO && item.haStatus !== 0 ? <Tooltip title="Topic主备切换中"><Icon type="swap" style={{ color: '#27D687' }} /></Tooltip>
|
||||
: null}
|
||||
</> : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const getAdminClusterColumns = (active = true) => {
|
||||
return [
|
||||
{
|
||||
title: '物理集群ID',
|
||||
dataIndex: 'clusterId',
|
||||
key: 'clusterId',
|
||||
sorter: (a: IMetaData, b: IMetaData) => b.clusterId - a.clusterId,
|
||||
sorter: (a: IMetaData, b: IMetaData) => a.clusterId - b.clusterId,
|
||||
width: active ? 115 : 111,
|
||||
render: (text: number) => active ? text : `(${text ?? 0})`,
|
||||
},
|
||||
{
|
||||
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),
|
||||
render: (text: string, item: IMetaData) => renderClusterName(text, item, 1, active),
|
||||
width: 235,
|
||||
},
|
||||
{
|
||||
title: 'Topic数',
|
||||
dataIndex: 'topicNum',
|
||||
key: 'topicNum',
|
||||
sorter: (a: any, b: IMetaData) => b.topicNum - a.topicNum,
|
||||
render: (text: number, item: IMetaData) => renderClusterHref(text, item, 2),
|
||||
render: (text: number, item: IMetaData) => renderTopicNum(text, item, 2, active),
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: 'Broker数',
|
||||
@@ -305,6 +355,7 @@ export const getAdminClusterColumns = () => {
|
||||
key: 'brokerNum',
|
||||
sorter: (a: IMetaData, b: IMetaData) => b.brokerNum - a.brokerNum,
|
||||
render: (text: number, item: IMetaData) => renderClusterHref(text, item, 3),
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: 'Consumer数',
|
||||
@@ -312,6 +363,8 @@ export const getAdminClusterColumns = () => {
|
||||
key: 'consumerGroupNum',
|
||||
sorter: (a: IMetaData, b: IMetaData) => b.consumerGroupNum - a.consumerGroupNum,
|
||||
render: (text: number, item: IMetaData) => renderClusterHref(text, item, 4),
|
||||
width: 150,
|
||||
|
||||
},
|
||||
{
|
||||
title: 'Region数',
|
||||
@@ -319,6 +372,8 @@ export const getAdminClusterColumns = () => {
|
||||
key: 'regionNum',
|
||||
sorter: (a: IMetaData, b: IMetaData) => b.regionNum - a.regionNum,
|
||||
render: (text: number, item: IMetaData) => renderClusterHref(text, item, 5),
|
||||
width: 140,
|
||||
|
||||
},
|
||||
{
|
||||
title: 'Controllerld',
|
||||
@@ -326,12 +381,15 @@ export const getAdminClusterColumns = () => {
|
||||
key: 'controllerId',
|
||||
sorter: (a: IMetaData, b: IMetaData) => b.controllerId - a.controllerId,
|
||||
render: (text: number, item: IMetaData) => renderClusterHref(text, item, 7),
|
||||
width: 150,
|
||||
|
||||
},
|
||||
{
|
||||
title: '监控中',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
sorter: (a: IMetaData, b: IMetaData) => b.key - a.key,
|
||||
width: 140,
|
||||
render: (value: number) => value === 1 ?
|
||||
<span className="success">是</span > : <span className="fail">否</span>,
|
||||
},
|
||||
|
||||
@@ -44,7 +44,7 @@ export class MyCluster extends SearchAndFilterContainer {
|
||||
label: '所属应用',
|
||||
rules: [{ required: true, message: '请选择所属应用' }],
|
||||
type: 'select',
|
||||
options: app.data.map((item) => {
|
||||
options: app.clusterAppData.map((item) => {
|
||||
return {
|
||||
label: item.name,
|
||||
value: item.appId,
|
||||
@@ -135,8 +135,8 @@ export class MyCluster extends SearchAndFilterContainer {
|
||||
if (!cluster.clusterModes.length) {
|
||||
cluster.getClusterModes();
|
||||
}
|
||||
if (!app.data.length) {
|
||||
app.getAppList();
|
||||
if (!app.clusterAppData.length) {
|
||||
app.getAppListByClusterId(-1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ export const Header = observer((props: IHeader) => {
|
||||
<div className="left-content">
|
||||
<img className="kafka-header-icon" src={logoUrl} alt="" />
|
||||
<span className="kafka-header-text">LogiKM</span>
|
||||
<a className='kafka-header-version' href="https://github.com/didi/Logi-KafkaManager/releases" target='_blank'>v2.6.1</a>
|
||||
<a className='kafka-header-version' href="https://github.com/didi/Logi-KafkaManager/releases" target='_blank'>v2.8.0</a>
|
||||
{/* 添加版本超链接 */}
|
||||
</div>
|
||||
<div className="mid-content">
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
import * as React from 'react';
|
||||
import { Modal, Progress, Tooltip } from 'antd';
|
||||
import { IMetaData } from 'types/base-type';
|
||||
import { Alert, Badge, Button, Input, message, notification, Table } from 'component/antd';
|
||||
import { getJobDetail, getJobState, getJobLog, switchAsJobs } from 'lib/api';
|
||||
import moment from 'moment';
|
||||
import { timeFormat } from 'constants/strategy';
|
||||
|
||||
interface IProps {
|
||||
reload: any;
|
||||
visible?: boolean;
|
||||
handleVisible?: any;
|
||||
currentCluster?: IMetaData;
|
||||
}
|
||||
|
||||
interface IJobState {
|
||||
failedNu: number;
|
||||
jobNu: number;
|
||||
runningNu: number;
|
||||
successNu: number;
|
||||
waitingNu: number;
|
||||
runningInTimeoutNu: number;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
interface IJobDetail {
|
||||
standbyClusterPhyId: number;
|
||||
status: number;
|
||||
sumLag: number;
|
||||
timeoutUnitSecConfig: number;
|
||||
topicName: string;
|
||||
activeClusterPhyName: string;
|
||||
standbyClusterPhyName: string;
|
||||
}
|
||||
|
||||
interface ILog {
|
||||
bizKeyword: string;
|
||||
bizType: number;
|
||||
content: string;
|
||||
id: number;
|
||||
printTime: number;
|
||||
}
|
||||
interface IJobLog {
|
||||
logList: ILog[];
|
||||
endLogId: number;
|
||||
}
|
||||
const STATUS_MAP = {
|
||||
'-1': '未知',
|
||||
'30': '运行中',
|
||||
'32': '超时运行中',
|
||||
'101': '成功',
|
||||
'102': '失败',
|
||||
} as any;
|
||||
const STATUS_COLORS = {
|
||||
'-1': '#575757',
|
||||
'30': '#575757',
|
||||
'32': '#F5202E',
|
||||
'101': '#2FC25B',
|
||||
'102': '#F5202E',
|
||||
} as any;
|
||||
const STATUS_COLOR_MAP = {
|
||||
'-1': 'black',
|
||||
'30': 'black',
|
||||
'32': 'red',
|
||||
'101': 'green',
|
||||
'102': 'red',
|
||||
} as any;
|
||||
|
||||
const getFilters = () => {
|
||||
const keys = Object.keys(STATUS_MAP);
|
||||
const filters = [];
|
||||
for (const key of keys) {
|
||||
filters.push({
|
||||
text: STATUS_MAP[key],
|
||||
value: key,
|
||||
});
|
||||
}
|
||||
return filters;
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
dataIndex: 'key',
|
||||
title: '编号',
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
dataIndex: 'topicName',
|
||||
title: 'Topic名称',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
dataIndex: 'sumLag',
|
||||
title: '延迟',
|
||||
width: 100,
|
||||
render: (value: number) => value ?? '-',
|
||||
},
|
||||
{
|
||||
dataIndex: 'status',
|
||||
title: '状态',
|
||||
width: 100,
|
||||
filters: getFilters(),
|
||||
onFilter: (value: string, record: IJobDetail) => record.status === Number(value),
|
||||
render: (t: number) => (
|
||||
<span className={'col-status ' + STATUS_COLOR_MAP[t]}>
|
||||
<Badge color={STATUS_COLORS[t]} text={STATUS_MAP[t]} />
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export class TopicSwitchLog extends React.Component<IProps> {
|
||||
public state = {
|
||||
radioCheck: 'all',
|
||||
jobDetail: [] as IJobDetail[],
|
||||
jobState: {} as IJobState,
|
||||
jobLog: {} as IJobLog,
|
||||
textStr: '',
|
||||
primaryTargetKeys: [] as string[],
|
||||
loading: false,
|
||||
};
|
||||
public timer = null as number;
|
||||
public jobId = this.props.currentCluster?.haClusterVO?.haASSwitchJobId as number;
|
||||
|
||||
public handleOk = () => {
|
||||
this.props.handleVisible(false);
|
||||
this.props.reload();
|
||||
}
|
||||
|
||||
public handleCancel = () => {
|
||||
this.props.handleVisible(false);
|
||||
this.props.reload();
|
||||
}
|
||||
|
||||
public iTimer = () => {
|
||||
this.timer = window.setInterval(() => {
|
||||
const { jobLog } = this.state;
|
||||
this.getContentJobLog(jobLog.endLogId);
|
||||
this.getContentJobState();
|
||||
this.getContentJobDetail();
|
||||
}, 10 * 1 * 1000);
|
||||
}
|
||||
|
||||
public getTextAreaStr = (logList: ILog[]) => {
|
||||
const strs = [];
|
||||
|
||||
for (const item of logList) {
|
||||
strs.push(`${moment(item.printTime).format(timeFormat)} ${item.content}`);
|
||||
}
|
||||
|
||||
return strs.join(`\n`);
|
||||
}
|
||||
|
||||
public getContentJobLog = (startId?: number) => {
|
||||
getJobLog(this.jobId, startId).then((res: IJobLog) => {
|
||||
const { jobLog } = this.state;
|
||||
const logList = (jobLog.logList || []);
|
||||
logList.push(...(res?.logList || []));
|
||||
|
||||
const newJobLog = {
|
||||
endLogId: res?.endLogId,
|
||||
logList,
|
||||
};
|
||||
|
||||
this.setState({
|
||||
textStr: this.getTextAreaStr(logList),
|
||||
jobLog: newJobLog,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public getContentJobState = () => {
|
||||
getJobState(this.jobId).then((res: IJobState) => {
|
||||
// 成功后清除调用
|
||||
if (res?.jobNu === res.successNu) {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
this.setState({
|
||||
jobState: res || {},
|
||||
});
|
||||
});
|
||||
}
|
||||
public getContentJobDetail = () => {
|
||||
getJobDetail(this.jobId).then((res: IJobDetail[]) => {
|
||||
this.setState({
|
||||
jobDetail: (res || []).map((row, index) => ({
|
||||
...row,
|
||||
key: index,
|
||||
})),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public switchJobs = () => {
|
||||
const { jobState } = this.state;
|
||||
Modal.confirm({
|
||||
title: '强制切换',
|
||||
content: `当前有${jobState.runningNu}个Topic切换中,${jobState.runningInTimeoutNu}个Topic切换超时,强制切换会使这些Topic有数据丢失的风险,确定强制切换吗?`,
|
||||
onOk: () => {
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
switchAsJobs(this.jobId, {
|
||||
action: 'force',
|
||||
allJumpWaitInSync: true,
|
||||
jumpWaitInSyncActiveTopicList: [],
|
||||
}).then(res => {
|
||||
message.success('强制切换成功');
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.getContentJobDetail();
|
||||
this.getContentJobState();
|
||||
this.getContentJobLog();
|
||||
setTimeout(this.iTimer, 0);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { visible, currentCluster } = this.props;
|
||||
const { jobState, jobDetail, textStr, loading } = this.state;
|
||||
const runtimeJob = jobDetail.filter(item => item.status === 32);
|
||||
const percent = jobState?.progress;
|
||||
return (
|
||||
<Modal
|
||||
title="主备切换日志"
|
||||
wrapClassName="no-padding"
|
||||
visible={visible}
|
||||
onOk={this.handleOk}
|
||||
onCancel={this.handleCancel}
|
||||
maskClosable={false}
|
||||
width={590}
|
||||
okText="确认"
|
||||
cancelText="取消"
|
||||
>
|
||||
{runtimeJob.length ?
|
||||
<Alert
|
||||
message={`${runtimeJob[0].topicName}消息同步已经超时${runtimeJob[0].timeoutUnitSecConfig}s,建议立即强制切换`}
|
||||
type="warning"
|
||||
showIcon={true}
|
||||
/>
|
||||
: null}
|
||||
<div className="switch-warning">
|
||||
<Tooltip title="不用等待所有topic数据完成同步,立即进行切换,但是有数据丢失的风险。">
|
||||
<Button loading={loading} type="primary" disabled={!runtimeJob.length} onClick={this.switchJobs} className={loading ? 'btn loading' : runtimeJob.length ? 'btn' : 'btn disabled'}>强制切换</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="log-panel">
|
||||
<div className="title">
|
||||
<div className="divider" />
|
||||
<span>Topic切换详情:</span>
|
||||
</div>
|
||||
<div className="log-process">
|
||||
<div className="name">
|
||||
<span>源集群 {jobDetail?.[0]?.standbyClusterPhyName || ''}</span>
|
||||
<span>目标集群 {jobDetail?.[0]?.activeClusterPhyName || ''}</span>
|
||||
</div>
|
||||
<Progress percent={percent} strokeColor="#F38031" status={percent === 100 ? 'normal' : 'active'} />
|
||||
</div>
|
||||
<div className="log-info">
|
||||
Topic总数 <span className="text-num">{jobState.jobNu ?? '-'}</span> 个,
|
||||
切换成功 <span className="text-num">{jobState.successNu ?? '-'}</span> 个,
|
||||
切换超时 <span className="warning-num">{jobState.failedNu ?? '-'}</span> 个,
|
||||
待切换 <span className="warning-num">{jobState.waitingNu ?? '-'}</span> 个。
|
||||
</div>
|
||||
<Table
|
||||
className="log-table"
|
||||
columns={columns}
|
||||
dataSource={jobDetail}
|
||||
size="small"
|
||||
rowKey="topicName"
|
||||
pagination={false}
|
||||
bordered={false}
|
||||
scroll={{ y: 138 }}
|
||||
/>
|
||||
<div className="title">
|
||||
<div className="divider" />
|
||||
<span>集群切换日志:</span>
|
||||
</div>
|
||||
<div>
|
||||
<Input.TextArea value={textStr} rows={7} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Modal >
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
import * as React from 'react';
|
||||
import { admin } from 'store/admin';
|
||||
import { Modal, Form, Radio } from 'antd';
|
||||
import { IBrokersMetadata, IBrokersRegions, IMetaData } from 'types/base-type';
|
||||
import { Alert, message, notification, Table, Tooltip, Transfer } from 'component/antd';
|
||||
import { getClusterHaTopicsStatus, setHaTopics, unbindHaTopics } from 'lib/api';
|
||||
import { cellStyle } from 'constants/table';
|
||||
|
||||
const layout = {
|
||||
labelCol: { span: 3 },
|
||||
wrapperCol: { span: 21 },
|
||||
};
|
||||
|
||||
interface IXFormProps {
|
||||
form: any;
|
||||
reload: any;
|
||||
formData?: any;
|
||||
visible?: boolean;
|
||||
handleVisible?: any;
|
||||
currentCluster?: IMetaData;
|
||||
}
|
||||
|
||||
interface IHaTopic {
|
||||
clusterId: number;
|
||||
clusterName: string;
|
||||
haRelation: number;
|
||||
topicName: string;
|
||||
key: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const resColumns = [
|
||||
{
|
||||
title: 'TopicName',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'code',
|
||||
key: 'code',
|
||||
width: 60,
|
||||
render: (t: number) => {
|
||||
return (
|
||||
<span className={t === 0 ? 'success' : 'fail'}>
|
||||
{t === 0 ? '成功' : '失败'}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '原因',
|
||||
dataIndex: 'message',
|
||||
key: 'message',
|
||||
width: 125,
|
||||
onCell: () => ({
|
||||
style: {
|
||||
maxWidth: 120,
|
||||
...cellStyle,
|
||||
},
|
||||
}),
|
||||
render: (text: string) => {
|
||||
return (
|
||||
<Tooltip placement="bottomLeft" title={text} >
|
||||
{text}
|
||||
</Tooltip>);
|
||||
},
|
||||
},
|
||||
];
|
||||
class TopicHaRelation extends React.Component<IXFormProps> {
|
||||
public state = {
|
||||
radioCheck: 'spec',
|
||||
haTopics: [] as IHaTopic[],
|
||||
targetKeys: [] as string[],
|
||||
confirmLoading: false,
|
||||
firstMove: true,
|
||||
primaryActiveKeys: [] as string[],
|
||||
primaryStandbyKeys: [] as string[],
|
||||
};
|
||||
|
||||
public handleOk = () => {
|
||||
this.props.form.validateFields((err: any, values: any) => {
|
||||
const unbindTopics = [];
|
||||
const bindTopics = [];
|
||||
|
||||
if (values.rule === 'all') {
|
||||
setHaTopics({
|
||||
all: true,
|
||||
activeClusterId: this.props.currentCluster.clusterId,
|
||||
standbyClusterId: this.props.currentCluster.haClusterVO.clusterId,
|
||||
topicNames: [],
|
||||
}).then(res => {
|
||||
handleMsg(res, '关联成功');
|
||||
this.setState({
|
||||
confirmLoading: false,
|
||||
});
|
||||
this.handleCancel();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of this.state.primaryStandbyKeys) {
|
||||
if (!this.state.targetKeys.includes(item)) {
|
||||
unbindTopics.push(item);
|
||||
}
|
||||
}
|
||||
for (const item of this.state.targetKeys) {
|
||||
if (!this.state.primaryStandbyKeys.includes(item)) {
|
||||
bindTopics.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (!unbindTopics.length && !bindTopics.length) {
|
||||
return message.info('请选择您要操作的Topic');
|
||||
}
|
||||
|
||||
const handleMsg = (res: any[], successTip: string) => {
|
||||
const errorRes = res.filter(item => item.code !== 0);
|
||||
|
||||
if (errorRes.length) {
|
||||
Modal.confirm({
|
||||
title: '执行结果',
|
||||
width: 520,
|
||||
icon: null,
|
||||
content: (
|
||||
<Table
|
||||
columns={resColumns}
|
||||
rowKey="id"
|
||||
dataSource={res}
|
||||
scroll={{ y: 260 }}
|
||||
pagination={false}
|
||||
/>
|
||||
),
|
||||
});
|
||||
} else {
|
||||
notification.success({ message: successTip });
|
||||
}
|
||||
|
||||
this.props.reload();
|
||||
};
|
||||
|
||||
if (bindTopics.length) {
|
||||
this.setState({
|
||||
confirmLoading: true,
|
||||
});
|
||||
setHaTopics({
|
||||
all: false,
|
||||
activeClusterId: this.props.currentCluster.clusterId,
|
||||
standbyClusterId: this.props.currentCluster.haClusterVO.clusterId,
|
||||
topicNames: bindTopics,
|
||||
}).then(res => {
|
||||
this.setState({
|
||||
confirmLoading: false,
|
||||
});
|
||||
this.handleCancel();
|
||||
handleMsg(res, '关联成功');
|
||||
});
|
||||
}
|
||||
|
||||
if (unbindTopics.length) {
|
||||
this.setState({
|
||||
confirmLoading: true,
|
||||
});
|
||||
unbindHaTopics({
|
||||
all: false,
|
||||
activeClusterId: this.props.currentCluster.clusterId,
|
||||
standbyClusterId: this.props.currentCluster.haClusterVO.clusterId,
|
||||
topicNames: unbindTopics,
|
||||
}).then(res => {
|
||||
this.setState({
|
||||
confirmLoading: false,
|
||||
});
|
||||
this.handleCancel();
|
||||
handleMsg(res, '解绑成功');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public handleCancel = () => {
|
||||
this.props.handleVisible(false);
|
||||
this.props.form.resetFields();
|
||||
}
|
||||
|
||||
public handleRadioChange = (e: any) => {
|
||||
this.setState({
|
||||
radioCheck: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
public isPrimaryStatus = (targetKeys: string[]) => {
|
||||
const { primaryStandbyKeys } = this.state;
|
||||
let isReset = false;
|
||||
// 判断当前移动是否还原为最初的状态
|
||||
if (primaryStandbyKeys.length === targetKeys.length) {
|
||||
targetKeys.sort((a, b) => +a - (+b));
|
||||
primaryStandbyKeys.sort((a, b) => +a - (+b));
|
||||
let i = 0;
|
||||
while (i < targetKeys.length) {
|
||||
if (targetKeys[i] === primaryStandbyKeys[i]) {
|
||||
i++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
isReset = i === targetKeys.length;
|
||||
}
|
||||
return isReset;
|
||||
}
|
||||
|
||||
public setTopicsStatus = (targetKeys: string[], disabled: boolean, isAll = false) => {
|
||||
const { haTopics } = this.state;
|
||||
const newTopics = Array.from(haTopics);
|
||||
if (isAll) {
|
||||
for (let i = 0; i < haTopics.length; i++) {
|
||||
newTopics[i].disabled = disabled;
|
||||
}
|
||||
} else {
|
||||
for (const key of targetKeys) {
|
||||
const index = haTopics.findIndex(item => item.key === key);
|
||||
if (index > -1) {
|
||||
newTopics[index].disabled = disabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setState(({
|
||||
haTopics: newTopics,
|
||||
}));
|
||||
}
|
||||
|
||||
public onTransferChange = (targetKeys: string[], direction: string, moveKeys: string[]) => {
|
||||
const { primaryStandbyKeys, firstMove, primaryActiveKeys } = this.state;
|
||||
// 判断当前移动是否还原为最初的状态
|
||||
const isReset = this.isPrimaryStatus(targetKeys);
|
||||
if (firstMove) {
|
||||
const primaryKeys = direction === 'right' ? primaryStandbyKeys : primaryActiveKeys;
|
||||
this.setTopicsStatus(primaryKeys, true, false);
|
||||
this.setState(({
|
||||
firstMove: false,
|
||||
targetKeys,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是还原为初始状态则还原禁用状态
|
||||
if (isReset) {
|
||||
this.setTopicsStatus([], false, true);
|
||||
this.setState(({
|
||||
firstMove: true,
|
||||
targetKeys,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
targetKeys,
|
||||
});
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
Promise.all([
|
||||
getClusterHaTopicsStatus(this.props.currentCluster.clusterId, true),
|
||||
getClusterHaTopicsStatus(this.props.currentCluster.clusterId, false),
|
||||
]).then(([activeRes, standbyRes]: IHaTopic[][]) => {
|
||||
activeRes = (activeRes || []).map(row => ({
|
||||
...row,
|
||||
key: row.topicName,
|
||||
})).filter(item => item.haRelation === null);
|
||||
standbyRes = (standbyRes || []).map(row => ({
|
||||
...row,
|
||||
key: row.topicName,
|
||||
})).filter(item => item.haRelation === 1 || item.haRelation === 0);
|
||||
this.setState({
|
||||
haTopics: [].concat([...activeRes, ...standbyRes]).sort((a, b) => a.topicName.localeCompare(b.topicName)),
|
||||
primaryActiveKeys: activeRes.map(row => row.topicName),
|
||||
primaryStandbyKeys: standbyRes.map(row => row.topicName),
|
||||
targetKeys: standbyRes.map(row => row.topicName),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { formData = {} as any, visible, currentCluster } = this.props;
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
let metadata = [] as IBrokersMetadata[];
|
||||
metadata = admin.brokersMetadata ? admin.brokersMetadata : metadata;
|
||||
let regions = [] as IBrokersRegions[];
|
||||
regions = admin.brokersRegions ? admin.brokersRegions : regions;
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title="Topic高可用关联"
|
||||
wrapClassName="no-padding"
|
||||
visible={visible}
|
||||
onOk={this.handleOk}
|
||||
onCancel={this.handleCancel}
|
||||
maskClosable={false}
|
||||
confirmLoading={this.state.confirmLoading}
|
||||
width={590}
|
||||
okText="确认"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Alert
|
||||
message={`将【集群${currentCluster.clusterName}】和【集群${currentCluster.haClusterVO?.clusterName}】的Topic关联高可用关系`}
|
||||
type="info"
|
||||
showIcon={true}
|
||||
/>
|
||||
<Form {...layout} name="basic" className="x-form">
|
||||
{/* <Form.Item label="规则">
|
||||
{getFieldDecorator('rule', {
|
||||
initialValue: 'spec',
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请选择规则',
|
||||
}],
|
||||
})(<Radio.Group onChange={this.handleRadioChange} >
|
||||
<Radio value="all">应用于所有Topic</Radio>
|
||||
<Radio value="spec">应用于特定Topic</Radio>
|
||||
</Radio.Group>)}
|
||||
</Form.Item> */}
|
||||
{this.state.radioCheck === 'spec' ? <Form.Item className="no-label" label="" >
|
||||
{getFieldDecorator('topicNames', {
|
||||
initialValue: this.state.targetKeys,
|
||||
rules: [{
|
||||
required: false,
|
||||
message: '请选择Topic',
|
||||
}],
|
||||
})(
|
||||
<Transfer
|
||||
className="transfe-list"
|
||||
dataSource={this.state.haTopics}
|
||||
targetKeys={this.state.targetKeys}
|
||||
showSearch={true}
|
||||
onChange={this.onTransferChange}
|
||||
render={item => item.topicName}
|
||||
titles={['未关联', '已关联']}
|
||||
locale={{
|
||||
itemUnit: '',
|
||||
itemsUnit: '',
|
||||
}}
|
||||
/>,
|
||||
)}
|
||||
</Form.Item> : ''}
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
export const TopicHaRelationWrapper = Form.create<IXFormProps>()(TopicHaRelation);
|
||||
@@ -0,0 +1,718 @@
|
||||
import * as React from 'react';
|
||||
import { admin } from 'store/admin';
|
||||
import { Modal, Form, Radio, Tag, Popover, Button } from 'antd';
|
||||
import { IBrokersMetadata, IBrokersRegions, IMetaData } from 'types/base-type';
|
||||
import { Alert, Icon, message, Table, Transfer } from 'component/antd';
|
||||
import { getClusterHaTopics, getAppRelatedTopics, createSwitchTask } from 'lib/api';
|
||||
import { TooltipPlacement } from 'antd/es/tooltip';
|
||||
import * as XLSX from 'xlsx';
|
||||
import moment from 'moment';
|
||||
import { timeMinute } from 'constants/strategy';
|
||||
|
||||
const layout = {
|
||||
labelCol: { span: 3 },
|
||||
wrapperCol: { span: 21 },
|
||||
};
|
||||
|
||||
interface IXFormProps {
|
||||
form: any;
|
||||
reload: any;
|
||||
formData?: any;
|
||||
visible?: boolean;
|
||||
handleVisible?: any;
|
||||
currentCluster?: IMetaData;
|
||||
}
|
||||
|
||||
interface IHaTopic {
|
||||
clusterId: number;
|
||||
topicName: string;
|
||||
key: string;
|
||||
activeClusterId: number;
|
||||
consumeAclNum: number;
|
||||
produceAclNum: number;
|
||||
standbyClusterId: number;
|
||||
status: number;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface IKafkaUser {
|
||||
clusterPhyId: number;
|
||||
kafkaUser: string;
|
||||
notHaTopicNameList: string[];
|
||||
notSelectTopicNameList: string[];
|
||||
selectedTopicNameList: string[];
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
dataIndex: 'topicName',
|
||||
title: '名称',
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
dataIndex: 'produceAclNum',
|
||||
title: '生产者数量',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
dataIndex: 'consumeAclNum',
|
||||
title: '消费者数量',
|
||||
width: 80,
|
||||
},
|
||||
];
|
||||
|
||||
const kafkaUserColumn = [
|
||||
{
|
||||
dataIndex: 'kafkaUser',
|
||||
title: 'kafkaUser',
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
dataIndex: 'selectedTopicNameList',
|
||||
title: '已选中Topic',
|
||||
width: 120,
|
||||
render: (text: string[]) => {
|
||||
return text?.length ? renderAttributes({ data: text, limit: 3 }) : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'notSelectTopicNameList',
|
||||
title: '选中关联Topic',
|
||||
width: 120,
|
||||
render: (text: string[]) => {
|
||||
return text?.length ? renderAttributes({ data: text, limit: 3 }) : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'notHaTopicNameList',
|
||||
title: '未建立HA Topic',
|
||||
width: 120,
|
||||
render: (text: string[]) => {
|
||||
return text?.length ? renderAttributes({ data: text, limit: 3 }) : '-';
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const renderAttributes = (params: {
|
||||
data: any;
|
||||
type?: string;
|
||||
limit?: number;
|
||||
splitType?: string;
|
||||
placement?: TooltipPlacement;
|
||||
}) => {
|
||||
const { data, type = ',', limit = 2, splitType = ';', placement } = params;
|
||||
let attrArray = data;
|
||||
if (!Array.isArray(data) && data) {
|
||||
attrArray = data.split(type);
|
||||
}
|
||||
const showItems = attrArray.slice(0, limit) || [];
|
||||
const hideItems = attrArray.slice(limit, attrArray.length) || [];
|
||||
const content = hideItems.map((item: string, index: number) => (
|
||||
<Tag key={index} className="tag-gray">
|
||||
{item}
|
||||
</Tag>
|
||||
));
|
||||
const showItemsContent = showItems.map((item: string, index: number) => (
|
||||
<Tag key={index} className="tag-gray">
|
||||
{item}
|
||||
</Tag>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="attribute-content">
|
||||
{showItems.length > 0 ? showItemsContent : '-'}
|
||||
{hideItems.length > 0 && (
|
||||
<Popover placement={placement || 'bottomRight'} content={content} overlayClassName="attribute-tag">
|
||||
共{attrArray.length}个<Icon className="icon" type="down" />
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
class TopicHaSwitch extends React.Component<IXFormProps> {
|
||||
public state = {
|
||||
radioCheck: 'spec',
|
||||
targetKeys: [] as string[],
|
||||
selectedKeys: [] as string[],
|
||||
topics: [] as IHaTopic[],
|
||||
kafkaUsers: [] as IKafkaUser[],
|
||||
primaryActiveKeys: [] as string[],
|
||||
primaryStandbyKeys: [] as string[],
|
||||
firstMove: true,
|
||||
};
|
||||
|
||||
public isPrimaryStatus = (targetKeys: string[]) => {
|
||||
const { primaryStandbyKeys } = this.state;
|
||||
let isReset = false;
|
||||
// 判断当前移动是否还原为最初的状态
|
||||
if (primaryStandbyKeys.length === targetKeys.length) {
|
||||
targetKeys.sort((a, b) => +a - (+b));
|
||||
primaryStandbyKeys.sort((a, b) => +a - (+b));
|
||||
let i = 0;
|
||||
while (i < targetKeys.length) {
|
||||
if (targetKeys[i] === primaryStandbyKeys[i]) {
|
||||
i++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
isReset = i === targetKeys.length;
|
||||
}
|
||||
return isReset;
|
||||
}
|
||||
|
||||
public getTargetTopics = (currentKeys: string[], primaryKeys: string[]) => {
|
||||
const targetTopics = [];
|
||||
for (const key of currentKeys) {
|
||||
if (!primaryKeys.includes(key)) {
|
||||
const topic = this.state.topics.find(item => item.key === key)?.topicName;
|
||||
targetTopics.push(topic);
|
||||
}
|
||||
}
|
||||
return targetTopics;
|
||||
}
|
||||
|
||||
public handleOk = () => {
|
||||
const { primaryStandbyKeys, primaryActiveKeys, topics } = this.state;
|
||||
const standbyClusterId = this.props.currentCluster.haClusterVO.clusterId;
|
||||
const activeClusterId = this.props.currentCluster.clusterId;
|
||||
|
||||
this.props.form.validateFields((err: any, values: any) => {
|
||||
|
||||
if (values.rule === 'all') {
|
||||
createSwitchTask({
|
||||
activeClusterPhyId: activeClusterId,
|
||||
all: true,
|
||||
mustContainAllKafkaUserTopics: true,
|
||||
standbyClusterPhyId: standbyClusterId,
|
||||
topicNameList: [],
|
||||
}).then(res => {
|
||||
message.success('任务创建成功');
|
||||
this.handleCancel();
|
||||
this.props.reload(res);
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 判断当前移动是否还原为最初的状态
|
||||
const isPrimary = this.isPrimaryStatus(values.targetKeys || []);
|
||||
if (isPrimary) {
|
||||
return message.info('请选择您要切换的Topic');
|
||||
}
|
||||
|
||||
// 右侧框值
|
||||
const currentStandbyKeys = values.targetKeys || [];
|
||||
// 左侧框值
|
||||
const currentActiveKeys = [];
|
||||
for (const item of topics) {
|
||||
if (!currentStandbyKeys.includes(item.key)) {
|
||||
currentActiveKeys.push(item.key);
|
||||
}
|
||||
}
|
||||
|
||||
const currentKeys = currentStandbyKeys.length > primaryStandbyKeys.length ? currentStandbyKeys : currentActiveKeys;
|
||||
const primaryKeys = currentStandbyKeys.length > primaryStandbyKeys.length ? primaryStandbyKeys : primaryActiveKeys;
|
||||
const activeClusterPhyId = currentStandbyKeys.length > primaryStandbyKeys.length ? standbyClusterId : activeClusterId;
|
||||
const standbyClusterPhyId = currentStandbyKeys.length > primaryStandbyKeys.length ? activeClusterId : standbyClusterId;
|
||||
const targetTopics = this.getTargetTopics(currentKeys, primaryKeys);
|
||||
createSwitchTask({
|
||||
activeClusterPhyId,
|
||||
all: false,
|
||||
mustContainAllKafkaUserTopics: true,
|
||||
standbyClusterPhyId,
|
||||
topicNameList: targetTopics,
|
||||
}).then(res => {
|
||||
message.success('任务创建成功');
|
||||
this.handleCancel();
|
||||
this.props.reload(res);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public handleCancel = () => {
|
||||
this.props.handleVisible(false);
|
||||
this.props.form.resetFields();
|
||||
}
|
||||
|
||||
public handleRadioChange = (e: any) => {
|
||||
this.setState({
|
||||
radioCheck: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
public getNewSelectKeys = (removeKeys: string[], selectedKeys: string[]) => {
|
||||
const { topics, kafkaUsers } = this.state;
|
||||
// 根据移除的key找与该key关联的其他key,一起移除
|
||||
let relatedTopics: string[] = [];
|
||||
const relatedKeys: string[] = [];
|
||||
const newSelectKeys = [];
|
||||
for (const key of removeKeys) {
|
||||
const topicName = topics.find(row => row.key === key)?.topicName;
|
||||
for (const item of kafkaUsers) {
|
||||
if (item.selectedTopicNameList.includes(topicName)) {
|
||||
relatedTopics = relatedTopics.concat(item.selectedTopicNameList);
|
||||
relatedTopics = relatedTopics.concat(item.notSelectTopicNameList);
|
||||
}
|
||||
}
|
||||
for (const item of relatedTopics) {
|
||||
const key = topics.find(row => row.topicName === item)?.key;
|
||||
if (key) {
|
||||
relatedKeys.push(key);
|
||||
}
|
||||
}
|
||||
for (const key of selectedKeys) {
|
||||
if (!relatedKeys.includes(key)) {
|
||||
newSelectKeys.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
return newSelectKeys;
|
||||
}
|
||||
|
||||
public setTopicsStatus = (targetKeys: string[], disabled: boolean, isAll = false) => {
|
||||
const { topics } = this.state;
|
||||
const newTopics = Array.from(topics);
|
||||
if (isAll) {
|
||||
for (let i = 0; i < topics.length; i++) {
|
||||
newTopics[i].disabled = disabled;
|
||||
}
|
||||
} else {
|
||||
for (const key of targetKeys) {
|
||||
const index = topics.findIndex(item => item.key === key);
|
||||
if (index > -1) {
|
||||
newTopics[index].disabled = disabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setState(({
|
||||
topics: newTopics,
|
||||
}));
|
||||
}
|
||||
|
||||
public getFilterTopics = (selectKeys: string[]) => {
|
||||
// 依据key值找topicName
|
||||
const filterTopics: string[] = [];
|
||||
const targetKeys = selectKeys;
|
||||
for (const key of targetKeys) {
|
||||
const topicName = this.state.topics.find(item => item.key === key)?.topicName;
|
||||
if (topicName) {
|
||||
filterTopics.push(topicName);
|
||||
}
|
||||
}
|
||||
return filterTopics;
|
||||
}
|
||||
|
||||
public getNewKafkaUser = (targetKeys: string[]) => {
|
||||
const { primaryStandbyKeys, topics } = this.state;
|
||||
const removeKeys = [];
|
||||
const addKeys = [];
|
||||
for (const key of primaryStandbyKeys) {
|
||||
if (targetKeys.indexOf(key) < 0) {
|
||||
// 移除的
|
||||
removeKeys.push(key);
|
||||
}
|
||||
}
|
||||
for (const key of targetKeys) {
|
||||
if (primaryStandbyKeys.indexOf(key) < 0) {
|
||||
// 新增的
|
||||
addKeys.push(key);
|
||||
}
|
||||
}
|
||||
const keepKeys = [...removeKeys, ...addKeys];
|
||||
const newKafkaUsers = this.state.kafkaUsers;
|
||||
|
||||
const moveTopics = this.getFilterTopics(keepKeys);
|
||||
|
||||
for (const topic of moveTopics) {
|
||||
for (const item of newKafkaUsers) {
|
||||
if (item.selectedTopicNameList.includes(topic)) {
|
||||
item.show = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const showKafaUsers = newKafkaUsers.filter(item => item.show === true);
|
||||
|
||||
for (const item of showKafaUsers) {
|
||||
let i = 0;
|
||||
while (i < moveTopics.length) {
|
||||
if (!item.selectedTopicNameList.includes(moveTopics[i])) {
|
||||
i++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 表示该kafkaUser不该展示
|
||||
if (i === moveTopics.length) {
|
||||
item.show = false;
|
||||
}
|
||||
}
|
||||
|
||||
return showKafaUsers;
|
||||
}
|
||||
|
||||
public getAppRelatedTopicList = (selectedKeys: string[]) => {
|
||||
const { topics, targetKeys, primaryStandbyKeys, kafkaUsers } = this.state;
|
||||
const filterTopicNameList = this.getFilterTopics(selectedKeys);
|
||||
const isReset = this.isPrimaryStatus(targetKeys);
|
||||
|
||||
if (!filterTopicNameList.length && isReset) {
|
||||
// targetKeys
|
||||
this.setState({
|
||||
kafkaUsers: kafkaUsers.map(item => ({
|
||||
...item,
|
||||
show: false,
|
||||
})),
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
// 保留选中项与移动的的项
|
||||
this.setState({
|
||||
kafkaUsers: this.getNewKafkaUser(targetKeys),
|
||||
});
|
||||
}
|
||||
|
||||
// 单向选择,所以取当前值的aactiveClusterId
|
||||
const clusterPhyId = topics.find(item => item.topicName === filterTopicNameList[0]).activeClusterId;
|
||||
getAppRelatedTopics({
|
||||
clusterPhyId,
|
||||
filterTopicNameList,
|
||||
}).then((res: IKafkaUser[]) => {
|
||||
let notSelectTopicNames: string[] = [];
|
||||
const notSelectTopicKeys: string[] = [];
|
||||
for (const item of (res || [])) {
|
||||
notSelectTopicNames = notSelectTopicNames.concat(item.notSelectTopicNameList || []);
|
||||
}
|
||||
|
||||
for (const item of notSelectTopicNames) {
|
||||
const key = topics.find(row => row.topicName === item)?.key;
|
||||
|
||||
if (key) {
|
||||
notSelectTopicKeys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
const newSelectedKeys = selectedKeys.concat(notSelectTopicKeys);
|
||||
const newKafkaUsers = (res || []).map(item => ({
|
||||
...item,
|
||||
show: true,
|
||||
}));
|
||||
const { kafkaUsers } = this.state;
|
||||
|
||||
for (const item of kafkaUsers) {
|
||||
const resItem = res.find(row => row.kafkaUser === item.kafkaUser);
|
||||
if (!resItem) {
|
||||
newKafkaUsers.push(item);
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
kafkaUsers: newKafkaUsers,
|
||||
selectedKeys: newSelectedKeys,
|
||||
});
|
||||
|
||||
if (notSelectTopicKeys.length) {
|
||||
this.getAppRelatedTopicList(newSelectedKeys);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getRelatedKeys = (currentKeys: string[]) => {
|
||||
// 未被选中的项
|
||||
const removeKeys = [];
|
||||
// 对比上一次记录的选中的值找出本次取消的项
|
||||
const { selectedKeys } = this.state;
|
||||
for (const preKey of selectedKeys) {
|
||||
if (!currentKeys.includes(preKey)) {
|
||||
removeKeys.push(preKey);
|
||||
}
|
||||
}
|
||||
|
||||
return removeKeys?.length ? this.getNewSelectKeys(removeKeys, currentKeys) : currentKeys;
|
||||
}
|
||||
|
||||
public handleTopicChange = (sourceSelectedKeys: string[], targetSelectedKeys: string[]) => {
|
||||
const { topics, targetKeys } = this.state;
|
||||
// 条件限制只允许选中一边,单向操作
|
||||
const keys = [...sourceSelectedKeys, ...targetSelectedKeys];
|
||||
|
||||
// 判断当前选中项属于哪一类
|
||||
if (keys.length) {
|
||||
const activeClusterId = topics.find(item => item.key === keys[0]).activeClusterId;
|
||||
const needDisabledKeys = topics.filter(item => item.activeClusterId !== activeClusterId).map(row => row.key);
|
||||
this.setTopicsStatus(needDisabledKeys, true);
|
||||
}
|
||||
const selectedKeys = this.state.selectedKeys.length ? this.getRelatedKeys(keys) : keys;
|
||||
|
||||
const isReset = this.isPrimaryStatus(targetKeys);
|
||||
if (!selectedKeys.length && isReset) {
|
||||
this.setTopicsStatus([], false, true);
|
||||
}
|
||||
this.setState({
|
||||
selectedKeys,
|
||||
});
|
||||
this.getAppRelatedTopicList(selectedKeys);
|
||||
}
|
||||
|
||||
public onDirectChange = (targetKeys: string[], direction: string, moveKeys: string[]) => {
|
||||
const { primaryStandbyKeys, firstMove, primaryActiveKeys, topics } = this.state;
|
||||
|
||||
const getKafkaUser = () => {
|
||||
const newKafkaUsers = this.state.kafkaUsers;
|
||||
const moveTopics = this.getFilterTopics(moveKeys);
|
||||
for (const topic of moveTopics) {
|
||||
for (const item of newKafkaUsers) {
|
||||
if (item.selectedTopicNameList.includes(topic)) {
|
||||
item.show = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return newKafkaUsers;
|
||||
};
|
||||
// 判断当前移动是否还原为最初的状态
|
||||
const isReset = this.isPrimaryStatus(targetKeys);
|
||||
if (firstMove) {
|
||||
const primaryKeys = direction === 'right' ? primaryStandbyKeys : primaryActiveKeys;
|
||||
this.setTopicsStatus(primaryKeys, true, false);
|
||||
this.setState(({
|
||||
firstMove: false,
|
||||
kafkaUsers: getKafkaUser(),
|
||||
targetKeys,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
// 如果是还原为初始状态则还原禁用状态
|
||||
if (isReset) {
|
||||
this.setTopicsStatus([], false, true);
|
||||
this.setState(({
|
||||
firstMove: true,
|
||||
targetKeys,
|
||||
kafkaUsers: [],
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// 切换后重新判定展示项
|
||||
this.setState(({
|
||||
targetKeys,
|
||||
kafkaUsers: this.getNewKafkaUser(targetKeys),
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
public downloadData = () => {
|
||||
const { kafkaUsers } = this.state;
|
||||
const tableData = kafkaUsers.map(item => {
|
||||
return {
|
||||
// tslint:disable
|
||||
'kafkaUser': item.kafkaUser,
|
||||
'已选中Topic': item.selectedTopicNameList?.join('、'),
|
||||
'选中关联Topic': item.notSelectTopicNameList?.join('、'),
|
||||
'未建立HA Topic': item.notHaTopicNameList?.join(`、`),
|
||||
};
|
||||
});
|
||||
const data = [].concat(tableData);
|
||||
const wb = XLSX.utils.book_new();
|
||||
// json转sheet
|
||||
const ws = XLSX.utils.json_to_sheet(data, {
|
||||
header: ['kafkaUser', '已选中Topic', '选中关联Topic', '未建立HA Topic'],
|
||||
});
|
||||
// XLSX.utils.
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'kafkaUser');
|
||||
// 输出
|
||||
XLSX.writeFile(wb, 'kafkaUser-' + moment((new Date()).getTime()).format(timeMinute) + '.xlsx');
|
||||
}
|
||||
|
||||
public judgeSubmitStatus = () => {
|
||||
const { kafkaUsers } = this.state;
|
||||
|
||||
const newKafkaUsers = kafkaUsers.filter(item => item.show)
|
||||
for (const item of newKafkaUsers) {
|
||||
if (item.notHaTopicNameList.length) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const standbyClusterId = this.props.currentCluster.haClusterVO.clusterId;
|
||||
const activeClusterId = this.props.currentCluster.clusterId;
|
||||
getClusterHaTopics(this.props.currentCluster.clusterId, standbyClusterId).then((res: IHaTopic[]) => {
|
||||
res = res.map((item, index) => ({
|
||||
key: index.toString(),
|
||||
...item,
|
||||
}));
|
||||
const targetKeys = (res || []).filter((item) => item.activeClusterId === standbyClusterId).map(row => row.key);
|
||||
const primaryActiveKeys = (res || []).filter((item) => item.activeClusterId === activeClusterId).map(row => row.key);
|
||||
this.setState({
|
||||
topics: res || [],
|
||||
primaryStandbyKeys: targetKeys,
|
||||
primaryActiveKeys,
|
||||
targetKeys,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { visible, currentCluster } = this.props;
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
let metadata = [] as IBrokersMetadata[];
|
||||
metadata = admin.brokersMetadata ? admin.brokersMetadata : metadata;
|
||||
let regions = [] as IBrokersRegions[];
|
||||
regions = admin.brokersRegions ? admin.brokersRegions : regions;
|
||||
const tableData = this.state.kafkaUsers.filter(row => row.show);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Topic主备切换"
|
||||
wrapClassName="no-padding"
|
||||
visible={visible}
|
||||
onCancel={this.handleCancel}
|
||||
maskClosable={false}
|
||||
width={800}
|
||||
footer={<>
|
||||
<Button onClick={this.handleCancel}>取消</Button>
|
||||
<Button disabled={this.judgeSubmitStatus()} style={{ marginLeft: 8 }} type="primary" onClick={() => this.handleOk()}>确定</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Alert
|
||||
message={`注意:必须把同一个kafkauser关联的所有Topic都建立高可用关系,并且都选中,才能执行任务`}
|
||||
type="info"
|
||||
showIcon={true}
|
||||
/>
|
||||
<Form {...layout} name="basic" className="x-form">
|
||||
{/* <Form.Item label="规则" >
|
||||
{getFieldDecorator('rule', {
|
||||
initialValue: 'spec',
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请选择规则',
|
||||
}],
|
||||
})(<Radio.Group onChange={this.handleRadioChange} >
|
||||
<Radio value="all">应用于所有Topic</Radio>
|
||||
<Radio value="spec">应用于特定Topic</Radio>
|
||||
</Radio.Group>)}
|
||||
</Form.Item> */}
|
||||
{this.state.radioCheck === 'spec' ? <Form.Item className="no-label" label="" >
|
||||
{getFieldDecorator('targetKeys', {
|
||||
initialValue: this.state.targetKeys,
|
||||
rules: [{
|
||||
required: false,
|
||||
message: '请选择Topic',
|
||||
}],
|
||||
})(
|
||||
<TransferTable
|
||||
selectedKeys={this.state.selectedKeys}
|
||||
topicChange={this.handleTopicChange}
|
||||
onDirectChange={this.onDirectChange}
|
||||
dataSource={this.state.topics}
|
||||
currentCluster={currentCluster}
|
||||
/>,
|
||||
)}
|
||||
</Form.Item> : ''}
|
||||
</Form>
|
||||
{this.state.radioCheck === 'spec' ?
|
||||
<>
|
||||
<Table
|
||||
className="modal-table-content"
|
||||
columns={kafkaUserColumn}
|
||||
dataSource={tableData}
|
||||
size="small"
|
||||
rowKey="kafkaUser"
|
||||
pagination={false}
|
||||
scroll={{ y: 300 }}
|
||||
/>
|
||||
{this.state.kafkaUsers.length ? <div onClick={this.downloadData} className="modal-table-download"><a>下载表单内容</a></div> : null}
|
||||
</>
|
||||
: null}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
export const TopicSwitchWrapper = Form.create<IXFormProps>()(TopicHaSwitch);
|
||||
|
||||
const TableTransfer = ({ leftColumns, ...restProps }: any) => (
|
||||
<Transfer {...restProps} showSelectAll={true}>
|
||||
{({
|
||||
filteredItems,
|
||||
direction,
|
||||
onItemSelect,
|
||||
selectedKeys: listSelectedKeys,
|
||||
}) => {
|
||||
const columns = leftColumns;
|
||||
|
||||
const rowSelection = {
|
||||
columnWidth: 40,
|
||||
getCheckboxProps: (item: any) => ({
|
||||
disabled: item.disabled,
|
||||
}),
|
||||
onSelect({ key }: any, selected: any) {
|
||||
onItemSelect(key, selected);
|
||||
},
|
||||
selectedRowKeys: listSelectedKeys,
|
||||
};
|
||||
return (
|
||||
<Table
|
||||
rowSelection={rowSelection}
|
||||
columns={columns}
|
||||
dataSource={filteredItems}
|
||||
size="small"
|
||||
pagination={false}
|
||||
scroll={{ y: 320 }}
|
||||
style={{ marginBottom: 14 }}
|
||||
bordered={false}
|
||||
onRow={({ key, disabled }) => ({
|
||||
onClick: () => {
|
||||
if (disabled) return;
|
||||
onItemSelect(key, !listSelectedKeys.includes(key));
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Transfer>
|
||||
);
|
||||
|
||||
interface IProps {
|
||||
value?: any;
|
||||
onChange?: any;
|
||||
onDirectChange?: any;
|
||||
currentCluster: any;
|
||||
topicChange: any;
|
||||
dataSource: any[];
|
||||
selectedKeys: string[];
|
||||
}
|
||||
|
||||
export class TransferTable extends React.Component<IProps> {
|
||||
public onChange = (nextTargetKeys: any, direction: string, moveKeys: string[]) => {
|
||||
this.props.onDirectChange(nextTargetKeys, direction, moveKeys);
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
this.props.onChange && this.props.onChange(nextTargetKeys);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { currentCluster, dataSource, value, topicChange, selectedKeys } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<TableTransfer
|
||||
dataSource={dataSource}
|
||||
targetKeys={value || []}
|
||||
selectedKeys={selectedKeys}
|
||||
showSearch={true}
|
||||
onChange={this.onChange}
|
||||
onSelectChange={topicChange}
|
||||
leftColumns={columns}
|
||||
titles={[`集群${currentCluster.clusterName}`, `集群${currentCluster.haClusterVO.clusterName}`]}
|
||||
locale={{
|
||||
itemUnit: '',
|
||||
itemsUnit: '',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,17 @@ import { modal } from 'store/modal';
|
||||
import { TopicAppSelect } from '../topic/topic-app-select';
|
||||
import Url from 'lib/url-parser';
|
||||
import { expandRemarks, quotaRemarks } from 'constants/strategy';
|
||||
import { getAppListByClusterId } from 'lib/api';
|
||||
|
||||
const updateApplyTopicFormModal = (clusterId: number) => {
|
||||
const formMap = wrapper.xFormWrapper.formMap;
|
||||
const formData = wrapper.xFormWrapper.formData;
|
||||
getAppListByClusterId(clusterId).then(res => {
|
||||
formMap[2].customFormItem = <AppSelect selectData={res} />;
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
wrapper.ref && wrapper.ref.updateFormMap$(formMap, formData);
|
||||
});
|
||||
};
|
||||
|
||||
export const applyTopic = () => {
|
||||
const xFormModal = {
|
||||
@@ -28,6 +39,9 @@ export const applyTopic = () => {
|
||||
rules: [{ required: true, message: '请选择' }],
|
||||
attrs: {
|
||||
placeholder: '请选择',
|
||||
onChange(value: number) {
|
||||
updateApplyTopicFormModal(value);
|
||||
},
|
||||
},
|
||||
}, {
|
||||
key: 'topicName',
|
||||
@@ -49,7 +63,7 @@ export const applyTopic = () => {
|
||||
type: 'custom',
|
||||
defaultValue: '',
|
||||
rules: [{ required: true, message: '请选择' }],
|
||||
customFormItem: <AppSelect selectData={app.data} />,
|
||||
customFormItem: <AppSelect selectData={[]} />,
|
||||
}, {
|
||||
key: 'peakBytesIn',
|
||||
label: '峰值流量',
|
||||
@@ -88,7 +102,7 @@ export const applyTopic = () => {
|
||||
],
|
||||
formData: {},
|
||||
visible: true,
|
||||
title: <div><span>申请Topic</span><a className='applicationDocument' href="https://github.com/didi/Logi-KafkaManager/blob/master/docs/user_guide/resource_apply.md" target='_blank'>资源申请文档</a></div>,
|
||||
title: <div><span>申请Topic</span><a className="applicationDocument" href="https://github.com/didi/Logi-KafkaManager/blob/master/docs/user_guide/resource_apply.md" target="_blank">资源申请文档</a></div>,
|
||||
okText: '确认',
|
||||
// customRenderElement: <span className="tips">集群资源充足时,预计1分钟自动审批通过</span>,
|
||||
isWaitting: true,
|
||||
@@ -106,7 +120,7 @@ export const applyTopic = () => {
|
||||
};
|
||||
return topic.applyTopic(quotaParams).then(data => {
|
||||
window.location.href = `${urlPrefix}/user/order-detail/?orderId=${data.id}®ion=${region.currentRegion}`;
|
||||
})
|
||||
});
|
||||
},
|
||||
onSubmitFaild: (err: any, ref: any, formData: any, formMap: any) => {
|
||||
if (err.message === 'topic already existed') {
|
||||
@@ -115,10 +129,10 @@ export const applyTopic = () => {
|
||||
topicName: {
|
||||
value: topic,
|
||||
errors: [new Error('该topic名称已存在')],
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
wrapper.open(xFormModal);
|
||||
};
|
||||
@@ -186,7 +200,7 @@ export const showApplyQuatoModal = (item: ITopic | IAppsIdInfo, record: IQuotaQu
|
||||
// rules: [{ required: true, message: '' }],
|
||||
// attrs: { disabled: true },
|
||||
// invisible: !item.hasOwnProperty('clusterName'),
|
||||
// },
|
||||
// },
|
||||
{
|
||||
key: 'topicName',
|
||||
label: 'Topic名称',
|
||||
@@ -300,7 +314,7 @@ export const showTopicApplyQuatoModal = (item: ITopic) => {
|
||||
// attrs: { disabled: true },
|
||||
// defaultValue: item.clusterName,
|
||||
// // invisible: !item.hasOwnProperty('clusterName'),
|
||||
// },
|
||||
// },
|
||||
{
|
||||
key: 'topicName',
|
||||
label: 'Topic名称',
|
||||
@@ -380,12 +394,19 @@ export const showTopicApplyQuatoModal = (item: ITopic) => {
|
||||
consumeQuota: transMBToB(value.consumeQuota),
|
||||
produceQuota: transMBToB(value.produceQuota),
|
||||
});
|
||||
|
||||
if (item.isPhysicalClusterId) {
|
||||
Object.assign(quota, {
|
||||
isPhysicalClusterId: true,
|
||||
});
|
||||
}
|
||||
const quotaParams = {
|
||||
type: 2,
|
||||
applicant: users.currentUser.username,
|
||||
description: value.description,
|
||||
extensions: JSON.stringify(quota),
|
||||
};
|
||||
|
||||
topic.applyQuota(quotaParams).then((data) => {
|
||||
notification.success({ message: '申请配额成功' });
|
||||
window.location.href = `${urlPrefix}/user/order-detail/?orderId=${data.id}®ion=${region.currentRegion}`;
|
||||
@@ -454,23 +475,24 @@ const judgeAccessStatus = (access: number) => {
|
||||
|
||||
export const showAllPermissionModal = (item: ITopic) => {
|
||||
let appId: string = null;
|
||||
app.getAppListByClusterId(item.clusterId).then(res => {
|
||||
if (!app.clusterAppData || !app.clusterAppData.length) {
|
||||
return notification.info({
|
||||
message: (
|
||||
<>
|
||||
<span>
|
||||
您的账号暂无可用应用,请先
|
||||
<a href={`${urlPrefix}/topic/app-list?application=1`}>申请应用</a>
|
||||
</span>
|
||||
</>),
|
||||
});
|
||||
}
|
||||
const index = app.clusterAppData.findIndex(row => row.appId === item.appId);
|
||||
|
||||
if (!app.data || !app.data.length) {
|
||||
return notification.info({
|
||||
message: (
|
||||
<>
|
||||
<span>
|
||||
您的账号暂无可用应用,请先
|
||||
<a href={`${urlPrefix}/topic/app-list?application=1`}>申请应用</a>
|
||||
</span>
|
||||
</>),
|
||||
appId = index > -1 ? item.appId : app.clusterAppData[0].appId;
|
||||
topic.getAuthorities(appId, item.clusterId, item.topicName).then((data) => {
|
||||
showAllPermission(appId, item, data.access);
|
||||
});
|
||||
}
|
||||
const index = app.data.findIndex(row => row.appId === item.appId);
|
||||
|
||||
appId = index > -1 ? item.appId : app.data[0].appId;
|
||||
topic.getAuthorities(appId, item.clusterId, item.topicName).then((data) => {
|
||||
showAllPermission(appId, item, data.access);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -494,7 +516,7 @@ const showAllPermission = (appId: string, item: ITopic, access: number) => {
|
||||
defaultValue: appId,
|
||||
rules: [{ required: true, message: '请选择应用' }],
|
||||
type: 'custom',
|
||||
customFormItem: <TopicAppSelect selectData={app.data} parameter={item} />,
|
||||
customFormItem: <TopicAppSelect selectData={app.clusterAppData} parameter={item} />,
|
||||
},
|
||||
{
|
||||
key: 'access',
|
||||
|
||||
@@ -18,7 +18,7 @@ interface IFilterParams {
|
||||
}
|
||||
|
||||
interface ISearchAndFilterState {
|
||||
[filter: string]: boolean | string | number | any[];
|
||||
[filter: string]: boolean | string | number | any;
|
||||
}
|
||||
|
||||
export class SearchAndFilterContainer extends React.Component<any, ISearchAndFilterState> {
|
||||
|
||||
@@ -331,11 +331,13 @@ export class TopicDetail extends React.Component<any> {
|
||||
public render() {
|
||||
const role = users.currentUser.role;
|
||||
const baseInfo = topic.baseInfo as ITopicBaseInfo;
|
||||
const showEditBtn = (role == 1 || role == 2) || (topic.topicBusiness && topic.topicBusiness.principals.includes(users.currentUser.username));
|
||||
const showEditBtn = (role == 1 || role == 2) ||
|
||||
(topic.topicBusiness && topic.topicBusiness.principals.includes(users.currentUser.username));
|
||||
const topicRecord = {
|
||||
clusterId: this.clusterId,
|
||||
topicName: this.topicName,
|
||||
clusterName: this.clusterName
|
||||
clusterName: this.clusterName,
|
||||
isPhysicalClusterId: !!this.isPhysicalTrue,
|
||||
} as ITopic;
|
||||
|
||||
return (
|
||||
@@ -349,9 +351,12 @@ export class TopicDetail extends React.Component<any> {
|
||||
title={this.topicName || ''}
|
||||
extra={
|
||||
<>
|
||||
{this.needAuth == "true" && <Button key="0" type="primary" onClick={() => showAllPermissionModal(topicRecord)} >申请权限</Button>}
|
||||
<Button key="1" type="primary" onClick={() => applyTopicQuotaQuery(topicRecord)} >申请配额</Button>
|
||||
<Button key="2" type="primary" onClick={() => applyExpandModal(topicRecord)} >申请分区</Button>
|
||||
{this.needAuth == 'true' &&
|
||||
<Button key="0" type="primary" onClick={() => showAllPermissionModal(topicRecord)} >申请权限</Button>}
|
||||
{baseInfo.haRelation === 0 ? null :
|
||||
<Button key="1" type="primary" onClick={() => applyTopicQuotaQuery(topicRecord)} >申请配额</Button>}
|
||||
{baseInfo.haRelation === 0 ? null :
|
||||
<Button key="2" type="primary" onClick={() => applyExpandModal(topicRecord)} >申请分区</Button>}
|
||||
<Button key="3" type="primary" onClick={() => this.props.history.push(`/alarm/add`)} >新建告警</Button>
|
||||
<Button key="4" type="primary" onClick={this.showDrawer.bind(this)} >数据采样</Button>
|
||||
{/* {showEditBtn && <Button key="5" onClick={() => this.compileDetails()} type="primary">编辑</Button>} */}
|
||||
|
||||
@@ -248,6 +248,10 @@ export const getAppTopicList = (appId: string, mine: boolean) => {
|
||||
return fetch(`/normal/apps/${appId}/topics?mine=${mine}`);
|
||||
};
|
||||
|
||||
export const getAppListByClusterId = (clusterId: number) => {
|
||||
return fetch(`/normal/apps/${clusterId}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 专家服务
|
||||
*/
|
||||
@@ -418,8 +422,69 @@ export const getMetaData = (needDetail: boolean = true) => {
|
||||
return fetch(`/rd/clusters/basic-info?need-detail=${needDetail}`);
|
||||
};
|
||||
|
||||
export const getHaMetaData = () => {
|
||||
return fetch(`/rd/clusters/ha/basic-info`);
|
||||
};
|
||||
|
||||
export const getClusterHaTopics = (firstClusterId: number, secondClusterId?: number) => {
|
||||
return fetch(`/rd/clusters/${firstClusterId}/ha-topics?secondClusterId=${secondClusterId || ''}`);
|
||||
};
|
||||
|
||||
export const getClusterHaTopicsStatus = (firstClusterId: number, checkMetadata: boolean) => {
|
||||
return fetch(`/rd/clusters/${firstClusterId}/ha-topics/status?checkMetadata=${checkMetadata}`);
|
||||
};
|
||||
|
||||
export const setHaTopics = (params: any) => {
|
||||
return fetch(`/op/ha-topics`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const getAppRelatedTopics = (params: any) => {
|
||||
return fetch(`/rd/apps/relate-topics
|
||||
`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
// 取消Topic高可用
|
||||
export const unbindHaTopics = (params: any) => {
|
||||
return fetch(`/op/ha-topics`, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
// 创建Topic主备切换任务
|
||||
export const createSwitchTask = (params: any) => {
|
||||
return fetch(`/op/as-switch-jobs`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const getJobDetail = (jobId: number) => {
|
||||
return fetch(`/op/as-switch-jobs/${jobId}/job-detail`);
|
||||
};
|
||||
|
||||
export const getJobLog = (jobId: number, startLogId?: number) => {
|
||||
return fetch(`/op/as-switch-jobs/${jobId}/job-logs?startLogId=${startLogId || ''}`);
|
||||
};
|
||||
|
||||
export const getJobState = (jobId: number) => {
|
||||
return fetch(`/op/as-switch-jobs/${jobId}/job-state`);
|
||||
};
|
||||
|
||||
export const switchAsJobs = (jobId: number, params: any) => {
|
||||
return fetch(`/op/as-switch-jobs/${jobId}/action`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const getOperationRecordData = (params: any) => {
|
||||
return fetch(`/rd/operate-record`,{
|
||||
return fetch(`/rd/operate-record`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
@@ -569,15 +634,15 @@ export const getCandidateController = (clusterId: number) => {
|
||||
return fetch(`/rd/clusters/${clusterId}/controller-preferred-candidates`);
|
||||
};
|
||||
|
||||
export const addCandidateController = (params:any) => {
|
||||
return fetch(`/op/cluster-controller/preferred-candidates`, {
|
||||
export const addCandidateController = (params: any) => {
|
||||
return fetch(`/op/cluster-controller/preferred-candidates`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteCandidateCancel = (params:any)=>{
|
||||
return fetch(`/op/cluster-controller/preferred-candidates`, {
|
||||
export const deleteCandidateCancel = (params: any) => {
|
||||
return fetch(`/op/cluster-controller/preferred-candidates`, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
|
||||
@@ -33,7 +33,6 @@ const checkStatus = (res: Response) => {
|
||||
};
|
||||
|
||||
const filter = (init: IInit) => (res: IRes) => {
|
||||
|
||||
if (res.code !== 0 && res.code !== 200) {
|
||||
if (!init.errorNoTips) {
|
||||
notification.error({
|
||||
@@ -117,7 +116,7 @@ export default function fetch(url: string, init?: IInit) {
|
||||
|
||||
export function formFetch(url: string, init?: IInit) {
|
||||
url = url.indexOf('?') > 0 ?
|
||||
`${url}&dataCenter=${region.currentRegion}` : `${url}?dataCenter=${region.currentRegion}`;
|
||||
`${url}&dataCenter=${region.currentRegion}` : `${url}?dataCenter=${region.currentRegion}`;
|
||||
let realUrl = url;
|
||||
|
||||
if (!/^http(s)?:\/\//.test(url)) {
|
||||
@@ -127,8 +126,8 @@ export function formFetch(url: string, init?: IInit) {
|
||||
init = addCustomHeader(init);
|
||||
|
||||
return window
|
||||
.fetch(realUrl, init)
|
||||
.then(res => checkStatus(res))
|
||||
.then((res) => res.json())
|
||||
.then(filter(init));
|
||||
.fetch(realUrl, init)
|
||||
.then(res => checkStatus(res))
|
||||
.then((res) => res.json())
|
||||
.then(filter(init));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@@ -13,7 +12,9 @@ li {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
html, body, .router-nav {
|
||||
html,
|
||||
body,
|
||||
.router-nav {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: PingFangSC-Regular;
|
||||
@@ -52,11 +53,12 @@ html, body, .router-nav {
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th, .ant-table-tbody > tr > td {
|
||||
.ant-table-thead>tr>th,
|
||||
.ant-table-tbody>tr>td {
|
||||
padding: 13px;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
.ant-table-tbody>tr>td {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@@ -72,15 +74,11 @@ html, body, .router-nav {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mb-24 {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th .ant-table-filter-icon {
|
||||
.ant-table-thead>tr>th .ant-table-filter-icon {
|
||||
right: initial;
|
||||
}
|
||||
|
||||
@@ -100,7 +98,7 @@ html, body, .router-nav {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.config-info{
|
||||
.config-info {
|
||||
white-space: pre-line;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
@@ -112,5 +110,4 @@ html, body, .router-nav {
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BrowserRouter as Router, Route } from 'react-router-dom';
|
||||
import { hot } from 'react-hot-loader/root';
|
||||
import * as React from 'react';
|
||||
import zhCN from 'antd/lib/locale/zh_CN';
|
||||
|
||||
import Home from './page/topic';
|
||||
import Admin from './page/admin';
|
||||
@@ -12,58 +13,62 @@ import { urlPrefix } from 'constants/left-menu';
|
||||
import ErrorPage from './page/error';
|
||||
import Login from './page/login';
|
||||
import InfoPage from './page/info';
|
||||
import { ConfigProvider } from 'antd';
|
||||
|
||||
class RouterDom extends React.Component {
|
||||
public render() {
|
||||
return (
|
||||
<Router basename={urlPrefix}>
|
||||
<Route path="/" exact={true} component={Home} />
|
||||
<Route path={`/topic`} exact={true} component={Home} />
|
||||
<Route
|
||||
path={`/topic/:page`}
|
||||
exact={true}
|
||||
component={Home}
|
||||
/>
|
||||
<ConfigProvider locale={zhCN}>
|
||||
|
||||
<Route path={`/admin`} exact={true} component={Admin} />
|
||||
<Route
|
||||
path={`/admin/:page`}
|
||||
exact={true}
|
||||
component={Admin}
|
||||
/>
|
||||
<Router basename={urlPrefix}>
|
||||
<Route path="/" exact={true} component={Home} />
|
||||
<Route path={`/topic`} exact={true} component={Home} />
|
||||
<Route
|
||||
path={`/topic/:page`}
|
||||
exact={true}
|
||||
component={Home}
|
||||
/>
|
||||
|
||||
<Route path={`/user`} exact={true} component={User} />
|
||||
<Route path={`/user/:page`} exact={true} component={User} />
|
||||
<Route path={`/admin`} exact={true} component={Admin} />
|
||||
<Route
|
||||
path={`/admin/:page`}
|
||||
exact={true}
|
||||
component={Admin}
|
||||
/>
|
||||
|
||||
<Route path={`/cluster`} exact={true} component={Cluster} />
|
||||
<Route
|
||||
path={`/cluster/:page`}
|
||||
exact={true}
|
||||
component={Cluster}
|
||||
/>
|
||||
<Route path={`/user`} exact={true} component={User} />
|
||||
<Route path={`/user/:page`} exact={true} component={User} />
|
||||
|
||||
<Route path={`/expert`} exact={true} component={Expert} />
|
||||
<Route
|
||||
path={`/expert/:page`}
|
||||
exact={true}
|
||||
component={Expert}
|
||||
/>
|
||||
<Route path={`/cluster`} exact={true} component={Cluster} />
|
||||
<Route
|
||||
path={`/cluster/:page`}
|
||||
exact={true}
|
||||
component={Cluster}
|
||||
/>
|
||||
|
||||
<Route path={`/alarm`} exact={true} component={Alarm} />
|
||||
<Route
|
||||
path={`/alarm/:page`}
|
||||
exact={true}
|
||||
component={Alarm}
|
||||
/>
|
||||
<Route path={`/expert`} exact={true} component={Expert} />
|
||||
<Route
|
||||
path={`/expert/:page`}
|
||||
exact={true}
|
||||
component={Expert}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={`/login`}
|
||||
exact={true}
|
||||
component={Login}
|
||||
/>
|
||||
<Route path={`/error`} exact={true} component={ErrorPage} />
|
||||
<Route path={`/info`} exact={true} component={InfoPage} />
|
||||
</Router>
|
||||
<Route path={`/alarm`} exact={true} component={Alarm} />
|
||||
<Route
|
||||
path={`/alarm/:page`}
|
||||
exact={true}
|
||||
component={Alarm}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={`/login`}
|
||||
exact={true}
|
||||
component={Login}
|
||||
/>
|
||||
<Route path={`/error`} exact={true} component={ErrorPage} />
|
||||
<Route path={`/info`} exact={true} component={InfoPage} />
|
||||
</Router>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,8 +57,9 @@ import {
|
||||
getBillStaffDetail,
|
||||
getCandidateController,
|
||||
addCandidateController,
|
||||
deleteCandidateCancel
|
||||
} from 'lib/api';
|
||||
deleteCandidateCancel,
|
||||
getHaMetaData,
|
||||
} from 'lib/api';
|
||||
import { getControlMetricOption, getClusterMetricOption } from 'lib/line-charts-config';
|
||||
|
||||
import { copyValueMap } from 'constants/status-map';
|
||||
@@ -104,12 +105,15 @@ class Admin {
|
||||
@observable
|
||||
public metaList: IMetaData[] = [];
|
||||
|
||||
@observable
|
||||
public haMetaList: IMetaData[] = [];
|
||||
|
||||
@observable
|
||||
public oRList: any[] = [];
|
||||
|
||||
@observable
|
||||
public oRparams:any={
|
||||
moduleId:0
|
||||
public oRparams: any = {
|
||||
moduleId: 0
|
||||
};
|
||||
|
||||
@observable
|
||||
@@ -169,9 +173,9 @@ class Admin {
|
||||
@observable
|
||||
public controllerCandidate: IController[] = [];
|
||||
|
||||
@observable
|
||||
@observable
|
||||
public filtercontrollerCandidate: string = '';
|
||||
|
||||
|
||||
@observable
|
||||
public brokersPartitions: IBrokersPartitions[] = [];
|
||||
|
||||
@@ -329,9 +333,20 @@ class Admin {
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setOperationRecordList(data:any){
|
||||
public setHaMetaList(data: IMetaData[]) {
|
||||
this.setLoading(false);
|
||||
this.oRList = data ? data.map((item:any, index: any) => {
|
||||
this.haMetaList = data ? data.map((item, index) => {
|
||||
item.key = index;
|
||||
return item;
|
||||
}) : [];
|
||||
this.haMetaList = this.haMetaList.sort((a, b) => a.clusterId - b.clusterId);
|
||||
return this.haMetaList;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setOperationRecordList(data: any) {
|
||||
this.setLoading(false);
|
||||
this.oRList = data ? data.map((item: any, index: any) => {
|
||||
item.key = index;
|
||||
return item;
|
||||
}) : [];
|
||||
@@ -394,9 +409,9 @@ class Admin {
|
||||
item.key = index;
|
||||
return item;
|
||||
}) : [];
|
||||
this.filtercontrollerCandidate = data?data.map((item,index)=>{
|
||||
this.filtercontrollerCandidate = data ? data.map((item, index) => {
|
||||
return item.brokerId
|
||||
}).join(','):''
|
||||
}).join(',') : ''
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@@ -479,8 +494,8 @@ class Admin {
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setBrokersMetadata(data: IBrokersMetadata[]|any) {
|
||||
this.brokersMetadata = data ? data.map((item:any, index:any) => {
|
||||
public setBrokersMetadata(data: IBrokersMetadata[] | any) {
|
||||
this.brokersMetadata = data ? data.map((item: any, index: any) => {
|
||||
item.key = index;
|
||||
return {
|
||||
...item,
|
||||
@@ -675,6 +690,11 @@ class Admin {
|
||||
getMetaData(needDetail).then(this.setMetaList);
|
||||
}
|
||||
|
||||
public getHaMetaData() {
|
||||
this.setLoading(true);
|
||||
return getHaMetaData().then(this.setHaMetaList);
|
||||
}
|
||||
|
||||
public getOperationRecordData(params: any) {
|
||||
this.setLoading(true);
|
||||
this.oRparams = params
|
||||
@@ -738,17 +758,17 @@ class Admin {
|
||||
}
|
||||
|
||||
public getCandidateController(clusterId: number) {
|
||||
return getCandidateController(clusterId).then(data=>{
|
||||
return getCandidateController(clusterId).then(data => {
|
||||
return this.setCandidateController(data)
|
||||
});
|
||||
}
|
||||
|
||||
public addCandidateController(clusterId: number, brokerIdList: any) {
|
||||
return addCandidateController({clusterId, brokerIdList}).then(()=>this.getCandidateController(clusterId));
|
||||
return addCandidateController({ clusterId, brokerIdList }).then(() => this.getCandidateController(clusterId));
|
||||
}
|
||||
|
||||
public deleteCandidateCancel(clusterId: number, brokerIdList: any){
|
||||
return deleteCandidateCancel({clusterId, brokerIdList}).then(()=>this.getCandidateController(clusterId));
|
||||
public deleteCandidateCancel(clusterId: number, brokerIdList: any) {
|
||||
return deleteCandidateCancel({ clusterId, brokerIdList }).then(() => this.getCandidateController(clusterId));
|
||||
}
|
||||
|
||||
public getBrokersBasicInfo(clusterId: number, brokerId: number) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { observable, action } from 'mobx';
|
||||
import { getAppList, getAppDetail, getAppTopicList, applyOrder, modfiyApplication, modfiyAdminApp, getAdminAppList, getAppsConnections, getTopicAppQuota } from 'lib/api';
|
||||
import { getAppList, getAppDetail, getAppTopicList, applyOrder, modfiyApplication, modfiyAdminApp, getAdminAppList, getAppsConnections, getTopicAppQuota, getAppListByClusterId } from 'lib/api';
|
||||
import { IAppItem, IAppQuota, ITopic, IOrderParams, IConnectionInfo } from 'types/base-type';
|
||||
|
||||
class App {
|
||||
@@ -12,6 +12,9 @@ class App {
|
||||
@observable
|
||||
public data: IAppItem[] = [];
|
||||
|
||||
@observable
|
||||
public clusterAppData: IAppItem[] = [];
|
||||
|
||||
@observable
|
||||
public adminAppData: IAppItem[] = [];
|
||||
|
||||
@@ -19,7 +22,7 @@ class App {
|
||||
public selectData: IAppItem[] = [{
|
||||
appId: '-1',
|
||||
name: '所有关联应用',
|
||||
} as IAppItem,
|
||||
} as IAppItem,
|
||||
];
|
||||
|
||||
@observable
|
||||
@@ -51,12 +54,12 @@ class App {
|
||||
@action.bound
|
||||
public setTopicAppQuota(data: IAppQuota[]) {
|
||||
return this.appQuota = data.map((item, index) => {
|
||||
return {
|
||||
...item,
|
||||
label: item.appName,
|
||||
value: item.appId,
|
||||
key: index,
|
||||
};
|
||||
return {
|
||||
...item,
|
||||
label: item.appName,
|
||||
value: item.appId,
|
||||
key: index,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -87,6 +90,16 @@ class App {
|
||||
this.setLoading(false);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setClusterAppData(data: IAppItem[] = []) {
|
||||
this.clusterAppData = data.map((item, index) => ({
|
||||
...item,
|
||||
key: index,
|
||||
principalList: item.principals ? item.principals.split(',') : [],
|
||||
}));
|
||||
return this.clusterAppData;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
public setAdminData(data: IAppItem[] = []) {
|
||||
this.adminAppData = data.map((item, index) => ({
|
||||
@@ -133,6 +146,10 @@ class App {
|
||||
getAppList().then(this.setData);
|
||||
}
|
||||
|
||||
public getAppListByClusterId(clusterId: number) {
|
||||
return getAppListByClusterId(clusterId).then(this.setClusterAppData);
|
||||
}
|
||||
|
||||
public getTopicAppQuota(clusterId: number, topicName: string) {
|
||||
return getTopicAppQuota(clusterId, topicName).then(this.setTopicAppQuota);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface ITopicBaseInfo {
|
||||
physicalClusterId: number;
|
||||
percentile: string;
|
||||
regionNameList: any;
|
||||
haRelation: number;
|
||||
}
|
||||
|
||||
export interface IRealTimeTraffic {
|
||||
|
||||
@@ -474,7 +474,14 @@ export interface IMetaData {
|
||||
status: number;
|
||||
topicNum: number;
|
||||
zookeeper: string;
|
||||
haRelation?: number;
|
||||
haASSwitchJobId?: number;
|
||||
haStatus?: number;
|
||||
haClusterVO?: IMetaData;
|
||||
activeTopicCount?: number;
|
||||
standbyTopicCount?: number;
|
||||
key?: number;
|
||||
mutualBackupClusterName?: string;
|
||||
}
|
||||
|
||||
export interface IConfigure {
|
||||
@@ -641,6 +648,7 @@ export interface IClusterTopics {
|
||||
properties: any;
|
||||
clusterName: string;
|
||||
logicalClusterId: number;
|
||||
haRelation?: number;
|
||||
key?: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -130,9 +130,7 @@ module.exports = {
|
||||
historyApiFallback: true,
|
||||
proxy: {
|
||||
'/api/v1/': {
|
||||
// target: 'http://127.0.0.1:8080',
|
||||
target: 'http://10.179.37.199:8008',
|
||||
// target: 'http://99.11.45.164:8888',
|
||||
target: 'http://127.0.0.1:8080/',
|
||||
changeOrigin: true,
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user