mirror of
https://github.com/didi/KnowStreaming.git
synced 2026-01-06 05:22:16 +08:00
初始化3.0.0版本
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { timeFormat } from '../../constants/common';
|
||||
import moment from 'moment';
|
||||
import { getSizeAndUnit } from '../../constants/common';
|
||||
|
||||
export const getControllerChangeLogListColumns = (arg?: any) => {
|
||||
const columns = [
|
||||
{
|
||||
title: 'Change Time',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
render: (t: number) => (t ? moment(t).format(timeFormat) : '-'),
|
||||
},
|
||||
{
|
||||
title: 'Broker ID',
|
||||
dataIndex: 'brokerId',
|
||||
key: 'brokerId',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (t: number, r: any) => {
|
||||
return t === -1 ? (
|
||||
'-'
|
||||
) : (
|
||||
<a
|
||||
onClick={() => {
|
||||
window.location.hash = `brokerId=${t || ''}&host=${r.brokerHost || ''}`;
|
||||
}}
|
||||
>
|
||||
{t}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Broker Host',
|
||||
dataIndex: 'brokerHost',
|
||||
key: 'brokerHost',
|
||||
},
|
||||
];
|
||||
return columns;
|
||||
};
|
||||
|
||||
export const defaultPagination = {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
position: 'bottomRight',
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
.controllerList{
|
||||
.d-table-box-header{
|
||||
padding: 0 0 12px 0 ;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useHistory, useLocation } from 'react-router-dom';
|
||||
import { ProTable, Utils, AppContainer } from 'knowdesign';
|
||||
import API from '../../api';
|
||||
import { getControllerChangeLogListColumns, defaultPagination } from './config';
|
||||
import BrokerDetail from '../BrokerDetail';
|
||||
import BrokerHealthCheck from '@src/components/CardBar/BrokerHealthCheck';
|
||||
import DBreadcrumb from 'knowdesign/lib/extend/d-breadcrumb';
|
||||
import './index.less';
|
||||
|
||||
const { request } = Utils;
|
||||
const ControllerChangeLogList: React.FC = (props: any) => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const urlParams = useParams<any>(); // 获取地址栏参数
|
||||
const history = useHistory();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
const [searchKeywords, setSearchKeywords] = useState('');
|
||||
const [filteredInfo, setFilteredInfo] = useState(null);
|
||||
const [pagination, setPagination] = useState<any>(defaultPagination);
|
||||
const [clusterName, setClusterName] = useState<any>(null);
|
||||
// const [visible, setVisible] = useState(false);
|
||||
// const [record, setRecord] = useState(null); // 获取当前点击行的数据;
|
||||
// 默认排序
|
||||
|
||||
const defaultSorter = {
|
||||
sortField: 'changeTime',
|
||||
sortType: 'desc',
|
||||
};
|
||||
|
||||
// 请求接口获取数据
|
||||
const genData = ({ pageNo, pageSize, filters = null, sorter = null }: any) => {
|
||||
if (urlParams?.clusterId === undefined) return;
|
||||
filters = filters || filteredInfo;
|
||||
setLoading(true);
|
||||
// const params = dealTableRequestParams({ searchKeywords, pageNo, pageSize, sorter, filters });
|
||||
const params = {
|
||||
searchKeywords: searchKeywords.slice(0, 128),
|
||||
pageNo,
|
||||
pageSize,
|
||||
};
|
||||
|
||||
request(API.getChangeLogList(urlParams?.clusterId), { params: { ...params, ...defaultSorter } })
|
||||
.then((res: any) => {
|
||||
setPagination({
|
||||
current: res.pagination?.pageNo,
|
||||
pageSize: res.pagination?.pageSize,
|
||||
total: res.pagination?.total,
|
||||
});
|
||||
setData(res?.bizData || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
// mock
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
setFilteredInfo(filters);
|
||||
genData({ pageNo: pagination.current, pageSize: pagination.pageSize, filters, sorter });
|
||||
};
|
||||
|
||||
const getSearchKeywords = (value: string) => {
|
||||
setSearchKeywords(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
genData({
|
||||
pageNo: 1,
|
||||
pageSize: pagination.pageSize,
|
||||
// sorter: defaultSorter
|
||||
});
|
||||
}, [searchKeywords]);
|
||||
|
||||
return (
|
||||
<div className="controllerList">
|
||||
<div className="breadcrumb" style={{ marginBottom: '10px' }}>
|
||||
<DBreadcrumb
|
||||
breadcrumbs={[
|
||||
{ label: '多集群管理', aHref: '/' },
|
||||
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
|
||||
{ label: 'Broker', aHref: `/cluster/${urlParams?.clusterId}/broker` },
|
||||
{ label: 'Controller', aHref: `` },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ margin: '12px 0' }}>
|
||||
<BrokerHealthCheck />
|
||||
</div>
|
||||
<div className="clustom-table-content">
|
||||
<ProTable
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
showHeader: true,
|
||||
rowKey: 'path',
|
||||
loading: loading,
|
||||
columns: getControllerChangeLogListColumns(),
|
||||
dataSource: data,
|
||||
paginationProps: { ...pagination },
|
||||
tableHeaderSearchInput: {
|
||||
// 搜索配置
|
||||
submit: getSearchKeywords,
|
||||
searchInputType: 'search',
|
||||
searchAttr: {
|
||||
placeholder: '请输入Broker Host',
|
||||
style: {
|
||||
width: '248px',
|
||||
},
|
||||
},
|
||||
},
|
||||
attrs: {
|
||||
onChange: onTableChange,
|
||||
bordered: false,
|
||||
scroll: { y: 'calc(100vh - 400px)' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{<BrokerDetail />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ControllerChangeLogList;
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { MetricType } from '@src/api';
|
||||
import BrokerHealthCheck from '@src/components/CardBar/BrokerHealthCheck';
|
||||
import DashboardDragChart from '@src/components/DashboardDragChart';
|
||||
import DBreadcrumb from 'knowdesign/lib/extend/d-breadcrumb';
|
||||
import { AppContainer } from 'knowdesign';
|
||||
|
||||
const BrokerDashboard = (): JSX.Element => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
return (
|
||||
<>
|
||||
<div className="breadcrumb" style={{ marginBottom: '10px' }}>
|
||||
<DBreadcrumb
|
||||
breadcrumbs={[
|
||||
{ label: '多集群管理', aHref: '/' },
|
||||
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
|
||||
{ label: 'Broker', aHref: `` },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<BrokerHealthCheck />
|
||||
<DashboardDragChart type={MetricType.Broker} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrokerDashboard;
|
||||
@@ -0,0 +1,175 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { AppContainer, Checkbox, ProTable, Utils } from 'knowdesign';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Api from '@src/api';
|
||||
import { getConfigurationColmns } from './config';
|
||||
import { ConfigurationEdit } from './ConfigurationEdit';
|
||||
import { hashDataParse } from 'src/constants/common';
|
||||
import { ClustersPermissionMap } from '../CommonConfig';
|
||||
const { request } = Utils;
|
||||
|
||||
const BrokerConfiguration = (props: any) => {
|
||||
const { hashData } = props;
|
||||
const urlParams = useParams<any>(); // 获取地址栏参数
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
// const [filterType, setFilterType] = useState<number>(0); // 多选框的筛选结果 filterType
|
||||
const [checkedBoxList, setCheckedBoxList] = useState<string[]>([]); // 多选框的选中的列表
|
||||
const [pagination, setPagination] = useState<any>({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
position: 'bottomRight',
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
|
||||
showTotal: (total: number) => `共 ${total} 条目`,
|
||||
});
|
||||
const [editVisible, setEditVisible] = useState(false);
|
||||
const [record, setRecord] = useState(null); // 获取当前点击行的数据;
|
||||
const [readOnlyVisible, setReadOnlyVisible] = useState(null);
|
||||
const [readOnlyRecord, setReadOnlyRecord] = useState(null);
|
||||
// 请求接口获取数据
|
||||
const genData = async ({ pageNo, pageSize }: any) => {
|
||||
if (urlParams?.clusterId === undefined || hashData?.brokerId === undefined) return;
|
||||
setLoading(true);
|
||||
const params = {
|
||||
searchKeywords: props.searchKeywords ? props.searchKeywords.slice(0, 128) : undefined,
|
||||
pageNo,
|
||||
pageSize,
|
||||
preciseFilterDTOList:
|
||||
checkedBoxList.length > 0
|
||||
? checkedBoxList.map((item) => {
|
||||
return {
|
||||
fieldName: item,
|
||||
fieldValueList: [item === 'readOnly' ? false : true],
|
||||
include: true,
|
||||
};
|
||||
})
|
||||
: undefined,
|
||||
};
|
||||
|
||||
request(Api.getBrokerConfigs(hashData?.brokerId, urlParams?.clusterId), { data: params, method: 'POST' })
|
||||
.then((res: any) => {
|
||||
setPagination({
|
||||
current: res.pagination?.pageNo,
|
||||
pageSize: res.pagination?.pageSize,
|
||||
total: res.pagination?.total,
|
||||
});
|
||||
setData(res?.bizData || []);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
// setPagination(pagination);
|
||||
// const asc = sorter?.order && sorter?.order === 'ascend' ? true : false;
|
||||
// const sortColumn = sorter.field && toLine(sorter.field);
|
||||
genData({ pageNo: pagination.current, pageSize: pagination.pageSize });
|
||||
};
|
||||
|
||||
// 多选配置
|
||||
const checkedBoxOptions = [
|
||||
{ label: 'Hide read-only', value: 'readOnly' },
|
||||
{ label: 'Show Overrides Only', value: 'override' },
|
||||
];
|
||||
|
||||
const checkedBoxChange = (e: any) => {
|
||||
// 通过checked转换filterType
|
||||
// const newfilterType =
|
||||
// e.includes('readOnly') && e.includes('override')
|
||||
// ? 0
|
||||
// : e.includes('readOnly') && !e.includes('override')
|
||||
// ? 1
|
||||
// : !e.includes('readOnly') && e.includes('override')
|
||||
// ? 2
|
||||
// : 3;
|
||||
|
||||
// setFilterType(newfilterType);
|
||||
setCheckedBoxList(e);
|
||||
// 调用接口
|
||||
};
|
||||
|
||||
const setEditOp = (record: any) => {
|
||||
setEditVisible(true);
|
||||
setRecord(record);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
genData({
|
||||
pageNo: 1,
|
||||
pageSize: pagination.pageSize,
|
||||
// sorter: defaultSorter
|
||||
});
|
||||
}, [props.searchKeywords, checkedBoxList, hashData?.brokerId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'detail-header-cases'} style={{ padding: '0 0 12px' }}>
|
||||
<Checkbox.Group options={checkedBoxOptions} value={checkedBoxList} onChange={checkedBoxChange} />
|
||||
<div className={'detail-header-cases-right'}>
|
||||
<div>
|
||||
<div className="icon normal"></div>
|
||||
<div>正常</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="icon differ"></div>
|
||||
<div>在集群内存在差异</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="icon unique"></div>
|
||||
<div>该broker独有</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ProTable
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
rowKey: 'path',
|
||||
loading: loading,
|
||||
columns: getConfigurationColmns({
|
||||
setEditOp,
|
||||
readOnlyRecord,
|
||||
readOnlyVisible,
|
||||
allowEdit: global.hasPermission && global.hasPermission(ClustersPermissionMap.BROKER_CHANGE_CONFIG),
|
||||
}),
|
||||
dataSource: data,
|
||||
paginationProps: { ...pagination },
|
||||
attrs: {
|
||||
// className: 'frameless-table', // 纯无边框表格类名
|
||||
// bordered: true, // 表格边框
|
||||
onChange: onTableChange,
|
||||
bordered: false,
|
||||
onRow: (record: any) => {
|
||||
if (!!record?.readOnly) {
|
||||
return {
|
||||
onMouseEnter: () => {
|
||||
setReadOnlyVisible(true);
|
||||
setReadOnlyRecord(record);
|
||||
},
|
||||
onMouseLeave: () => {
|
||||
setReadOnlyVisible(false);
|
||||
setReadOnlyRecord(null);
|
||||
},
|
||||
};
|
||||
}
|
||||
return {};
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ConfigurationEdit
|
||||
record={record}
|
||||
hashData={hashData}
|
||||
visible={editVisible}
|
||||
setVisible={setEditVisible}
|
||||
genData={genData}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrokerConfiguration;
|
||||
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import { Drawer, Form, Input, Space, Button, Checkbox, Utils, Row, Col, IconFont, Divider, message } from 'knowdesign';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Api from '@src/api';
|
||||
export const ConfigurationEdit = (props: any) => {
|
||||
const urlParams = useParams<any>();
|
||||
const [form] = Form.useForm();
|
||||
const onClose = () => {
|
||||
props.setVisible(false);
|
||||
};
|
||||
|
||||
const onOk = () => {
|
||||
form.validateFields().then((res: any) => {
|
||||
const data = {
|
||||
applyAll: res.applyAll,
|
||||
brokerId: Number(props.hashData?.brokerId),
|
||||
changedProps: {
|
||||
[props.record?.name]: res.newValue,
|
||||
},
|
||||
clusterId: Number(urlParams.clusterId),
|
||||
};
|
||||
Utils.put(Api.getBrokerEditConfig(), data)
|
||||
.then((res: any) => {
|
||||
message.success('编辑配置成功');
|
||||
props.setVisible(false);
|
||||
props.genData({ pageNo: props.pagination.current, pageSize: props.pagination.pageSize });
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.log(err, 'err');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={
|
||||
<Space size={0}>
|
||||
<Button className="drawer-title-left-button" type="text" size="small" icon={<IconFont type="icon-fanhui1" />} onClick={onClose} />
|
||||
<Divider type="vertical" />
|
||||
<span style={{ paddingLeft: '5px' }}>编辑配置</span>
|
||||
</Space>
|
||||
}
|
||||
width={580}
|
||||
visible={props.visible}
|
||||
onClose={() => props.setVisible(false)}
|
||||
maskClosable={false}
|
||||
extra={
|
||||
<Space>
|
||||
<Button size="small" onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button size="small" type="primary" onClick={onOk}>
|
||||
确认
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Row gutter={[12, 12]} className="desc-row">
|
||||
<Col span={3} className="label-col">
|
||||
配置名:
|
||||
</Col>
|
||||
<Col span={21} className="value-col">
|
||||
{props.record?.name || '-'}
|
||||
</Col>
|
||||
<Col span={3} className="label-col">
|
||||
描述:
|
||||
</Col>
|
||||
<Col span={21} className="value-col">
|
||||
{props.record?.documentation || '-'}
|
||||
</Col>
|
||||
</Row>
|
||||
<Form form={form} layout="vertical" initialValues={props.record}>
|
||||
<Form.Item name="defaultValue" label="Kafka默认配置">
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
<Form.Item name="value" label="当前配置">
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
<Form.Item name="newValue" label="新配置" rules={[{ required: true, message: '请输入新的配置值!!!' }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="applyAll" valuePropName="checked">
|
||||
<Checkbox defaultChecked={false}>应用到全部Broker</Checkbox>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ProTable, Utils } from 'knowdesign';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Api from '@src/api';
|
||||
import { getDataLogsColmns } from './config';
|
||||
const { request } = Utils;
|
||||
|
||||
const BrokerDataLogs = (props: any) => {
|
||||
const { hashData } = props;
|
||||
const urlParams = useParams<any>(); // 获取地址栏参数
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
const [pagination, setPagination] = useState<any>({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
position: 'bottomRight',
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
|
||||
showTotal: (total: number) => `共 ${total} 条目`,
|
||||
// locale: {
|
||||
// items_per_page: '条',
|
||||
// },
|
||||
// selectComponentClass: CustomSelect,
|
||||
});
|
||||
|
||||
// 请求接口获取数据
|
||||
const genData = async ({ pageNo, pageSize }: any) => {
|
||||
if (urlParams?.clusterId === undefined || hashData?.brokerId === undefined) return;
|
||||
setData([]);
|
||||
setLoading(true);
|
||||
|
||||
const params = {
|
||||
searchKeywords: props.searchKeywords ? props.searchKeywords.slice(0, 128) : '',
|
||||
pageNo,
|
||||
pageSize,
|
||||
};
|
||||
|
||||
request(Api.getBrokerDataLogs(hashData?.brokerId, urlParams?.clusterId), { params })
|
||||
.then((res: any) => {
|
||||
setPagination({
|
||||
current: res.pagination?.pageNo,
|
||||
pageSize: res.pagination?.pageSize,
|
||||
total: res.pagination?.total,
|
||||
});
|
||||
setData(res?.bizData || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
// setPagination(pagination);
|
||||
// const asc = sorter?.order && sorter?.order === 'ascend' ? true : false;
|
||||
// const sortColumn = sorter.field && toLine(sorter.field);
|
||||
genData({ pageNo: pagination.current, pageSize: pagination.pageSize });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
genData({
|
||||
pageNo: 1,
|
||||
pageSize: pagination.pageSize,
|
||||
// sorter: defaultSorter
|
||||
});
|
||||
}, [props.searchKeywords, hashData?.brokerId]);
|
||||
|
||||
return (
|
||||
<ProTable
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
rowKey: 'path',
|
||||
loading: loading,
|
||||
columns: getDataLogsColmns(),
|
||||
dataSource: data,
|
||||
paginationProps: { ...pagination },
|
||||
attrs: {
|
||||
// className: 'frameless-table', // 纯无边框表格类名
|
||||
// bordered: true, // 表格边框
|
||||
onChange: onTableChange,
|
||||
bordered: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrokerDataLogs;
|
||||
@@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import { Utils, IconFont, Tooltip } from 'knowdesign';
|
||||
export const getConfigurationColmns = (arg: any) => {
|
||||
const columns: any = [
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'readOnly',
|
||||
key: 'readOnly',
|
||||
align: 'right',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (t: string, r: any) => {
|
||||
return t ? (
|
||||
<Tooltip title="该配置无法修改" visible={r.name === arg?.readOnlyRecord?.name && arg?.readOnlyVisible}>
|
||||
<IconFont style={{ color: '#556EE6', fontSize: '16px' }} type="icon-suoding" />
|
||||
</Tooltip>
|
||||
) : null;
|
||||
},
|
||||
width: 56,
|
||||
className: 'table-suoding',
|
||||
},
|
||||
{
|
||||
title: '配置名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 250,
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'documentation',
|
||||
key: 'documentation',
|
||||
width: 300,
|
||||
lineClampTwo: true,
|
||||
needTooltip: true,
|
||||
},
|
||||
{
|
||||
title: '配置值',
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
width: 250,
|
||||
lineClampTwo: true,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'state',
|
||||
key: 'state',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (t: string, r: any) => {
|
||||
return r.differentiated ? (
|
||||
<div className="differ"></div>
|
||||
) : r.exclusive ? (
|
||||
<div className="unique"></div>
|
||||
) : (
|
||||
<div className="normal"></div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (arg.allowEdit) {
|
||||
columns.push({
|
||||
title: '操作',
|
||||
dataIndex: 'options',
|
||||
key: 'options',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (_t: any, r: any) => {
|
||||
return !r.readOnly ? <a onClick={() => arg.setEditOp(r)}>编辑</a> : '-';
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return columns;
|
||||
};
|
||||
|
||||
export const getDataLogsColmns = () => {
|
||||
const columns = [
|
||||
{
|
||||
title: 'Folder',
|
||||
dataIndex: 'dir',
|
||||
key: 'dir',
|
||||
},
|
||||
{
|
||||
title: 'Topic',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
},
|
||||
{
|
||||
title: 'Partition',
|
||||
dataIndex: 'partitionId',
|
||||
key: 'partitionId',
|
||||
},
|
||||
{
|
||||
title: 'Offset Lag',
|
||||
dataIndex: 'offsetLag',
|
||||
key: 'offsetLag',
|
||||
},
|
||||
{
|
||||
title: 'Size(MB)',
|
||||
dataIndex: 'logSizeUnitB',
|
||||
key: 'logSizeUnitB',
|
||||
render: (t: number, r: any) => {
|
||||
return t || t === 0 ? Utils.transBToMB(t) : '-';
|
||||
},
|
||||
},
|
||||
];
|
||||
return columns;
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
.broker-detail-drawer {
|
||||
.card-bar-container {
|
||||
background: rgba(86, 110, 230, 0.04) !important;
|
||||
|
||||
.card-bar-colunms {
|
||||
background-color: rgba(86, 110, 230, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.detail-header-cases {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&-right {
|
||||
display: flex;
|
||||
|
||||
&>div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #74788D;
|
||||
margin-right: 12px;
|
||||
line-height: 18px;
|
||||
|
||||
.icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.normal {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #D5DCFF;
|
||||
border: 0.5px solid #BBC3EB;
|
||||
border-radius: 2.75px;
|
||||
}
|
||||
|
||||
.differ {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #FEE4C7;
|
||||
border: 0.5px solid #F9D1A5;
|
||||
border-radius: 2.75px;
|
||||
}
|
||||
|
||||
.unique {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #FEC7C7;
|
||||
border: 0.5px solid #F9ADA5;
|
||||
border-radius: 2.75px;
|
||||
}
|
||||
|
||||
.table-suoding {
|
||||
font-size: 18px !important;
|
||||
color: #556EE6 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.desc-row {
|
||||
margin-bottom: 24px;
|
||||
.label-col,
|
||||
.value-col {
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.label-col {
|
||||
color: #74788d;
|
||||
text-align: right;
|
||||
}
|
||||
.value-col {
|
||||
color: #495057;
|
||||
}
|
||||
}
|
||||
|
||||
.init-drawer{
|
||||
.dcloud-drawer-close{
|
||||
position: inherit !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useHistory, useLocation } from 'react-router-dom';
|
||||
import { Tabs, Drawer, Tag, Utils, AppContainer, SearchInput, Empty } from 'knowdesign';
|
||||
import Api from '@src/api';
|
||||
import { hashDataParse } from '../../constants/common';
|
||||
import DataLogs from './DataLogs';
|
||||
import Configuration from './Configuration';
|
||||
import BrokerDetailHealthCheck from '@src/components/CardBar/BrokerDetailHealthCheck';
|
||||
|
||||
import './index.less';
|
||||
import { ControlStatusMap } from '../CommonRoute';
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
const OperationsSlot: any = {
|
||||
// eslint-disable-next-line react/display-name
|
||||
['Configuration']: (arg: any) => {
|
||||
return (
|
||||
<SearchInput
|
||||
onSearch={arg.setSearchKeywords}
|
||||
attrs={{
|
||||
value: arg.searchValue,
|
||||
onChange: arg.setSearchValue,
|
||||
placeholder: '请输入配置名',
|
||||
size: 'small',
|
||||
style: { width: '210px', marginRight: '2px' },
|
||||
maxLength: 128,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
// eslint-disable-next-line react/display-name
|
||||
['DataLogs']: (arg: any) => {
|
||||
return (
|
||||
<SearchInput
|
||||
onSearch={arg.setSearchKeywords}
|
||||
attrs={{
|
||||
value: arg.searchValue,
|
||||
onChange: arg.setSearchValue,
|
||||
placeholder: '请输入TopicName',
|
||||
size: 'small',
|
||||
style: { width: '210px', marginRight: '2px' },
|
||||
maxLength: 128,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const BrokerDetail = (props: any) => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const urlParams = useParams<{ clusterId: string; brokerId: string }>();
|
||||
const urlLocation = useLocation<any>();
|
||||
const history = useHistory();
|
||||
const [positionType, setPositionType] = useState<string>('Configuration');
|
||||
const [searchKeywords, setSearchKeywords] = useState<string>('');
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [hashData, setHashData] = useState<any>({});
|
||||
const callback = (key: any) => {
|
||||
setSearchValue('');
|
||||
setSearchKeywords('');
|
||||
setPositionType(key);
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
setVisible(false);
|
||||
setSearchValue('');
|
||||
setSearchKeywords('');
|
||||
setPositionType('Configuration');
|
||||
// clean hash
|
||||
history.push(urlLocation.pathname);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
global?.clusterInfo?.id && hashDataParse(urlLocation.hash).brokerId
|
||||
? Utils.request(Api.getBrokerMetadata(hashDataParse(urlLocation.hash).brokerId, global?.clusterInfo?.id), {
|
||||
init: {
|
||||
errorNoTips: true,
|
||||
},
|
||||
})
|
||||
.then((brokerData: any) => {
|
||||
if (brokerData?.exist && brokerData?.alive && hashDataParse(urlLocation.hash).host === brokerData.host) {
|
||||
setHashData(brokerData);
|
||||
setVisible(true);
|
||||
} else {
|
||||
history.replace(urlLocation.pathname);
|
||||
setVisible(false);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
history.replace(urlLocation.pathname);
|
||||
setVisible(false);
|
||||
})
|
||||
: setVisible(false);
|
||||
}, [hashDataParse(urlLocation.hash).brokerId, global?.clusterInfo, urlLocation]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
push={false}
|
||||
title={
|
||||
<span>
|
||||
<span style={{ fontSize: '18px', fontFamily: 'PingFangSC-Semibold', color: '#495057' }}>Broker {hashData?.brokerId}</span>
|
||||
{hashData?.host && (
|
||||
<Tag
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
color: '#495057',
|
||||
textAlign: 'center',
|
||||
background: '#ECECF6',
|
||||
borderRadius: '4px',
|
||||
marginLeft: '10px',
|
||||
padding: '1px 10px',
|
||||
}}
|
||||
>
|
||||
{hashData?.host}
|
||||
</Tag>
|
||||
)}
|
||||
{global?.clusterInfo?.name && (
|
||||
<Tag
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
color: '#495057',
|
||||
textAlign: 'center',
|
||||
background: '#ECECF6',
|
||||
borderRadius: '4px',
|
||||
marginLeft: '10px',
|
||||
padding: '1px 10px',
|
||||
}}
|
||||
>
|
||||
{global?.clusterInfo?.name}
|
||||
</Tag>
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
width={1080}
|
||||
placement="right"
|
||||
onClose={onClose}
|
||||
visible={visible}
|
||||
className="broker-detail-drawer"
|
||||
destroyOnClose
|
||||
maskClosable={false}
|
||||
>
|
||||
<BrokerDetailHealthCheck record={{ brokerId: hashData?.brokerId }} />
|
||||
{hashData && positionType && (
|
||||
<Tabs
|
||||
className={'custom_tabs_class'}
|
||||
defaultActiveKey="Configuration"
|
||||
// activeKey={positionType}
|
||||
onChange={callback}
|
||||
tabBarExtraContent={
|
||||
OperationsSlot[positionType] &&
|
||||
global.isShowControl &&
|
||||
global.isShowControl(
|
||||
positionType === 'Configuration' ? ControlStatusMap.BROKER_DETAIL_CONFIG : ControlStatusMap.BROKER_DETAIL_DATALOGS
|
||||
) &&
|
||||
OperationsSlot[positionType]({ setSearchKeywords, setSearchValue, searchValue, positionType })
|
||||
}
|
||||
destroyInactiveTabPane
|
||||
>
|
||||
<TabPane tab="Configuration" key="Configuration">
|
||||
{global.isShowControl && global.isShowControl(ControlStatusMap.BROKER_DETAIL_CONFIG) ? (
|
||||
<Configuration searchKeywords={searchKeywords} positionType={positionType} hashData={hashData} />
|
||||
) : (
|
||||
<Empty description="当前版本过低,不支持该功能!" />
|
||||
)}
|
||||
</TabPane>
|
||||
<TabPane tab="DataLogs" key="DataLogs">
|
||||
{global.isShowControl && global.isShowControl(ControlStatusMap.BROKER_DETAIL_DATALOGS) ? (
|
||||
<DataLogs searchKeywords={searchKeywords} positionType={positionType} hashData={hashData} />
|
||||
) : (
|
||||
<Empty description="当前版本过低,不支持该功能!" />
|
||||
)}
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrokerDetail;
|
||||
@@ -0,0 +1,158 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import React from 'react';
|
||||
import { timeFormat, getSizeAndUnit } from '../../constants/common';
|
||||
import moment from 'moment';
|
||||
import { Tag, Tooltip } from 'knowdesign';
|
||||
|
||||
export const getBrokerListColumns = (arg?: any) => {
|
||||
const columns = [
|
||||
{
|
||||
title: 'Broker ID',
|
||||
dataIndex: 'brokerId',
|
||||
key: 'brokerId',
|
||||
sorter: true,
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (t: number, r: any) => {
|
||||
return r?.alive ? (
|
||||
<a
|
||||
onClick={() => {
|
||||
window.location.hash = `brokerId=${t || t === 0 ? t : ''}&host=${r.host || ''}`;
|
||||
}}
|
||||
>
|
||||
{t}
|
||||
</a>
|
||||
) : (
|
||||
<span>{t}</span>
|
||||
);
|
||||
},
|
||||
fixed: 'left',
|
||||
width: 120,
|
||||
},
|
||||
// {
|
||||
// title: 'Rack',
|
||||
// dataIndex: 'rack',
|
||||
// key: 'rack',
|
||||
// },
|
||||
{
|
||||
title: 'Broker Host',
|
||||
dataIndex: 'host',
|
||||
key: 'host',
|
||||
width: 180,
|
||||
render: (t: any, r: any) => {
|
||||
return (
|
||||
<span>
|
||||
{t}
|
||||
{r?.alive ? <Tag className="tag-success">Live</Tag> : <Tag className="tag-error">Down</Tag>}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: ' JMX Port',
|
||||
dataIndex: 'jmxPort',
|
||||
key: 'jmxPort',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: 'Partitions',
|
||||
dataIndex: 'Partitions',
|
||||
key: 'Partitions',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: 'Partitions Skew',
|
||||
dataIndex: 'PartitionsSkew',
|
||||
key: 'PartitionsSkew',
|
||||
width: 140,
|
||||
render: (t: number) => {
|
||||
return t || t === 0 ? (t * 100).toFixed(2) + '%' : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Leaders Partition',
|
||||
dataIndex: 'Leaders',
|
||||
key: 'Leaders',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: 'Leaders Skew',
|
||||
dataIndex: 'LeadersSkew',
|
||||
key: 'LeadersSkew',
|
||||
width: 120,
|
||||
render: (t: number) => {
|
||||
return t || t === 0 ? (t * 100).toFixed(2) + '%' : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Message Size',
|
||||
dataIndex: 'LogSize',
|
||||
key: 'LogSize',
|
||||
sorter: true,
|
||||
width: 150,
|
||||
render: (t: number) => {
|
||||
return getSizeAndUnit(t, 'B').valueWithUnit;
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: 'Status',
|
||||
// dataIndex: 'alive',
|
||||
// key: 'alive',
|
||||
// width: 100,
|
||||
// // eslint-disable-next-line react/display-name
|
||||
// render: (t: boolean) => {
|
||||
// return <span className={t ? 'success' : 'error'}>{t ? 'Live' : 'Down'}</span>;
|
||||
// },
|
||||
// },
|
||||
{
|
||||
title: 'Roles',
|
||||
dataIndex: 'kafkaRoleList',
|
||||
key: 'kafkaRoleList',
|
||||
width: 100,
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (t: any[]) => {
|
||||
if (t) {
|
||||
const newContant = t
|
||||
?.filter((item: string) => item.slice(0, 3) == '__c' || item.slice(0, 3) == '__t')
|
||||
?.map((subItem: any) => {
|
||||
if (subItem.slice(0, 3) == '__c') {
|
||||
return 'GC';
|
||||
} else if (subItem.slice(0, 3) == '__t') {
|
||||
return 'TC';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const newTooltip = newContant.map((item: string) => {
|
||||
if (item === 'GC') {
|
||||
return 'GroupCoordinator';
|
||||
} else if (item === 'TC') {
|
||||
return 'TransactionCoordinator';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
return (
|
||||
<Tooltip title={newTooltip.join()}>
|
||||
<span>{newContant.length > 0 ? newContant.join('、') : '-'}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Start Time',
|
||||
dataIndex: 'startTimeUnitMs',
|
||||
key: 'startTimeUnitMs',
|
||||
width: 200,
|
||||
render: (t: number) => (t ? moment(t).format(timeFormat) : '-'),
|
||||
},
|
||||
];
|
||||
return columns;
|
||||
};
|
||||
|
||||
export const defaultPagination = {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
position: 'bottomRight',
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
.brokerList{
|
||||
.d-table-box-header{
|
||||
padding: 0 0 12px 0;
|
||||
}
|
||||
.tag-success,.tag-error{
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.tag-success{
|
||||
background: #F1FAF2;
|
||||
color: #73d380;
|
||||
}
|
||||
.tag-error{
|
||||
background: #FFF0EF;
|
||||
color: #ff7066;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import React, { useState, useEffect, memo } from 'react';
|
||||
import { useParams, useHistory, useLocation } from 'react-router-dom';
|
||||
import { ProTable, Drawer, Utils, AppContainer } from 'knowdesign';
|
||||
import API from '../../api';
|
||||
import { getBrokerListColumns, defaultPagination } from './config';
|
||||
import { dealTableRequestParams } from '../../constants/common';
|
||||
import BrokerDetail from '../BrokerDetail';
|
||||
import CardBar from '@src/components/CardBar';
|
||||
import BrokerHealthCheck from '@src/components/CardBar/BrokerHealthCheck';
|
||||
import DBreadcrumb from 'knowdesign/lib/extend/d-breadcrumb';
|
||||
import './index.less';
|
||||
const { request } = Utils;
|
||||
|
||||
const BrokerList: React.FC = (props: any) => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
|
||||
const urlParams = useParams<any>(); // 获取地址栏参数
|
||||
const history = useHistory();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
const [searchKeywords, setSearchKeywords] = useState('');
|
||||
// const [filteredInfo, setFilteredInfo] = useState(null);
|
||||
const [pagination, setPagination] = useState<any>(defaultPagination);
|
||||
const [clusterName, setClusterName] = useState<any>(null);
|
||||
// 默认排序
|
||||
const defaultSorter = {
|
||||
sortField: 'brokerId',
|
||||
sortType: 'asc',
|
||||
};
|
||||
|
||||
// 请求接口获取数据
|
||||
const genData = async ({ pageNo, pageSize, filters, sorter }: any) => {
|
||||
if (urlParams?.clusterId === undefined) return;
|
||||
// filters = filters || filteredInfo;
|
||||
setLoading(true);
|
||||
// const params = dealTableRequestParams({ searchKeywords, pageNo, pageSize });
|
||||
const params = {
|
||||
searchKeywords: searchKeywords.slice(0, 128),
|
||||
pageNo,
|
||||
pageSize,
|
||||
latestMetricNames: ['PartitionsSkew', 'Leaders', 'LeadersSkew', 'LogSize'],
|
||||
sortField: sorter?.field || 'brokerId',
|
||||
sortType: sorter?.order ? sorter.order.substring(0, sorter.order.indexOf('end')) : 'asc',
|
||||
};
|
||||
|
||||
// API.getBrokersList(urlParams?.clusterId)
|
||||
request(API.getBrokersList(urlParams?.clusterId), { method: 'POST', data: params })
|
||||
.then((res: any) => {
|
||||
setPagination({
|
||||
current: res.pagination?.pageNo,
|
||||
pageSize: res.pagination?.pageSize,
|
||||
total: res.pagination?.total,
|
||||
});
|
||||
const newData =
|
||||
res?.bizData.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
...item?.latestMetrics?.metrics,
|
||||
};
|
||||
}) || [];
|
||||
setData(newData);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
// setFilteredInfo(filters);
|
||||
genData({ pageNo: pagination.current, pageSize: pagination.pageSize, filters, sorter });
|
||||
};
|
||||
|
||||
const getSearchKeywords = (value: string) => {
|
||||
setSearchKeywords(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
genData({
|
||||
pageNo: 1,
|
||||
pageSize: pagination.pageSize,
|
||||
// sorter: defaultSorter
|
||||
});
|
||||
}, [searchKeywords]);
|
||||
|
||||
return (
|
||||
<div key="brokerList" className="brokerList">
|
||||
<div className="breadcrumb" style={{ marginBottom: '10px' }}>
|
||||
<DBreadcrumb
|
||||
breadcrumbs={[
|
||||
{ label: '多集群管理', aHref: '/' },
|
||||
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
|
||||
{ label: 'Broker', aHref: `/cluster/${urlParams?.clusterId}/broker` },
|
||||
{ label: 'Brokers', aHref: `` },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ margin: '12px 0' }}>
|
||||
<BrokerHealthCheck />
|
||||
</div>
|
||||
<div className="clustom-table-content">
|
||||
<ProTable
|
||||
key="brokerTable"
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
showHeader: true,
|
||||
rowKey: 'broker_list',
|
||||
loading: loading,
|
||||
columns: getBrokerListColumns(),
|
||||
dataSource: data,
|
||||
paginationProps: { ...pagination },
|
||||
tableHeaderSearchInput: {
|
||||
// 搜索配置
|
||||
submit: getSearchKeywords,
|
||||
searchInputType: 'search',
|
||||
searchAttr: {
|
||||
placeholder: '请输入Broker Host',
|
||||
maxLength: 128,
|
||||
style: {
|
||||
width: '248px',
|
||||
borderRiadus: '8px',
|
||||
},
|
||||
},
|
||||
},
|
||||
attrs: {
|
||||
onChange: onTableChange,
|
||||
scroll: { x: 'max-content', y: 'calc(100vh - 400px)' },
|
||||
bordered: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{<BrokerDetail />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrokerList;
|
||||
@@ -0,0 +1,119 @@
|
||||
import React, { useLayoutEffect } from 'react';
|
||||
import { Utils, AppContainer } from 'knowdesign';
|
||||
import api, { MetricType } from '../api';
|
||||
import { goLogin } from '@src/constants/axiosConfig';
|
||||
|
||||
// 权限对应表
|
||||
export enum ClustersPermissionMap {
|
||||
CLUSTERS_MANAGE = '多集群管理',
|
||||
// Cluster
|
||||
CLUSTER_ADD = '接入集群',
|
||||
CLUSTER_DEL = '删除集群',
|
||||
CLUSTER_CHANGE_HEALTHY = 'Cluster-修改健康规则',
|
||||
CLUSTER_CHANGE_INFO = 'Cluster-修改集群信息',
|
||||
// Broker
|
||||
BROKER_CHANGE_CONFIG = 'Broker-修改Broker配置',
|
||||
// Topic
|
||||
TOPIC_CHANGE_CONFIG = 'Topic-修改Topic配置',
|
||||
TOPIC_RESET_OFFSET = 'Topic-重置Offset',
|
||||
TOPIC_DEL = 'Topic-删除Topic',
|
||||
TOPIC_EXPOND = 'Topic-扩分区',
|
||||
TOPIC_ADD = 'Topic-新增Topic',
|
||||
// Consumers
|
||||
CONSUMERS_RESET_OFFSET = 'Consumers-重置Offset',
|
||||
// Test
|
||||
TEST_CONSUMER = 'Test-Consumer',
|
||||
TEST_PRODUCER = 'Test-Producer',
|
||||
}
|
||||
|
||||
export interface PermissionNode {
|
||||
id: number;
|
||||
permissionName: ClustersPermissionMap | null;
|
||||
parentId: number | null;
|
||||
has: boolean;
|
||||
leaf: boolean;
|
||||
childList: PermissionNode[];
|
||||
}
|
||||
|
||||
export interface MetricsDefine {
|
||||
[metricName: string]: {
|
||||
category: string;
|
||||
type: number;
|
||||
name: string;
|
||||
desc: string;
|
||||
unit: string;
|
||||
support: boolean | null;
|
||||
minVersion: number;
|
||||
maxVersion: number;
|
||||
};
|
||||
}
|
||||
|
||||
const CommonConfig = () => {
|
||||
const [global, setGlobal] = AppContainer.useGlobalValue();
|
||||
const userInfo = localStorage.getItem('userInfo');
|
||||
|
||||
// 获取权限树
|
||||
const getPermissions = (userId: number) => {
|
||||
const getUserInfo = Utils.request(api.getUserInfo(userId), {
|
||||
retryTimes: 2,
|
||||
});
|
||||
const getPermissionTree = Utils.request(api.getPermissionTree, {
|
||||
retryTimes: 2,
|
||||
});
|
||||
|
||||
Promise.all([getPermissionTree, getUserInfo]).then(([permissionTree, userDetail]: [PermissionNode, any]) => {
|
||||
const allPermissions = permissionTree.childList;
|
||||
|
||||
// 获取用户在多集群管理拥有的权限
|
||||
const userPermissionTree = userDetail.permissionTreeVO.childList;
|
||||
const clustersPermissions = userPermissionTree.find(
|
||||
(sys: PermissionNode) => sys.permissionName === ClustersPermissionMap.CLUSTERS_MANAGE
|
||||
);
|
||||
const userPermissions: ClustersPermissionMap[] = [];
|
||||
clustersPermissions &&
|
||||
clustersPermissions.childList.forEach((node: PermissionNode) => node.has && userPermissions.push(node.permissionName));
|
||||
|
||||
const hasPermission = (permissionName: ClustersPermissionMap) => permissionName && userPermissions.includes(permissionName);
|
||||
|
||||
setGlobal((curState: any) => ({ ...curState, permissions: allPermissions, userPermissions, hasPermission, userInfo }));
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// 获取指标信息
|
||||
const getMetricsDefine = () => {
|
||||
Utils.request(api.getKafkaVersionItems(), {
|
||||
retryTimes: 2,
|
||||
}).then((metricsDefine: MetricsDefine) => {
|
||||
const getMetricDefine = (type: MetricType, metricName: string) => metricsDefine[`${type}@${metricName}`] || null;
|
||||
setGlobal((curState: any) => ({ ...curState, metricsDefine, getMetricDefine }));
|
||||
});
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// 如果未登录,直接跳转到登录页
|
||||
const userInfo = localStorage.getItem('userInfo');
|
||||
let userId: number;
|
||||
|
||||
try {
|
||||
userId = JSON.parse(userInfo).id;
|
||||
if (!userId) throw 'err';
|
||||
} catch (_) {
|
||||
goLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
// 仅获取一次全局权限
|
||||
if (!global.permissions) {
|
||||
getPermissions(userId);
|
||||
}
|
||||
if (!global.metricsDefine) {
|
||||
getMetricsDefine();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default CommonConfig;
|
||||
@@ -0,0 +1,87 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { AppContainer, Utils } from 'knowdesign';
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
import API, { MetricType } from '../api';
|
||||
import CommonConfig from './CommonConfig';
|
||||
|
||||
interface ControlInfo {
|
||||
type: MetricType.Controls;
|
||||
name: ControlStatusMap;
|
||||
desc: string;
|
||||
support: boolean;
|
||||
unit: string | null;
|
||||
minVersion: number;
|
||||
maxVersion: number;
|
||||
}
|
||||
|
||||
export enum ControlStatusMap {
|
||||
// Broker
|
||||
BROKER_DETAIL_CONFIG = 'FEBrokersSpecifiedBrokerConfigList',
|
||||
BROKER_DETAIL_DATALOGS = 'FEBrokersSpecifiedBrokerDataLogsList',
|
||||
// Testing
|
||||
TESTING_PRODUCER_HEADER = 'FETestingProducerHeader',
|
||||
TESTING_PRODUCER_COMPRESSION_TYPE_ZSTD = 'FETestingProducerCompressionTypeZSTD',
|
||||
TESTING_CONSUMER_HEADER = 'FETestingConsumerHeader',
|
||||
}
|
||||
|
||||
const CommonRoute: React.FC = (props: any) => {
|
||||
const [global, setGlobal] = AppContainer.useGlobalValue();
|
||||
const { clusterId } = useParams<{ clusterId: string }>();
|
||||
const history = useHistory();
|
||||
const { Consumer } = AppContainer.context;
|
||||
|
||||
useEffect(() => {
|
||||
getPhyClusterInfo();
|
||||
getControlsStatus();
|
||||
}, []);
|
||||
|
||||
const getPhyClusterInfo = () => {
|
||||
Utils.request(API.getPhyClusterBasic(+clusterId), {
|
||||
init: {
|
||||
// needCode: true,
|
||||
errorNoTips: true,
|
||||
},
|
||||
retryTimes: 2,
|
||||
// timeout: 200,
|
||||
})
|
||||
.then((res: any) => {
|
||||
if (res) {
|
||||
setGlobal((curState: any) => ({ ...curState, clusterInfo: res }));
|
||||
} else {
|
||||
history.replace('/');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
history.replace('/');
|
||||
// notification.error({
|
||||
// message: '错误',
|
||||
// duration: 3,
|
||||
// description: `${'集群不存在或集群ID有误'}`,
|
||||
// });
|
||||
});
|
||||
};
|
||||
|
||||
// 获取控件状态
|
||||
const getControlsStatus = () => {
|
||||
Utils.request(API.getSupportKafkaVersions(clusterId, MetricType.Controls), {
|
||||
retryTimes: 2,
|
||||
init: {
|
||||
errorNoTips: true,
|
||||
},
|
||||
}).then((controlsStatus: ControlInfo[]) => {
|
||||
const isShowControl = (controlName: ControlStatusMap) => {
|
||||
const controlInfo = controlName && controlsStatus.find(({ name }) => name === controlName);
|
||||
return controlInfo ? controlInfo.support : false;
|
||||
};
|
||||
setGlobal((curState: any) => ({ ...curState, controlsStatus, isShowControl }));
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommonConfig />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommonRoute;
|
||||
@@ -0,0 +1,532 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import { AppContainer, Divider, Drawer, IconFont, ProTable, Select, SingleChart, Space, Tooltip, Utils } from 'knowdesign';
|
||||
import { DRangeTime } from 'knowdesign';
|
||||
import { getBasicChartConfig } from '@src/constants/chartConfig';
|
||||
import Api from '@src/api/index';
|
||||
import { hashDataParse } from '@src/constants/common';
|
||||
import { ClustersPermissionMap } from '../CommonConfig';
|
||||
import ResetOffsetDrawer from './ResetOffsetDrawer';
|
||||
import { CheckCircleFilled } from '@ant-design/icons';
|
||||
import SwitchTab from '@src/components/SwitchTab';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
export interface MetricLine {
|
||||
createTime?: number;
|
||||
metricPoints: Array<{
|
||||
aggType: string;
|
||||
createTime: number;
|
||||
timeStamp: number;
|
||||
unit: string;
|
||||
updateTime: number;
|
||||
value: number;
|
||||
}>;
|
||||
name: string;
|
||||
updateTime?: number;
|
||||
}
|
||||
export interface MetricData {
|
||||
metricLines?: Array<MetricLine>;
|
||||
metricLine?: MetricLine;
|
||||
metricName: string;
|
||||
}
|
||||
|
||||
export interface HashData {
|
||||
groupName: string;
|
||||
topicName: string;
|
||||
}
|
||||
|
||||
const metricConsts = ['LogEndOffset', 'OffsetConsumed', 'Lag'];
|
||||
const metricWithType = [
|
||||
{ metricName: 'LogEndOffset', metricType: 104 },
|
||||
{ metricName: 'OffsetConsumed', metricType: 102 },
|
||||
{ metricName: 'Lag', metricType: 102 },
|
||||
];
|
||||
|
||||
const ContentWithCopy = (props: { content: string }) => {
|
||||
const { content } = props;
|
||||
const [visible, setVisible] = useState(false);
|
||||
return (
|
||||
<CopyToClipboard text={content}>
|
||||
<div className="content-with-copy">
|
||||
<Tooltip title={content}>
|
||||
<span className="content">{content}</span>
|
||||
</Tooltip>
|
||||
{content && (
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
<CheckCircleFilled style={{ color: '#00b365' }} /> 复制成功
|
||||
</span>
|
||||
}
|
||||
visible={visible}
|
||||
onVisibleChange={() => setVisible(false)}
|
||||
>
|
||||
<IconFont className="copy-icon" type="icon-fuzhi" onClick={() => setVisible(true)} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</CopyToClipboard>
|
||||
);
|
||||
};
|
||||
|
||||
export default (props: any) => {
|
||||
const { scene } = props;
|
||||
const params = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const history = useHistory();
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
// const { record } = props;
|
||||
const now = Date.now();
|
||||
const [allGroupMetricsData, setAllGroupMetricsData] = useState<Array<MetricData>>([]);
|
||||
const [groupMetricsData, setGroupMetricsData] = useState<Array<MetricData>>([]);
|
||||
const [timeRange, setTimeRange] = useState([now - 24 * 60 * 60 * 1000, now]);
|
||||
const [consumerList, setConsumerList] = useState([]);
|
||||
const [partitionList, setPartitionList] = useState([]);
|
||||
const [curPartition, setCurPartition] = useState<string>('');
|
||||
const [showMode, setShowMode] = useState('table');
|
||||
const [pageIndex, setPageIndex] = useState(1);
|
||||
const [pageTotal, setPageTotal] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [consumerListLoading, setConsumerListLoading] = useState(false);
|
||||
const [consumerChartLoading, setConsumerChartLoading] = useState(false);
|
||||
const [hashData, setHashData] = useState<HashData>({ groupName: '', topicName: '' });
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [sortObj, setSortObj] = useState<{
|
||||
sortField: string;
|
||||
sortType: 'desc' | 'asc' | '';
|
||||
}>({ sortField: '', sortType: '' });
|
||||
const clusterId = Number(params.clusterId);
|
||||
const columns = [
|
||||
{
|
||||
title: 'Topic Partition',
|
||||
dataIndex: 'partitionId',
|
||||
key: 'partitionId',
|
||||
render: (v: string, record: any) => {
|
||||
return `${record.topicName}-${v}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Member ID',
|
||||
dataIndex: 'memberId',
|
||||
key: 'memberId',
|
||||
width: 200,
|
||||
render: (v: string) => {
|
||||
return <ContentWithCopy content={v} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Current Offset',
|
||||
dataIndex: 'OffsetConsumed',
|
||||
key: 'OffsetConsumed',
|
||||
render: (v: any, record: any) => {
|
||||
return record?.latestMetrics?.metrics?.OffsetConsumed;
|
||||
},
|
||||
sorter: true,
|
||||
// sorter: {
|
||||
// compare: (a: any, b: any) => {
|
||||
// let value1 = a?.metrics?.find((item: any) => item.metricName === 'OffsetConsumed' && item.metricType === 102)?.metricValue
|
||||
// let value2 = b?.metrics?.find((item: any) => item.metricName === 'OffsetConsumed' && item.metricType === 102)?.metricValue
|
||||
// return value1 - value2
|
||||
// },
|
||||
// multiple: 1
|
||||
// }
|
||||
},
|
||||
{
|
||||
title: 'Log End Offset',
|
||||
dataIndex: 'LogEndOffset',
|
||||
key: 'LogEndOffset',
|
||||
render: (v: any, record: any) => {
|
||||
return record?.latestMetrics?.metrics?.LogEndOffset;
|
||||
},
|
||||
sorter: true,
|
||||
// sorter: {
|
||||
// compare: (a: any, b: any) => {
|
||||
// let value1 = a?.metrics?.find((item: any) => item.metricName === 'LogEndOffset' && item.metricType === 104)?.metricValue
|
||||
// let value2 = b?.metrics?.find((item: any) => item.metricName === 'LogEndOffset' && item.metricType === 104)?.metricValue
|
||||
// return value1 - value2
|
||||
// },
|
||||
// multiple: 2
|
||||
// }
|
||||
},
|
||||
{
|
||||
title: 'Lag',
|
||||
dataIndex: 'Lag',
|
||||
key: 'Lag',
|
||||
render: (v: any, record: any) => {
|
||||
return record?.latestMetrics?.metrics?.Lag;
|
||||
},
|
||||
sorter: true,
|
||||
// sorter: {
|
||||
// compare: (a: any, b: any) => {
|
||||
// let value1 = a?.metrics?.find((item: any) => item.metricName === 'Lag' && item.metricType === 102)?.metricValue
|
||||
// let value2 = b?.metrics?.find((item: any) => item.metricName === 'Lag' && item.metricType === 102)?.metricValue
|
||||
// return value1 - value2
|
||||
// },
|
||||
// multiple: 3
|
||||
// }
|
||||
},
|
||||
{
|
||||
title: 'Host',
|
||||
dataIndex: 'host',
|
||||
key: 'host',
|
||||
},
|
||||
{
|
||||
title: 'Client ID',
|
||||
dataIndex: 'clientId',
|
||||
key: 'clientId',
|
||||
needTooltip: true,
|
||||
lineClampOne: true,
|
||||
width: 200,
|
||||
},
|
||||
];
|
||||
const getTopicGroupMetric = (hashData: HashData) => {
|
||||
setConsumerListLoading(true);
|
||||
const params: any = {
|
||||
// metricRealTimes: metricWithType,
|
||||
latestMetricNames: metricConsts,
|
||||
pageNo: pageIndex,
|
||||
pageSize,
|
||||
};
|
||||
if (sortObj.sortField && sortObj.sortType) {
|
||||
params.sortField = sortObj.sortField;
|
||||
params.sortType = sortObj.sortType;
|
||||
}
|
||||
return Utils.post(
|
||||
Api.getTopicGroupMetric({
|
||||
clusterId,
|
||||
groupName: hashData.groupName,
|
||||
topicName: hashData.topicName,
|
||||
}),
|
||||
params
|
||||
).then((data: any) => {
|
||||
if (!data) return;
|
||||
setConsumerListLoading(false);
|
||||
setConsumerList(data?.bizData);
|
||||
setPageIndex(data?.pagination?.pageNo);
|
||||
setPageSize(data?.pagination?.pageSize);
|
||||
setPageTotal(data?.pagination?.total);
|
||||
});
|
||||
};
|
||||
const getTopicGroupPartitionsHistory = (hashData: HashData) => {
|
||||
return Utils.request(Api.getTopicGroupPartitionsHistory(clusterId, hashData.groupName), {
|
||||
params: {
|
||||
startTime: timeRange[0],
|
||||
endTime: timeRange[1],
|
||||
},
|
||||
});
|
||||
};
|
||||
const getTopicGroupMetricHistory = (partitions: Array<any>, hashData: HashData) => {
|
||||
const params = {
|
||||
aggType: 'sum',
|
||||
groupTopics: partitions.map((p) => ({
|
||||
partition: p.partition,
|
||||
topic: hashData.topicName,
|
||||
})),
|
||||
group: hashData.groupName,
|
||||
metricsNames: metricWithType.map((item) => item.metricName),
|
||||
startTime: timeRange[0],
|
||||
endTime: timeRange[1],
|
||||
topNu: 0,
|
||||
};
|
||||
Utils.post(Api.getTopicGroupMetricHistory(clusterId), params).then((data: Array<MetricData>) => {
|
||||
setAllGroupMetricsData(data);
|
||||
});
|
||||
};
|
||||
const getConsumersMetadata = (hashData: HashData) => {
|
||||
return Utils.request(Api.getConsumersMetadata(clusterId, hashData.groupName, hashData.topicName));
|
||||
};
|
||||
const onClose = () => {
|
||||
setVisible(false);
|
||||
setSortObj({
|
||||
sortField: '',
|
||||
sortType: '',
|
||||
});
|
||||
// clean hash'
|
||||
scene === 'topicDetail' && history.goBack();
|
||||
scene !== 'topicDetail' && window.history.pushState('', '', location.pathname);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (curPartition === '' || allGroupMetricsData.length === 0) return;
|
||||
const filteredData = allGroupMetricsData.map((item) => {
|
||||
const allData = item.metricLines.reduce(
|
||||
(acc, cur) => {
|
||||
if (acc.metricLine.metricPoints.length === 0) {
|
||||
acc.metricLine.metricPoints = cur.metricPoints.map((p) => ({
|
||||
timeStamp: p.timeStamp,
|
||||
value: Number(p.value),
|
||||
}));
|
||||
} else {
|
||||
acc.metricLine.metricPoints.forEach((mp) => {
|
||||
const curMetricPoint = cur.metricPoints.find((curmp) => curmp.timeStamp === mp.timeStamp);
|
||||
mp.value += curMetricPoint ? Number(curMetricPoint.value) : 0;
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
metricName: item.metricName,
|
||||
metricLine: {
|
||||
name: 'all',
|
||||
metricPoints: [],
|
||||
},
|
||||
}
|
||||
);
|
||||
return curPartition === '__all__'
|
||||
? allData
|
||||
: {
|
||||
metricName: item.metricName,
|
||||
// metricLine: curPartition === '__all__' ? {} : item.metricLines.find(line => line.name.indexOf(curPartition) >= 0)
|
||||
metricLine: item.metricLines.find((line) => line.name.indexOf(curPartition) >= 0),
|
||||
};
|
||||
});
|
||||
setGroupMetricsData(filteredData);
|
||||
}, [curPartition, allGroupMetricsData]);
|
||||
useEffect(() => {
|
||||
visible && getTopicGroupMetric(hashData);
|
||||
}, [sortObj]);
|
||||
useEffect(() => {
|
||||
const hashData = hashDataParse(location.hash);
|
||||
if (!hashData.groupName || !hashData.topicName) return;
|
||||
setHashData(hashData);
|
||||
// 获取分区列表 为图表模式做准备
|
||||
getConsumersMetadata(hashData).then((res: any) => {
|
||||
if (!res.exist) {
|
||||
setVisible(false);
|
||||
history.push(`/cluster/${params?.clusterId}/consumers`);
|
||||
return;
|
||||
}
|
||||
setVisible(true);
|
||||
getTopicGroupPartitionsHistory(hashData)
|
||||
.then((data: any) => {
|
||||
if (data.length > 0) {
|
||||
setCurPartition(data[0].partition);
|
||||
}
|
||||
setPartitionList(data);
|
||||
return data;
|
||||
})
|
||||
.then((data) => {
|
||||
getTopicGroupMetricHistory(data, hashData);
|
||||
})
|
||||
.catch((e) => {
|
||||
history.push(`/cluster/${params?.clusterId}/consumers`);
|
||||
setVisible(false);
|
||||
});
|
||||
// 获取Consumer列表 表格模式
|
||||
getTopicGroupMetric(hashData);
|
||||
});
|
||||
}, [hashDataParse(location.hash).groupName]);
|
||||
useEffect(() => {
|
||||
if (partitionList.length === 0) return;
|
||||
getTopicGroupMetricHistory(partitionList, hashData);
|
||||
}, [timeRange]);
|
||||
return (
|
||||
<Drawer
|
||||
push={false}
|
||||
title="Consumer Group详情"
|
||||
width={1080}
|
||||
placement="right"
|
||||
onClose={onClose}
|
||||
visible={visible}
|
||||
className="consumer-group-detail-drawer"
|
||||
maskClosable={false}
|
||||
destroyOnClose
|
||||
extra={
|
||||
<Space>
|
||||
{global.hasPermission &&
|
||||
global.hasPermission(
|
||||
scene === 'topicDetail' ? ClustersPermissionMap.TOPIC_RESET_OFFSET : ClustersPermissionMap.CONSUMERS_RESET_OFFSET
|
||||
) && <ResetOffsetDrawer record={hashData}></ResetOffsetDrawer>}
|
||||
<Divider type="vertical" />
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div className="consumer-group-detail">
|
||||
<div className="title-and-mode">
|
||||
<div className="title-and-mode-header">Consumer列表</div>
|
||||
<div className="right">
|
||||
{showMode === 'chart' && (
|
||||
<Select
|
||||
style={{ width: 140, marginRight: 8 }}
|
||||
size="small"
|
||||
value={curPartition}
|
||||
onChange={(id) => {
|
||||
setCurPartition(id);
|
||||
}}
|
||||
>
|
||||
<Option value={'__all__'}>全部Partition</Option>
|
||||
{partitionList.map((partition) => (
|
||||
<Option key={partition.partition} value={partition.partition}>
|
||||
{partition.partition}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
{showMode === 'chart' && (
|
||||
<DRangeTime
|
||||
rangeTimeArr={timeRange}
|
||||
timeChange={(o: any) => {
|
||||
setTimeRange(o);
|
||||
}}
|
||||
></DRangeTime>
|
||||
)}
|
||||
{showMode === 'chart' && <div className="divider"></div>}
|
||||
<SwitchTab defaultKey={showMode} onChange={(key) => setShowMode(key)}>
|
||||
<SwitchTab.TabItem key="chart">
|
||||
<div style={{ width: 34, height: 23 }}>
|
||||
<IconFont type="icon-tubiao"></IconFont>
|
||||
</div>
|
||||
</SwitchTab.TabItem>
|
||||
<SwitchTab.TabItem key="table">
|
||||
<div style={{ width: 34, height: 23 }}>
|
||||
<IconFont type="icon-biaoge"></IconFont>
|
||||
</div>
|
||||
</SwitchTab.TabItem>
|
||||
</SwitchTab>
|
||||
</div>
|
||||
</div>
|
||||
{showMode === 'table' && (
|
||||
// <Table
|
||||
// rowKey={'partitionId'}
|
||||
// columns={columns}
|
||||
// className="table"
|
||||
// loading={consumerListLoading}
|
||||
// dataSource={consumerList}
|
||||
// pagination={{
|
||||
// current: pageIndex,
|
||||
// pageSize: pageSize,
|
||||
// total: pageTotal,
|
||||
// simple: true
|
||||
// }}
|
||||
// onChange={(pagination: any, filters: any, sorter: any) => {
|
||||
// setSortObj({
|
||||
// sortField: sorter.field || '',
|
||||
// sortType: sorter.order ? sorter.order.substring(0, sorter.order.indexOf('end')) : '',
|
||||
// });
|
||||
// setPageIndex(pagination.current);
|
||||
// }}
|
||||
// ></Table>
|
||||
<ProTable
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
loading: consumerListLoading,
|
||||
showHeader: false,
|
||||
rowKey: 'partitionId',
|
||||
columns: columns,
|
||||
dataSource: consumerList,
|
||||
paginationProps:
|
||||
pageTotal > 0
|
||||
? {
|
||||
current: pageIndex,
|
||||
total: pageTotal,
|
||||
pageSize: pageSize,
|
||||
}
|
||||
: null,
|
||||
attrs: {
|
||||
sortDirections: ['descend', 'ascend', 'default'],
|
||||
scroll: { x: 1032 },
|
||||
// className: 'frameless-table', // 纯无边框表格类名
|
||||
onChange: (pagination: any, filters: any, sorter: any) => {
|
||||
setSortObj({
|
||||
sortField: sorter.field || '',
|
||||
sortType: sorter.order ? sorter.order.substring(0, sorter.order.indexOf('end')) : '',
|
||||
});
|
||||
setPageIndex(pagination.current);
|
||||
},
|
||||
bordered: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showMode === 'chart' && (
|
||||
<div className="single-chart">
|
||||
<SingleChart
|
||||
showHeader={false}
|
||||
wrapStyle={{
|
||||
width: '100%',
|
||||
height: 242,
|
||||
}}
|
||||
option={getBasicChartConfig({
|
||||
xAxis: {
|
||||
type: 'time',
|
||||
},
|
||||
title: {
|
||||
show: false,
|
||||
},
|
||||
legend: {
|
||||
left: 'center',
|
||||
},
|
||||
grid: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 10,
|
||||
},
|
||||
tooltip: {
|
||||
customWidth: 200,
|
||||
},
|
||||
})}
|
||||
// option={{
|
||||
// title: {
|
||||
// show: false,
|
||||
// },
|
||||
// xAxis: {
|
||||
// show: true,
|
||||
// type: 'category',
|
||||
// },
|
||||
// yAxis: {
|
||||
// type: 'value',
|
||||
// show: true,
|
||||
// },
|
||||
// legend: {
|
||||
// show: true,
|
||||
// right: 'auto',
|
||||
// top: 220,
|
||||
// },
|
||||
// toolBox: {
|
||||
// show: false,
|
||||
// },
|
||||
// grid: {
|
||||
// left: 0,
|
||||
// right: 0,
|
||||
// top: 10,
|
||||
// },
|
||||
// }}
|
||||
chartTypeProp="line"
|
||||
propChartData={groupMetricsData}
|
||||
// xAxisCallback={(data: []) => {
|
||||
// // @ts-ignore
|
||||
// return data.length > 0 ? data[0].metricLine.metricPoints.map((item: any) => item.timeStamp) : '';
|
||||
// }}
|
||||
seriesCallback={(data: any) => {
|
||||
// return data.map((metricData: any) => {
|
||||
// const partitionMetricData = metricData.metricLine?.metricPoints || []
|
||||
// return {
|
||||
// name: metricData.metricName,
|
||||
// data: partitionMetricData.map((pmd: any) => pmd.value)
|
||||
// }
|
||||
// })
|
||||
return data.map((metricData: any) => {
|
||||
const partitionMetricData = metricData.metricLine?.metricPoints || [];
|
||||
return {
|
||||
name: metricData.metricName,
|
||||
data: partitionMetricData.map((item: any) => [item.timeStamp, item.value, item.unit]),
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
},
|
||||
symbol: 'emptyCircle',
|
||||
symbolSize: 4,
|
||||
emphasis: {
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,179 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, DatePicker, Drawer, Form, notification, Radio, Utils, Space, Divider } from 'knowdesign';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import EditTable from '../TestingProduce/component/EditTable';
|
||||
import Api from '@src/api/index';
|
||||
import moment from 'moment';
|
||||
|
||||
const CustomSelectResetTime = (props: { value?: string; onChange?: (val: Number | String) => void }) => {
|
||||
const { value, onChange } = props;
|
||||
const [timeSetMode, setTimeSetMode] = useState('newest');
|
||||
useEffect(() => {
|
||||
onChange('newest');
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<Radio.Group
|
||||
style={{
|
||||
marginBottom: 20,
|
||||
}}
|
||||
onChange={(e) => {
|
||||
setTimeSetMode(e.target.value);
|
||||
if (e.target.value === 'newest') {
|
||||
onChange('newest');
|
||||
}
|
||||
}}
|
||||
value={timeSetMode}
|
||||
>
|
||||
<Radio value={'newest'}>最新Offset</Radio>
|
||||
<Radio value={'custom'}>自定义</Radio>
|
||||
</Radio.Group>
|
||||
{timeSetMode === 'custom' && (
|
||||
<DatePicker
|
||||
value={moment(value === 'newest' ? Date.now() : value)}
|
||||
style={{ width: '100%' }}
|
||||
showTime={true}
|
||||
onChange={(v) => {
|
||||
onChange(v.valueOf());
|
||||
}}
|
||||
></DatePicker>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default (props: any) => {
|
||||
const { record } = props;
|
||||
const routeParams = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const [form] = Form.useForm();
|
||||
const defaultResetType = 'assignedTime';
|
||||
const [resetType, setResetType] = useState(defaultResetType);
|
||||
const [resetOffsetVisible, setResetOffsetVisible] = useState(false);
|
||||
const customFormRef: any = React.createRef();
|
||||
const clusterPhyId = Number(routeParams.clusterId);
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({
|
||||
resetType: defaultResetType,
|
||||
});
|
||||
}, []);
|
||||
const confirm = () => {
|
||||
let tableData;
|
||||
if (customFormRef.current) {
|
||||
tableData = customFormRef.current.getTableData();
|
||||
}
|
||||
const formData = form.getFieldsValue();
|
||||
let resetParams: any = {
|
||||
clusterId: clusterPhyId,
|
||||
createIfNotExist: false,
|
||||
groupName: record.groupName,
|
||||
topicName: record.topicName,
|
||||
};
|
||||
if (formData.resetType === 'assignedTime') {
|
||||
resetParams.resetType = formData.timestamp === 'newest' ? 0 : 2;
|
||||
if (resetParams.resetType === 2) {
|
||||
resetParams.timestamp = formData.timestamp;
|
||||
}
|
||||
}
|
||||
if (formData.resetType === 'partition') {
|
||||
resetParams.resetType = 3;
|
||||
resetParams.offsetList = tableData
|
||||
? tableData.map((item: { key: string; value: string }) => ({ partitionId: item.key, offset: item.value }))
|
||||
: [];
|
||||
}
|
||||
Utils.put(Api.resetGroupOffset(), resetParams).then((data) => {
|
||||
if (data === null) {
|
||||
notification.success({
|
||||
message: '重置offset成功',
|
||||
});
|
||||
setResetOffsetVisible(false);
|
||||
} else {
|
||||
notification.error({
|
||||
message: '重置offset失败',
|
||||
});
|
||||
setResetOffsetVisible(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={(_) => {
|
||||
setResetOffsetVisible(true);
|
||||
}}
|
||||
>
|
||||
重置Offset
|
||||
</Button>
|
||||
|
||||
<Drawer
|
||||
title="重置Offset"
|
||||
width={480}
|
||||
visible={resetOffsetVisible}
|
||||
maskClosable={false}
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={(_) => {
|
||||
setResetOffsetVisible(false);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button size="small" type="primary" onClick={confirm}>
|
||||
确定
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
</Space>
|
||||
}
|
||||
className="cluster-detail-consumer-resetoffset"
|
||||
onClose={(_) => {
|
||||
setResetOffsetVisible(false);
|
||||
}}
|
||||
>
|
||||
<Form form={form} labelCol={{ span: 5 }} layout="vertical" className="reset-offset-form">
|
||||
<Form.Item name="resetType" label="重置类型" required>
|
||||
<Radio.Group
|
||||
defaultValue="assignedTime"
|
||||
value={resetType}
|
||||
onChange={(e) => {
|
||||
setResetType(e.target.value);
|
||||
}}
|
||||
>
|
||||
<Radio value={'assignedTime'}>重置到指定时间</Radio>
|
||||
<Radio value={'partition'}>重置分区</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
{resetType === 'assignedTime' && (
|
||||
<Form.Item name="timestamp" label="时间" required>
|
||||
<CustomSelectResetTime />
|
||||
</Form.Item>
|
||||
)}
|
||||
{resetType === 'partition' && (
|
||||
<Form.Item name="partition" label="分区及偏移" required>
|
||||
<EditTable
|
||||
ref={customFormRef}
|
||||
colCustomConfigs={[
|
||||
{
|
||||
title: 'PartitionID',
|
||||
inputType: 'number',
|
||||
placeholder: '请输入Partition',
|
||||
},
|
||||
{
|
||||
title: 'Offset',
|
||||
inputType: 'number',
|
||||
placeholder: '请输入Offset',
|
||||
},
|
||||
]}
|
||||
></EditTable>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,149 @@
|
||||
.operating-state {
|
||||
.operation-bar {
|
||||
.left {
|
||||
.dcloud-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.dcloud-form-item:first-of-type {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.pro-table-wrap {
|
||||
// padding: 17px 24px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
.consumer-group-detail-drawer {
|
||||
.dcloud-drawer-extra {
|
||||
button {
|
||||
width: 90px;
|
||||
}
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 17px;
|
||||
margin: 0 16px;
|
||||
background: rgba(206, 212, 218, 0.6);
|
||||
}
|
||||
}
|
||||
.dcloud-drawer-body {
|
||||
padding-top: 2px !important;
|
||||
}
|
||||
}
|
||||
.consumer-group-detail {
|
||||
// border: 1px solid #EFF2F7;
|
||||
// border-radius: 8px;
|
||||
// padding: 12px 16px;
|
||||
.title-and-mode {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 60px;
|
||||
&-header{
|
||||
font-family: @font-family-bold;
|
||||
font-size: 13px;
|
||||
}
|
||||
h4 {
|
||||
font-family: PingFangSC-Semibold;
|
||||
}
|
||||
.right {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.d-range-time-input {
|
||||
height: 27px !important;
|
||||
}
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: rgba(206, 212, 218, 0.6);
|
||||
margin: 0 8px;
|
||||
}
|
||||
.switch-mode {
|
||||
.dcloud-radio-button-wrapper {
|
||||
font-size: 14px;
|
||||
width: 34px;
|
||||
height: 23px;
|
||||
padding: 0;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.single-chart,
|
||||
.table {
|
||||
background: #f8f9fa;
|
||||
// margin-top: 12px;
|
||||
border-radius: 8px;
|
||||
.dcloud-table {
|
||||
height: 210px;
|
||||
overflow: auto;
|
||||
background-color: transparent;
|
||||
.dcloud-table-content .dcloud-table-cell {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
.dcloud-pagination {
|
||||
height: 32px;
|
||||
margin-bottom: 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
.single-chart {
|
||||
padding: 16px 22px 4px;
|
||||
.single-chart-header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.content-with-copy {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.content {
|
||||
flex: 1;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
.copy-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding-top: 2px;
|
||||
border-radius: 50%;
|
||||
margin-left: 4px;
|
||||
font-size: 16px;
|
||||
color: #adb5bc;
|
||||
opacity: 0;
|
||||
&:hover {
|
||||
background: rgba(33, 37, 41, 0.04);
|
||||
color: #74788d;
|
||||
}
|
||||
}
|
||||
}
|
||||
.dcloud-table-cell-row-hover {
|
||||
.copy-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cluster-detail-consumer-resetoffset {
|
||||
.reset-offset-form {
|
||||
.dcloud-radio-wrapper {
|
||||
width: 154px;
|
||||
}
|
||||
}
|
||||
.operate-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.operate-btns-divider {
|
||||
width: 1px;
|
||||
height: 17px;
|
||||
background: rgba(0, 0, 0, 0.13);
|
||||
margin: 0 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { AppContainer, Form, Input, ProTable, Select, Utils } from 'knowdesign';
|
||||
import './index.less';
|
||||
import Api from '@src/api/index';
|
||||
import { getOperatingStateListParams } from './interface';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import ConsumerGroupDetail from './ConsumerGroupDetail';
|
||||
import ConsumerGroupHealthCheck from '@src/components/CardBar/ConsumerGroupHealthCheck';
|
||||
import DBreadcrumb from 'knowdesign/lib/extend/d-breadcrumb';
|
||||
import { hashDataParse } from '@src/constants/common';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const AutoPage = (props: any) => {
|
||||
const { scene, detailParams = { searchKeywords: '' } } = props;
|
||||
const routeParams = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [consumerGroupList, setConsumerGroupList] = useState([]);
|
||||
const [showMode, setShowMode] = useState('list');
|
||||
const [searchGroupName, setSearchGroupName] = useState(detailParams.searchKeywords || '');
|
||||
const [searchTopicName, setSearchTopicName] = useState('');
|
||||
const [pageIndex, setPageIndex] = useState(1);
|
||||
const [pageTotal, setPageTotal] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [consumersListLoading, setConsumersListLoading] = useState(false);
|
||||
const clusterPhyId = Number(routeParams.clusterId);
|
||||
|
||||
const searchFn = () => {
|
||||
const params: getOperatingStateListParams = {
|
||||
pageNo: pageIndex,
|
||||
pageSize,
|
||||
fuzzySearchDTOList: [],
|
||||
};
|
||||
if (searchGroupName) {
|
||||
// params.groupName = searchGroupName;
|
||||
params.fuzzySearchDTOList.push({ fieldName: 'groupName', fieldValue: searchGroupName });
|
||||
}
|
||||
if (searchTopicName) {
|
||||
params.fuzzySearchDTOList.push({ fieldName: 'topicName', fieldValue: searchTopicName });
|
||||
}
|
||||
const topicName = hashDataParse(location.hash)?.topicName;
|
||||
if (topicName) {
|
||||
params.topicName = topicName;
|
||||
}
|
||||
getOperatingStateList(params);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
searchFn();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchGroupName(detailParams.searchKeywords);
|
||||
}, [detailParams.searchKeywords]);
|
||||
|
||||
const getOperatingStateList = (params: getOperatingStateListParams) => {
|
||||
setConsumersListLoading(true);
|
||||
Utils.post(Api.getOperatingStateList(clusterPhyId), params).then((data: any) => {
|
||||
setConsumersListLoading(false);
|
||||
setConsumerGroupList(data?.bizData || []);
|
||||
setPageIndex(data.pagination.pageNo);
|
||||
setPageTotal(data.pagination.total);
|
||||
setPageSize(data.pagination.pageSize);
|
||||
});
|
||||
};
|
||||
|
||||
const columns = () => {
|
||||
const baseColumns: any = [
|
||||
{
|
||||
title: 'ConsumerGroup',
|
||||
dataIndex: 'groupName',
|
||||
key: 'groupName',
|
||||
render: (v: any, r: any) => {
|
||||
return (
|
||||
<a
|
||||
onClick={() => {
|
||||
window.location.hash = `groupName=${v || ''}&topicName=${r.topicName}`;
|
||||
}}
|
||||
>
|
||||
{v}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '消费的Topic',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
},
|
||||
// {
|
||||
// title: 'Principle',
|
||||
// dataIndex: 'kafkaUser',
|
||||
// key: 'kafkaUser',
|
||||
// },
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'state',
|
||||
key: 'state',
|
||||
},
|
||||
{
|
||||
title: 'Max Lag',
|
||||
dataIndex: 'maxLag',
|
||||
key: 'maxLag',
|
||||
},
|
||||
{
|
||||
title: 'Member数',
|
||||
dataIndex: 'memberCount',
|
||||
key: 'memberCount',
|
||||
},
|
||||
];
|
||||
|
||||
// if (
|
||||
// global.hasPermission &&
|
||||
// global.hasPermission(
|
||||
// scene === 'topicDetail' ? ClustersPermissionMap.TOPIC_RESET_OFFSET : ClustersPermissionMap.CONSUMERS_RESET_OFFSET
|
||||
// )
|
||||
// ) {
|
||||
// baseColumns.push({
|
||||
// title: '操作',
|
||||
// dataIndex: 'desc',
|
||||
// key: 'desc',
|
||||
// render: (txt: any, record: any) => {
|
||||
// return <ResetOffsetDrawer record={record}></ResetOffsetDrawer>;
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
return baseColumns;
|
||||
};
|
||||
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
const params: getOperatingStateListParams = {
|
||||
pageNo: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
fuzzySearchDTOList: [],
|
||||
};
|
||||
// setFilteredInfo(filters);
|
||||
if (searchGroupName) {
|
||||
// params.groupName = searchGroupName;
|
||||
params.fuzzySearchDTOList.push({ fieldName: 'groupName', fieldValue: searchGroupName });
|
||||
}
|
||||
if (searchTopicName) {
|
||||
params.fuzzySearchDTOList.push({ fieldName: 'topicName', fieldValue: searchTopicName });
|
||||
}
|
||||
const topicName = hashDataParse(location.hash)?.topicName;
|
||||
if (topicName) {
|
||||
params.topicName = topicName;
|
||||
}
|
||||
getOperatingStateList(params);
|
||||
};
|
||||
|
||||
const showModes = [
|
||||
{ label: '列表模式', value: 'list' },
|
||||
{ label: '关系图模式', value: 'graph' },
|
||||
];
|
||||
return (
|
||||
<>
|
||||
{scene !== 'topicDetail' && (
|
||||
<div className="breadcrumb" style={{ marginBottom: '10px' }}>
|
||||
<DBreadcrumb
|
||||
breadcrumbs={[
|
||||
{ label: '多集群管理', aHref: '/' },
|
||||
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
|
||||
{ label: 'Consumers', aHref: `` },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{scene !== 'topicDetail' && (
|
||||
<div style={{ margin: '12px 0' }}>
|
||||
{' '}
|
||||
<ConsumerGroupHealthCheck></ConsumerGroupHealthCheck>
|
||||
</div>
|
||||
)}
|
||||
<div className={`operating-state ${scene !== 'topicDetail' && 'clustom-table-content'}`}>
|
||||
{/* <CardBar cardColumns={data}></CardBar> */}
|
||||
{scene !== 'topicDetail' && (
|
||||
<div className="operation-bar">
|
||||
<div className="left">
|
||||
{/* <Radio.Group
|
||||
options={showModes}
|
||||
optionType="button"
|
||||
onChange={(e) => {
|
||||
setShowMode(e.target.value);
|
||||
}}
|
||||
value={showMode}
|
||||
/> */}
|
||||
<Form.Item label="">
|
||||
<Input
|
||||
className="search-input-short"
|
||||
placeholder="请输入Consumer Group"
|
||||
// suffix={<SearchOutlined />}
|
||||
onChange={(e) => {
|
||||
setSearchGroupName(e.target.value);
|
||||
}}
|
||||
onPressEnter={searchFn}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="">
|
||||
<Input
|
||||
className="search-input-short"
|
||||
placeholder="请输入Topic name"
|
||||
// suffix={<SearchOutlined />}
|
||||
onChange={(e) => {
|
||||
setSearchTopicName(e.target.value);
|
||||
}}
|
||||
onPressEnter={searchFn}
|
||||
/>
|
||||
</Form.Item>
|
||||
{/* <Button type="primary" className="add-btn" onClick={searchFn}>
|
||||
查询
|
||||
</Button> */}
|
||||
</div>
|
||||
<div className="right"></div>
|
||||
</div>
|
||||
)}
|
||||
{/* <Table columns={columns} dataSource={consumerGroupList} scroll={{ x: 1500 }} />
|
||||
{pageTotal > 0 && <Pagination
|
||||
className='pro-table-pagination-custom'
|
||||
defaultCurrent={1}
|
||||
current={pageIndex}
|
||||
total={pageTotal}
|
||||
pageSize={pageSize} />} */}
|
||||
<div className="pro-table-wrap">
|
||||
<ProTable
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
loading: consumersListLoading,
|
||||
showHeader: false,
|
||||
rowKey: 'groupName',
|
||||
columns: columns(),
|
||||
dataSource: consumerGroupList,
|
||||
paginationProps:
|
||||
pageTotal > 0
|
||||
? {
|
||||
current: pageIndex,
|
||||
total: pageTotal,
|
||||
pageSize: pageSize,
|
||||
}
|
||||
: null,
|
||||
attrs: {
|
||||
// expandable: {
|
||||
// expandedRowRender: (record: any) => {
|
||||
// return <ConsumerGroupDetail record={record}></ConsumerGroupDetail>;
|
||||
// },
|
||||
// rowExpandable: (record: any) => true,
|
||||
// },
|
||||
onChange: onTableChange,
|
||||
scroll: { y: 'calc(100vh - 390px)' },
|
||||
// className: `frameless-table ${scene !== 'topicDetail' && 'clustom-table-content'}`, // 纯无边框表格类名
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{<ConsumerGroupDetail scene={scene}></ConsumerGroupDetail>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutoPage;
|
||||
@@ -0,0 +1,56 @@
|
||||
export interface getOperatingStateListParams {
|
||||
fuzzySearchDTOList?: Array<{
|
||||
fieldName: string;
|
||||
fieldValue: string;
|
||||
}>;
|
||||
groupName?: string;
|
||||
pageNo: number;
|
||||
pageSize: number;
|
||||
searchKeywords?: string;
|
||||
topicName?: string;
|
||||
}
|
||||
export interface getTopicGroupMetricParams {
|
||||
clusterId: number;
|
||||
dto: {
|
||||
metricRealTimes: Array<{
|
||||
metricName: string;
|
||||
metricType: number;
|
||||
}>;
|
||||
pageNo?: number;
|
||||
pageSize?: number;
|
||||
searchKeywords?: string;
|
||||
};
|
||||
topicName: string;
|
||||
groupName: string;
|
||||
}
|
||||
export interface getTopicGroupMetricHistoryParams {
|
||||
clusterPhyId: number;
|
||||
param: {
|
||||
groupTopics: Array<{
|
||||
groupName: string;
|
||||
partitionIdList: Array<string>;
|
||||
topicName: string;
|
||||
}>;
|
||||
groups: Array<string>;
|
||||
metricsName: Array<string>;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
}
|
||||
export interface getTopicGroupPartitionsHistoryParams {
|
||||
clusterPhyId: number;
|
||||
groupName: string;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
}
|
||||
export interface ResetGroupOffset {
|
||||
clusterId: number;
|
||||
groupName: string;
|
||||
offsetList: Array<{
|
||||
offset: number;
|
||||
partitionId: number;
|
||||
}>;
|
||||
resetType: number;
|
||||
timestamp: number;
|
||||
topicName: string;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { timeFormat } from '../../constants/common';
|
||||
import moment from 'moment';
|
||||
import { getSizeAndUnit } from '../../constants/common';
|
||||
|
||||
export const getGroupListColumns = (arg: any) => {
|
||||
const columns = [
|
||||
{
|
||||
title: 'ConsumerGroup',
|
||||
dataIndex: 'resourceName',
|
||||
key: 'resourceName',
|
||||
},
|
||||
{
|
||||
title: 'Operations',
|
||||
dataIndex: 'aclOperation',
|
||||
key: 'aclOperation',
|
||||
},
|
||||
{
|
||||
title: 'Permission Type',
|
||||
dataIndex: 'aclPermissionType',
|
||||
key: 'aclPermissionType',
|
||||
},
|
||||
{
|
||||
title: 'Pattern Type',
|
||||
dataIndex: 'resourcePatternType',
|
||||
key: 'resourcePatternType',
|
||||
},
|
||||
{
|
||||
title: 'Principle',
|
||||
dataIndex: 'kafkaUser',
|
||||
key: 'kafkaUser',
|
||||
},
|
||||
];
|
||||
return columns;
|
||||
};
|
||||
|
||||
export const defaultPagination = {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
position: 'bottomRight',
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ProTable, Drawer, Utils, Input } from 'knowdesign';
|
||||
import API from '../../api';
|
||||
import { getGroupListColumns, defaultPagination } from './config';
|
||||
import { dealTableRequestParams } from '../../constants/common';
|
||||
|
||||
const { request } = Utils;
|
||||
const ControllerChangeLogList: React.FC = (props: any) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
const [searchKeywords, setSearchKeywords] = useState('');
|
||||
const [filteredInfo, setFilteredInfo] = useState(null);
|
||||
const [pagination, setPagination] = useState<any>(defaultPagination);
|
||||
// 默认排序
|
||||
// const defaultSorter = {
|
||||
// field: 'brokerId',
|
||||
// order: 'ascend',
|
||||
// };
|
||||
|
||||
const solveClick = (record: any) => {};
|
||||
|
||||
// 请求接口获取数据
|
||||
const genData = async ({ pageNo, pageSize, filters = null, sorter = null }: any) => {
|
||||
// if (clusterId === undefined) return;
|
||||
filters = filters || filteredInfo;
|
||||
setLoading(true);
|
||||
const params = dealTableRequestParams({ searchKeywords, pageNo, pageSize, sorter, filters });
|
||||
|
||||
request(API.getGroupACLBindingList(2), { params })
|
||||
.then((res: any) => {
|
||||
setPagination({
|
||||
current: res.pagination?.pageNo,
|
||||
pageSize: res.pagination?.pageSize,
|
||||
total: res.pagination?.total,
|
||||
});
|
||||
setData(res?.bizData || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setPagination({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 20,
|
||||
});
|
||||
// mock
|
||||
setData([
|
||||
{
|
||||
aclClientHost: '127.0.0.1',
|
||||
aclOperation: 1,
|
||||
aclPermissionType: 2,
|
||||
kafkaUser: 'know-streaming',
|
||||
resourceName: 'know-streaming',
|
||||
resourcePatternType: 2,
|
||||
resourceType: 2,
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
setFilteredInfo(filters);
|
||||
genData({ pageNo: pagination.current, pageSize: pagination.pageSize, filters, sorter });
|
||||
};
|
||||
|
||||
const getSearchKeywords = (value: string) => {
|
||||
setSearchKeywords(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
genData({
|
||||
pageNo: 1,
|
||||
pageSize: pagination.pageSize,
|
||||
// sorter: defaultSorter
|
||||
});
|
||||
}, [searchKeywords]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProTable
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
showHeader: true,
|
||||
rowKey: 'path',
|
||||
loading: loading,
|
||||
columns: getGroupListColumns(solveClick),
|
||||
dataSource: data,
|
||||
paginationProps: { ...pagination },
|
||||
tableHeaderSearchInput: {
|
||||
// 搜索配置
|
||||
submit: getSearchKeywords,
|
||||
placeholder: '请输入Broker Host',
|
||||
width: '248px',
|
||||
searchTrigger: 'enter',
|
||||
},
|
||||
attrs: {
|
||||
className: 'frameless-table', // 纯无边框表格类名
|
||||
onChange: onTableChange,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ControllerChangeLogList;
|
||||
@@ -0,0 +1,191 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Badge, ProTable, Tooltip, Utils, Progress } from 'knowdesign';
|
||||
import { formatAssignSize, runningStatusEnum } from './config';
|
||||
|
||||
const columns: any = [
|
||||
{
|
||||
title: 'Partition',
|
||||
dataIndex: 'partitionId',
|
||||
key: 'partitionId',
|
||||
render: (t: any, r: any) => {
|
||||
// return runningStatusEnum[r?.status];
|
||||
return (
|
||||
<span>
|
||||
{t}
|
||||
<Badge
|
||||
style={{ marginLeft: '6px' }}
|
||||
status={r?.status === 1 ? 'warning' : r?.status === 4 ? 'error' : r?.status === 3 ? 'success' : 'warning'}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '源BrokerID',
|
||||
dataIndex: 'sourceBrokerIds',
|
||||
key: 'sourceBrokerIds',
|
||||
render: (t: any, r: any) => {
|
||||
return t && t.length > 0 ? t.join('、') : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '目标BrokerID',
|
||||
dataIndex: 'desBrokerIds',
|
||||
key: 'desBrokerIds',
|
||||
render(t: any, r: any) {
|
||||
return t && t.length > 0 ? t.join('、') : '-';
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: '需迁移MessageSize(MB)',
|
||||
// dataIndex: 'totalSize',
|
||||
// key: 'totalSize',
|
||||
// render(t: any, r: any) {
|
||||
// return t || t === 0 ? formatAssignSize(t, 'MB') : '-';
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// title: '已完成MessageSize (MB)',
|
||||
// dataIndex: 'movedSize',
|
||||
// key: 'movedSize',
|
||||
// render(t: any, r: any) {
|
||||
// return t || t === 0 ? formatAssignSize(t, 'MB') : '-';
|
||||
// },
|
||||
// },
|
||||
{
|
||||
title: 'MessageSize迁移进度(MB)',
|
||||
dataIndex: 'movedSize',
|
||||
key: 'movedSize',
|
||||
render(t: any, r: any) {
|
||||
const movedSize = r.movedSize ? Number(Utils.formatAssignSize(t, 'MB')) : 0;
|
||||
const totalSize = r.totalSize ? Number(Utils.formatAssignSize(t, 'MB')) : 0;
|
||||
return (
|
||||
<div className="message-size">
|
||||
<Tooltip title={(movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0) + '%'}>
|
||||
<Progress
|
||||
percent={movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0}
|
||||
strokeColor="#556EE6"
|
||||
trailColor="#ECECF1"
|
||||
showInfo={false}
|
||||
/>
|
||||
</Tooltip>
|
||||
<span>{movedSize + '/' + totalSize}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: '状态',
|
||||
// key: 'status',
|
||||
// width: 100,
|
||||
// render: (t: any, r: any) => {
|
||||
// return (
|
||||
// <span>
|
||||
// <Badge status={r?.status === 1 ? 'warning' : r?.status === 4 ? 'error' : r?.status === 3 ? 'success' : 'warning'} />
|
||||
// {runningStatusEnum[r?.status]}
|
||||
// </span>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// title: 'BytesIn(MB/s)',
|
||||
// dataIndex: 'byteIn',
|
||||
// key: 'byteIn',
|
||||
// render(t: any, r: any) {
|
||||
// return t ? t : '-';
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// title: '同步速率(MB/s)',
|
||||
// dataIndex: 'byteMove',
|
||||
// key: 'byteMove',
|
||||
// render(t: any, r: any) {
|
||||
// return t ? t : '-';
|
||||
// },
|
||||
// },
|
||||
{
|
||||
title: '预计剩余时长',
|
||||
dataIndex: 'remainTime',
|
||||
key: 'remainTime',
|
||||
render(t: any, r: any) {
|
||||
return t ? Utils.transUnitTime(t) : t === 0 ? t : '-';
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const ExpandedRow: any = ({ record, data, loading }: any) => {
|
||||
const [pagination, setPagination] = useState<any>({
|
||||
current: 1,
|
||||
pageSize: 5,
|
||||
// total: data[record.key]?.length,
|
||||
simple: true,
|
||||
hideOnSinglePage: false,
|
||||
});
|
||||
const [status, setStatus] = useState({
|
||||
total: 0,
|
||||
success: 0,
|
||||
doing: 0,
|
||||
fail: 0,
|
||||
});
|
||||
const onTableChange = (pagination: any, filters: any) => {
|
||||
setPagination(pagination);
|
||||
};
|
||||
|
||||
const calcStatus = () => {
|
||||
const success = data[record.key]?.filter((item: any) => runningStatusEnum[item.status] === 'Success').length || 0;
|
||||
const doing = data[record.key]?.filter((item: any) => runningStatusEnum[item.status] === 'Doing').length || 0;
|
||||
const fail = data[record.key]?.filter((item: any) => runningStatusEnum[item.status] === 'Fail').length || 0;
|
||||
const total = data[record.key]?.length || 0;
|
||||
setStatus({ total, success, doing, fail });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
calcStatus();
|
||||
}, [data[record.key]]);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={record.key}
|
||||
style={{ position: 'relative', padding: '12px 16px', border: '1px solid #EFF2F7', borderRadius: '8px', backgroundColor: '#ffffff' }}
|
||||
>
|
||||
<ProTable
|
||||
// bordered
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
loading: loading[record.key],
|
||||
rowKey: 'key',
|
||||
dataSource: data[record.key] || [],
|
||||
columns,
|
||||
lineFillColor: true,
|
||||
// noPagination: true,
|
||||
paginationProps: pagination,
|
||||
attrs: {
|
||||
className: 'expanded-table',
|
||||
onChange: onTableChange,
|
||||
scroll: { x: 'max-content' },
|
||||
size: 'small',
|
||||
bordered: false,
|
||||
rowClassName: 'table-small-bgcolor',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<div style={{ position: 'absolute', bottom: '12px', left: '16px' }}>
|
||||
<span>执行情况:</span>
|
||||
<span>Partition总数 {(status?.success || 0) + (status?.doing || 0) + (status?.fail || 0)}</span>
|
||||
<span style={{ marginLeft: '34px', display: 'inline-block' }}>
|
||||
<Badge status="success" />
|
||||
Success {status?.success || 0}
|
||||
</span>
|
||||
<span style={{ marginLeft: '34px', display: 'inline-block' }}>
|
||||
<Badge status="error" />
|
||||
Fail {status?.fail || 0}
|
||||
</span>
|
||||
<span style={{ marginLeft: '34px', display: 'inline-block' }}>
|
||||
<Badge status="warning" />
|
||||
Doing {status?.doing || 0}
|
||||
</span>
|
||||
{/* <Pagination size="small" onChange={onTableChange} simple total={data[record.key]?.length} /> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Alert, Badge, Dropdown, ProTable, Space, Table, Utils } from 'knowdesign';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Api from '@src/api';
|
||||
import { getNodeTrafficColumns } from './config';
|
||||
|
||||
const { request, post } = Utils;
|
||||
|
||||
const NodeTraffic = (props: any) => {
|
||||
const { hashData } = props;
|
||||
const urlParams = useParams<any>(); // 获取地址栏参数
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [data, setData] = useState([]);
|
||||
const [pagination, setPagination] = useState<any>({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
position: 'bottomRight',
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
|
||||
});
|
||||
|
||||
const [popoverVisible, setPopoverVisible] = useState(false);
|
||||
|
||||
// 获取
|
||||
const genData = async ({ pageNo, pageSize }: any) => {
|
||||
if (urlParams?.clusterId === undefined) return;
|
||||
setData([]);
|
||||
setLoading(true);
|
||||
post(Api.getJobNodeTraffic(urlParams?.clusterId, props?.jobId))
|
||||
.then((res: any) => {
|
||||
// setPagination({
|
||||
// current: res.pagination?.pageNo,
|
||||
// pageSize: res.pagination?.pageSize,
|
||||
// total: res.pagination?.total,
|
||||
// });
|
||||
const mockData = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
mockData.push({
|
||||
byteInJob: 0,
|
||||
byteInTotal: 0,
|
||||
byteOutJob: 0,
|
||||
byteOutTotal: 0,
|
||||
createTime: 1645608135717,
|
||||
host: 'string',
|
||||
id: 0,
|
||||
updateTime: 1645608135717,
|
||||
});
|
||||
}
|
||||
setData(res || mockData);
|
||||
// setData(res?.bizData || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
// setPagination(pagination);
|
||||
// const asc = sorter?.order && sorter?.order === 'ascend' ? true : false;
|
||||
// const sortColumn = sorter.field && toLine(sorter.field);
|
||||
genData({ pageNo: pagination.current, pageSize: pagination.pageSize });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
genData({
|
||||
pageNo: 1,
|
||||
pageSize: pagination.pageSize,
|
||||
// sorter: defaultSorter
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ maxWidth: '1032px' }}>
|
||||
<ProTable
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
rowKey: 'key',
|
||||
loading: loading,
|
||||
columns: getNodeTrafficColumns({ popoverVisible, setPopoverVisible }),
|
||||
dataSource: data,
|
||||
paginationProps: { ...pagination },
|
||||
attrs: {
|
||||
bordered: false,
|
||||
// className: 'frameless-table', // 纯无边框表格类名
|
||||
onChange: onTableChange,
|
||||
size: 'middle',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeTraffic;
|
||||
@@ -0,0 +1,75 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button, ProTable, Popover } from 'knowdesign';
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
|
||||
export const PopoverBroker = (props: any) => {
|
||||
const [visible, setVisible] = useState<boolean>(false);
|
||||
const columns = [
|
||||
{
|
||||
title: 'BrokerID',
|
||||
dataIndex: 'brokerId',
|
||||
key: 'brokerHost',
|
||||
},
|
||||
{
|
||||
title: 'Host',
|
||||
dataIndex: 'brokerHost',
|
||||
key: 'brokerHost',
|
||||
},
|
||||
];
|
||||
const newPropsData = Object.keys(props.data).map((item) => {
|
||||
return {
|
||||
brokerId: item,
|
||||
brokerHost: props.data[item],
|
||||
};
|
||||
});
|
||||
return (
|
||||
<Popover
|
||||
// getPopupContainer={(triggerNode: any) => {
|
||||
// return triggerNode;
|
||||
// }}
|
||||
placement="bottomLeft"
|
||||
overlayClassName="custom-popover-borker"
|
||||
trigger={'click'}
|
||||
title={
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div style={{ fontSize: '16px' }}>{props?.title}</div>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CloseOutlined className="close-icon" />}
|
||||
onClick={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
visible={visible}
|
||||
onVisibleChange={(visible) => setVisible(visible)}
|
||||
content={
|
||||
<div className="container">
|
||||
<div className="main">
|
||||
<ProTable
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
noPagination: true,
|
||||
showHeader: false,
|
||||
rowKey: 'key',
|
||||
// loading: loading,
|
||||
columns,
|
||||
dataSource: newPropsData,
|
||||
// paginationProps: { ...pagination },
|
||||
attrs: {
|
||||
bordered: false,
|
||||
// className: 'frameless-table', // 纯无边框表格类名
|
||||
// onChange: onTableChange,
|
||||
size: 'middle',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{newPropsData && newPropsData.length > 0 && <a style={{ display: 'inline-block', marginLeft: '8px' }}>查看详情</a>}
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,294 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Alert, Badge, Descriptions, Dropdown, ProTable, DTable, Table, Utils, Spin, Tag } from 'knowdesign';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Api from '@src/api';
|
||||
import { getNodeTrafficColumns } from './config';
|
||||
import { getSizeAndUnit } from '@src/constants/common';
|
||||
interface PropsType {
|
||||
jobId?: any;
|
||||
balanceData?: any;
|
||||
status?: any;
|
||||
[name: string]: any;
|
||||
// [clasgfag: any]: any;
|
||||
}
|
||||
|
||||
const typeObj: any = {
|
||||
1: '周期均衡',
|
||||
2: '立即均衡',
|
||||
};
|
||||
|
||||
const { request, post } = Utils;
|
||||
|
||||
const RebalancePlan = (props: PropsType) => {
|
||||
const { jobId, balanceData, status } = props;
|
||||
const urlParams = useParams<any>(); // 获取地址栏参数
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [data, setData] = useState<any>({});
|
||||
const [pagination, setPagination] = useState<any>({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
position: 'bottomRight',
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
|
||||
// hideOnSinglePage: false,
|
||||
// showTotal: (total: number) => `共 ${total} 条目`,
|
||||
});
|
||||
|
||||
// 获取
|
||||
const genData = async ({ pageNo, pageSize }: any) => {
|
||||
if (urlParams?.clusterId === undefined) return;
|
||||
setLoading(true);
|
||||
if (balanceData) {
|
||||
setData(balanceData);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setData([]);
|
||||
request(Api.getJobsPlanRebalance(urlParams?.clusterId, jobId))
|
||||
.then((res: any) => {
|
||||
// setPagination({
|
||||
// current: res.pagination?.pageNo,
|
||||
// pageSize: res.pagination?.pageSize,
|
||||
// total: res.pagination?.total,
|
||||
// });
|
||||
|
||||
// setData(mockData);
|
||||
|
||||
setData(res || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
setPagination(pagination);
|
||||
// const asc = sorter?.order && sorter?.order === 'ascend' ? true : false;
|
||||
// const sortColumn = sorter.field && toLine(sorter.field);
|
||||
// genData({ pageNo: pagination.current, pageSize: pagination.pageSize });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
genData({
|
||||
pageNo: 1,
|
||||
pageSize: pagination.pageSize,
|
||||
// sorter: defaultSorter
|
||||
});
|
||||
}, []);
|
||||
|
||||
const columns: any = [
|
||||
{
|
||||
title: '节点',
|
||||
dataIndex: 'host',
|
||||
key: 'host',
|
||||
fixed: 'left',
|
||||
render: (t: any, r: any) => {
|
||||
return (
|
||||
<span>
|
||||
{t}
|
||||
<Badge style={{ marginLeft: '4px' }} status={r?.status === 0 ? 'success' : 'error'} />
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<div>
|
||||
Disk使用率
|
||||
<span className="balance-list-title">
|
||||
{`(`}当前
|
||||
<span className="titleImg"></span>
|
||||
均衡后{`)`}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'diskBefore',
|
||||
key: 'diskBefore',
|
||||
width: 140,
|
||||
render: (t: any, r: any) => {
|
||||
const befaore = r?.diskBefore || r?.diskBefore === 0 ? r?.diskBefore.toFixed(2) + '%' : '0%';
|
||||
const after = r?.diskAfter || r?.diskAfter === 0 ? r?.diskAfter.toFixed(2) + '%' : '0%';
|
||||
return (
|
||||
<span className="balance-list-contert">
|
||||
<span className="balance-list-contert-text">{befaore}</span>
|
||||
<span className="balance-list-contert-img"></span>
|
||||
<span className="balance-list-contert-text-right">{after}</span>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<div>
|
||||
BytesIn使用率
|
||||
<span className="balance-list-title">
|
||||
{`(`}当前
|
||||
<span className="titleImg"></span>
|
||||
均衡后{`)`}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'byteInBefore',
|
||||
key: 'byteInBefore',
|
||||
width: 140,
|
||||
render: (t: any, r: any) => {
|
||||
const befaore = r?.byteInBefore || r?.byteInBefore === 0 ? r?.byteInBefore.toFixed(2) + '%' : '0%';
|
||||
const after = r?.byteInAfter || r?.byteInAfter === 0 ? r?.byteInAfter.toFixed(2) + '%' : '0%';
|
||||
return (
|
||||
<span className="balance-list-contert">
|
||||
<span className="balance-list-contert-text">{befaore}</span>
|
||||
<span className="balance-list-contert-img"></span>
|
||||
<span className="balance-list-contert-text-right">{after}</span>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<div>
|
||||
BytesOut使用率
|
||||
<span className="balance-list-title">
|
||||
{`(`}当前
|
||||
<span className="titleImg"></span>
|
||||
均衡后{`)`}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'byteOutBefore',
|
||||
key: 'byteOutBefore',
|
||||
width: 140,
|
||||
render: (t: any, r: any) => {
|
||||
const befaore = r?.byteOutBefore || r?.byteOutBefore === 0 ? r?.byteOutBefore.toFixed(2) + '%' : '0%';
|
||||
const after = r?.byteOutAfter || r?.byteOutAfter === 0 ? r?.byteOutAfter.toFixed(2) + '%' : '0%';
|
||||
return (
|
||||
<span className="balance-list-contert">
|
||||
<span className="balance-list-contert-text">{befaore}</span>
|
||||
<span className="balance-list-contert-img"></span>
|
||||
<span className="balance-list-contert-text-right">{after}</span>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '移入情况',
|
||||
dataIndex: 'inReplica',
|
||||
key: 'inReplica',
|
||||
render: (t: any, r: any) => {
|
||||
return (t || t === 0 ? t : 0) + '/' + (r?.inSize || r?.inSize === 0 ? getSizeAndUnit(r?.inSize, 'B', 0).valueWithUnit : 0);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '移出情况',
|
||||
dataIndex: 'outReplica',
|
||||
key: 'outReplica',
|
||||
render: (t: any, r: any) => {
|
||||
return (t || t === 0 ? t : 0) + '/' + (r?.outSize || r?.outSize === 0 ? getSizeAndUnit(r?.outSize, 'B', 0).valueWithUnit : 0);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mockdata: any = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
mockdata.push({
|
||||
key: i,
|
||||
name: 'John Brown',
|
||||
age: i + 1,
|
||||
street: 'Lake Park',
|
||||
building: 'C',
|
||||
number: 2035,
|
||||
companyAddress: 'Lake Street 42',
|
||||
companyName: 'SoftLake Co',
|
||||
gender: 'M',
|
||||
});
|
||||
}
|
||||
|
||||
// const download= (url:any, name:any)=>{
|
||||
// const a:any = document.createElement(\'a\')
|
||||
// a.download = name
|
||||
// a.rel = \'noopener\'
|
||||
// a.href = url
|
||||
// // 触发模拟点击
|
||||
// // a.dispatchEvent(new MouseEvent(\'click\'))
|
||||
// // 或者模拟点击
|
||||
// a.click()
|
||||
// }
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 style={{ fontSize: '16px' }}>计划概览</h3>
|
||||
<Spin spinning={loading}>
|
||||
<Descriptions
|
||||
style={{ fontSize: '13px' }}
|
||||
column={2}
|
||||
labelStyle={{
|
||||
width: '100px',
|
||||
textAlign: 'right',
|
||||
display: 'flex',
|
||||
justifyContent: 'end',
|
||||
color: '#74788D',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
contentStyle={{ fontSize: '13px' }}
|
||||
>
|
||||
<Descriptions.Item labelStyle={{ width: '79px' }} label="任务类型">
|
||||
{typeObj[data?.type] || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item labelStyle={{ width: '79px' }} label="总迁移大小">
|
||||
{Utils.getSizeAndUnit(data?.moveSize, 'B').valueWithUnit}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Topic黑名单">
|
||||
{data?.blackTopics && data?.blackTopics?.length > 0 ? data?.blackTopics.join('、') : '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item labelStyle={{ width: '79px' }} label="迁移副本数">
|
||||
{data?.replicas || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item labelStyle={{ width: '79px' }} label="均衡阈值">
|
||||
{data?.clusterBalanceIntervalList
|
||||
? data?.clusterBalanceIntervalList?.map((item: any) => {
|
||||
return (
|
||||
<Tag style={{ padding: '4px 8px', backgroundColor: 'rgba(33,37,41,0.08)', marginRight: '4px' }} key={item?.priority}>
|
||||
{item.type + ':' + item.intervalPercent + '%'}
|
||||
</Tag>
|
||||
);
|
||||
})
|
||||
: '-'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Spin>
|
||||
<h3 style={{ fontSize: '16px' }}>计划明细</h3>
|
||||
<div style={{ maxWidth: '1032px' }}>
|
||||
{/* <Table columns={columns} dataSource={mockdata} bordered scroll={{ x: 'max-content' }}></Table> */}
|
||||
<ProTable
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
loading,
|
||||
rowKey: 'key',
|
||||
dataSource: data?.detail,
|
||||
paginationProps: pagination,
|
||||
columns,
|
||||
lineFillColor: true,
|
||||
attrs: {
|
||||
onChange: onTableChange,
|
||||
scroll: { x: 'max-content' },
|
||||
// className: 'remove-last-border',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{data?.reassignmentJson && (
|
||||
<>
|
||||
<h3 style={{ fontSize: '16px', marginTop: '22px' }}>执行文件</h3>
|
||||
<div>
|
||||
<a href={`data:,${data?.reassignmentJson}`} rel="noopener" download="reassignment.json">
|
||||
Reassignment json file(点击下载)
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RebalancePlan;
|
||||
@@ -0,0 +1,148 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Alert, Badge, Dropdown, IconFont, ProTable, Space, Table, Utils } from 'knowdesign';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Api from '@src/api';
|
||||
import { getTaskDetailsColumns, getMoveBalanceColumns } from './config';
|
||||
import { ExpandedRow } from './ExpandedRow';
|
||||
const { request, post } = Utils;
|
||||
|
||||
const TaskDetails = (props: any) => {
|
||||
const { hashData } = props;
|
||||
const urlParams = useParams<any>(); // 获取地址栏参数
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
const [expandedData, setExpandedData] = useState([]);
|
||||
const [loadingObj, setLoadingObj] = useState<any>({});
|
||||
const [pagination, setPagination] = useState<any>({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
position: 'bottomRight',
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
});
|
||||
|
||||
// TODO 获取行详情数据接口
|
||||
const getRowDetailData = (topicName: string, key?: number) => {
|
||||
return post(Api.getJobPartitionDetail(urlParams?.clusterId, props?.jobId, topicName));
|
||||
};
|
||||
// TODO 获取行详情数据接口
|
||||
const queryExpandedData = async (record: any, key: any) => {
|
||||
if (urlParams?.clusterId === undefined) return;
|
||||
try {
|
||||
const table = { ...expandedData };
|
||||
const loading = { ...loadingObj };
|
||||
getRowDetailData(record.topicName).then((res) => {
|
||||
table[key] = res;
|
||||
loading[key] = false;
|
||||
setExpandedData(table);
|
||||
setLoadingObj(loading);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const onClickExpand = (expanded: any, record: any) => {
|
||||
const key = record?.key;
|
||||
// 之前展开过
|
||||
if (expandedData[key]?.length) return;
|
||||
// 第一次展开
|
||||
const loading = { ...loadingObj };
|
||||
loading[key] = true;
|
||||
setLoadingObj(loading);
|
||||
queryExpandedData(record, key);
|
||||
};
|
||||
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
setPagination(pagination);
|
||||
// const asc = sorter?.order && sorter?.order === 'ascend' ? true : false;
|
||||
// const sortColumn = sorter.field && toLine(sorter.field);
|
||||
// genData({ pageNo: pagination.current, pageSize: pagination.pageSize });
|
||||
};
|
||||
|
||||
const newData =
|
||||
props?.detailData?.subJobs?.map((item: any, index: number) => {
|
||||
return {
|
||||
...item,
|
||||
key: index,
|
||||
};
|
||||
}) || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Alert
|
||||
message={
|
||||
<div style={{ color: '#592D00', fontSize: '14px' }}>
|
||||
<span>执行情况:</span>
|
||||
<span>Topic总数 {newData?.length}</span>
|
||||
<span style={{ marginLeft: '34px', display: 'inline-block' }}>
|
||||
<Badge status="success" />
|
||||
Success {props?.detailData?.success}
|
||||
</span>
|
||||
<span style={{ marginLeft: '34px', display: 'inline-block' }}>
|
||||
<Badge status="error" />
|
||||
Fail {props?.detailData?.fail}
|
||||
</span>
|
||||
<span style={{ marginLeft: '34px', display: 'inline-block' }}>
|
||||
<Badge status="warning" />
|
||||
Doing {props?.detailData?.doing}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
style={{ background: '#FFF9E6', padding: '6px 12px', border: 'none', marginBottom: '12px', borderRadius: '4px' }}
|
||||
/>
|
||||
<div className="job-detail">
|
||||
<ProTable
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
rowKey: 'key',
|
||||
loading: loading,
|
||||
columns: props?.detailData?.jobType === 1 ? getTaskDetailsColumns() : getMoveBalanceColumns(),
|
||||
dataSource: newData,
|
||||
paginationProps: { ...pagination },
|
||||
// noPagination: true,
|
||||
attrs: {
|
||||
bordered: false,
|
||||
onChange: onTableChange,
|
||||
tableLayout: 'auto',
|
||||
scroll: { x: 'max-content' },
|
||||
expandable: {
|
||||
expandedRowRender: (r: any) => <ExpandedRow record={r} data={expandedData} loading={loadingObj} />,
|
||||
// expandedRowRender,
|
||||
onExpand: onClickExpand,
|
||||
columnWidth: '20px',
|
||||
fixed: 'left',
|
||||
expandIcon: ({ expanded, onExpand, record }: any) => {
|
||||
return expanded ? (
|
||||
<IconFont
|
||||
style={{ fontSize: '16px' }}
|
||||
type="icon-xia"
|
||||
onClick={(e: any) => {
|
||||
onExpand(record, e);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<IconFont
|
||||
style={{ fontSize: '16px' }}
|
||||
type="icon-jiantou_1"
|
||||
onClick={(e: any) => {
|
||||
onExpand(record, e);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
style: {
|
||||
width: '1032px',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskDetails;
|
||||
@@ -0,0 +1,149 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import moment from 'moment';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Button, Drawer, Utils, Descriptions, Tabs, Input, IconFont, message, Spin, InputNumber } from 'knowdesign';
|
||||
import TaskDetails from './TeskDetails';
|
||||
import NodeTraffic from './NodeTraffic';
|
||||
import RebalancePlan from './RebalancePlan';
|
||||
import { jobTypeEnum, runningStatusEnum } from './config';
|
||||
import { timeFormat, getSizeAndUnit } from '../../constants/common';
|
||||
import Api from '@src/api';
|
||||
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
const { request, post } = Utils;
|
||||
const { TabPane } = Tabs;
|
||||
export const ViewJobsProgress = (props: any) => {
|
||||
const urlParams = useParams<any>(); // 获取地址栏参数
|
||||
const [detailData, setDetailData] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [positionType, setPositionType] = useState<string>('TeskDetails');
|
||||
const [editFlowLimitStatus, setEditFlowLimitStatus] = useState<boolean>(false); // 修改限流状态
|
||||
const [flowLimit, setFlowLimit] = useState<any>(); // 修改限流数值
|
||||
const callback = (key: any) => {
|
||||
// setSearchValue('');
|
||||
// setSearchKeywords('');
|
||||
setPositionType(key);
|
||||
};
|
||||
|
||||
// * 修改限流事件
|
||||
const editFlowLimit = () => {
|
||||
if (!flowLimit) {
|
||||
message.error('限流值不能为空');
|
||||
return;
|
||||
}
|
||||
if (flowLimit < 0) {
|
||||
message.error('限流值最小值为0');
|
||||
return;
|
||||
}
|
||||
const newSize = Utils.transMBToB(flowLimit);
|
||||
post(Api.getJobTraffic(urlParams.clusterId, props?.record?.id, newSize)).then((res) => {
|
||||
message.success('修改限流成功');
|
||||
getDetailData();
|
||||
setEditFlowLimitStatus(false);
|
||||
});
|
||||
};
|
||||
// * 获取详情和任务明细数据
|
||||
const getDetailData = () => {
|
||||
setLoading(true);
|
||||
props?.record &&
|
||||
request(Api.getJobDetail(urlParams.clusterId, props?.record?.id))
|
||||
.then((res: any) => {
|
||||
setDetailData(res || []);
|
||||
const newFlowLimit = Utils.transBToMB(res?.flowLimit as number);
|
||||
setFlowLimit(newFlowLimit);
|
||||
// setDetailData(res || mockData);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getDetailData();
|
||||
}, [props?.record]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
width={1080}
|
||||
title="查看进度"
|
||||
visible={props.visible}
|
||||
onClose={() => props.setVisble(false)}
|
||||
destroyOnClose={true}
|
||||
maskClosable={false}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
<Descriptions
|
||||
style={{ fontSize: '13px' }}
|
||||
column={3}
|
||||
labelStyle={{
|
||||
width: '92px',
|
||||
textAlign: 'right',
|
||||
display: 'flex',
|
||||
justifyContent: 'end',
|
||||
color: '#74788D',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
contentStyle={{ fontSize: '13px' }}
|
||||
>
|
||||
<Descriptions.Item label="任务类型">{jobTypeEnum[detailData?.jobType] || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="运行状态">{runningStatusEnum[detailData?.jobStatus] || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="限流">
|
||||
{/* 修改限流 */}
|
||||
<div>
|
||||
{editFlowLimitStatus ? (
|
||||
<div className="edit-flow-limit" style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<InputNumber
|
||||
controls={false}
|
||||
size="small"
|
||||
defaultValue={Utils.transBToMB(detailData?.flowLimit)}
|
||||
value={flowLimit}
|
||||
onChange={(e: any) => setFlowLimit(e)}
|
||||
min={0}
|
||||
max={99999}
|
||||
style={{ width: '150px' }}
|
||||
/>
|
||||
<CheckOutlined style={{ margin: '0 10px', color: 'green' }} onClick={editFlowLimit} />
|
||||
<CloseOutlined style={{ color: 'red' }} onClick={() => setEditFlowLimitStatus(false)} />
|
||||
</div>
|
||||
) : (
|
||||
<span>
|
||||
{detailData?.flowLimit ? Utils.transBToMB(detailData?.flowLimit) + ' MB/s ' : '-'}
|
||||
{runningStatusEnum[detailData?.jobStatus] && runningStatusEnum[detailData?.jobStatus] !== 'Success' ? (
|
||||
<IconFont style={{ fontSize: '14px' }} type="icon-bianji1" onClick={() => setEditFlowLimitStatus(true)} />
|
||||
) : null}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="计划执行时间">
|
||||
{detailData?.planTime ? moment(detailData?.planTime).format(timeFormat) : '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="实际执行时间">
|
||||
{detailData?.startTime ? moment(detailData?.startTime).format(timeFormat) : '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="任务完成时间">
|
||||
{detailData?.endTime ? moment(detailData?.endTime).format(timeFormat) : '-'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Spin>
|
||||
<Tabs className={'custom_tabs_class'} defaultActiveKey="Configuration" onChange={callback} destroyInactiveTabPane>
|
||||
{props?.record?.jobType === 2 && (
|
||||
<TabPane tab="均衡计划" key="Rebalance">
|
||||
<RebalancePlan status={props?.record?.jobStatus} jobId={props?.record?.id} />
|
||||
</TabPane>
|
||||
)}
|
||||
{/* {
|
||||
<TabPane tab="均衡计划" key="Rebalance">
|
||||
<RebalancePlan jobId={props?.record?.id} />
|
||||
</TabPane>
|
||||
} */}
|
||||
<TabPane tab="任务明细" key="TeskDetails">
|
||||
{<TaskDetails detailData={detailData} jobId={props?.record?.id} />}
|
||||
</TabPane>
|
||||
<TabPane tab="节点流量情况" key="NodeTraffic">
|
||||
{<NodeTraffic jobId={props?.record?.id} />}
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
669
km-console/packages/layout-clusters-fe/src/pages/Jobs/config.tsx
Normal file
669
km-console/packages/layout-clusters-fe/src/pages/Jobs/config.tsx
Normal file
@@ -0,0 +1,669 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import React from 'react';
|
||||
import { timeFormat, getSizeAndUnit } from '../../constants/common';
|
||||
import moment from 'moment';
|
||||
import { Tooltip, Tag, Badge, Utils, Progress } from 'knowdesign';
|
||||
import { renderTableOpts } from 'knowdesign/es/common-pages/render-table-opts';
|
||||
import TagsWithHide from '@src/components/TagsWithHide';
|
||||
import { PopoverBroker } from './PopoverBroker';
|
||||
|
||||
// 任务类型下拉
|
||||
export const jobType = [
|
||||
{
|
||||
label: 'Topic迁移',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: '扩缩副本',
|
||||
value: 1,
|
||||
},
|
||||
process.env.BUSSINESS_VERSION
|
||||
? {
|
||||
label: '集群均衡',
|
||||
value: 2,
|
||||
}
|
||||
: undefined,
|
||||
].filter((t) => t);
|
||||
|
||||
//
|
||||
export const jobTypeEnum: any = {
|
||||
0: 'Topic迁移',
|
||||
1: '扩缩副本',
|
||||
2: '集群均衡',
|
||||
};
|
||||
|
||||
// 运行状态下拉
|
||||
export const runningStatus = [
|
||||
{
|
||||
label: 'Doing',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: 'Prepare',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
label: 'Success',
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
label: 'Failed',
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
label: 'Canceled',
|
||||
value: 5,
|
||||
},
|
||||
];
|
||||
|
||||
export const runningStatusEnum: any = {
|
||||
1: 'Doing',
|
||||
2: 'Prepare',
|
||||
3: 'Success',
|
||||
4: 'Failed',
|
||||
5: 'Canceled',
|
||||
};
|
||||
|
||||
export const getJobsListColumns = (arg?: any) => {
|
||||
const columns = [
|
||||
// {
|
||||
// title: '任务队列',
|
||||
// dataIndex: 'taskQueue',
|
||||
// key: 'taskQueue',
|
||||
// },
|
||||
{
|
||||
title: '任务ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
},
|
||||
{
|
||||
title: '任务类型',
|
||||
dataIndex: 'jobType',
|
||||
key: 'jobType',
|
||||
render(t: any, r: any) {
|
||||
return jobTypeEnum[t];
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '任务执行对象',
|
||||
dataIndex: 'target',
|
||||
key: 'target',
|
||||
render(t: any, r: any) {
|
||||
return (
|
||||
<div style={{ width: '232px' }}>
|
||||
<TagsWithHide placement="bottom" list={t.split(',')} expandTagContent={(num: any) => `共有${num}个`} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '运行状态',
|
||||
dataIndex: 'jobStatus',
|
||||
key: 'jobStatus',
|
||||
render(t: any, r: any) {
|
||||
return (
|
||||
<Tag
|
||||
className="12312512512"
|
||||
style={{
|
||||
background: t === 1 ? 'rgba(85,110,230,0.10)' : t === 4 ? '#fff3e4' : t === 3 ? 'rgba(0,192,162,0.10)' : '#ebebf6',
|
||||
color: t === 1 ? '#556EE6' : t === 4 ? '#F58342' : t === 3 ? '#00C0A2' : '#495057',
|
||||
padding: '3px 6px',
|
||||
}}
|
||||
>
|
||||
{runningStatusEnum[t]}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '运行进度',
|
||||
dataIndex: 'progress',
|
||||
key: 'progress',
|
||||
width: 90,
|
||||
render: (_t: any, r: any) => {
|
||||
const { success, total } = r;
|
||||
return (success || 0) + '/' + (total || 0);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '运行结果',
|
||||
dataIndex: 'result',
|
||||
key: 'result',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (_t: any, r: any) => {
|
||||
const { success, fail, waiting, doing } = r;
|
||||
return (
|
||||
<div className="run-result">
|
||||
<span>
|
||||
成功:
|
||||
<span>
|
||||
{success === 0 || success ? (
|
||||
(success + '').length < 3 ? (
|
||||
success
|
||||
) : (
|
||||
<Tooltip title={success}>{(success + '').slice(0, 2) + '...'}</Tooltip>
|
||||
)
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
失败:
|
||||
{fail === 0 || fail ? (fail + '').length < 3 ? fail : <Tooltip title={fail}>{(fail + '').slice(0, 2) + '...'}</Tooltip> : '-'}
|
||||
</span>
|
||||
<span>
|
||||
运行中:
|
||||
{doing === 0 || doing ? (
|
||||
(doing + '').length < 3 ? (
|
||||
doing
|
||||
) : (
|
||||
<Tooltip title={doing}>{(doing + '').slice(0, 2) + '...'}</Tooltip>
|
||||
)
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
待运行:
|
||||
{waiting === 0 || waiting ? (
|
||||
(waiting + '').length < 3 ? (
|
||||
waiting
|
||||
) : (
|
||||
<Tooltip title={waiting}>{(waiting + '').slice(0, 2) + '...'}</Tooltip>
|
||||
)
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'jobDesc',
|
||||
key: 'jobDesc',
|
||||
width: 150,
|
||||
needTooltip: true,
|
||||
},
|
||||
{
|
||||
title: '提交人',
|
||||
dataIndex: 'creator',
|
||||
key: 'creator',
|
||||
},
|
||||
{
|
||||
title: '计划执行时间',
|
||||
dataIndex: 'planTime',
|
||||
width: 160,
|
||||
key: 'planTime',
|
||||
render: (t: any, r: any) => {
|
||||
return t ? moment(t).format(timeFormat) : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '实际执行时间',
|
||||
dataIndex: 'startTime',
|
||||
key: 'startTime',
|
||||
width: 160,
|
||||
render: (t: any, r: any) => {
|
||||
if (moment(t).format('x') < moment(r.planTime).format('x')) {
|
||||
return '已逾期';
|
||||
}
|
||||
// 判断是否为 mysql 默认的1971-01-01 00:00:00
|
||||
if (+moment(t).format('x') === 31507200000) {
|
||||
return '-';
|
||||
}
|
||||
return t ? moment(t).format(timeFormat) : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'options',
|
||||
key: 'options',
|
||||
width: 150,
|
||||
filterTitle: true,
|
||||
fixed: 'right',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (_t: any, r: any) => {
|
||||
return (
|
||||
<div>
|
||||
{r.jobStatus !== 2 && r.jobStatus !== 5 ? <a onClick={() => arg.setViewProgress(r)}>查看进度</a> : null}
|
||||
{/* 编辑任务 */}
|
||||
{r.jobStatus === 2 ? (
|
||||
<a style={{ marginRight: '16px' }} onClick={() => arg.setViewProgress(r, r.jobType)}>
|
||||
编辑任务
|
||||
</a>
|
||||
) : null}
|
||||
{r.jobStatus === 2 ? <a onClick={() => arg.onDelete(r)}>删除</a> : null}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return columns;
|
||||
};
|
||||
// * 获取任务明细-扩缩副本列表配置
|
||||
export const getTaskDetailsColumns = (arg?: any) => {
|
||||
const columns = [
|
||||
{
|
||||
title: 'Topic',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
fixed: 'left',
|
||||
width: 102,
|
||||
render: (t: any, r: any) => {
|
||||
return (
|
||||
<span>
|
||||
{t}
|
||||
<Badge
|
||||
style={{ marginLeft: '6px' }}
|
||||
status={r?.status === 1 ? 'warning' : r?.status === 4 ? 'error' : r?.status === 3 ? 'success' : 'warning'}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '当前副本数',
|
||||
dataIndex: 'oldReplicaNu',
|
||||
key: 'oldReplicaNu',
|
||||
},
|
||||
{
|
||||
title: '源BrokerID',
|
||||
dataIndex: 'sourceBrokers',
|
||||
key: 'sourceBrokers',
|
||||
render: (t: any, r: any) => {
|
||||
return t && t.length > 0 ? t.join('、') : '-';
|
||||
},
|
||||
// render(t: any, r: any) {
|
||||
// return (
|
||||
// <div style={{ width: '100px' }}>
|
||||
// <TagsWithHide list={t} expandTagContent={(num: any) => `共有${num}个`} />
|
||||
// </div>
|
||||
// );
|
||||
// },
|
||||
},
|
||||
{
|
||||
title: '新副本数',
|
||||
dataIndex: 'newReplicaNu',
|
||||
key: 'newReplicaNu',
|
||||
},
|
||||
{
|
||||
title: '目标BrokerID',
|
||||
dataIndex: 'desBrokers',
|
||||
key: 'desBrokers',
|
||||
render: (t: any, r: any) => {
|
||||
return t && t.length > 0 ? t.join('、') : '-';
|
||||
},
|
||||
// render(t: any, r: any) {
|
||||
// return (
|
||||
// <div style={{ width: '100px' }}>
|
||||
// <TagsWithHide list={t} expandTagContent={(num: any) => `共有${num}个`} />
|
||||
// </div>
|
||||
// );
|
||||
// },
|
||||
},
|
||||
{
|
||||
title: 'MessageSize迁移进度(MB)',
|
||||
dataIndex: 'movedSize',
|
||||
key: 'movedSize',
|
||||
render(t: any, r: any) {
|
||||
const movedSize = r.movedSize ? Number(Utils.formatAssignSize(t, 'MB')) : 0;
|
||||
const totalSize = r.totalSize ? Number(Utils.formatAssignSize(t, 'MB')) : 0;
|
||||
return (
|
||||
<div className="message-size">
|
||||
<Tooltip title={(movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0) + '%'}>
|
||||
<Progress
|
||||
percent={movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0}
|
||||
strokeColor="#556EE6"
|
||||
showInfo={false}
|
||||
/>
|
||||
</Tooltip>
|
||||
<span>{movedSize + '/' + totalSize}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '分区进度',
|
||||
dataIndex: 'partitionRate',
|
||||
key: 'partitionRate',
|
||||
render: (t: any, r: any) => {
|
||||
return (r.success || r.success === 0 ? r.success : '-') + '/' + (r.total || r.total === 0 ? r.total : '-');
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '预计剩余时长',
|
||||
dataIndex: 'remainTime',
|
||||
key: 'remainTime',
|
||||
render(t: any, r: any) {
|
||||
return t ? Utils.transUnitTime(t) : t === 0 ? t : '-';
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: '当前副本数',
|
||||
// dataIndex: 'progress',
|
||||
// key: 'progress',
|
||||
// render: (_t: any, r: any) => {
|
||||
// return r.success + '/' + (r.success + r.doing + r.fail);
|
||||
// },
|
||||
// },
|
||||
];
|
||||
return columns;
|
||||
};
|
||||
|
||||
// * 获取任务明细-Topic迁移列表配置
|
||||
export const getMoveBalanceColumns = (arg?: any) => {
|
||||
const columns = [
|
||||
{
|
||||
title: 'Topic',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
fixed: 'left',
|
||||
width: 102,
|
||||
render: (t: any, r: any) => {
|
||||
return (
|
||||
<span>
|
||||
{t}
|
||||
<Badge
|
||||
style={{ marginLeft: '6px' }}
|
||||
status={r?.status === 1 ? 'warning' : r?.status === 4 ? 'error' : r?.status === 3 ? 'success' : 'warning'}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '分区',
|
||||
dataIndex: 'partitions',
|
||||
key: 'partitions',
|
||||
render: (t: any, r: any) => {
|
||||
return t && t.length > 0 ? t.join('、') : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '源BrokerID',
|
||||
dataIndex: 'sourceBrokers',
|
||||
key: 'sourceBrokers',
|
||||
render: (t: any, r: any) => {
|
||||
return t && t.length > 0 ? t.join('、') : '-';
|
||||
},
|
||||
// render(t: any, r: any) {
|
||||
// return (
|
||||
// <div style={{ width: '100px' }}>
|
||||
// <TagsWithHide list={t} expandTagContent={(num: any) => `共有${num}个`} />
|
||||
// </div>
|
||||
// );
|
||||
// },
|
||||
},
|
||||
{
|
||||
title: '目标BrokerID',
|
||||
dataIndex: 'desBrokers',
|
||||
key: 'desBrokers',
|
||||
render: (t: any, r: any) => {
|
||||
return t && t.length > 0 ? t.join('、') : '-';
|
||||
},
|
||||
// render(t: any, r: any) {
|
||||
// return (
|
||||
// <div style={{ width: '100px' }}>
|
||||
// <TagsWithHide list={t} expandTagContent={(num: any) => `共有${num}个`} />
|
||||
// </div>
|
||||
// );
|
||||
// },
|
||||
},
|
||||
{
|
||||
title: '当前数据保存时间 (h)',
|
||||
dataIndex: 'currentTimeSpent',
|
||||
key: 'currentTimeSpent',
|
||||
render: (t: any, r: any) => {
|
||||
return t || t === 0 ? Utils.transMSecondToHour(+t) : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '迁移数据时间范围 (h)',
|
||||
dataIndex: 'moveTimeSpent',
|
||||
key: 'moveTimeSpent',
|
||||
render: (t: any, r: any) => {
|
||||
return t || t === 0 ? Utils.transMSecondToHour(+t) : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'MessageSize迁移进度(MB)',
|
||||
dataIndex: 'movedSize',
|
||||
key: 'movedSize',
|
||||
render(t: any, r: any) {
|
||||
const movedSize = r.movedSize ? Number(Utils.formatAssignSize(t, 'MB')) : 0;
|
||||
const totalSize = r.totalSize ? Number(Utils.formatAssignSize(t, 'MB')) : 0;
|
||||
return (
|
||||
<div className="message-size">
|
||||
<Tooltip title={(movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0) + '%'}>
|
||||
<Progress
|
||||
percent={movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0}
|
||||
strokeColor="#556EE6"
|
||||
showInfo={false}
|
||||
/>
|
||||
</Tooltip>
|
||||
<span>{movedSize + '/' + totalSize}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: '需迁移MessageSize(MB)',
|
||||
// dataIndex: 'totalSize',
|
||||
// key: 'totalSize',
|
||||
// width: 100,
|
||||
// render: (t: any, r: any) => {
|
||||
// return t || t === 0 ? formatAssignSize(t, 'MB') : '-';
|
||||
// },
|
||||
// },
|
||||
{
|
||||
title: '分区进度',
|
||||
dataIndex: 'partitionProgress',
|
||||
key: 'partitionProgress',
|
||||
render: (t: any, r: any) => {
|
||||
return (r.success || r.success === 0 ? r.success : '-') + '/' + (r.total || r.total === 0 ? r.total : '-');
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: '已完成MessageSize (MB)',
|
||||
// dataIndex: 'movedSize',
|
||||
// key: 'movedSize',
|
||||
// width: 100,
|
||||
// render: (t: any, r: any) => {
|
||||
// return t || t === 0 ? formatAssignSize(t, 'MB') : '-';
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// title: '状态',
|
||||
// dataIndex: 'status',
|
||||
// key: 'status',
|
||||
// width: 100,
|
||||
// render: (t: any) => {
|
||||
// return (
|
||||
// <span>
|
||||
// <Badge status={t === 1 ? 'warning' : t === 4 ? 'error' : t === 3 ? 'success' : 'warning'} />
|
||||
// {runningStatusEnum[t]}
|
||||
// </span>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
{
|
||||
title: '预计剩余时长',
|
||||
dataIndex: 'remainTime',
|
||||
key: 'remainTime',
|
||||
render(t: any, r: any) {
|
||||
return t ? Utils.transUnitTime(t) : t === 0 ? t : '-';
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: '当前副本数',
|
||||
// dataIndex: 'progress',
|
||||
// key: 'progress',
|
||||
// render: (_t: any, r: any) => {
|
||||
// return r.success + '/' + (r.success + r.doing + r.fail);
|
||||
// },
|
||||
// },
|
||||
];
|
||||
return columns;
|
||||
};
|
||||
|
||||
// * 获取任务明细-集群均衡列表配置
|
||||
export const getClusterBalanceColumns = (arg?: any) => {
|
||||
const columns = [
|
||||
{
|
||||
title: 'Topic',
|
||||
dataIndex: 'topicName',
|
||||
key: 'topicName',
|
||||
width: 102,
|
||||
needTooltip: true,
|
||||
},
|
||||
{
|
||||
title: '分区',
|
||||
dataIndex: 'partition',
|
||||
key: 'partition',
|
||||
},
|
||||
{
|
||||
title: '源BrokerID',
|
||||
dataIndex: 'sourceBrokerIds',
|
||||
key: 'sourceBrokerIds',
|
||||
},
|
||||
{
|
||||
title: '目标BrokerID',
|
||||
dataIndex: 'desBrokerIds',
|
||||
key: 'desBrokerIds',
|
||||
},
|
||||
{
|
||||
title: '当前数据保存时间 (h)',
|
||||
dataIndex: 'currentTimeSpent',
|
||||
key: 'currentTimeSpent',
|
||||
},
|
||||
{
|
||||
title: '迁移数据时间范围 (h)',
|
||||
dataIndex: 'currentTimeSpent',
|
||||
key: 'currentTimeSpent',
|
||||
},
|
||||
{
|
||||
title: '需迁移MessageSize(MB)',
|
||||
dataIndex: 'totalSize',
|
||||
key: 'totalSize',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '分区进度',
|
||||
dataIndex: 'partitionProgress',
|
||||
key: 'partitionProgress',
|
||||
},
|
||||
{
|
||||
title: '已完成MessageSize (MB)',
|
||||
dataIndex: 'movedSize',
|
||||
key: 'movedSize',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render(t: any, r: any) {
|
||||
return runningStatusEnum[t];
|
||||
},
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '预计剩余时长',
|
||||
dataIndex: 'remainTime',
|
||||
key: 'remainTime',
|
||||
},
|
||||
// {
|
||||
// title: '当前副本数',
|
||||
// dataIndex: 'progress',
|
||||
// key: 'progress',
|
||||
// render: (_t: any, r: any) => {
|
||||
// return r.success + '/' + (r.success + r.doing + r.fail);
|
||||
// },
|
||||
// },
|
||||
];
|
||||
return columns;
|
||||
};
|
||||
|
||||
export const getNodeTrafficColumns = (arg?: any) => {
|
||||
const columns = [
|
||||
{
|
||||
title: 'Broker',
|
||||
dataIndex: 'brokerId',
|
||||
key: 'brokerId',
|
||||
},
|
||||
{
|
||||
title: 'Host',
|
||||
dataIndex: 'brokerHost',
|
||||
key: 'brokerHost',
|
||||
},
|
||||
{
|
||||
title: 'Job Bytes In(MB/s)',
|
||||
dataIndex: 'byteInJob',
|
||||
key: 'byteInJob',
|
||||
render: (t: any, r: any) => {
|
||||
return (
|
||||
<div>
|
||||
<span>{t || t === 0 ? Utils.transBToMB(t) : '-'}</span>
|
||||
{<PopoverBroker title="Job Bytes In" data={r?.inBrokers} />}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Job Bytes Out(MB/s)',
|
||||
dataIndex: 'byteOutJob',
|
||||
key: 'byteOutJob',
|
||||
render: (t: any, r: any) => {
|
||||
return (
|
||||
<div>
|
||||
<span>{t || t === 0 ? Utils.transBToMB(t) : '-'}</span>
|
||||
{r?.outBrokers && <PopoverBroker title="Job Bytes Out" data={r?.outBrokers} />}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Total BytesIn(MB/s)',
|
||||
dataIndex: 'byteInTotal',
|
||||
key: 'byteInTotal',
|
||||
render: (t: any, r: any) => {
|
||||
return t || t === 0 ? Utils.transBToMB(t) : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Total Bytes Out(MB/s)',
|
||||
dataIndex: 'byteOutTotal',
|
||||
key: 'byteOutTotal',
|
||||
render: (t: any, r: any) => {
|
||||
return t || t === 0 ? Utils.transBToMB(t) : '-';
|
||||
},
|
||||
},
|
||||
];
|
||||
return columns;
|
||||
};
|
||||
|
||||
export const defaultPagination = {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
position: 'bottomRight',
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
|
||||
};
|
||||
|
||||
const KB = 1024;
|
||||
const MB = KB * KB;
|
||||
const GB = MB * KB;
|
||||
const TB = GB * KB;
|
||||
|
||||
export const formatAssignSize = (size: number, convertTarget: string, fix = 2) => {
|
||||
if (size === undefined || size === null) return '';
|
||||
if (convertTarget === undefined || convertTarget === null) return size;
|
||||
if (convertTarget === 'KB') return `${(size / KB).toFixed(fix)}`;
|
||||
if (convertTarget === 'MB') return `${(size / MB).toFixed(fix)}`;
|
||||
if (convertTarget === 'GB') return `${(size / GB).toFixed(fix)}`;
|
||||
|
||||
return `${(size / TB).toFixed(fix)}TB`;
|
||||
};
|
||||
156
km-console/packages/layout-clusters-fe/src/pages/Jobs/index.less
Normal file
156
km-console/packages/layout-clusters-fe/src/pages/Jobs/index.less
Normal file
@@ -0,0 +1,156 @@
|
||||
// .table {
|
||||
// background: #F8F9FA;
|
||||
// // margin-top: 12px;
|
||||
// border-radius: 8px;
|
||||
// .dcloud-table {
|
||||
// height: 210px;
|
||||
// overflow: auto;
|
||||
// background-color: transparent;
|
||||
// .dcloud-table-content .dcloud-table-cell {
|
||||
// background-color: transparent;
|
||||
// }
|
||||
// }
|
||||
// .dcloud-pagination {
|
||||
// height: 32px;
|
||||
// margin-bottom: 0;
|
||||
// margin-top: 8px;
|
||||
// }
|
||||
// }
|
||||
|
||||
.edit-flow-limit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
input {
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-expanded-table {
|
||||
border-radius: 20px;
|
||||
.dcloud-table-container {
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.dcloud-table {
|
||||
td,
|
||||
th {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-popover-borker {
|
||||
width: 281px;
|
||||
padding-top: 0 !important;
|
||||
.dcloud-popover-arrow {
|
||||
display: none;
|
||||
}
|
||||
.dcloud-popover-inner {
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.04), 0 6px 12px 12px rgba(0, 0, 0, 0.04), 0 6px 10px 0 rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.dcloud-popover-inner-content {
|
||||
padding: 0 24px 12px;
|
||||
}
|
||||
.dcloud-popover-title {
|
||||
padding: 12px 24px;
|
||||
font-family: @font-family-bold;
|
||||
font-size: 16px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
// .dcloud-table{
|
||||
// margin: 0 !important;
|
||||
// }
|
||||
}
|
||||
|
||||
.job-detail {
|
||||
.dcloud-table-cell {
|
||||
padding: 7px 16px 8px 2px !important;
|
||||
}
|
||||
.dcloud-table-row-expand-icon-cell {
|
||||
padding: 7px 7px 5px 24px !important;
|
||||
}
|
||||
|
||||
.dcloud-table-expanded-row-fixed {
|
||||
padding: 16px 20px !important;
|
||||
.dcloud-table-cell {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.run-result {
|
||||
& > span {
|
||||
display: inline-block;
|
||||
}
|
||||
& > span:not(:last-child) {
|
||||
margin-right: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.expanded-table {
|
||||
.dcloud-table-container::after {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.dcloud-pagination{
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.balance-list-title {
|
||||
font-size: 12px;
|
||||
color: #74788d;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
.titleImg {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 17px;
|
||||
margin-left: 2px;
|
||||
background-image: url('../../assets/arrowSmall.svg');
|
||||
background-position: center, center;
|
||||
background-repeat: no-repeat, repeat;
|
||||
}
|
||||
}
|
||||
|
||||
.balance-list-contert {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
&-text,&-text-right{
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
}
|
||||
&-text-right{
|
||||
text-align: right;
|
||||
color: #556EE6;
|
||||
}
|
||||
&-img {
|
||||
display: inline-block;
|
||||
width: 26px;
|
||||
height: 17px;
|
||||
margin: 0 3px 0 5px;
|
||||
background-image: url('../../assets/arrowLarge.svg');
|
||||
background-position: center, center;
|
||||
background-repeat: no-repeat, repeat;
|
||||
}
|
||||
}
|
||||
|
||||
.message-size {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.dcloud-progress {
|
||||
width: 140px;
|
||||
&-inner {
|
||||
background-color: #ececf1;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
& > span {
|
||||
margin-left: 4px;
|
||||
color: #adb5bc;
|
||||
transform: scale(0.83, 0.83);
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
285
km-console/packages/layout-clusters-fe/src/pages/Jobs/index.tsx
Normal file
285
km-console/packages/layout-clusters-fe/src/pages/Jobs/index.tsx
Normal file
@@ -0,0 +1,285 @@
|
||||
import React, { useState, useEffect, memo } from 'react';
|
||||
import { useParams, useHistory, useLocation } from 'react-router-dom';
|
||||
import { ProTable, Drawer, Utils, AppContainer, Form, Select, Input, Button, message, Modal } from 'knowdesign';
|
||||
import API from '../../api';
|
||||
import { getJobsListColumns, defaultPagination, runningStatus, jobType } from './config';
|
||||
import JobsCheck from '@src/components/CardBar/JobsCheck';
|
||||
import DBreadcrumb from 'knowdesign/lib/extend/d-breadcrumb';
|
||||
import { ViewJobsProgress } from './ViewJobsProgress';
|
||||
import './index.less';
|
||||
import ReplicaChange from '@src/components/TopicJob/ReplicaChange';
|
||||
import ReplicaMove from '@src/components/TopicJob/ReplicaMove';
|
||||
import BalanceDrawer from '../LoadRebalance/BalanceDrawer';
|
||||
const { request } = Utils;
|
||||
|
||||
const JobsList: React.FC = (props: any) => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [form] = Form.useForm();
|
||||
const urlParams = useParams<any>(); // 获取地址栏参数
|
||||
const history = useHistory();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
const [searchWords, setSearchWords] = useState<{ type: any; jobTarget: any; status: any }>();
|
||||
// const [filteredInfo, setFilteredInfo] = useState(null);
|
||||
const [pagination, setPagination] = useState<any>(defaultPagination);
|
||||
const [viewVisble, setViewVisble] = useState<any>(false);
|
||||
const [record, setRecord] = useState(null); // 获取当前点击行的数据;
|
||||
const [changeVisible, setChangeVisible] = useState(false);
|
||||
const [moveVisible, setMoveVisible] = useState(false);
|
||||
const [balanceVisible, setBalanceVisible] = useState(false);
|
||||
const [balanceFromData, setBalanceFormData] = useState(false);
|
||||
|
||||
// 默认排序
|
||||
const defaultSorter = {
|
||||
sortField: 'brokerId',
|
||||
sortType: 'asc',
|
||||
};
|
||||
|
||||
// 请求接口获取数据
|
||||
const genData = async ({ pageNo, pageSize }: any) => {
|
||||
if (urlParams?.clusterId === undefined) return;
|
||||
// filters = filters || filteredInfo;
|
||||
setLoading(true);
|
||||
const params = {
|
||||
...searchWords,
|
||||
pageNo,
|
||||
pageSize,
|
||||
type: searchWords?.type === 0 || searchWords?.type ? searchWords?.type : -1,
|
||||
};
|
||||
|
||||
request(API.getJobsList(urlParams?.clusterId), { method: 'POST', data: params })
|
||||
.then((res: any) => {
|
||||
setPagination({
|
||||
current: res.pagination?.pageNo,
|
||||
pageSize: res.pagination?.pageSize,
|
||||
total: res.pagination?.total,
|
||||
});
|
||||
setData(res?.bizData || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setData([]);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
// 获取立即均衡formData
|
||||
const getBalanceFormData = (jobId: any, clusterId: any) => {
|
||||
const params = {
|
||||
clusterId,
|
||||
jobId,
|
||||
};
|
||||
return request(API.getJobsTaskData(params.clusterId, params.jobId), params);
|
||||
};
|
||||
|
||||
// 查询
|
||||
const onFinish = (formData: any) => {
|
||||
setSearchWords(formData);
|
||||
};
|
||||
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
// setFilteredInfo(filters);
|
||||
genData({ pageNo: pagination.current, pageSize: pagination.pageSize, filters, sorter });
|
||||
};
|
||||
|
||||
const getSearchKeywords = (value: string) => {
|
||||
// setSearchKeywords(value);
|
||||
};
|
||||
|
||||
// 删除modal
|
||||
const onDelete = (record: any) => {
|
||||
Modal.confirm({
|
||||
title: `确认删除"任务【${record.id}】"吗?`,
|
||||
okText: '删除',
|
||||
okType: 'primary',
|
||||
centered: true,
|
||||
cancelText: '取消',
|
||||
okButtonProps: {
|
||||
// disabled: record.status === 1,
|
||||
size: 'small',
|
||||
danger: true,
|
||||
},
|
||||
cancelButtonProps: {
|
||||
size: 'small',
|
||||
},
|
||||
maskClosable: false,
|
||||
onOk(close) {
|
||||
// 缺少判断当前任务是否是doing的状态
|
||||
return Utils.delete(API.getJobsDelete(urlParams?.clusterId, record?.id)).then((_) => {
|
||||
message.success('删除任务成功');
|
||||
// getConfigList();
|
||||
genData({
|
||||
pageNo: 1,
|
||||
pageSize: pagination.pageSize,
|
||||
});
|
||||
close();
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const setViewProgress = (record: any, visibleType?: number) => {
|
||||
setRecord(record);
|
||||
if (visibleType === 0) {
|
||||
setMoveVisible(true);
|
||||
} else if (visibleType === 1) {
|
||||
setChangeVisible(true);
|
||||
} else if (visibleType === 2) {
|
||||
getBalanceFormData(record.id, urlParams.clusterId).then((res: any) => {
|
||||
const jobData = (res && JSON.parse(res?.jobData)) || {};
|
||||
setBalanceFormData({
|
||||
...jobData,
|
||||
jobId: record.id,
|
||||
record,
|
||||
jobData,
|
||||
});
|
||||
setBalanceVisible(true);
|
||||
});
|
||||
} else {
|
||||
setViewVisble(true);
|
||||
}
|
||||
};
|
||||
|
||||
const onclose = () => {
|
||||
setChangeVisible(false);
|
||||
setMoveVisible(false);
|
||||
setBalanceVisible(false);
|
||||
setRecord(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
genData({
|
||||
pageNo: 1,
|
||||
pageSize: pagination.pageSize,
|
||||
// sorter: defaultSorter
|
||||
});
|
||||
}, [searchWords]);
|
||||
|
||||
return (
|
||||
<div key="brokerList">
|
||||
<div className="breadcrumb">
|
||||
<DBreadcrumb
|
||||
breadcrumbs={[
|
||||
{ label: '多集群管理', aHref: '/' },
|
||||
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
|
||||
{ label: 'Job', aHref: `` },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ margin: '12px 0' }}>
|
||||
<JobsCheck />
|
||||
</div>
|
||||
{/* <Form form={form} layout="inline" onFinish={onFinish}> */}
|
||||
<div className="clustom-table-content">
|
||||
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '12px' }}>
|
||||
<Form form={form} layout="inline" onFinish={onFinish}>
|
||||
<Form.Item name="type">
|
||||
<Select
|
||||
allowClear
|
||||
options={jobType}
|
||||
style={{ width: '190px' }}
|
||||
className={'detail-table-select'}
|
||||
placeholder="选择任务类型"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="jobTarget">
|
||||
<Input allowClear style={{ width: '190px' }} placeholder="请输入执行任务对象" />
|
||||
</Form.Item>
|
||||
<Form.Item name="status">
|
||||
<Select
|
||||
mode="multiple"
|
||||
maxTagCount={1}
|
||||
options={runningStatus}
|
||||
style={{ width: '190px' }}
|
||||
className={'detail-table-select'}
|
||||
placeholder="选择运行状态"
|
||||
showArrow
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<div>
|
||||
<Form style={{ justifyContent: 'flex-end' }} form={form} layout="inline" onFinish={onFinish}>
|
||||
<Form.Item style={{ marginRight: 0 }}>
|
||||
<Button type="primary" ghost htmlType="submit">
|
||||
查询
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
{/* </Form> */}
|
||||
<ProTable
|
||||
key="brokerTable"
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
tableId: 'jobs_list',
|
||||
showHeader: false,
|
||||
rowKey: 'jobs_list',
|
||||
loading: loading,
|
||||
columns: getJobsListColumns({ onDelete, setViewProgress }),
|
||||
dataSource: data,
|
||||
paginationProps: { ...pagination },
|
||||
attrs: {
|
||||
// className: 'frameless-table', // 纯无边框表格类名
|
||||
onChange: onTableChange,
|
||||
scroll: { x: 'max-content', y: 'calc(100vh - 400px)' }, // calc(100vh - 270px)
|
||||
bordered: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{viewVisble && <ViewJobsProgress visible={viewVisble} setVisble={setViewVisble} record={record} />}
|
||||
{/* 批量扩缩副本 */}
|
||||
{changeVisible && (
|
||||
<ReplicaChange
|
||||
drawerVisible={changeVisible}
|
||||
jobId={record?.id}
|
||||
jobStatus={record?.jobStatus}
|
||||
topics={record?.target?.split(',') || []}
|
||||
onClose={onclose}
|
||||
genData={() =>
|
||||
genData({
|
||||
pageNo: 1,
|
||||
pageSize: pagination.pageSize,
|
||||
// sorter: defaultSorter
|
||||
})
|
||||
}
|
||||
></ReplicaChange>
|
||||
)}
|
||||
{/* 批量迁移 */}
|
||||
{moveVisible && (
|
||||
<ReplicaMove
|
||||
drawerVisible={moveVisible}
|
||||
jobId={record?.id}
|
||||
jobStatus={record?.jobStatus}
|
||||
topics={record?.target?.split(',') || []}
|
||||
onClose={onclose}
|
||||
genData={() =>
|
||||
genData({
|
||||
pageNo: 1,
|
||||
pageSize: pagination.pageSize,
|
||||
// sorter: defaultSorter
|
||||
})
|
||||
}
|
||||
></ReplicaMove>
|
||||
)}
|
||||
{/* 立即均衡 */}
|
||||
{balanceVisible && (
|
||||
<BalanceDrawer
|
||||
visible={balanceVisible}
|
||||
formData={balanceFromData}
|
||||
onClose={onclose}
|
||||
genData={() =>
|
||||
genData({
|
||||
pageNo: 1,
|
||||
pageSize: pagination.pageSize,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JobsList;
|
||||
@@ -0,0 +1,706 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Utils,
|
||||
Drawer,
|
||||
Button,
|
||||
Form,
|
||||
Space,
|
||||
Divider,
|
||||
AppContainer,
|
||||
Radio,
|
||||
InputNumber,
|
||||
Transfer,
|
||||
Select,
|
||||
message,
|
||||
IconFont,
|
||||
Tooltip,
|
||||
} from 'knowdesign';
|
||||
|
||||
import CronInput from './CronInput';
|
||||
import BalanceEditTable from './BalanceEditTable';
|
||||
import PlanDrawer from './PlanDrawer';
|
||||
import api from '../../api';
|
||||
import './style/BalanceDrawer.less';
|
||||
interface PropsType {
|
||||
onClose: Function;
|
||||
visible: boolean;
|
||||
isCycle?: boolean;
|
||||
formData?: any;
|
||||
genData?: any;
|
||||
}
|
||||
|
||||
const IndexCalculations = [
|
||||
{
|
||||
label: '近5mins',
|
||||
value: 5 * 60,
|
||||
},
|
||||
{
|
||||
label: '近10mins',
|
||||
value: 10 * 60,
|
||||
},
|
||||
{
|
||||
label: '近30mins',
|
||||
value: 30 * 60,
|
||||
},
|
||||
{
|
||||
label: '近1h',
|
||||
value: 60 * 60,
|
||||
},
|
||||
{
|
||||
label: '近6h',
|
||||
value: 6 * 60 * 60,
|
||||
},
|
||||
{
|
||||
label: '近12h',
|
||||
value: 12 * 60 * 60,
|
||||
},
|
||||
{
|
||||
label: '近24h',
|
||||
value: 24 * 60 * 60,
|
||||
},
|
||||
];
|
||||
|
||||
const BalancedDimensions = [
|
||||
// 本期没有CPU
|
||||
// {
|
||||
// label: 'CPU',
|
||||
// value: 'cpu',
|
||||
// },
|
||||
{
|
||||
label: 'Disk',
|
||||
value: 'disk',
|
||||
},
|
||||
{
|
||||
label: 'BytesIn',
|
||||
value: 'bytesIn',
|
||||
},
|
||||
{
|
||||
label: 'BytesOut',
|
||||
value: 'bytesOut',
|
||||
},
|
||||
];
|
||||
|
||||
const Schedules1 = [
|
||||
{
|
||||
label: '每天',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '每周',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
label: '每月',
|
||||
value: 3,
|
||||
},
|
||||
];
|
||||
|
||||
const SchedulesWeeks = [
|
||||
{
|
||||
label: '每周一',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '每周二',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
label: '每周三',
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
label: '每周四',
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
label: '每周五',
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
label: '每周六',
|
||||
value: 6,
|
||||
},
|
||||
{
|
||||
label: '每周日',
|
||||
value: 7,
|
||||
},
|
||||
];
|
||||
|
||||
const SchedulesMouths: any[] = [];
|
||||
for (let i = 1; i <= 28; i++) {
|
||||
SchedulesMouths.push({
|
||||
label: `每月${i}日`,
|
||||
value: i,
|
||||
});
|
||||
}
|
||||
|
||||
const BalanceDrawer: React.FC<PropsType> = ({ onClose, visible, isCycle = false, formData, genData }) => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [form] = Form.useForm();
|
||||
const customFormRef = useRef<any>();
|
||||
const [nodeData, setNodeData] = useState([]);
|
||||
const [nodeTargetKeys, setNodeTargetKeys] = useState([]);
|
||||
const [topicData, setTopicData] = useState([]);
|
||||
const [topicTargetKeys, setTopicTargetKeys] = useState([]);
|
||||
const [tableData, setTableData] = useState<any[]>([]);
|
||||
const [dimension, setDimension] = useState<string[]>(['disk', 'bytesIn', 'bytesOut']);
|
||||
|
||||
const [planVisible, setPlanVisible] = useState<boolean>(false);
|
||||
const [planDetailData, setPlanDetailData] = useState({});
|
||||
const [parallelNum, setParallelNum] = useState(0);
|
||||
const [executionStrategy, setExecutionStrategy] = useState(1);
|
||||
useEffect(() => {
|
||||
getNodeList();
|
||||
getTopicList();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
init();
|
||||
}, [visible, formData]);
|
||||
|
||||
useEffect(() => {
|
||||
setNodeTargetKeys(formData?.brokers || []);
|
||||
}, [nodeData]);
|
||||
|
||||
useEffect(() => {
|
||||
setTopicTargetKeys(formData?.topicBlackList || []);
|
||||
}, [topicData]);
|
||||
|
||||
const init = () => {
|
||||
if (formData && Object.keys(formData).length > 0) {
|
||||
console.log(formData, '有FormData');
|
||||
const tableData = formData?.clusterBalanceIntervalList?.map((item: any) => {
|
||||
const finfIndex = BalancedDimensions.findIndex((item1) => item1?.value === item?.type);
|
||||
return {
|
||||
...item,
|
||||
name: BalancedDimensions[finfIndex]?.label,
|
||||
};
|
||||
});
|
||||
setTableData(tableData || []);
|
||||
const dimension = formData?.clusterBalanceIntervalList?.map((item: any) => {
|
||||
return item?.type;
|
||||
});
|
||||
|
||||
form.setFieldsValue({
|
||||
...formData,
|
||||
brokers: formData?.brokers || [],
|
||||
topicBlackList: formData?.topicBlackList || [],
|
||||
dimension,
|
||||
throttleUnitM: formData?.throttleUnitB / 1024 / 1024,
|
||||
});
|
||||
} else {
|
||||
const defaultDimension = ['disk', 'bytesIn', 'bytesOut'];
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ dimension: defaultDimension, metricCalculationPeriod: 600 });
|
||||
const res = defaultDimension?.map((item, index) => {
|
||||
const finfIndex = BalancedDimensions.findIndex((item1) => item1.value === item);
|
||||
return {
|
||||
type: item,
|
||||
name: BalancedDimensions[finfIndex]?.label,
|
||||
intervalPercent: 10,
|
||||
priority: index + 1,
|
||||
};
|
||||
});
|
||||
console.log(res, '表单回显立即均衡');
|
||||
setTableData(res);
|
||||
setDimension(['disk', 'bytesIn', 'bytesOut']);
|
||||
setNodeTargetKeys([]);
|
||||
setTopicTargetKeys([]);
|
||||
}
|
||||
};
|
||||
const submit = () => {
|
||||
// 周期均衡 / 立即均衡
|
||||
customFormRef.current.editTableValidate().then((res: any) => {
|
||||
form.validateFields().then((values) => {
|
||||
const params = {
|
||||
...values,
|
||||
clusterId: global?.clusterInfo?.id,
|
||||
clusterBalanceIntervalList: tableData,
|
||||
scheduleJob: isCycle || false,
|
||||
throttleUnitB: values?.throttleUnitM * 1024 * 1024,
|
||||
};
|
||||
|
||||
if (!isCycle) {
|
||||
if (values?.priority === 'throughput') {
|
||||
params.parallelNum = 0;
|
||||
params.executionStrategy = 1;
|
||||
} else if (values?.priority === 'stability') {
|
||||
params.parallelNum = 1;
|
||||
params.executionStrategy = 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (formData?.jobId) {
|
||||
const handledData = {
|
||||
creator: JSON.parse(global?.userInfo)?.userName,
|
||||
jobType: 2, // type 0 topic迁移 1 扩缩容 2集群均衡
|
||||
planTime: formData?.record?.planTime,
|
||||
jobStatus: formData?.record?.jobStatus || 2, //status 2 创建
|
||||
target: formData?.record?.target,
|
||||
id: formData?.record?.id,
|
||||
jobDesc: formData?.record?.description || '',
|
||||
jobData: JSON.stringify({ ...formData?.jobData, ...params }),
|
||||
};
|
||||
Utils.put(api.putJobsTaskData(global?.clusterInfo?.id), handledData)
|
||||
.then(() => {
|
||||
message.success('集群均衡任务编辑成功');
|
||||
drawerClose(true);
|
||||
setPlanVisible(false);
|
||||
genData();
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.log(err, 'err');
|
||||
});
|
||||
} else {
|
||||
Utils.request(api.balanceStrategy(global?.clusterInfo?.id), {
|
||||
method: 'POST',
|
||||
data: params,
|
||||
}).then((res: any) => {
|
||||
const dataDe = res || [];
|
||||
message.success(isCycle ? '成功创建周期均衡策略' : '成功创建立即均衡策略');
|
||||
drawerClose(true);
|
||||
setPlanVisible(false);
|
||||
isCycle && genData();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const preview = () => {
|
||||
// 立即均衡
|
||||
customFormRef.current.editTableValidate().then((res: any) => {
|
||||
form.validateFields().then((values) => {
|
||||
const params = {
|
||||
...values,
|
||||
clusterId: global?.clusterInfo?.id,
|
||||
clusterBalanceIntervalList: tableData,
|
||||
scheduleJob: isCycle || false,
|
||||
throttleUnitB: values?.throttleUnitM * 1024 * 1024,
|
||||
// parallelNum: !isCycle ? values?.priority === 'throughput'? 0 : : null
|
||||
};
|
||||
|
||||
if (!isCycle) {
|
||||
if (values?.priority === 'throughput') {
|
||||
params.parallelNum = 0;
|
||||
params.executionStrategy = 1;
|
||||
} else if (values?.priority === 'stability') {
|
||||
params.parallelNum = 1;
|
||||
params.executionStrategy = 2;
|
||||
}
|
||||
}
|
||||
|
||||
// 预览计划
|
||||
if (formData?.jobId) {
|
||||
Utils.request(api.getBalancePlan(global?.clusterInfo?.id, formData?.jobId), {
|
||||
method: 'GET',
|
||||
}).then((res: any) => {
|
||||
const dataDe = res || {};
|
||||
setPlanDetailData(dataDe);
|
||||
setPlanVisible(true);
|
||||
});
|
||||
} else {
|
||||
Utils.request(api.balancePreview(global?.clusterInfo?.id), {
|
||||
method: 'POST',
|
||||
data: params,
|
||||
}).then((res: any) => {
|
||||
const dataDe = res || {};
|
||||
setPlanDetailData(dataDe);
|
||||
setPlanVisible(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const nodeChange = (val: any) => {
|
||||
setNodeTargetKeys(val);
|
||||
};
|
||||
|
||||
const topicChange = (val: any) => {
|
||||
setTopicTargetKeys(val);
|
||||
};
|
||||
|
||||
const getNodeList = () => {
|
||||
Utils.request(api.getBrokersMetaList(global?.clusterInfo?.id), {
|
||||
method: 'GET',
|
||||
}).then((res: any) => {
|
||||
const dataDe = res || [];
|
||||
const dataHandle = dataDe.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
key: item.brokerId,
|
||||
title: `${item.brokerId} (${item.host})`,
|
||||
};
|
||||
});
|
||||
setNodeData(dataHandle);
|
||||
});
|
||||
};
|
||||
const getTopicList = () => {
|
||||
Utils.request(api.getTopicMetaList(global?.clusterInfo?.id), {
|
||||
method: 'GET',
|
||||
}).then((res: any) => {
|
||||
const dataDe = res || [];
|
||||
const dataHandle = dataDe.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
key: item.topicName,
|
||||
title: item.topicName,
|
||||
};
|
||||
});
|
||||
setTopicData(dataHandle);
|
||||
});
|
||||
};
|
||||
|
||||
const dimensionChange = (val: string[]) => {
|
||||
const res = val?.map((item, index) => {
|
||||
const finfIndex = BalancedDimensions.findIndex((item1) => item1.value === item);
|
||||
const tableIndex = tableData?.findIndex((item2) => item2.type === item);
|
||||
return {
|
||||
type: item,
|
||||
name: BalancedDimensions[finfIndex]?.label,
|
||||
intervalPercent: tableIndex > -1 ? tableData[tableIndex].intervalPercent : 10,
|
||||
priority: index + 1,
|
||||
};
|
||||
});
|
||||
setTableData(res);
|
||||
setDimension(val);
|
||||
};
|
||||
|
||||
const tableDataChange = (data: any[]) => {
|
||||
setTableData(data);
|
||||
};
|
||||
|
||||
const planClose = () => {
|
||||
setPlanVisible(false);
|
||||
// onClose();
|
||||
};
|
||||
|
||||
const balanceImmediate = () => {
|
||||
submit();
|
||||
};
|
||||
|
||||
const drawerClose = (isArg?: boolean) => {
|
||||
isArg ? onClose(isArg) : onClose();
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const priorityChange = (e: any) => {
|
||||
if (e.target.value === 'throughput') {
|
||||
setParallelNum(0);
|
||||
setExecutionStrategy(1);
|
||||
} else if (e.target.value === 'stability') {
|
||||
setParallelNum(1);
|
||||
setExecutionStrategy(2);
|
||||
} else {
|
||||
form.setFieldsValue({ parallelNum, executionStrategy });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PlanDrawer
|
||||
visible={planVisible}
|
||||
onClose={planClose}
|
||||
balanceImmediate={balanceImmediate}
|
||||
detailData={planDetailData}
|
||||
isPrevew={true}
|
||||
isEdit={formData?.jobId ? true : false}
|
||||
/>
|
||||
<Drawer
|
||||
title={isCycle ? '周期均衡' : '立即均衡'}
|
||||
width="600px"
|
||||
destroyOnClose={true}
|
||||
className="balance-drawer"
|
||||
onClose={() => drawerClose()}
|
||||
visible={visible}
|
||||
maskClosable={false}
|
||||
extra={
|
||||
<Space>
|
||||
<Button size="small" onClick={() => drawerClose()}>
|
||||
取消
|
||||
</Button>
|
||||
{isCycle ? (
|
||||
<Button type="primary" size="small" disabled={false} onClick={submit}>
|
||||
确定
|
||||
</Button>
|
||||
) : (
|
||||
<Button className="btn-width84" type="primary" size="small" disabled={false} onClick={preview}>
|
||||
预览计划
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Divider type="vertical" />
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
preserve={false}
|
||||
initialValues={{
|
||||
status: 1,
|
||||
}}
|
||||
>
|
||||
{/* {!isCycle && (
|
||||
<>
|
||||
<h6 className="form-title">均衡节点范围</h6>
|
||||
<Form.Item
|
||||
name="brokers"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `请选择!`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Transfer
|
||||
dataSource={nodeData}
|
||||
titles={['待选节点', '已选节点']}
|
||||
customHeader
|
||||
showSelectedCount
|
||||
locale={{
|
||||
itemUnit: '',
|
||||
itemsUnit: '',
|
||||
}}
|
||||
showSearch
|
||||
filterOption={(inputValue, option) => option.host.indexOf(inputValue) > -1}
|
||||
targetKeys={nodeTargetKeys}
|
||||
onChange={nodeChange}
|
||||
render={(item) => item.title}
|
||||
suffix={<IconFont type="icon-fangdajing" />}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)} */}
|
||||
|
||||
<h6 className="form-title">均衡策略</h6>
|
||||
<Form.Item
|
||||
name="metricCalculationPeriod"
|
||||
label="指标计算周期"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `请选择!`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select placeholder={`请选择指标计算周期`} options={IndexCalculations} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="均衡维度"
|
||||
name="dimension"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `请选择!`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
placeholder={`请选择均衡维度`}
|
||||
mode="multiple"
|
||||
value={dimension}
|
||||
options={BalancedDimensions}
|
||||
onChange={dimensionChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<BalanceEditTable ref={customFormRef} tableData={tableData} tableDataChange={tableDataChange} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="topicBlackList"
|
||||
label="Topic黑名单"
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
message: `请选择!`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Transfer
|
||||
dataSource={topicData}
|
||||
titles={['待选黑名单', '已选黑名单']}
|
||||
customHeader
|
||||
showSelectedCount
|
||||
locale={{
|
||||
itemUnit: '',
|
||||
itemsUnit: '',
|
||||
}}
|
||||
showSearch
|
||||
filterOption={(inputValue, option) => option.topicName.indexOf(inputValue) > -1}
|
||||
targetKeys={topicTargetKeys}
|
||||
onChange={topicChange}
|
||||
render={(item) => item.title}
|
||||
suffix={<IconFont type="icon-fangdajing" />}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<h6 className="form-title">运行配置</h6>
|
||||
{!isCycle && (
|
||||
<Form.Item label="" name="priority" rules={[{ required: true, message: 'Principle 不能为空' }]} initialValue="throughput">
|
||||
<Radio.Group onChange={priorityChange}>
|
||||
<Radio value="throughput">吞吐量优先</Radio>
|
||||
<Radio value="stability">稳定性优先</Radio>
|
||||
<Radio value="custom">自定义</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{!isCycle && (
|
||||
<Form.Item dependencies={['priority']} style={{ marginBottom: 0 }}>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue('priority') === 'custom' ? (
|
||||
<div className="form-item-group">
|
||||
<Form.Item
|
||||
name="parallelNum"
|
||||
label={
|
||||
<span>
|
||||
任务并行度
|
||||
<Tooltip title="每个节点同时迁移的副本数量">
|
||||
<IconFont style={{ fontSize: '14px', marginLeft: '5px' }} type="icon-zhushi" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `请输入!`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={999} placeholder="请输入任务并行度" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="executionStrategy"
|
||||
label={
|
||||
<span>
|
||||
执行策略
|
||||
<Tooltip title="不同大小副本执行的顺序">
|
||||
<IconFont style={{ fontSize: '14px', marginLeft: '5px' }} type="icon-zhushi" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `请选择!`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value={1}>优先最大副本</Radio>
|
||||
<Radio value={2}>优先最小副本</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{isCycle && (
|
||||
<Form.Item
|
||||
name="parallelNum"
|
||||
label={
|
||||
<span>
|
||||
任务并行度
|
||||
<Tooltip title="每个节点同时迁移的副本数量">
|
||||
<IconFont style={{ fontSize: '14px', marginLeft: '5px' }} type="icon-zhushi" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `请输入!`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={999} placeholder="请输入任务并行度" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{isCycle && (
|
||||
<Form.Item
|
||||
className="schedule-cron"
|
||||
name="scheduleCron"
|
||||
label="任务周期"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `请输入!`,
|
||||
},
|
||||
{
|
||||
validator: (_, value) => {
|
||||
const valArr = value.split(' ');
|
||||
if (valArr[1] === '*' || valArr[2] === '*') {
|
||||
return Promise.reject(new Error('任务周期必须指定分钟、小时'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CronInput />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{isCycle && (
|
||||
<Form.Item
|
||||
name="executionStrategy"
|
||||
label={
|
||||
<span>
|
||||
执行策略
|
||||
<Tooltip title="不同大小副本执行的顺序">
|
||||
<IconFont style={{ fontSize: '14px', marginLeft: '5px' }} type="icon-zhushi" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `请选择!`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value={1}>优先最大副本</Radio>
|
||||
<Radio value={2}>优先最小副本</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
name="throttleUnitM"
|
||||
label="限流"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `请输入!`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={1} max={99999} placeholder="请输入限流" addonAfter="MB/s" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
{isCycle && (
|
||||
<>
|
||||
<Form.Item name="status" label="是否启用">
|
||||
<Radio.Group>
|
||||
<Radio value={1}>启用</Radio>
|
||||
<Radio value={0}>禁用</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BalanceDrawer;
|
||||
@@ -0,0 +1,183 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Table, Input, InputNumber, Form, Tooltip, Button, message, IconFont } from 'knowdesign';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
|
||||
const EditableCell = ({ dataIndex, editable, title, inputType, handleSave, placeholder, record, index, children, ...restProps }: any) => {
|
||||
const inputRef = useRef(null);
|
||||
|
||||
const save = async () => {
|
||||
try {
|
||||
handleSave({ ...record });
|
||||
} catch (errInfo) {
|
||||
console.log('Save failed:', errInfo);
|
||||
}
|
||||
};
|
||||
const inputNode =
|
||||
inputType === 'number' ? (
|
||||
<InputNumber
|
||||
style={{ width: '130px' }}
|
||||
min={1}
|
||||
max={100}
|
||||
autoComplete="off"
|
||||
placeholder={placeholder}
|
||||
ref={inputRef}
|
||||
formatter={(value: number | string) => `${value}%`}
|
||||
parser={(value: number | string) => value!.toString().replace('%', '')}
|
||||
onPressEnter={save}
|
||||
onBlur={save}
|
||||
prefix="avg+-"
|
||||
/>
|
||||
) : (
|
||||
<Input autoComplete="off" placeholder={placeholder} />
|
||||
);
|
||||
return (
|
||||
<td {...restProps}>
|
||||
{editable ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span style={{ marginRight: '5px' }}>avg ±</span>
|
||||
<Form.Item
|
||||
name={dataIndex}
|
||||
style={{
|
||||
margin: 0,
|
||||
}}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `请输入!`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{inputNode}
|
||||
</Form.Item>
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
||||
const BalanceEditTable = React.forwardRef((props: any, ref: any) => {
|
||||
const { colCustomConfigs, tableData, tableDataChange } = props;
|
||||
const [form] = Form.useForm();
|
||||
const [dataSource, setDataSource] = useState(tableData);
|
||||
|
||||
useEffect(() => {
|
||||
setDataSource(tableData);
|
||||
const formData: any = {};
|
||||
tableData?.forEach((item: any) => {
|
||||
formData[`${item.type}-intervalPercent`] = item.intervalPercent;
|
||||
});
|
||||
form.setFieldsValue({ ...formData });
|
||||
}, [tableData]);
|
||||
|
||||
const editTableValidate = async () => {
|
||||
const values = await form.validateFields();
|
||||
return values;
|
||||
};
|
||||
|
||||
const handleSave = async (row: any) => {
|
||||
let values: any = {};
|
||||
form
|
||||
.validateFields()
|
||||
.then((data) => {
|
||||
values = data;
|
||||
tableChange(values);
|
||||
})
|
||||
.catch((errs) => {
|
||||
values = errs?.values;
|
||||
tableChange(values);
|
||||
});
|
||||
};
|
||||
|
||||
const tableChange = (formData: any) => {
|
||||
const newData = [...dataSource];
|
||||
Object.keys(formData).forEach((key) => {
|
||||
const type = key.split('-')[0];
|
||||
const index = newData.findIndex((item) => type === item.type);
|
||||
const item = newData[index];
|
||||
newData.splice(index, 1, {
|
||||
...item,
|
||||
intervalPercent: formData[key],
|
||||
});
|
||||
});
|
||||
tableDataChange(newData);
|
||||
};
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
editTableValidate,
|
||||
}));
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '均衡维度',
|
||||
dataIndex: 'name',
|
||||
width: '40%',
|
||||
},
|
||||
{
|
||||
title: () => (
|
||||
<span>
|
||||
均衡区间{' '}
|
||||
<Tooltip title="单位:%,大于0,小于100">
|
||||
{' '}
|
||||
<QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
</span>
|
||||
),
|
||||
dataIndex: 'intervalPercent',
|
||||
width: '40%',
|
||||
editable: true,
|
||||
},
|
||||
{
|
||||
title: '优先级',
|
||||
dataIndex: 'priority',
|
||||
width: '20%',
|
||||
},
|
||||
];
|
||||
|
||||
const mergedColumns = columns.map((col, index) => {
|
||||
if (!col.editable) {
|
||||
return col;
|
||||
}
|
||||
|
||||
return {
|
||||
...col,
|
||||
onCell: (record: any, index: number) =>
|
||||
Object.assign(
|
||||
{
|
||||
record,
|
||||
inputType: 'number',
|
||||
dataIndex: `${record.type}-${col.dataIndex}`,
|
||||
title: col.title,
|
||||
editable: col.editable,
|
||||
index: index,
|
||||
handleSave,
|
||||
form,
|
||||
},
|
||||
colCustomConfigs?.[index]
|
||||
),
|
||||
title: colCustomConfigs?.[index]?.title || col.title,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="edit-table-form">
|
||||
<Form form={form} component={false}>
|
||||
<Table
|
||||
components={{
|
||||
body: {
|
||||
cell: EditableCell,
|
||||
},
|
||||
}}
|
||||
dataSource={dataSource}
|
||||
columns={mergedColumns}
|
||||
rowClassName={() => 'editable-row'}
|
||||
pagination={false}
|
||||
/>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default BalanceEditTable;
|
||||
@@ -0,0 +1,240 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Popover, IconFont, Row, Col, Select } from 'knowdesign';
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
|
||||
const balancePrefix = 'custom-popover-balance';
|
||||
interface FilterListType {
|
||||
id: number;
|
||||
firstLevel: string;
|
||||
secondLevel: number;
|
||||
}
|
||||
|
||||
const filterNorm = [
|
||||
{
|
||||
label: 'Disk',
|
||||
value: 'disk',
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: 'Byte In',
|
||||
value: 'bytesIn',
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: 'Byte Out',
|
||||
value: 'bytesOut',
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
const isBalance = [
|
||||
{
|
||||
label: '已均衡',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: '未均衡',
|
||||
value: 2,
|
||||
},
|
||||
];
|
||||
|
||||
export const BalanceFilter = (props: any) => {
|
||||
const [visible, setVisible] = useState<boolean>(false);
|
||||
const [filterList, setFilterList] = useState<FilterListType[]>(null);
|
||||
const [filterNormList, setFilterNormList] = useState(filterNorm);
|
||||
|
||||
// 添加一个筛选条件
|
||||
const addClick = (key: number) => {
|
||||
if (filterList.length >= filterNorm.length) return;
|
||||
const newFilterList = JSON.parse(JSON.stringify(filterList));
|
||||
const getDate = new Date().getTime();
|
||||
newFilterList.push({ id: getDate, firstLevel: '', secondLevel: null });
|
||||
setFilterList(newFilterList);
|
||||
};
|
||||
|
||||
// 减少一个筛选条件
|
||||
const reduceClick = (key: number) => {
|
||||
const newFilterList = filterList.filter((item, index) => {
|
||||
return item.id !== key;
|
||||
});
|
||||
// 取消已清除的指标的禁用
|
||||
const filterNewFilterList = newFilterList.map((item: any) => item.firstLevel);
|
||||
const newfilterNormList = JSON.parse(JSON.stringify(filterNorm)).map((item: any) => {
|
||||
if (filterNewFilterList.includes(item.value)) {
|
||||
return { ...item, disabled: true };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
newfilterNormList.sort((a: any, b: any) => a.disabled - b.disabled);
|
||||
|
||||
setFilterList(newFilterList);
|
||||
setFilterNormList(newfilterNormList);
|
||||
};
|
||||
|
||||
// 第一列下拉操作
|
||||
const firstLevelChange = (value: any, rowId: number) => {
|
||||
const newFilterList = JSON.parse(JSON.stringify(filterList));
|
||||
const newFilterListIndex = newFilterList.findIndex((item: any) => item.id === rowId);
|
||||
newFilterList[newFilterListIndex].firstLevel = value;
|
||||
|
||||
// 控制已选中的指标无法再次被选中
|
||||
const filterNewFilterList = newFilterList.map((item: any) => item.firstLevel);
|
||||
const newfilterNormList = JSON.parse(JSON.stringify(filterNorm)).map((item: any) => {
|
||||
if (filterNewFilterList.includes(item.value)) {
|
||||
return { ...item, disabled: true };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
newfilterNormList.sort((a: any, b: any) => a.disabled - b.disabled);
|
||||
|
||||
setFilterList(newFilterList);
|
||||
setFilterNormList(newfilterNormList);
|
||||
};
|
||||
|
||||
// 第二列下拉操作
|
||||
const secondLevelChange = (value: any, rowId: number) => {
|
||||
const newFilterList = JSON.parse(JSON.stringify(filterList));
|
||||
const newFilterListIndex = newFilterList.findIndex((item: any) => item.id === rowId);
|
||||
newFilterList[newFilterListIndex].secondLevel = value;
|
||||
setFilterList(newFilterList);
|
||||
};
|
||||
|
||||
const submitClick = () => {
|
||||
const newFilterList = filterList.filter((item: any) => {
|
||||
return item.firstLevel && (item.secondLevel === 0 || item.secondLevel);
|
||||
});
|
||||
props?.getNorms(newFilterList);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
// 控制已选中的指标无法再次被选中
|
||||
if (props?.filterList && props?.filterList.length > 0) {
|
||||
const filterNewFilterList = props?.filterList.map((item: any) => item.firstLevel);
|
||||
const newfilterNormList = JSON.parse(JSON.stringify(filterNorm)).map((item: any) => {
|
||||
if (filterNewFilterList.includes(item.value)) {
|
||||
return { ...item, disabled: true };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
newfilterNormList.sort((a: any, b: any) => a.disabled - b.disabled);
|
||||
setFilterList(props?.filterList);
|
||||
setFilterNormList(newfilterNormList);
|
||||
} else {
|
||||
setFilterList([
|
||||
{
|
||||
id: 0,
|
||||
firstLevel: null,
|
||||
secondLevel: null,
|
||||
},
|
||||
]);
|
||||
setFilterNormList(filterNorm);
|
||||
}
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 控制已选中的指标无法再次被选中
|
||||
if (props?.filterList && props?.filterList.length > 0) {
|
||||
const filterNewFilterList = props?.filterList.map((item: any) => item.firstLevel);
|
||||
const newfilterNormList = JSON.parse(JSON.stringify(filterNorm)).map((item: any) => {
|
||||
if (filterNewFilterList.includes(item.value)) {
|
||||
return { ...item, disabled: true };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
newfilterNormList.sort((a: any, b: any) => a.disabled - b.disabled);
|
||||
setFilterList(props?.filterList);
|
||||
setFilterNormList(newfilterNormList);
|
||||
} else {
|
||||
setFilterList([
|
||||
{
|
||||
id: 0,
|
||||
firstLevel: null,
|
||||
secondLevel: null,
|
||||
},
|
||||
]);
|
||||
setFilterNormList(filterNorm);
|
||||
}
|
||||
}, [props?.filterList]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
placement="bottomLeft"
|
||||
overlayClassName={balancePrefix}
|
||||
trigger={'click'}
|
||||
title={
|
||||
<div className={`${balancePrefix}-title`}>
|
||||
<div style={{ fontSize: '16px' }}>{props?.title}</div>
|
||||
<Button type="text" icon={<CloseOutlined className="close-icon" />} onClick={onClose} />
|
||||
</div>
|
||||
}
|
||||
visible={visible}
|
||||
onVisibleChange={(visible) => setVisible(visible)}
|
||||
destroyTooltipOnHide
|
||||
content={
|
||||
<div className={`${balancePrefix}-container`}>
|
||||
<div className={`${balancePrefix}-container-main`}>
|
||||
<div>
|
||||
{filterList &&
|
||||
filterList.length > 0 &&
|
||||
filterList.map((item, index) => {
|
||||
return (
|
||||
<Row key={item.id} style={{ padding: '9px 0' }}>
|
||||
{index !== 0 && (
|
||||
<Col span={2}>
|
||||
<span className={`${balancePrefix}-container-main-andlink`}>
|
||||
<span>and</span>
|
||||
</span>
|
||||
</Col>
|
||||
)}
|
||||
<Col span={index !== 0 ? 10 : 12}>
|
||||
<Select
|
||||
size="small"
|
||||
value={item.firstLevel || null}
|
||||
placeholder="请选择"
|
||||
style={{ width: index !== 0 ? 148 : 180 }}
|
||||
options={filterNormList}
|
||||
onChange={(e) => firstLevelChange(e, item.id)}
|
||||
></Select>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Select
|
||||
size="small"
|
||||
value={item.secondLevel}
|
||||
placeholder="请选择"
|
||||
options={isBalance}
|
||||
style={{ width: 150 }}
|
||||
onChange={(e) => secondLevelChange(e, item.id)}
|
||||
></Select>
|
||||
</Col>
|
||||
<Col className={`${balancePrefix}-container-main-option`} span={2}>
|
||||
<span onClick={() => addClick(item.id)}>
|
||||
<IconFont type="icon-jiahao" />
|
||||
</span>
|
||||
{index !== 0 && (
|
||||
<span onClick={() => reduceClick(item.id)}>
|
||||
<IconFont type="icon-jian" />
|
||||
</span>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className={`${balancePrefix}-container-main-footer`}>
|
||||
<Button onClick={onClose} size="small">
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={submitClick} style={{ marginLeft: '10px' }} size="small" type="primary">
|
||||
确定
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button icon={<IconFont type="icon-shaixuan" />}>筛选列表</Button>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,194 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Utils,
|
||||
Drawer,
|
||||
Button,
|
||||
Form,
|
||||
Space,
|
||||
Divider,
|
||||
AppContainer,
|
||||
Input,
|
||||
Transfer,
|
||||
message,
|
||||
IconFont,
|
||||
InputNumber,
|
||||
} from 'knowdesign';
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
import api from '../../api';
|
||||
import './style/BalanceDrawer.less';
|
||||
interface PropsType extends React.HTMLAttributes<HTMLDivElement> {
|
||||
onClose: () => void;
|
||||
visible: boolean;
|
||||
isCycle?: boolean;
|
||||
formData?: any;
|
||||
genData?: any;
|
||||
}
|
||||
const ClusterNorms: React.FC<PropsType> = ({ onClose, visible, genData }) => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [form] = Form.useForm();
|
||||
const [nodeData, setNodeData] = useState([]);
|
||||
const [nodeTargetKeys, setNodeTargetKeys] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
visible && getNodeList();
|
||||
visible && getTopicList();
|
||||
}, [visible]);
|
||||
|
||||
const submit = () => {
|
||||
// 周期均衡
|
||||
form.validateFields().then((values) => {
|
||||
const params = values?.brokers?.map((item: any) => {
|
||||
const brokerId = nodeData?.filter((key) => key.brokerId === item && item)[0]?.brokerId;
|
||||
const newValue = brokerId && { brokerId, cpu: values?.cpu, disk: values?.disk, flow: values?.flow };
|
||||
return {
|
||||
clusterId: global?.clusterInfo?.id + '',
|
||||
value: JSON.stringify(newValue),
|
||||
valueGroup: 'BROKER_SPEC',
|
||||
valueName: brokerId + '',
|
||||
description: '',
|
||||
};
|
||||
});
|
||||
|
||||
Utils.put(api.putPlatformConfig(), params).then((res: any) => {
|
||||
const dataDe = res || [];
|
||||
onClose();
|
||||
message.success('设置集群规格成功');
|
||||
genData();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const nodeChange = (val: any) => {
|
||||
setNodeTargetKeys(val);
|
||||
};
|
||||
|
||||
const getNodeList = () => {
|
||||
Utils.request(api.getBrokersMetaList(global?.clusterInfo?.id), {
|
||||
method: 'GET',
|
||||
}).then((res: any) => {
|
||||
const dataDe = res || [];
|
||||
const dataHandle = dataDe.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
key: item.brokerId,
|
||||
title: `${item.brokerId} (${item.host})`,
|
||||
};
|
||||
});
|
||||
setNodeData(dataHandle);
|
||||
});
|
||||
};
|
||||
|
||||
const getTopicList = () => {
|
||||
Utils.request(api.getPlatformConfig(global?.clusterInfo?.id, 'BROKER_SPEC')).then((res: any) => {
|
||||
const targetKeys = res?.map((item: any) => {
|
||||
return JSON.parse(item.value).brokerId;
|
||||
});
|
||||
setNodeTargetKeys(targetKeys || []);
|
||||
const newValues = JSON.parse(res?.[0].value);
|
||||
const fieldValue = {
|
||||
cpu: newValues?.cpu,
|
||||
disk: newValues?.disk,
|
||||
flow: newValues?.flow,
|
||||
brokers: targetKeys || [],
|
||||
};
|
||||
form.setFieldsValue(fieldValue);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
title={'设置集群规格'}
|
||||
width="600px"
|
||||
destroyOnClose={true}
|
||||
className="balance-drawer"
|
||||
onClose={onClose}
|
||||
visible={visible}
|
||||
maskClosable={false}
|
||||
extra={
|
||||
<Space>
|
||||
<Button size="small" onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" size="small" disabled={false} onClick={submit}>
|
||||
确定
|
||||
</Button>
|
||||
|
||||
<Divider type="vertical" />
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
preserve={false}
|
||||
initialValues={{
|
||||
status: 1,
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="brokers"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `请选择!`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Transfer
|
||||
dataSource={nodeData}
|
||||
showSearch
|
||||
filterOption={(inputValue, option) => option.host.indexOf(inputValue) > -1}
|
||||
targetKeys={nodeTargetKeys}
|
||||
onChange={nodeChange}
|
||||
render={(item) => item.title}
|
||||
titles={['待选节点', '已选节点']}
|
||||
customHeader
|
||||
showSelectedCount
|
||||
locale={{ itemUnit: '', itemsUnit: '' }}
|
||||
suffix={<IconFont type="icon-fangdajing" />}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="cpu"
|
||||
label="单机核数"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `请输入!`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber decimalSeparator={'0'} min={0} max={99999} placeholder="请输入单机核数" addonAfter="C" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="disk"
|
||||
label="单机磁盘"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `请输入!`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={99999} placeholder="请输入磁盘大小" addonAfter="GB" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="flow"
|
||||
label="单机网络"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `请输入!`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={99999} placeholder="请输入单机网络" addonAfter="MB/s" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClusterNorms;
|
||||
@@ -0,0 +1,27 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Dropdown, Input } from 'knowdesign';
|
||||
import Cron from 'react-cron-antd';
|
||||
// import QnnReactCron, { CronProps, CronFns } from "qnn-react-cron";
|
||||
import './style/CronInput.less';
|
||||
interface PropsType extends React.HTMLAttributes<HTMLDivElement> {
|
||||
onChange?: Function
|
||||
value?: boolean;
|
||||
}
|
||||
|
||||
const CronInput: React.FC<PropsType> = (props) => {
|
||||
const { value, onChange } = props;
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
placement="bottomLeft"
|
||||
overlayClassName="cron-input-dropDown"
|
||||
overlay={<Cron className="cron-input" style={{ width: '553px' }} value={value} onOk={onChange} />}
|
||||
>
|
||||
<Input value={value} placeholder="请输入任务周期" />
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
export default CronInput;
|
||||
@@ -0,0 +1,170 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Utils, Drawer, Button, ProTable, Space, Divider, AppContainer } from 'knowdesign';
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
import moment from 'moment';
|
||||
import api from '../../api';
|
||||
import { defaultPagination } from './index';
|
||||
import PlanDrawer from './PlanDrawer';
|
||||
import { timeFormat } from '../../constants/common';
|
||||
|
||||
interface PropsType extends React.HTMLAttributes<HTMLDivElement> {
|
||||
onClose: () => void;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
const HistoryDrawer: React.FC<PropsType> = ({ onClose, visible }) => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [pagination, setPagination] = useState<any>(defaultPagination);
|
||||
const [data, setData] = useState([]);
|
||||
const [planDetailData, setPlanDetailData] = useState({});
|
||||
const [planVisible, setPlanVisible] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
getList();
|
||||
}, []);
|
||||
|
||||
const columns = () => [
|
||||
{
|
||||
title: '执行时间',
|
||||
dataIndex: 'begin',
|
||||
key: 'begin',
|
||||
render: (t: number) => (t ? moment(t).format(timeFormat) : '-'),
|
||||
},
|
||||
{
|
||||
title: '结束时间',
|
||||
dataIndex: 'end',
|
||||
key: 'end',
|
||||
render: (t: number) => (t ? moment(t).format(timeFormat) : '-'),
|
||||
},
|
||||
// {
|
||||
// title: 'CPU均衡率',
|
||||
// dataIndex: 'cpu',
|
||||
// render: (text: any, row: any) => {
|
||||
// return `${row?.sub?.cpu?.successNu} (已均衡) / ${row?.sub?.cpu?.failedNu} (未均衡)`
|
||||
// }
|
||||
// },
|
||||
{
|
||||
title: 'Disk均衡率',
|
||||
dataIndex: 'disk',
|
||||
render: (text: any, row: any) => {
|
||||
return `${row?.sub?.disk?.successNu} (已均衡) / ${row?.sub?.disk?.failedNu} (未均衡)`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'BytesIn均衡率',
|
||||
dataIndex: 'bytesIn',
|
||||
render: (text: any, row: any) => {
|
||||
return `${row?.sub?.bytesIn?.successNu} (已均衡) / ${row?.sub?.bytesIn?.failedNu} (未均衡)`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'BytesOut均衡率',
|
||||
dataIndex: 'bytesOut',
|
||||
render: (text: any, row: any) => {
|
||||
return `${row?.sub?.bytesOut?.successNu} (已均衡) / ${row?.sub?.bytesOut?.failedNu} (未均衡)`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
render: (text: any, row: any) => {
|
||||
return (
|
||||
<Button type="link" size="small" onClick={() => CheckDetail(row.jobId)}>
|
||||
查看详情
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const getList = (query = {}) => {
|
||||
const queryParams = {
|
||||
pageNo: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
...query,
|
||||
};
|
||||
|
||||
setLoading(true);
|
||||
Utils.request(api.getBalanceHistory(global?.clusterInfo?.id), {
|
||||
method: 'POST',
|
||||
data: queryParams,
|
||||
}).then(
|
||||
(res: any) => {
|
||||
const { pageNo, pageSize, pages, total } = res.pagination;
|
||||
|
||||
setPagination({
|
||||
...pagination,
|
||||
current: pageNo,
|
||||
pageSize,
|
||||
total,
|
||||
});
|
||||
const dataDe = res?.bizData || [];
|
||||
const dataHandle = dataDe.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
};
|
||||
});
|
||||
setData(dataHandle);
|
||||
setLoading(false);
|
||||
},
|
||||
() => setLoading(false)
|
||||
);
|
||||
};
|
||||
|
||||
const CheckDetail = (jobId: any) => {
|
||||
Utils.request(api.getBalancePlan(global?.clusterInfo?.id, jobId), {
|
||||
method: 'GET',
|
||||
}).then((res: any) => {
|
||||
const dataDe = res || {};
|
||||
setPlanDetailData(dataDe);
|
||||
setPlanVisible(true);
|
||||
});
|
||||
};
|
||||
|
||||
const onTableChange = (curPagination: any) => {
|
||||
getList({ page: curPagination.current, size: curPagination.pageSize });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PlanDrawer visible={planVisible} onClose={() => setPlanVisible(false)} detailData={planDetailData} isPrevew={false} />
|
||||
<Drawer
|
||||
title={'均衡历史'}
|
||||
width="1080px"
|
||||
destroyOnClose={true}
|
||||
className="plan-drawer"
|
||||
onClose={onClose}
|
||||
visible={visible}
|
||||
closable={false}
|
||||
maskClosable={false}
|
||||
extra={
|
||||
<Space>
|
||||
<Button type="text" size="small" icon={<CloseOutlined />} onClick={onClose} />
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<ProTable
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
loading,
|
||||
rowKey: 'jobId',
|
||||
dataSource: data,
|
||||
paginationProps: pagination,
|
||||
columns: columns() as any,
|
||||
lineFillColor: true,
|
||||
attrs: {
|
||||
onChange: onTableChange,
|
||||
scroll: {
|
||||
x: 'max-content',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HistoryDrawer;
|
||||
@@ -0,0 +1,64 @@
|
||||
import React, { useState } from 'react';
|
||||
import { message, Drawer, Button, Space, Divider, AppContainer, IconFont } from 'knowdesign';
|
||||
|
||||
import RebalancePlan from '../Jobs/RebalancePlan';
|
||||
interface PropsType extends React.HTMLAttributes<HTMLDivElement> {
|
||||
onClose: () => void;
|
||||
balanceImmediate?: Function;
|
||||
visible: boolean;
|
||||
isPrevew?: boolean;
|
||||
detailData?: any;
|
||||
isEdit?: boolean;
|
||||
}
|
||||
|
||||
const PlanDrawer: React.FC<PropsType> = ({ onClose, visible, detailData, isPrevew, balanceImmediate, isEdit }) => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
|
||||
const submit = () => {
|
||||
if (detailData?.replicas === 0) {
|
||||
message['warning']('replicas=0,该集群已达到均衡要求,不需要再执行均衡任务。');
|
||||
} else {
|
||||
balanceImmediate && balanceImmediate();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
title={
|
||||
<Space size={0}>
|
||||
<Button type="text" className="drawer-title-left-button" icon={<IconFont type="icon-fanhui1" />} onClick={onClose} />
|
||||
<Divider type="vertical" />
|
||||
<span>执行计划</span>
|
||||
</Space>
|
||||
}
|
||||
width="1080px"
|
||||
destroyOnClose={true}
|
||||
className="plan-drawer"
|
||||
onClose={onClose}
|
||||
visible={visible}
|
||||
maskClosable={false}
|
||||
extra={
|
||||
<Space>
|
||||
{!!isPrevew && (
|
||||
<>
|
||||
<Button size="small" onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button className={isEdit ? '' : 'btn-width84'} type="primary" size="small" disabled={false} onClick={submit}>
|
||||
{isEdit ? '确定' : '立即均衡'}
|
||||
</Button>
|
||||
|
||||
<Divider type="vertical" />
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<RebalancePlan balanceData={detailData} />
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlanDrawer;
|
||||
@@ -0,0 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import React from 'react';
|
||||
import { timeFormat, getSizeAndUnit } from '../../constants/common';
|
||||
import moment from 'moment';
|
||||
import { Tooltip } from 'knowdesign';
|
||||
@@ -0,0 +1,145 @@
|
||||
.load-rebalance-container {
|
||||
.balance-main {
|
||||
padding: 16px 24px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.01), 0 3px 6px 3px rgba(0,0,0,0.01), 0 2px 6px 0 rgba(0,0,0,0.03);
|
||||
// border-radius: 12px;
|
||||
|
||||
.dcloud-table-container {
|
||||
.dot {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
zoom: 0.5;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.isbalance {
|
||||
.dot {
|
||||
background-color: #00C0A2;
|
||||
|
||||
}
|
||||
}
|
||||
.noBalance {
|
||||
color: #FF7066;
|
||||
.dot {
|
||||
background-color: #FF7066;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-con {
|
||||
overflow: hidden;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.dcloud-form {
|
||||
float: left;
|
||||
.dcloud-form-item {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.float-r {
|
||||
font-family: @font-family-bold;
|
||||
float: right;
|
||||
.dcloud-btn {
|
||||
margin-left: 8px;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.balance-drawer{
|
||||
.form-item-group {
|
||||
padding: 16px 20px 1px 20px;
|
||||
margin-bottom: 16px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.dcloud-form-item-control-input {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-popover-balance{
|
||||
width: 432px;
|
||||
padding-top: 0 !important;
|
||||
|
||||
&-title{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-container{
|
||||
height: 100%;
|
||||
&-main{
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
&-andlink{
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background-color: #556EE6;
|
||||
color: #ffffff;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
&>span{
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
transform: scale(0.83,0.83);
|
||||
}
|
||||
}
|
||||
&-option{
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&-footer{
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: 23px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-popover-content{
|
||||
min-height: 268px;
|
||||
}
|
||||
.dcloud-popover-arrow{
|
||||
display: none;
|
||||
}
|
||||
.dcloud-popover-inner{
|
||||
height: 100%;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 0 8px 0 rgba(0,0,0,0.04), 0 6px 12px 12px rgba(0,0,0,0.04), 0 6px 10px 0 rgba(0,0,0,0.08);
|
||||
}
|
||||
.dcloud-popover-inner-content{
|
||||
padding: 0 24px 16px;
|
||||
height: 220px;
|
||||
}
|
||||
.dcloud-popover-title{
|
||||
padding: 12px 24px 0;
|
||||
font-family: @font-family-bold;
|
||||
font-size: 16px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
// .dcloud-table{
|
||||
// margin: 0 !important;
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Select, Form, Utils, AppContainer, Input, Button, ProTable, Badge, Tag, SearchInput } from 'knowdesign';
|
||||
import BalanceDrawer from './BalanceDrawer';
|
||||
import HistoryDrawer from './HistoryDrawer';
|
||||
import DBreadcrumb from 'knowdesign/lib/extend/d-breadcrumb';
|
||||
import { getSizeAndUnit } from '../../constants/common';
|
||||
import api from '../../api';
|
||||
import './index.less';
|
||||
import LoadRebalanceCardBar from '@src/components/CardBar/LoadRebalanceCardBar';
|
||||
import { BalanceFilter } from './BalanceFilter';
|
||||
|
||||
const Balance_Status_OPTIONS = [
|
||||
{
|
||||
label: '全部',
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
label: '已均衡',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: '未均衡',
|
||||
value: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const balanceStatus: any = {
|
||||
0: '已均衡',
|
||||
2: '未均衡',
|
||||
};
|
||||
|
||||
const filterNorms: any = {
|
||||
['disk']: 'Disk',
|
||||
['bytesIn']: 'Byte In',
|
||||
['bytesOut']: 'Byte Out',
|
||||
};
|
||||
|
||||
export const defaultPagination = {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
position: 'bottomRight',
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
};
|
||||
|
||||
const LoadBalance: React.FC = (props: any) => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [form] = Form.useForm();
|
||||
const [pagination, setPagination] = useState<any>(defaultPagination);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [data, setData] = useState([]);
|
||||
const [visible, setVisible] = useState<boolean>(false);
|
||||
const [isCycle, setIsCycle] = useState<boolean>(false);
|
||||
const [planVisible, setPlanVisible] = useState<boolean>(false);
|
||||
const [circleFormData, setCircleFormData] = useState(null);
|
||||
const [trigger, setTrigger] = useState(false);
|
||||
const [filterList, setFilterList] = useState<any>(null);
|
||||
const [balanceList, setBalanceList] = useState<string>(null);
|
||||
|
||||
const [searchKeywords, setSearchKeywords] = useState<string>('');
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
|
||||
const columns = () => [
|
||||
{
|
||||
title: 'Broker ID',
|
||||
dataIndex: 'brokerId',
|
||||
key: 'brokerId',
|
||||
fixed: 'left',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: 'Host',
|
||||
dataIndex: 'host',
|
||||
key: 'host',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: 'Leader',
|
||||
dataIndex: 'leader',
|
||||
key: 'leader',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: 'Replicas',
|
||||
dataIndex: 'replicas',
|
||||
key: 'replicas',
|
||||
width: 100,
|
||||
},
|
||||
// {
|
||||
// title: 'CPU',
|
||||
// children: [
|
||||
// {
|
||||
// title: '规格',
|
||||
// dataIndex: 'cpu_spec',
|
||||
// key: 'cpu_spec',
|
||||
// render: (text: any, row: any) => {
|
||||
// return text !== null ? `${text}` : '-';
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// title: 'AVG',
|
||||
// dataIndex: 'cpu_avg',
|
||||
// key: 'cpu_avg',
|
||||
// render: (text: any, row: any) => {
|
||||
// return text !== null ? `${text} (${(row.cpu_avg * 100 / row.cpu_spec).toFixed(2)}%)` : '-';
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// title: '是否均衡',
|
||||
// dataIndex: 'cpu_status',
|
||||
// key: 'cpu_status',
|
||||
|
||||
// render: (text: any, row: any) => {
|
||||
// // 0:已均衡,非0:未均衡
|
||||
// return text !== null ? (text === 0 ? (
|
||||
// <span className="isbalance">
|
||||
// <span className="dot"></span>已均衡
|
||||
// </span>
|
||||
// ) : (
|
||||
// <span className="noBalance">
|
||||
// <span className="dot"></span>未均衡
|
||||
// </span>
|
||||
// )) : '-';
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
title: 'Disk规格',
|
||||
dataIndex: 'disk_spec',
|
||||
key: 'disk_spec',
|
||||
width: '150px',
|
||||
render: (text: any, row: any) => {
|
||||
return text !== null ? `${text}GB` : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Disk AVG',
|
||||
dataIndex: 'disk_avg',
|
||||
key: 'disk_avg',
|
||||
width: '200px',
|
||||
render: (text: any, row: any) => {
|
||||
return text !== null ? (
|
||||
<span>
|
||||
<Badge status={row?.disk_status === 0 ? 'success' : 'error'} />
|
||||
{`${getSizeAndUnit(text, 'B').valueWithUnit} (${((row.disk_avg * 100) / Utils.transGBToB(row.disk_spec)).toFixed(2)}%)`}
|
||||
</span>
|
||||
) : (
|
||||
'-'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'BytesIn规格',
|
||||
dataIndex: 'bytesIn_spec',
|
||||
key: 'bytesIn_spec',
|
||||
width: '150px',
|
||||
render: (text: any, row: any) => {
|
||||
return text !== null ? `${text}MB/s` : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'BytesIn AVG',
|
||||
dataIndex: 'bytesIn_avg',
|
||||
key: 'bytesIn_avg',
|
||||
width: '200px',
|
||||
render: (text: any, row: any) => {
|
||||
return text !== null ? (
|
||||
<span>
|
||||
<Badge status={row?.bytesIn_status === 0 ? 'success' : 'error'} />
|
||||
{`${getSizeAndUnit(text, 'B/s').valueWithUnit} (${((row.bytesIn_avg * 100) / (row.bytesIn_spec * 1024 * 1024)).toFixed(2)}%)`}
|
||||
</span>
|
||||
) : (
|
||||
'-'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'BytesOut规格',
|
||||
dataIndex: 'bytesOut_spec',
|
||||
key: 'bytesOut_spec',
|
||||
width: '150px',
|
||||
render: (text: any, row: any) => {
|
||||
return text !== null ? `${text}MB/s` : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'BytesOut AVG',
|
||||
dataIndex: 'bytesOut_avg',
|
||||
key: 'bytesOut_avg',
|
||||
width: '200px',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (text: any, row: any) => {
|
||||
return text !== null ? (
|
||||
<span>
|
||||
<Badge status={row?.bytesOut_status === 0 ? 'success' : 'error'} />
|
||||
{`${getSizeAndUnit(text, 'B/s').valueWithUnit} (${((row.bytesOut_avg * 100) / (row.bytesOut_spec * 1024 * 1024)).toFixed(2)}%)`}
|
||||
</span>
|
||||
) : (
|
||||
'-'
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
getList();
|
||||
}, []);
|
||||
|
||||
const onTableChange = (curPagination: any) => {
|
||||
setPagination({
|
||||
...curPagination,
|
||||
});
|
||||
getList({ pageNo: curPagination.current, pageSize: curPagination.pageSize });
|
||||
};
|
||||
|
||||
const resetList = () => {
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageNo: 1,
|
||||
});
|
||||
getList();
|
||||
};
|
||||
|
||||
const hostSearch = (e: any) => {
|
||||
setFilterList([]);
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageNo: 1,
|
||||
});
|
||||
setSearchKeywords(e);
|
||||
getList({ searchKeywords: e, stateParam: balanceList });
|
||||
};
|
||||
|
||||
const getList = (query = {}) => {
|
||||
const formData = form.getFieldsValue();
|
||||
const queryParams = {
|
||||
pageNo: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
...formData,
|
||||
...query,
|
||||
};
|
||||
|
||||
setLoading(true);
|
||||
Utils.request(api.getBalanceList(global?.clusterInfo?.id), {
|
||||
method: 'POST',
|
||||
data: queryParams,
|
||||
}).then(
|
||||
(res: any) => {
|
||||
const { pageNo, pageSize, pages, total } = res.pagination;
|
||||
|
||||
setPagination({
|
||||
...pagination,
|
||||
current: pageNo,
|
||||
pageSize,
|
||||
total,
|
||||
});
|
||||
const dataDe = res?.bizData || [];
|
||||
const dataHandle = dataDe.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
cpu_spec: item?.sub?.cpu?.spec,
|
||||
cpu_avg: item?.sub?.cpu?.avg,
|
||||
cpu_status: item?.sub?.cpu?.status,
|
||||
disk_spec: item?.sub?.disk?.spec,
|
||||
disk_avg: item?.sub?.disk?.avg,
|
||||
disk_status: item?.sub?.disk?.status,
|
||||
bytesIn_spec: item?.sub?.bytesIn?.spec,
|
||||
bytesIn_avg: item?.sub?.bytesIn?.avg,
|
||||
bytesIn_status: item?.sub?.bytesIn?.status,
|
||||
bytesOut_spec: item?.sub?.bytesOut?.spec,
|
||||
bytesOut_avg: item?.sub?.bytesOut?.avg,
|
||||
bytesOut_status: item?.sub?.bytesOut?.status,
|
||||
};
|
||||
});
|
||||
setData(dataHandle);
|
||||
setLoading(false);
|
||||
},
|
||||
() => setLoading(false)
|
||||
);
|
||||
};
|
||||
|
||||
const drawerClose = (sure?: boolean) => {
|
||||
if (sure) {
|
||||
setTrigger(!trigger);
|
||||
}
|
||||
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const balanceClick = (val: boolean = false) => {
|
||||
if (val) {
|
||||
Utils.request(api.getBalanceForm(global?.clusterInfo?.id), {
|
||||
method: 'GET',
|
||||
})
|
||||
.then((res: any) => {
|
||||
const dataDe = res || {};
|
||||
setCircleFormData(dataDe);
|
||||
})
|
||||
.catch(() => {
|
||||
setCircleFormData(null);
|
||||
});
|
||||
} else {
|
||||
setCircleFormData(null);
|
||||
}
|
||||
setIsCycle(val);
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
const getNorms = (stateParamArr: any) => {
|
||||
const stateParam: any = {};
|
||||
stateParamArr.forEach((item: any) => {
|
||||
stateParam[item.firstLevel] = item.secondLevel;
|
||||
});
|
||||
setFilterList(stateParamArr);
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageNo: 1,
|
||||
});
|
||||
setBalanceList(stateParam);
|
||||
getList({ searchKeywords, stateParam });
|
||||
};
|
||||
|
||||
const filterNormsClose = (rowId: any) => {
|
||||
const newFilterList = filterList.filter((item: any) => item.id !== rowId);
|
||||
getNorms(newFilterList);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="breadcrumb" style={{ marginBottom: '10px' }}>
|
||||
<DBreadcrumb
|
||||
breadcrumbs={[
|
||||
{ label: '多集群管理', aHref: '/' },
|
||||
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
|
||||
{ label: 'Load Rebalance', aHref: `` },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ margin: '12px 0' }}>
|
||||
<LoadRebalanceCardBar trigger={trigger} genData={resetList} filterList={filterList} />
|
||||
</div>
|
||||
<div className="load-rebalance-container">
|
||||
<div className="balance-main clustom-table-content">
|
||||
<div className="header-con">
|
||||
{/* <Form form={form} layout="inline" onFinish={resetList}>
|
||||
<Form.Item name="status">
|
||||
<Select className="grid-select" placeholder="请选择状态" style={{ width: '180px' }} options={Balance_Status_OPTIONS} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="searchKeywords">
|
||||
<Input placeholder="请输入Host" style={{ width: '180px' }} />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" ghost htmlType="submit">
|
||||
查询
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form> */}
|
||||
<BalanceFilter title="负载均衡列表筛选" data={[]} getNorms={getNorms} filterList={filterList} />
|
||||
<div className="float-r">
|
||||
<SearchInput
|
||||
onSearch={hostSearch}
|
||||
attrs={{
|
||||
value: searchValue,
|
||||
onChange: setSearchValue,
|
||||
placeholder: '请输入 Host',
|
||||
style: { width: '210px' },
|
||||
maxLength: 128,
|
||||
}}
|
||||
/>
|
||||
<Button type="primary" ghost onClick={() => setPlanVisible(true)}>
|
||||
均衡历史
|
||||
</Button>
|
||||
<Button type="primary" ghost onClick={() => balanceClick(true)}>
|
||||
周期均衡
|
||||
</Button>
|
||||
<Button type="primary" onClick={() => balanceClick(false)}>
|
||||
立即均衡
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{filterList && filterList.length > 0 && (
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<span style={{ marginRight: '6px' }}>筛选结果:{pagination?.total || 0}条</span>
|
||||
{filterList.map((item: any) => {
|
||||
return (
|
||||
<Tag
|
||||
style={{ padding: '6px 10px', backgroundColor: 'rgba(33,37,41,0.08)', color: '#495057', borderRadius: '6px' }}
|
||||
key={item.id}
|
||||
closable
|
||||
onClose={() => filterNormsClose(item.id)}
|
||||
>
|
||||
<span>{filterNorms[item.firstLevel]}:</span>
|
||||
<span>{balanceStatus[item.secondLevel]}</span>
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<ProTable
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
loading,
|
||||
rowKey: 'brokerId',
|
||||
dataSource: data,
|
||||
paginationProps: pagination,
|
||||
columns: columns() as any,
|
||||
lineFillColor: true,
|
||||
attrs: {
|
||||
onChange: onTableChange,
|
||||
// bordered: false,
|
||||
// className: 'remove-last-border',
|
||||
scroll: { x: 'max-content', y: 'calc(100vh - 440px)' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<BalanceDrawer visible={visible} isCycle={isCycle} onClose={drawerClose} formData={circleFormData} genData={getList} />
|
||||
{planVisible && (
|
||||
<HistoryDrawer
|
||||
visible={planVisible}
|
||||
onClose={() => {
|
||||
setPlanVisible(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadBalance;
|
||||
@@ -0,0 +1,19 @@
|
||||
.balance-drawer {
|
||||
.form-title {
|
||||
font-size: 16px;
|
||||
color: #212529;
|
||||
letter-spacing: 0;
|
||||
line-height: 25px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
.schedule-cron {
|
||||
.dcloud-select {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-drawer-body{
|
||||
padding: 0 20px 14px !important;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
.cron-input-dropDown {
|
||||
.cron-input {
|
||||
width: 553px;
|
||||
}
|
||||
.ant-tabs-nav-list {
|
||||
// 秒
|
||||
// .ant-tabs-tab:nth-child(1) {
|
||||
// display: none;
|
||||
// }
|
||||
.ant-tabs-tab:nth-child(7) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.ant-tabs-content {
|
||||
// 秒
|
||||
.ant-tabs-tabpane:nth-child(1) {
|
||||
.ant-radio-group {
|
||||
.ant-radio-wrapper:nth-child(2) {
|
||||
display: none !important;
|
||||
}
|
||||
.ant-radio-wrapper:nth-child(3) {
|
||||
display: none !important;
|
||||
}
|
||||
.ant-radio-wrapper:nth-child(4) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 分
|
||||
.ant-tabs-tabpane:nth-child(2) {
|
||||
.ant-radio-group {
|
||||
.ant-radio-wrapper:nth-child(1) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 时
|
||||
.ant-tabs-tabpane:nth-child(3) {
|
||||
.ant-radio-group {
|
||||
.ant-radio-wrapper:nth-child(1) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { FormItemType, LoginForm } from './login';
|
||||
import React from 'react';
|
||||
import { CloseCircleFilled, LockOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { Input } from 'knowdesign';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
export enum LOGIN_TAB_KEY {
|
||||
login = 'login',
|
||||
register = 'register',
|
||||
}
|
||||
|
||||
export const LOGIN_MENU = [
|
||||
{
|
||||
label: '账号密码登录',
|
||||
key: LOGIN_TAB_KEY.login,
|
||||
render: (fn: unknown) => <LoginForm fn={fn} />,
|
||||
},
|
||||
] as any[];
|
||||
|
||||
const menuMap = new Map<string, any>();
|
||||
LOGIN_MENU.forEach((d) => {
|
||||
menuMap.set(d.key, d);
|
||||
});
|
||||
|
||||
export const LOGIN_MENU_MAP = menuMap;
|
||||
|
||||
export const FormMap = [
|
||||
{
|
||||
key: 'userName',
|
||||
label: '账号',
|
||||
type: FormItemType.input,
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: (
|
||||
<>
|
||||
<CloseCircleFilled /> 请输入用户账号
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
attrs: {
|
||||
placeholder: '请输入账号',
|
||||
prefix: <></>,
|
||||
// prefix: <UserOutlined style={{ color: 'rgba(0,0,0,.25)' }} />,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'pw',
|
||||
type: FormItemType.inputPassword,
|
||||
label: '密码',
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: (
|
||||
<>
|
||||
<CloseCircleFilled /> 请输入密码
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
attrs: {
|
||||
placeholder: '请输入密码',
|
||||
// prefix: <LockOutlined style={{ color: 'rgba(0,0,0,.25)' }} />,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 用户校验
|
||||
const UserNameCheck = (props: any) => {
|
||||
const onChange = (e: any) => {
|
||||
checkUserNameRepeat(e.target.value);
|
||||
};
|
||||
|
||||
const checkUserNameRepeat = debounce(async (value) => {
|
||||
//
|
||||
}, 1000) as any;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
key={'user-name1'}
|
||||
placeholder={'6-20个字符,支持英文字母、数字、标点符号(除空格)'}
|
||||
prefix={<LockOutlined style={{ color: 'rgba(0,0,0,.25)' }} />}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const RegisterFormMap = [
|
||||
{
|
||||
key: 'username',
|
||||
label: '用户账号',
|
||||
type: FormItemType.custom,
|
||||
customFormItem: <UserNameCheck />,
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
validator: (rule: any, value: string) => {
|
||||
const flat_5_50 = value && value.length > 4 && value.length <= 50;
|
||||
const reg = /^[0-9a-zA-Z_]{1,}$/;
|
||||
if (value === '-1') {
|
||||
return Promise.reject('账号重复');
|
||||
}
|
||||
if (flat_5_50 && reg.test(value)) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.reject('账号设置不符合要求');
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
type: FormItemType.inputPassword,
|
||||
label: '密码',
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '密码设置不符合要求',
|
||||
validator: (rule: any, value: string) => {
|
||||
const flat_6_20 = value && value.length > 5 && value.length <= 20;
|
||||
const reg = /^[a-zA-Z0-9_-]*$/;
|
||||
if (flat_6_20 && reg.test(value)) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.reject();
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
attrs: {
|
||||
placeholder: '6-20个字符,支持英文字母、数字、标点符号(除空格)',
|
||||
prefix: <LockOutlined style={{ color: 'rgba(0,0,0,.25)' }} />,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'confirm',
|
||||
type: FormItemType.inputPassword,
|
||||
label: '确认密码',
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '两次密码不统一',
|
||||
},
|
||||
({ getFieldValue }: any) => ({
|
||||
validator(params: any, value: string) {
|
||||
if (!value || getFieldValue('password') === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject('两次密码不统一');
|
||||
},
|
||||
}),
|
||||
],
|
||||
attrs: {
|
||||
placeholder: '请再次输入密码',
|
||||
prefix: <LockOutlined style={{ color: 'rgba(0,0,0,.25)' }} />,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'realName',
|
||||
label: '真实姓名',
|
||||
type: FormItemType.input,
|
||||
rules: [
|
||||
{
|
||||
required: false,
|
||||
validator: (rule: any, value: string) => {
|
||||
if (!value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const flat_1_50 = value && value.length > 0 && value.length <= 50;
|
||||
const reg = /^[a-zA-Z\u4e00-\u9fa5]+$/;
|
||||
if (!reg.test(value)) {
|
||||
return Promise.reject('请输入中文或英文');
|
||||
} else if (!flat_1_50) {
|
||||
return Promise.reject('1-50字符');
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
attrs: {
|
||||
placeholder: '真实姓名',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'phone',
|
||||
label: '手机号',
|
||||
type: FormItemType.input,
|
||||
rules: [
|
||||
{
|
||||
required: false,
|
||||
validator: (rule: any, value: string) => {
|
||||
if (!value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const reg = /^[1][3-9][0-9]{9}$/;
|
||||
if (!reg.test(value)) {
|
||||
return Promise.reject('请输入正确手机号码');
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
attrs: {
|
||||
placeholder: '手机号',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'mailbox',
|
||||
label: '邮箱',
|
||||
type: FormItemType.input,
|
||||
rules: [
|
||||
{
|
||||
required: false,
|
||||
validator: (rule: any, value: string) => {
|
||||
if (!value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const reg = /^[\w.-]+@(?:[a-z0-9]+(?:-[a-z0-9]+)*\.)+[a-z]{2,3}$/;
|
||||
if (!reg.test(value)) {
|
||||
return Promise.reject('请输入完整的邮件格式');
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
attrs: {
|
||||
placeholder: '邮箱',
|
||||
},
|
||||
},
|
||||
];
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
@@ -0,0 +1,187 @@
|
||||
@login-container-width: 500px;
|
||||
@layout-main-height: 700px;
|
||||
|
||||
@media screen and (max-width: 960px) {
|
||||
.login-page {
|
||||
.login-page-left {
|
||||
display: none;
|
||||
}
|
||||
.login-page-right {
|
||||
width: 100vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-page {
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
min-height: @layout-main-height;
|
||||
overflow: auto;
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: calc(100% - @login-container-width);
|
||||
height: 100%;
|
||||
background: no-repeat right top / 23% url('./img/login-bg-1.png'), no-repeat top left / cover url('./img/login-bg.png');
|
||||
|
||||
.dcloud-carousel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.slick-slider {
|
||||
height: 100%;
|
||||
}
|
||||
.slick-dots {
|
||||
li {
|
||||
width: 22px;
|
||||
button {
|
||||
height: 6px;
|
||||
opacity: 0.7;
|
||||
border-radius: 2px;
|
||||
background-color: #fff;
|
||||
}
|
||||
&.slick-active {
|
||||
button {
|
||||
opacity: 1;
|
||||
width: 26px;
|
||||
background-color: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.carousel-eg-ctr {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
min-height: @layout-main-height;
|
||||
&-one {
|
||||
&-img {
|
||||
width: 100%;
|
||||
&.img-one {
|
||||
max-width: 271px;
|
||||
}
|
||||
&.img-two {
|
||||
max-width: 940px;
|
||||
}
|
||||
}
|
||||
&-desc {
|
||||
&.desc-one {
|
||||
margin: 21px 0 17px 0;
|
||||
font-size: 20px;
|
||||
color: rgba(0, 0, 0, 0.84);
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-two {
|
||||
&-img {
|
||||
width: 100%;
|
||||
&.img-one {
|
||||
max-width: 820px;
|
||||
}
|
||||
}
|
||||
&-desc {
|
||||
&.desc-one {
|
||||
margin: 20px 0 10px 0;
|
||||
font-size: 15px;
|
||||
color: #525570;
|
||||
line-height: 33px;
|
||||
& > span {
|
||||
&:first-child {
|
||||
font-family: @font-family-bold;
|
||||
color: #333554;
|
||||
opacity: 0.84;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
font-size: 20px;
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.desc-two {
|
||||
padding: 0 24px;
|
||||
text-align: justify;
|
||||
font-size: 15px;
|
||||
opacity: 0.6;
|
||||
color: #333554;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: @login-container-width;
|
||||
height: 100%;
|
||||
background-color: #ffffff;
|
||||
|
||||
&-content {
|
||||
width: 340px;
|
||||
margin: 0 auto;
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
.logo {
|
||||
width: 240px;
|
||||
height: 31px;
|
||||
}
|
||||
.desc {
|
||||
margin-top: 12px;
|
||||
font-size: 16px;
|
||||
color: rgba(14, 16, 51, 0.84);
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
width: 100%;
|
||||
padding-top: 85px;
|
||||
.login-form {
|
||||
.dcloud-form-item-label > label {
|
||||
font-size: 14px;
|
||||
font-family: @font-family;
|
||||
color: #303a51;
|
||||
line-height: 20px;
|
||||
}
|
||||
.dcloud-input .dcloud-input-affix-wrapper {
|
||||
height: 40px;
|
||||
}
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
margin-top: 22px;
|
||||
> span {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 18px;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-bottom {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
letter-spacing: 0;
|
||||
text-align: center;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { LOGIN_MENU, LOGIN_MENU_MAP } from './config';
|
||||
import './index.less';
|
||||
import { AppContainer, Carousel } from 'knowdesign';
|
||||
import Logo from '@src/assets/ks-logo.png';
|
||||
import egOneTitle from './img/eg1-title.png';
|
||||
import egOneContent from './img/eg1-content.png';
|
||||
import egTwoContent from './img/eg2-content.png';
|
||||
|
||||
const carouselList = [
|
||||
<div key="2">
|
||||
<div className="carousel-eg-ctr carousel-eg-ctr-two">
|
||||
<img className="carousel-eg-ctr-two-img img-one" src={egTwoContent} />
|
||||
<div className="carousel-eg-ctr-two-desc desc-one">
|
||||
<span>Github: </span>
|
||||
<span>4K</span>
|
||||
<span>+ Star的项目 Know Streaming</span>
|
||||
</div>
|
||||
<div className="carousel-eg-ctr-two-desc desc-two">
|
||||
从开源至今社区内已经超过 2000+ 用户使用,从新创公司到巨头,尤其是得到各行业一线企业开发者的信赖。
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
// <div key="1">
|
||||
// <div className="carousel-eg-ctr carousel-eg-ctr-one">
|
||||
// <img className="carousel-eg-ctr-one-img img-one" src={egOneTitle} />
|
||||
// <div className="carousel-eg-ctr-one-desc desc-one">可能是北半球最简单易用的 Kafka 管控平台</div>
|
||||
// <img className="carousel-eg-ctr-one-img img-two" src={egOneContent} />
|
||||
// </div>
|
||||
// </div>,
|
||||
];
|
||||
|
||||
export const Login: React.FC<any> = () => {
|
||||
const [global, setGlobal] = AppContainer.useGlobalValue();
|
||||
const [selectedKeys, setSelectedKeys] = useState([LOGIN_MENU[0].key]);
|
||||
|
||||
const renderContent = () => {
|
||||
return LOGIN_MENU_MAP.get(selectedKeys[0])?.render(handleMenuClick) || LOGIN_MENU_MAP.get(LOGIN_MENU[0].key)?.render(handleMenuClick);
|
||||
};
|
||||
|
||||
const handleMenuClick = (e: string) => {
|
||||
setSelectedKeys([e]);
|
||||
window.location.hash = e;
|
||||
};
|
||||
|
||||
// 跳转到登录页时,清空全局状态
|
||||
useEffect(() => {
|
||||
if (Object.keys(global).length) {
|
||||
setGlobal({});
|
||||
}
|
||||
}, [global]);
|
||||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="login-page-left">
|
||||
<Carousel autoplay={true} autoplaySpeed={5000}>
|
||||
{carouselList}
|
||||
</Carousel>
|
||||
</div>
|
||||
<div className="login-page-right">
|
||||
<div className="login-page-right-content">
|
||||
<div className="login-page-right-content-title">
|
||||
<img className="logo" src={Logo} />
|
||||
<div className="desc">可能是北半球最简单易用的 Kafka 管控平台</div>
|
||||
</div>
|
||||
<div className="login-page-right-content-content">{renderContent()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import { Form, Button, Input, Row, InputNumber, Utils, message } from 'knowdesign';
|
||||
import { FormMap } from './config';
|
||||
import Api from '../../api';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { systemCipherKey } from '@src/constants/common';
|
||||
|
||||
export enum FormItemType {
|
||||
input = 'input',
|
||||
inputPassword = 'inputPassword',
|
||||
inputNumber = 'inputNumber',
|
||||
custom = 'custom',
|
||||
}
|
||||
|
||||
export interface IFormItem {
|
||||
key: string;
|
||||
type: FormItemType;
|
||||
attrs?: any;
|
||||
rules?: any[];
|
||||
invisible?: boolean;
|
||||
customFormItem?: any;
|
||||
}
|
||||
|
||||
export const renderFormItem = (item: IFormItem) => {
|
||||
switch (item.type) {
|
||||
default:
|
||||
case FormItemType.input:
|
||||
return <Input key={item.key} {...item.attrs} />;
|
||||
case FormItemType.inputPassword:
|
||||
return <Input.Password key={item.key} {...item.attrs} />;
|
||||
case FormItemType.inputNumber:
|
||||
return <InputNumber key={item.key} {...item.attrs} />;
|
||||
case FormItemType.custom:
|
||||
return (item as IFormItem).customFormItem;
|
||||
}
|
||||
};
|
||||
|
||||
export const LoginForm: React.FC<any> = (props) => {
|
||||
const [form] = Form.useForm();
|
||||
const history = useHistory();
|
||||
|
||||
const handleSubmit = async ({ userName, pw }: { userName: string; pw: string }) => {
|
||||
Utils.post(Api.login, {
|
||||
userName,
|
||||
pw: Utils.encryptAES(pw, systemCipherKey),
|
||||
}).then((res) => {
|
||||
message.success('登录成功');
|
||||
localStorage.setItem('userInfo', JSON.stringify(res));
|
||||
const redirectPath = window.location.search.slice('?redirect='.length) || '/';
|
||||
history.replace(redirectPath);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form name="normal_login" form={form} className="login-form" onFinish={handleSubmit} layout={'vertical'}>
|
||||
{FormMap.map((formItem) => {
|
||||
return (
|
||||
<Row key={formItem.key}>
|
||||
<Form.Item key={formItem.key} name={formItem.key} label={formItem.label} rules={formItem.rules} style={{ width: '100%' }}>
|
||||
{renderFormItem(formItem)}
|
||||
</Form.Item>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
<Form.Item key={'submit'}>
|
||||
<Row>
|
||||
<Button className="submit-btn" type="primary" htmlType="submit">
|
||||
登录
|
||||
</Button>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,630 @@
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
import { Button, Divider, Drawer, Form, Input, InputNumber, message, Radio, Select, Spin, Space, Utils } from 'knowdesign';
|
||||
import * as React from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import api from '../../api';
|
||||
import { regClusterName, regUsername } from '../../common/reg';
|
||||
import { bootstrapServersErrCodes, jmxErrCodes, zkErrCodes } from './config';
|
||||
import CodeMirrorFormItem from '@src/components/CodeMirrorFormItem';
|
||||
|
||||
const rows = 4;
|
||||
const lowKafkaVersion = '2.8.0';
|
||||
const clientPropertiesPlaceholder = `用于创建Kafka客户端进行信息获取的相关配置,
|
||||
例如开启SCRAM-SHA-256安全管控模式的集群需输入如下配置,
|
||||
未开启安全管控可不进行任何输入:
|
||||
{
|
||||
"security.protocol": "SASL_PLAINTEXT",
|
||||
"sasl.mechanism": "SCRAM-SHA-256",
|
||||
"sasl.jaas.config":
|
||||
"org.apache.kafka.common.security.scram.
|
||||
ScramLoginModule required username="xxxxxx"
|
||||
password="xxxxxx";"
|
||||
}
|
||||
`;
|
||||
|
||||
const AccessClusters = (props: any): JSX.Element => {
|
||||
const intl = useIntl();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const { afterSubmitSuccess, infoLoading, clusterInfo, visible } = props;
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [security, setSecurity] = React.useState(clusterInfo?.security || 'None');
|
||||
const [extra, setExtra] = React.useState({
|
||||
versionExtra: '',
|
||||
zooKeeperExtra: '',
|
||||
bootstrapExtra: '',
|
||||
jmxExtra: '',
|
||||
});
|
||||
const [isLowVersion, setIsLowVersion] = React.useState<any>(false);
|
||||
const [zookeeperErrorStatus, setZookeeperErrorStatus] = React.useState<any>(false);
|
||||
|
||||
const lastFormItemValue = React.useRef({
|
||||
bootstrap: clusterInfo?.bootstrapServers || '',
|
||||
zookeeper: clusterInfo?.zookeeper || '',
|
||||
clientProperties: clusterInfo?.clientProperties || {},
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const showLowVersion = !(clusterInfo?.zookeeper || !clusterInfo?.kafkaVersion || clusterInfo?.kafkaVersion >= lowKafkaVersion);
|
||||
lastFormItemValue.current.bootstrap = clusterInfo?.bootstrapServers || '';
|
||||
lastFormItemValue.current.zookeeper = clusterInfo?.zookeeper || '';
|
||||
lastFormItemValue.current.clientProperties = clusterInfo?.clientProperties || {};
|
||||
setIsLowVersion(showLowVersion);
|
||||
setExtra({
|
||||
...extra,
|
||||
versionExtra: showLowVersion ? intl.formatMessage({ id: 'access.cluster.low.version.tip' }) : '',
|
||||
});
|
||||
form.setFieldsValue({ ...clusterInfo });
|
||||
}, [clusterInfo]);
|
||||
|
||||
const onHandleValuesChange = (value: any, allValues: any) => {
|
||||
Object.keys(value).forEach((key) => {
|
||||
switch (key) {
|
||||
case 'security':
|
||||
setSecurity(value.security);
|
||||
break;
|
||||
case 'zookeeper':
|
||||
setExtra({
|
||||
...extra,
|
||||
zooKeeperExtra: '',
|
||||
bootstrapExtra: '',
|
||||
jmxExtra: '',
|
||||
});
|
||||
break;
|
||||
case 'bootstrapServers':
|
||||
setExtra({
|
||||
...extra,
|
||||
zooKeeperExtra: '',
|
||||
bootstrapExtra: '',
|
||||
jmxExtra: '',
|
||||
});
|
||||
break;
|
||||
case 'kafkaVersion':
|
||||
setExtra({
|
||||
...extra,
|
||||
versionExtra: '',
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
form.resetFields();
|
||||
setLoading(false);
|
||||
setZookeeperErrorStatus(false);
|
||||
setIsLowVersion(false);
|
||||
setSecurity('None');
|
||||
setExtra({
|
||||
versionExtra: '',
|
||||
zooKeeperExtra: '',
|
||||
bootstrapExtra: '',
|
||||
jmxExtra: '',
|
||||
});
|
||||
lastFormItemValue.current = { bootstrap: '', zookeeper: '', clientProperties: {} };
|
||||
props.setVisible && props.setVisible(false);
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
form.validateFields().then((res) => {
|
||||
let clientProperties = null;
|
||||
try {
|
||||
clientProperties = res.clientProperties && JSON.parse(res.clientProperties);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
const params = {
|
||||
bootstrapServers: res.bootstrapServers,
|
||||
clientProperties: clientProperties || {},
|
||||
description: res.description || '',
|
||||
jmxProperties: {
|
||||
jmxPort: res.jmxPort,
|
||||
maxConn: res.maxConn,
|
||||
openSSL: res.security === 'Password',
|
||||
token: res.token,
|
||||
username: res.username,
|
||||
},
|
||||
kafkaVersion: res.kafkaVersion,
|
||||
name: res.name,
|
||||
zookeeper: res.zookeeper || '',
|
||||
};
|
||||
setLoading(true);
|
||||
if (!isNaN(clusterInfo?.id)) {
|
||||
Utils.put(api.phyCluster, {
|
||||
...params,
|
||||
id: clusterInfo?.id,
|
||||
})
|
||||
.then(() => {
|
||||
message.success('编辑成功');
|
||||
afterSubmitSuccess && afterSubmitSuccess();
|
||||
onCancel();
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
Utils.post(api.phyCluster, params)
|
||||
.then(() => {
|
||||
message.success('集群接入成功。注意:新接入集群数据稳定需要1-2分钟');
|
||||
afterSubmitSuccess && afterSubmitSuccess();
|
||||
onCancel();
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const connectTest = () => {
|
||||
const bootstrapServers = form.getFieldValue('bootstrapServers');
|
||||
const zookeeper = form.getFieldValue('zookeeper');
|
||||
let clientProperties = {};
|
||||
try {
|
||||
clientProperties = form.getFieldValue('clientProperties') && JSON.parse(form.getFieldValue('clientProperties'));
|
||||
} catch (err) {
|
||||
console.error(`JSON.parse(form.getFieldValue('clientProperties')) ERROR: ${err}`);
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setIsLowVersion(false);
|
||||
setZookeeperErrorStatus(false);
|
||||
|
||||
return Utils.post(api.kafkaValidator, {
|
||||
bootstrapServers: bootstrapServers || '',
|
||||
zookeeper: zookeeper || '',
|
||||
clientProperties,
|
||||
})
|
||||
.then((res: any) => {
|
||||
form.setFieldsValue({
|
||||
jmxPort: res.jmxPort,
|
||||
});
|
||||
|
||||
if (props.kafkaVersion.indexOf(res.kafkaVersion) > -1) {
|
||||
form.setFieldsValue({
|
||||
kafkaVersion: res.kafkaVersion,
|
||||
});
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
kafkaVersion: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
form.setFieldsValue({
|
||||
zookeeper: zookeeper || res.zookeeper,
|
||||
});
|
||||
|
||||
const errList = res.errList || [];
|
||||
|
||||
const extraMsg = extra;
|
||||
|
||||
// 初始化信息为连接成功
|
||||
extraMsg.bootstrapExtra = bootstrapServers ? '连接成功' : '';
|
||||
extraMsg.zooKeeperExtra = zookeeper ? '连接成功' : '';
|
||||
|
||||
// 处理错误信息
|
||||
errList.forEach((item: any) => {
|
||||
const { code, message } = item;
|
||||
let modifyKey: 'bootstrapExtra' | 'zooKeeperExtra' | 'jmxExtra' | undefined;
|
||||
if (bootstrapServersErrCodes.includes(code)) {
|
||||
modifyKey = 'bootstrapExtra';
|
||||
} else if (zkErrCodes.includes(code)) {
|
||||
modifyKey = 'zooKeeperExtra';
|
||||
} else if (jmxErrCodes.includes(code)) {
|
||||
modifyKey = 'jmxExtra';
|
||||
}
|
||||
|
||||
if (modifyKey) {
|
||||
extraMsg[modifyKey] = `连接失败。${message}`;
|
||||
}
|
||||
});
|
||||
|
||||
// 如果kafkaVersion小于最低版本则提示
|
||||
const showLowVersion = !(clusterInfo?.zookeeper || !clusterInfo?.kafkaVersion || clusterInfo?.kafkaVersion >= lowKafkaVersion);
|
||||
setIsLowVersion(showLowVersion);
|
||||
setExtra({
|
||||
...extraMsg,
|
||||
versionExtra: showLowVersion ? intl.formatMessage({ id: 'access.cluster.low.version.tip' }) : '',
|
||||
});
|
||||
return res;
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
className="drawer-content drawer-access-cluster"
|
||||
onClose={onCancel}
|
||||
maskClosable={false}
|
||||
extra={
|
||||
<div className="operate-wrap">
|
||||
<Space>
|
||||
<Button size="small" onClick={onCancel}>
|
||||
取消
|
||||
</Button>
|
||||
<Button size="small" type="primary" onClick={onSubmit}>
|
||||
确定
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
title={intl.formatMessage({ id: props.title || 'access.cluster' })}
|
||||
visible={props.visible}
|
||||
placement="right"
|
||||
width={480}
|
||||
>
|
||||
<Spin spinning={loading || !!infoLoading}>
|
||||
<Form
|
||||
form={form}
|
||||
initialValues={{
|
||||
security,
|
||||
...clusterInfo,
|
||||
}}
|
||||
layout="vertical"
|
||||
onValuesChange={onHandleValuesChange}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="集群名称"
|
||||
validateTrigger="onBlur"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
validator: async (rule: any, value: string) => {
|
||||
if (!value) {
|
||||
return Promise.reject('集群名称不能为空');
|
||||
}
|
||||
|
||||
if (value === clusterInfo?.name) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (value?.length > 128) {
|
||||
return Promise.reject('集群名称长度限制在1~128字符');
|
||||
}
|
||||
if (!new RegExp(regClusterName).test(value)) {
|
||||
return Promise.reject(
|
||||
'集群名称支持中英文、数字、特殊字符 ! " # $ % & \' ( ) * + , - . / : ; < = > ? @ [ ] ^ _ ` { | } ~'
|
||||
);
|
||||
}
|
||||
return Utils.request(api.getClusterBasicExit(value)).then((res: any) => {
|
||||
const data = res || {};
|
||||
if (data?.exist) {
|
||||
return Promise.reject('集群名称重复');
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="bootstrapServers"
|
||||
label="Bootstrap Servers"
|
||||
extra={
|
||||
extra.bootstrapExtra.includes('连接成功') ? (
|
||||
<span>{extra.bootstrapExtra}</span>
|
||||
) : (
|
||||
<span className="error-extra-info">{extra.bootstrapExtra}</span>
|
||||
)
|
||||
}
|
||||
validateTrigger={'onBlur'}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
validator: async (rule: any, value: string) => {
|
||||
if (!value) {
|
||||
return Promise.reject('Bootstrap Servers不能为空');
|
||||
}
|
||||
if (value.length > 2000) {
|
||||
return Promise.reject('Bootstrap Servers长度限制在2000字符');
|
||||
}
|
||||
if (value && value !== lastFormItemValue.current.bootstrap) {
|
||||
return connectTest()
|
||||
.then((res: any) => {
|
||||
lastFormItemValue.current.bootstrap = value;
|
||||
|
||||
return Promise.resolve('');
|
||||
})
|
||||
.catch((err) => {
|
||||
return Promise.reject('连接失败');
|
||||
});
|
||||
}
|
||||
return Promise.resolve('');
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
placeholder="请输入Bootstrap Servers地址,例如:192.168.1.1:9092,192.168.1.2:9092,192.168.1.3:9092"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="zookeeper"
|
||||
label="Zookeeper"
|
||||
extra={
|
||||
extra.zooKeeperExtra.includes('连接成功') ? (
|
||||
<span>{extra.zooKeeperExtra}</span>
|
||||
) : (
|
||||
<span className="error-extra-info">{extra.zooKeeperExtra}</span>
|
||||
)
|
||||
}
|
||||
validateStatus={zookeeperErrorStatus ? 'error' : 'success'}
|
||||
validateTrigger={'onBlur'}
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
validator: async (rule: any, value: string) => {
|
||||
if (!value) {
|
||||
setZookeeperErrorStatus(false);
|
||||
return Promise.resolve('');
|
||||
}
|
||||
|
||||
if (value.length > 2000) {
|
||||
return Promise.reject('Zookeeper长度限制在2000字符');
|
||||
}
|
||||
|
||||
if (value && value !== lastFormItemValue.current.zookeeper) {
|
||||
return connectTest()
|
||||
.then((res: any) => {
|
||||
lastFormItemValue.current.zookeeper = value;
|
||||
setZookeeperErrorStatus(false);
|
||||
return Promise.resolve('');
|
||||
})
|
||||
.catch((err) => {
|
||||
setZookeeperErrorStatus(true);
|
||||
return Promise.reject('连接失败');
|
||||
});
|
||||
}
|
||||
return Promise.resolve('');
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
placeholder="请输入Zookeeper地址,例如:192.168.0.1:2181,192.168.0.2:2181,192.168.0.2:2181/ks-kafka"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="no-item-control"
|
||||
name="Metrics"
|
||||
label="Metrics"
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
message: '',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<></>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="jmxPort"
|
||||
label="JMX Port"
|
||||
className="inline-item adjust-height-style"
|
||||
extra={extra.jmxExtra}
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
message: '',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber style={{ width: 134 }} min={0} max={99999} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="maxConn"
|
||||
label="MaxConn"
|
||||
className="inline-item adjust-height-style"
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
message: '',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber style={{ width: 134 }} min={0} max={99999} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="security"
|
||||
label="Security"
|
||||
className="inline-item adjust-height-style"
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
message: '',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value="None">None</Radio>
|
||||
<Radio value="Password">Password Authentication</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
{security === 'Password' ? (
|
||||
<>
|
||||
<Form.Item
|
||||
className="inline-item max-width-66"
|
||||
name="username"
|
||||
label="User Info"
|
||||
style={{ width: '58%' }}
|
||||
rules={[
|
||||
{
|
||||
required: security === 'Password' || clusterInfo?.security === 'Password',
|
||||
validator: async (rule: any, value: string) => {
|
||||
if (!value) {
|
||||
return Promise.reject('用户名不能为空');
|
||||
}
|
||||
if (!new RegExp(regUsername).test(value)) {
|
||||
return Promise.reject('仅支持大小写、下划线、短划线(-)');
|
||||
}
|
||||
if (value.length > 128) {
|
||||
return Promise.reject('用户名长度限制在1~128字符');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入用户名" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="inline-item"
|
||||
name="token"
|
||||
label=""
|
||||
style={{ width: '38%', marginRight: 0 }}
|
||||
rules={[
|
||||
{
|
||||
required: security === 'Password' || clusterInfo?.security === 'Password',
|
||||
validator: async (rule: any, value: string) => {
|
||||
if (!value) {
|
||||
return Promise.reject('密码不能为空');
|
||||
}
|
||||
if (!new RegExp(regUsername).test(value)) {
|
||||
return Promise.reject('密码只能由大小写、下划线、短划线(-)组成');
|
||||
}
|
||||
if (value.length < 6 || value.length > 32) {
|
||||
return Promise.reject('密码长度限制在6~32字符');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入密码" />
|
||||
</Form.Item>
|
||||
</>
|
||||
) : null}
|
||||
<Form.Item
|
||||
name="kafkaVersion"
|
||||
label="Version"
|
||||
extra={<span className="error-extra-info">{extra.versionExtra}</span>}
|
||||
validateStatus={isLowVersion ? 'error' : 'success'}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
validator: async (rule: any, value: any) => {
|
||||
if (!value) {
|
||||
setIsLowVersion(true);
|
||||
return Promise.reject('版本号不能为空');
|
||||
}
|
||||
// 检测版本号小于2.8.0,如果没有填zookeeper信息,才会提示
|
||||
const zookeeper = form.getFieldValue('zookeeper');
|
||||
if (value < lowKafkaVersion && !zookeeper) {
|
||||
setIsLowVersion(true);
|
||||
setExtra({
|
||||
...extra,
|
||||
versionExtra: intl.formatMessage({ id: 'access.cluster.low.version.tip' }),
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
setIsLowVersion(false);
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select placeholder="请选择Kafka Version,如无匹配则选择相近版本">
|
||||
{(props.kafkaVersion || []).map((item: string) => (
|
||||
<Select.Option key={item} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="clientProperties"
|
||||
label="集群配置"
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
message: '请输入集群配置',
|
||||
},
|
||||
() => ({
|
||||
validator(_, value) {
|
||||
try {
|
||||
if (value) {
|
||||
JSON.parse(value);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
} catch (e) {
|
||||
return Promise.reject(new Error('输入内容必须为 JSON'));
|
||||
}
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<div>
|
||||
<CodeMirrorFormItem
|
||||
resize
|
||||
defaultInput={form.getFieldValue('clientProperties')}
|
||||
placeholder={clientPropertiesPlaceholder}
|
||||
onBeforeChange={(clientProperties: string) => {
|
||||
form.setFieldsValue({ clientProperties });
|
||||
form.validateFields(['clientProperties']);
|
||||
}}
|
||||
onBlur={(value: any) => {
|
||||
form.validateFields(['clientProperties']).then(() => {
|
||||
const bootstrapServers = form.getFieldValue('bootstrapServers');
|
||||
const zookeeper = form.getFieldValue('zookeeper');
|
||||
const clientProperties = form.getFieldValue('clientProperties');
|
||||
|
||||
if (
|
||||
clientProperties &&
|
||||
clientProperties !== lastFormItemValue.current.clientProperties &&
|
||||
(!!bootstrapServers || !!zookeeper)
|
||||
) {
|
||||
connectTest()
|
||||
.then((res: any) => {
|
||||
lastFormItemValue.current.clientProperties = clientProperties;
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error('连接失败');
|
||||
});
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="集群描述"
|
||||
rules={[
|
||||
{
|
||||
required: false,
|
||||
validator: async (rule: any, value: string) => {
|
||||
if (!value) {
|
||||
return Promise.resolve('');
|
||||
}
|
||||
if (value && value.length > 200) {
|
||||
return Promise.reject('集群描述长度限制在200字符');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea rows={rows} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Spin>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessClusters;
|
||||
@@ -0,0 +1,130 @@
|
||||
import { DoubleRightOutlined } from '@ant-design/icons';
|
||||
import { Checkbox } from 'knowdesign';
|
||||
import { debounce } from 'lodash';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
const CheckboxGroup = Checkbox.Group;
|
||||
|
||||
interface IVersion {
|
||||
firstLine: string[];
|
||||
leftVersion: string[];
|
||||
}
|
||||
|
||||
const CustomCheckGroup = (props: { kafkaVersion: string[]; onChangeCheckGroup: any }) => {
|
||||
const { kafkaVersion, onChangeCheckGroup } = props;
|
||||
const [checkedKafkaVersion, setCheckedKafkaVersion] = React.useState<IVersion>({
|
||||
firstLine: [],
|
||||
leftVersion: [],
|
||||
});
|
||||
const [allVersion, setAllVersion] = React.useState<IVersion>({
|
||||
firstLine: [],
|
||||
leftVersion: [],
|
||||
});
|
||||
|
||||
const [indeterminate, setIndeterminate] = React.useState(false);
|
||||
const [checkAll, setCheckAll] = React.useState(true);
|
||||
const [moreGroupWidth, setMoreGroupWidth] = React.useState(400);
|
||||
const [showMore, setShowMore] = React.useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', handleDocumentClick);
|
||||
return () => {
|
||||
document.removeEventListener('click', handleDocumentClick);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDocumentClick = (e: Event) => {
|
||||
setShowMore(false);
|
||||
}
|
||||
|
||||
const setCheckAllStauts = (list: string[], otherList: string[]) => {
|
||||
onChangeCheckGroup([...list, ...otherList]);
|
||||
setIndeterminate(!!list.length && list.length + otherList.length < kafkaVersion.length);
|
||||
setCheckAll(list.length + otherList.length === kafkaVersion.length);
|
||||
};
|
||||
|
||||
const getTwoPanelVersion = () => {
|
||||
const width = (document.getElementsByClassName('custom-check-group')[0] as any)?.offsetWidth;
|
||||
const checkgroupWidth = width - 100 - 86;
|
||||
const num = (checkgroupWidth / 108) | 0;
|
||||
const firstLine = Array.from(kafkaVersion).splice(0, num);
|
||||
setMoreGroupWidth(num * 108 + 88 + 66);
|
||||
const leftVersion = Array.from(kafkaVersion).splice(num);
|
||||
return { firstLine, leftVersion };
|
||||
};
|
||||
|
||||
const onFirstVersionChange = (list: []) => {
|
||||
setCheckedKafkaVersion({
|
||||
...checkedKafkaVersion,
|
||||
firstLine: list,
|
||||
});
|
||||
|
||||
setCheckAllStauts(list, checkedKafkaVersion.leftVersion);
|
||||
};
|
||||
|
||||
const onLeftVersionChange = (list: []) => {
|
||||
setCheckedKafkaVersion({
|
||||
...checkedKafkaVersion,
|
||||
leftVersion: list,
|
||||
});
|
||||
setCheckAllStauts(list, checkedKafkaVersion.firstLine);
|
||||
};
|
||||
|
||||
const onCheckAllChange = (e: any) => {
|
||||
const versions = getTwoPanelVersion();
|
||||
|
||||
setCheckedKafkaVersion(
|
||||
e.target.checked
|
||||
? versions
|
||||
: {
|
||||
firstLine: [],
|
||||
leftVersion: [],
|
||||
}
|
||||
);
|
||||
onChangeCheckGroup(e.target.checked ? [...versions.firstLine, ...versions.leftVersion] : []);
|
||||
|
||||
setIndeterminate(false);
|
||||
setCheckAll(e.target.checked);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleVersionLine = () => {
|
||||
const versions = getTwoPanelVersion();
|
||||
setAllVersion(versions);
|
||||
setCheckedKafkaVersion(versions);
|
||||
};
|
||||
handleVersionLine();
|
||||
|
||||
window.addEventListener('resize', handleVersionLine); //监听窗口大小改变
|
||||
return () => window.removeEventListener('resize', debounce(handleVersionLine, 500));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="custom-check-group" onClick={(e) => e.nativeEvent.stopImmediatePropagation()}>
|
||||
<div>
|
||||
<Checkbox className="check-all" indeterminate={indeterminate} onChange={onCheckAllChange} checked={checkAll}>
|
||||
全选
|
||||
</Checkbox>
|
||||
</div>
|
||||
<CheckboxGroup options={allVersion.firstLine} value={checkedKafkaVersion.firstLine} onChange={onFirstVersionChange} />
|
||||
{showMore ? (
|
||||
<CheckboxGroup
|
||||
style={{ width: moreGroupWidth }}
|
||||
className="more-check-group"
|
||||
options={allVersion.leftVersion}
|
||||
value={checkedKafkaVersion.leftVersion}
|
||||
onChange={onLeftVersionChange}
|
||||
/>
|
||||
) : null}
|
||||
<div className="more-btn" onClick={() => setShowMore(!showMore)}>
|
||||
<a>
|
||||
{!showMore ? '展开更多' : '收起更多'} <DoubleRightOutlined style={{ transform: `rotate(${showMore ? '270' : '90'}deg)` }} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomCheckGroup;
|
||||
@@ -0,0 +1,317 @@
|
||||
import React, { useEffect, useMemo, useRef, useState, useReducer } from 'react';
|
||||
import { Slider, Input, Select, Checkbox, Button, Utils, Spin, IconFont, AppContainer } from 'knowdesign';
|
||||
import API from '../../api';
|
||||
import TourGuide, { MultiPageSteps } from '@src/components/TourGuide';
|
||||
import './index.less';
|
||||
import { healthSorceList, linesMetric, pointsMetric, sortFieldList, sortTypes, statusFilters } from './config';
|
||||
import { oneDayMillims } from '../../constants/common';
|
||||
import ListScroll from './List';
|
||||
import AccessClusters from './AccessCluster';
|
||||
import CustomCheckGroup from './CustomCheckGroup';
|
||||
import { ClustersPermissionMap } from '../CommonConfig';
|
||||
|
||||
const CheckboxGroup = Checkbox.Group;
|
||||
const { Option } = Select;
|
||||
|
||||
const MultiClusterPage = () => {
|
||||
const [run, setRun] = useState<boolean>(false);
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [statusList, setStatusList] = React.useState([1, 0]);
|
||||
const [kafkaVersion, setKafkaVersion] = React.useState({});
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const [list, setList] = useState<[]>([]);
|
||||
const [healthScoreRange, setHealthScoreRange] = React.useState([0, 100]);
|
||||
const [checkedKafkaVersion, setCheckedKafkaVersion] = React.useState({});
|
||||
const [sortInfo, setSortInfo] = React.useState({
|
||||
sortField: 'HealthScore',
|
||||
sortType: 'asc',
|
||||
});
|
||||
const [clusterLoading, setClusterLoading] = useState(true);
|
||||
const [pageLoading, setPageLoading] = useState(true);
|
||||
const [isReload, setIsReload] = useState(false);
|
||||
const [versionLoading, setVersionLoading] = useState(true);
|
||||
const [searchKeywords, setSearchKeywords] = useState('');
|
||||
const [stateInfo, setStateInfo] = React.useState({
|
||||
downCount: 0,
|
||||
liveCount: 0,
|
||||
total: 0,
|
||||
});
|
||||
const [pagination, setPagination] = useState({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const searchKeyword = useRef('');
|
||||
|
||||
const getPhyClustersDashbord = (pageNo: number, pageSize: number) => {
|
||||
const endTime = new Date().getTime();
|
||||
const startTime = endTime - oneDayMillims;
|
||||
const params = {
|
||||
metricLines: {
|
||||
endTime,
|
||||
metricsNames: linesMetric,
|
||||
startTime,
|
||||
},
|
||||
latestMetricNames: pointsMetric,
|
||||
pageNo: pageNo || 1,
|
||||
pageSize: pageSize || 10,
|
||||
preciseFilterDTOList: [
|
||||
{
|
||||
fieldName: 'kafkaVersion',
|
||||
fieldValueList: checkedKafkaVersion,
|
||||
},
|
||||
],
|
||||
rangeFilterDTOList: [
|
||||
{
|
||||
fieldMaxValue: healthScoreRange[1],
|
||||
fieldMinValue: healthScoreRange[0],
|
||||
fieldName: 'HealthScore',
|
||||
},
|
||||
],
|
||||
searchKeywords,
|
||||
...sortInfo,
|
||||
};
|
||||
|
||||
if (statusList.length === 1) {
|
||||
params.preciseFilterDTOList.push({
|
||||
fieldName: 'Alive',
|
||||
fieldValueList: statusList,
|
||||
});
|
||||
}
|
||||
return Utils.post(API.phyClustersDashbord, params);
|
||||
};
|
||||
|
||||
const getSupportKafkaVersion = () => {
|
||||
setVersionLoading(true);
|
||||
Utils.request(API.supportKafkaVersion)
|
||||
.then((res) => {
|
||||
setKafkaVersion(res || {});
|
||||
setVersionLoading(false);
|
||||
setCheckedKafkaVersion(res ? Object.keys(res) : []);
|
||||
})
|
||||
.catch((err) => {
|
||||
setVersionLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const getPhyClusterState = () => {
|
||||
Utils.request(API.phyClusterState)
|
||||
.then((res: any) => {
|
||||
setStateInfo(res);
|
||||
})
|
||||
.finally(() => {
|
||||
setPageLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getPhyClusterState();
|
||||
getSupportKafkaVersion();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pageLoading && stateInfo.total) {
|
||||
setRun(true);
|
||||
}
|
||||
}, [pageLoading, stateInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (versionLoading) return;
|
||||
setClusterLoading(true);
|
||||
getPhyClustersDashbord(pagination.pageNo, pagination.pageSize)
|
||||
.then((res: any) => {
|
||||
setPagination(res.pagination);
|
||||
setList(res?.bizData || []);
|
||||
return res;
|
||||
})
|
||||
.finally(() => {
|
||||
setClusterLoading(false);
|
||||
});
|
||||
}, [sortInfo, checkedKafkaVersion, healthScoreRange, statusList, searchKeywords, isReload]);
|
||||
|
||||
const onSilderChange = (value: number[]) => {
|
||||
setHealthScoreRange(value);
|
||||
};
|
||||
|
||||
const onSelectChange = (type: string, value: string) => {
|
||||
setSortInfo({
|
||||
...sortInfo,
|
||||
[type]: value,
|
||||
});
|
||||
};
|
||||
|
||||
const onStatusChange = (list: []) => {
|
||||
setStatusList(list);
|
||||
};
|
||||
|
||||
const onInputChange = (e: any) => {
|
||||
const { value } = e.target;
|
||||
setSearchKeywords(value.trim());
|
||||
};
|
||||
|
||||
const onChangeCheckGroup = (list: []) => {
|
||||
setCheckedKafkaVersion(list);
|
||||
};
|
||||
|
||||
const afterSubmitSuccessAccessClusters = () => {
|
||||
getPhyClusterState();
|
||||
setIsReload(!isReload);
|
||||
};
|
||||
|
||||
const renderEmpty = () => {
|
||||
return (
|
||||
<div className="empty-page">
|
||||
<div className="title">Kafka 多集群管理</div>
|
||||
<div className="img">
|
||||
<div className="img-card-1" />
|
||||
<div className="img-card-2" />
|
||||
<div className="img-card-3" />
|
||||
</div>
|
||||
<div>
|
||||
<Button className="header-filter-top-button" type="primary" onClick={() => setVisible(true)}>
|
||||
<span>
|
||||
<IconFont type="icon-jiahao" />
|
||||
<span className="text">接入集群</span>
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderLoading = () => {
|
||||
return (
|
||||
<div style={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Spin spinning={true} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
return (
|
||||
<div className="multi-cluster-page" id="scrollableDiv">
|
||||
<div className="multi-cluster-page-fixed">
|
||||
<div className="content-container">
|
||||
<div className="multi-cluster-header">
|
||||
<div className="cluster-header-card">
|
||||
<div className="cluster-header-card-bg-left"></div>
|
||||
<div className="cluster-header-card-bg-right"></div>
|
||||
<h5 className="header-card-title">
|
||||
Clusters<span className="chinese-text"> 总数</span>
|
||||
</h5>
|
||||
<div className="header-card-total">{stateInfo.total}</div>
|
||||
<div className="header-card-info">
|
||||
<div className="card-info-item card-info-item-live">
|
||||
<div>
|
||||
live
|
||||
<span className="info-item-value">
|
||||
<em>{stateInfo.liveCount}</em>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-info-item card-info-item-down">
|
||||
<div>
|
||||
down
|
||||
<span className="info-item-value">
|
||||
<em>{stateInfo.downCount}</em>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="cluster-header-filter">
|
||||
<div className="header-filter-top">
|
||||
<div className="header-filter-top-input">
|
||||
<Input
|
||||
onPressEnter={onInputChange}
|
||||
onChange={(e) => (searchKeyword.current = e.target.value)}
|
||||
allowClear
|
||||
bordered={false}
|
||||
placeholder="请输入ClusterName进行搜索"
|
||||
suffix={<IconFont className="icon" type="icon-fangdajing" onClick={() => setSearchKeywords(searchKeyword.current)} />}
|
||||
/>
|
||||
</div>
|
||||
{global.hasPermission && global.hasPermission(ClustersPermissionMap.CLUSTER_ADD) ? (
|
||||
<>
|
||||
<div className="header-filter-top-divider"></div>
|
||||
<Button className="header-filter-top-button" type="primary" onClick={() => setVisible(true)}>
|
||||
<IconFont type="icon-jiahao" />
|
||||
<span className="text">接入集群</span>
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="header-filter-bottom">
|
||||
<div className="header-filter-bottom-item header-filter-bottom-item-checkbox">
|
||||
<h3 className="header-filter-bottom-item-title">版本选择</h3>
|
||||
<div className="header-filter-bottom-item-content flex">
|
||||
{Object.keys(kafkaVersion).length ? (
|
||||
<CustomCheckGroup kafkaVersion={Object.keys(kafkaVersion)} onChangeCheckGroup={onChangeCheckGroup} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="header-filter-bottom-item header-filter-bottom-item-slider">
|
||||
<h3 className="header-filter-bottom-item-title title-right">健康分</h3>
|
||||
<div className="header-filter-bottom-item-content">
|
||||
<Slider range step={20} defaultValue={[0, 100]} marks={healthSorceList} onAfterChange={onSilderChange} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="multi-cluster-filter">
|
||||
<div className="multi-cluster-filter-select">
|
||||
<Select
|
||||
onChange={(value) => onSelectChange('sortField', value)}
|
||||
defaultValue="HealthScore"
|
||||
style={{ width: 170, marginRight: 12 }}
|
||||
>
|
||||
{sortFieldList.map((item) => (
|
||||
<Option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select onChange={(value) => onSelectChange('sortType', value)} defaultValue="asc" style={{ width: 170 }}>
|
||||
{sortTypes.map((item) => (
|
||||
<Option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="multi-cluster-filter-checkbox">
|
||||
<CheckboxGroup options={statusFilters} value={statusList} onChange={onStatusChange} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="test-modal-23"></div>
|
||||
</div>
|
||||
</div>
|
||||
<Spin spinning={clusterLoading}>{renderList}</Spin>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderList = useMemo(() => {
|
||||
return <ListScroll list={list} pagination={pagination} loadMoreData={getPhyClustersDashbord} getPhyClusterState={getPhyClusterState} />;
|
||||
}, [list, pagination]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TourGuide guide={MultiPageSteps} run={run} />
|
||||
{pageLoading ? renderLoading() : stateInfo.total ? renderContent() : renderEmpty()}
|
||||
<AccessClusters
|
||||
visible={visible}
|
||||
setVisible={setVisible}
|
||||
kafkaVersion={Object.keys(kafkaVersion)}
|
||||
afterSubmitSuccess={afterSubmitSuccessAccessClusters}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiClusterPage;
|
||||
@@ -0,0 +1,373 @@
|
||||
import { AppContainer, Divider, Form, IconFont, Input, List, message, Modal, Progress, Spin, Tooltip, Utils } from 'knowdesign';
|
||||
import moment from 'moment';
|
||||
import React, { useEffect, useMemo, useState, useReducer } from 'react';
|
||||
import InfiniteScroll from 'react-infinite-scroll-component';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { timeFormat } from '../../constants/common';
|
||||
import { IMetricPoint, linesMetric } from './config';
|
||||
import { useIntl } from 'react-intl';
|
||||
import api, { MetricType } from '../../api';
|
||||
import { getHealthClassName, getHealthProcessColor, getHealthText } from '../SingleClusterDetail/config';
|
||||
import { ClustersPermissionMap } from '../CommonConfig';
|
||||
import { getUnit, getDataNumberUnit } from '@src/constants/chartConfig';
|
||||
import SmallChart from '@src/components/SmallChart';
|
||||
|
||||
const ListScroll = (props: { loadMoreData: any; list: any; pagination: any; getPhyClusterState: any }) => {
|
||||
const history = useHistory();
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [form] = Form.useForm();
|
||||
const [list, setList] = useState<[]>(props.list || []);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [clusterInfo, setClusterInfo] = useState({} as any);
|
||||
const [pagination, setPagination] = useState(
|
||||
props.pagination || {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
}
|
||||
);
|
||||
const intl = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
setList(props.list || []);
|
||||
setPagination(props.pagination || {});
|
||||
}, [props.list, props.pagination]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const loadMoreData = async () => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
|
||||
const res = await props.loadMoreData(pagination.pageNo + 1, pagination.pageSize);
|
||||
const _data = list.concat(res.bizData || []) as any;
|
||||
setList(_data);
|
||||
setPagination(res.pagination);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const RenderItem = (itemData: any) => {
|
||||
itemData = itemData || {};
|
||||
const metrics = linesMetric;
|
||||
const metricPoints = [] as IMetricPoint[];
|
||||
metrics.forEach((item) => {
|
||||
const line = {
|
||||
metricName: item,
|
||||
value: itemData.latestMetrics?.metrics?.[item] || 0,
|
||||
unit: (global.getMetricDefine && global.getMetricDefine(MetricType.Cluster, item).unit) || '',
|
||||
metricLines: {
|
||||
name: item,
|
||||
data: itemData.metricLines
|
||||
.find((metric: any) => metric.metricName === item)
|
||||
?.metricPoints.map((point: IMetricPoint) => [point.timeStamp, point.value]),
|
||||
},
|
||||
} as IMetricPoint;
|
||||
|
||||
// 如果单位是 字节 ,进行单位换算
|
||||
if (line.unit.toLowerCase().includes('byte')) {
|
||||
const [unit, size] = getUnit(line.value);
|
||||
line.value = Number((line.value / size).toFixed(2));
|
||||
line.unit = line.unit.toLowerCase().replace('byte', unit);
|
||||
}
|
||||
|
||||
// Messages 指标值特殊处理
|
||||
if (line.metricName === 'LeaderMessages') {
|
||||
const [unit, size] = getDataNumberUnit(line.value);
|
||||
line.value = Number((line.value / size).toFixed(2));
|
||||
line.unit = unit + line.unit;
|
||||
}
|
||||
|
||||
metricPoints.push(line);
|
||||
});
|
||||
|
||||
const {
|
||||
Brokers: brokers,
|
||||
Zookeepers: zks,
|
||||
HealthCheckPassed: healthCheckPassed,
|
||||
HealthCheckTotal: healthCheckTotal,
|
||||
HealthScore: healthScore,
|
||||
ZookeepersAvailable: zookeepersAvailable,
|
||||
LoadReBalanceCpu: loadReBalanceCpu,
|
||||
LoadReBalanceDisk: loadReBalanceDisk,
|
||||
LoadReBalanceEnable: loadReBalanceEnable,
|
||||
LoadReBalanceNwIn: loadReBalanceNwIn,
|
||||
LoadReBalanceNwOut: loadReBalanceNwOut,
|
||||
} = itemData.latestMetrics?.metrics || {};
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
onClick={() => {
|
||||
history.push(`/cluster/${itemData.id}/cluster`);
|
||||
}}
|
||||
>
|
||||
<div className={'multi-cluster-list-item'}>
|
||||
<div className="multi-cluster-list-item-healthy">
|
||||
<Progress
|
||||
type="circle"
|
||||
status={!itemData.alive ? 'exception' : healthScore >= 90 ? 'success' : 'normal'}
|
||||
strokeWidth={4}
|
||||
// className={healthScore > 90 ? 'green-circle' : ''}
|
||||
className={+itemData.alive <= 0 ? 'red-circle' : +healthScore < 90 ? 'blue-circle' : 'green-circle'}
|
||||
strokeColor={getHealthProcessColor(healthScore, itemData.alive)}
|
||||
percent={itemData.alive ? healthScore : 100}
|
||||
format={() => (
|
||||
<div className={`healthy-percent ${getHealthClassName(healthScore, itemData?.alive)}`}>
|
||||
{getHealthText(healthScore, itemData?.alive)}
|
||||
</div>
|
||||
)}
|
||||
width={70}
|
||||
/>
|
||||
<div className="healthy-degree">
|
||||
<span className="healthy-degree-status">通过</span>
|
||||
<span className="healthy-degree-proportion">
|
||||
{healthCheckPassed}/{healthCheckTotal}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="multi-cluster-list-item-right">
|
||||
<div className="multi-cluster-list-item-base">
|
||||
<div className="multi-cluster-list-item-base-left">
|
||||
<div className="base-name">{itemData.name ?? '-'}</div>
|
||||
<span className="base-version">{itemData.kafkaVersion ?? '-'}</span>
|
||||
{loadReBalanceEnable !== undefined && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{[
|
||||
['BytesIn', loadReBalanceEnable && loadReBalanceNwIn],
|
||||
['BytesOut', loadReBalanceEnable && loadReBalanceNwOut],
|
||||
['Disk', loadReBalanceEnable && loadReBalanceDisk],
|
||||
].map(([name, isBalanced]) => {
|
||||
return isBalanced ? (
|
||||
<div className="balance-box balanced">{name} 已均衡</div>
|
||||
) : loadReBalanceEnable ? (
|
||||
<div className="balance-box unbalanced">{name} 未均衡</div>
|
||||
) : (
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
尚未开启 {name} 均衡策略,
|
||||
<Link to={`/cluster/${itemData.id}/cluster/balance`}>前往开启</Link>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="balance-box unbalanced">{name} 未均衡</div>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="multi-cluster-list-item-base-date">{moment(itemData.createTime).format(timeFormat)}</div>
|
||||
</div>
|
||||
<div className="multi-cluster-list-item-Indicator">
|
||||
<div className="indicator-left">
|
||||
<div className="indicator-left-item">
|
||||
<div className="indicator-left-item-title">
|
||||
<span
|
||||
className="indicator-left-item-title-dot"
|
||||
style={{
|
||||
background: itemData.latestMetrics?.metrics?.BrokersNotAlive ? '#FF7066' : '#34C38F',
|
||||
}}
|
||||
></span>
|
||||
Brokers
|
||||
</div>
|
||||
<div className="indicator-left-item-value">{brokers}</div>
|
||||
</div>
|
||||
<div className="indicator-left-item">
|
||||
<div className="indicator-left-item-title">
|
||||
<span
|
||||
className="indicator-left-item-title-dot"
|
||||
style={{
|
||||
background: zookeepersAvailable === -1 ? '#e9e7e7' : zookeepersAvailable === 0 ? '#FF7066' : '#34C38F',
|
||||
}}
|
||||
></span>
|
||||
ZK
|
||||
</div>
|
||||
<div className="indicator-left-item-value">{zookeepersAvailable === -1 ? '-' : zks}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="indicator-right">
|
||||
{metricPoints.map((row, index) => {
|
||||
return (
|
||||
<div
|
||||
key={row.metricName + index}
|
||||
className={`indicator-right-item ${row.metricName === 'LeaderMessages' ? 'first-right-item' : ''}`}
|
||||
>
|
||||
<div className="indicator-right-item-total">
|
||||
<div className="indicator-right-item-total-name">
|
||||
{row.metricName === 'TotalLogSize' ? 'MessageSize' : row.metricName}
|
||||
</div>
|
||||
<div className="indicator-right-item-total-value">
|
||||
{row.value}
|
||||
<span className="total-value-unit">{row.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="indicator-right-item-chart">
|
||||
<SmallChart width={79} height={40} chartData={row.metricLines} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{global.hasPermission && global.hasPermission(ClustersPermissionMap.CLUSTER_DEL) ? (
|
||||
<div className="multi-cluster-list-item-btn">
|
||||
<div className="icon" onClick={(event) => onClickDeleteBtn(event, itemData)}>
|
||||
<IconFont type="icon-shanchu1" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</List.Item>
|
||||
);
|
||||
};
|
||||
|
||||
const onFinish = () => {
|
||||
form.validateFields().then((formData) => {
|
||||
Utils.delete(api.phyCluster, {
|
||||
params: {
|
||||
clusterPhyId: clusterInfo.id,
|
||||
},
|
||||
}).then((res) => {
|
||||
message.success('删除成功');
|
||||
setVisible(false);
|
||||
props?.getPhyClusterState();
|
||||
const fliterList: any = list.filter((item: any) => {
|
||||
return item?.id !== clusterInfo.id;
|
||||
});
|
||||
setList(fliterList || []);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const onClickDeleteBtn = (event: any, clusterInfo: any) => {
|
||||
event.stopPropagation();
|
||||
setClusterInfo(clusterInfo);
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{useMemo(
|
||||
() => (
|
||||
<InfiniteScroll
|
||||
dataLength={list.length}
|
||||
next={loadMoreData}
|
||||
hasMore={list.length < pagination.total}
|
||||
loader={<Spin style={{ paddingLeft: '50%', paddingTop: 15 }} spinning={loading} />}
|
||||
endMessage={
|
||||
!pagination.total ? (
|
||||
''
|
||||
) : (
|
||||
<Divider className="load-completed-tip" plain>
|
||||
加载完成 共{pagination.total}条
|
||||
</Divider>
|
||||
)
|
||||
}
|
||||
scrollableTarget="scrollableDiv"
|
||||
>
|
||||
<List
|
||||
bordered={false}
|
||||
split={false}
|
||||
className="multi-cluster-list"
|
||||
itemLayout="horizontal"
|
||||
dataSource={list}
|
||||
renderItem={RenderItem}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
),
|
||||
[list, pagination, loading]
|
||||
)}
|
||||
|
||||
<Modal
|
||||
width={570}
|
||||
destroyOnClose={true}
|
||||
centered={true}
|
||||
className="custom-modal"
|
||||
wrapClassName="del-topic-modal delete-modal"
|
||||
title={intl.formatMessage({
|
||||
id: 'delete.cluster.confirm.title',
|
||||
})}
|
||||
visible={visible}
|
||||
onOk={onFinish}
|
||||
okText={intl.formatMessage({
|
||||
id: 'btn.delete',
|
||||
})}
|
||||
cancelText={intl.formatMessage({
|
||||
id: 'btn.cancel',
|
||||
})}
|
||||
onCancel={() => setVisible(false)}
|
||||
okButtonProps={{
|
||||
style: {
|
||||
width: 56,
|
||||
},
|
||||
danger: true,
|
||||
size: 'small',
|
||||
}}
|
||||
cancelButtonProps={{
|
||||
style: {
|
||||
width: 56,
|
||||
},
|
||||
size: 'small',
|
||||
}}
|
||||
>
|
||||
<div className="tip-info">
|
||||
<IconFont type="icon-warning-circle"></IconFont>
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
id: 'delete.cluster.confirm.tip',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<Form form={form} className="form" labelCol={{ span: 4 }} wrapperCol={{ span: 16 }} autoComplete="off">
|
||||
<Form.Item label="集群名称" name="name" rules={[{ required: false, message: '' }]}>
|
||||
<span>{clusterInfo.name}</span>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="集群名称"
|
||||
name="clusterName"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'delete.cluster.confirm.cluster',
|
||||
}),
|
||||
validator: (rule: any, value: string) => {
|
||||
value = value || '';
|
||||
if (!value.trim() || value.trim() !== clusterInfo.name)
|
||||
return Promise.reject(
|
||||
intl.formatMessage({
|
||||
id: 'delete.cluster.confirm.cluster',
|
||||
})
|
||||
);
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListScroll;
|
||||
@@ -0,0 +1,155 @@
|
||||
import { FormItemType, IFormItem } from 'knowdesign/lib/extend/x-form';
|
||||
|
||||
export const bootstrapServersErrCodes = [10, 11, 12];
|
||||
export const zkErrCodes = [20, 21];
|
||||
export const jmxErrCodes = [30, 31];
|
||||
|
||||
export const statusFilters = [
|
||||
{
|
||||
label: 'Live',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: 'Down',
|
||||
value: 0,
|
||||
},
|
||||
];
|
||||
|
||||
export const sortFieldList = [
|
||||
{
|
||||
label: '接入时间',
|
||||
value: 'createTime',
|
||||
},
|
||||
{
|
||||
label: '健康分',
|
||||
value: 'HealthScore',
|
||||
},
|
||||
{
|
||||
label: 'Messages',
|
||||
value: 'LeaderMessages',
|
||||
},
|
||||
{
|
||||
label: 'MessageSize',
|
||||
value: 'TotalLogSize',
|
||||
},
|
||||
{
|
||||
label: 'BytesIn',
|
||||
value: 'BytesIn',
|
||||
},
|
||||
{
|
||||
label: 'BytesOut',
|
||||
value: 'BytesOut',
|
||||
},
|
||||
{
|
||||
label: 'Brokers',
|
||||
value: 'Brokers',
|
||||
},
|
||||
];
|
||||
|
||||
export const sortTypes = [
|
||||
{
|
||||
label: '升序',
|
||||
value: 'asc',
|
||||
},
|
||||
{
|
||||
label: '降序',
|
||||
value: 'desc',
|
||||
},
|
||||
];
|
||||
|
||||
export const linesMetric = ['LeaderMessages', 'TotalLogSize', 'BytesIn', 'BytesOut'];
|
||||
export const pointsMetric = ['HealthScore', 'HealthCheckPassed', 'HealthCheckTotal', 'Brokers', 'Zookeepers', ...linesMetric].concat(
|
||||
process.env.BUSINESS_VERSION
|
||||
? ['LoadReBalanceCpu', 'LoadReBalanceDisk', 'LoadReBalanceEnable', 'LoadReBalanceNwIn', 'LoadReBalanceNwOut']
|
||||
: []
|
||||
);
|
||||
|
||||
export const metricNameMap = {
|
||||
LeaderMessages: 'Messages',
|
||||
TotalLogSize: 'LogSize',
|
||||
} as {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
export const healthSorceList = {
|
||||
0: 0,
|
||||
10: '',
|
||||
20: 20,
|
||||
30: '',
|
||||
40: 40,
|
||||
50: '',
|
||||
60: 60,
|
||||
70: '',
|
||||
80: 80,
|
||||
90: '',
|
||||
100: 100,
|
||||
};
|
||||
|
||||
export interface IMetricPoint {
|
||||
aggType: string;
|
||||
createTime: number;
|
||||
metricName: string;
|
||||
timeStamp: number;
|
||||
unit: string;
|
||||
updateTime: number;
|
||||
value: number;
|
||||
metricLines?: {
|
||||
name: string;
|
||||
data: [number | string, number | string];
|
||||
};
|
||||
}
|
||||
|
||||
export const getFormConfig = () => {
|
||||
return [
|
||||
{
|
||||
key: 'name',
|
||||
label: '集群名称',
|
||||
type: FormItemType.input,
|
||||
rules: [{ required: true, message: '请输入集群名称' }],
|
||||
},
|
||||
{
|
||||
key: 'bootstrap',
|
||||
label: 'Bootstrap Servers',
|
||||
type: FormItemType.textArea,
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入Bootstrap Servers',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'Zookeeper',
|
||||
label: 'Zookeeper',
|
||||
type: FormItemType.textArea,
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入Zookeeper',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'config',
|
||||
label: '集群配置',
|
||||
type: FormItemType.textArea,
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入集群配置',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'desc',
|
||||
label: '集群描述',
|
||||
type: FormItemType.textArea,
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入集群描述',
|
||||
},
|
||||
],
|
||||
},
|
||||
] as unknown as IFormItem[];
|
||||
};
|
||||
@@ -0,0 +1,750 @@
|
||||
@error-color: #f46a6a;
|
||||
|
||||
.multi-cluster-page {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 48px;
|
||||
width: 100%;
|
||||
min-width: 1440px;
|
||||
height: calc(100% - 48px);
|
||||
overflow: auto;
|
||||
.dcloud-checkbox-wrapper {
|
||||
font-size: 13px;
|
||||
}
|
||||
&-fixed {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
.content-container {
|
||||
box-sizing: content-box;
|
||||
max-width: 1400px;
|
||||
min-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 40px 20px 40px;
|
||||
background-image: linear-gradient(#ebebf3, #ebebf3 95%, transparent);
|
||||
}
|
||||
.multi-cluster-header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
.dcloud-checkbox-group {
|
||||
font-size: 13px;
|
||||
}
|
||||
.dcloud-slider:hover {
|
||||
opacity: 1;
|
||||
.dcloud-slider-rail {
|
||||
background-color: #ececf1;
|
||||
}
|
||||
.dcloud-slider-track {
|
||||
background-color: #556ee6;
|
||||
background: #556ee6;
|
||||
}
|
||||
.dcloud-slider-track-1 {
|
||||
background-color: #556ee6;
|
||||
background: #556ee6;
|
||||
}
|
||||
.dcloud-slider-handle:not(.dcloud-tooltip-open) {
|
||||
border-color: #556ee6;
|
||||
}
|
||||
.dcloud-slider-handle:focus {
|
||||
border-color: #556ee6;
|
||||
background-color: #556ee6;
|
||||
background: #556ee6;
|
||||
}
|
||||
}
|
||||
.cluster-header-card {
|
||||
position: relative;
|
||||
width: 26%;
|
||||
min-width: 330px;
|
||||
max-width: 350px;
|
||||
height: 168px;
|
||||
margin-right: 12px;
|
||||
padding: 23px 30px 0;
|
||||
box-sizing: border-box;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
background-image: linear-gradient(to bottom right, #556ee6, #7389f3);
|
||||
|
||||
&-bg-left {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 113px;
|
||||
height: 135px;
|
||||
background-image: url('../../assets/leftTop.png');
|
||||
background-size: cover;
|
||||
background-position-x: right;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&-bg-right {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 186px;
|
||||
height: 145px;
|
||||
background-size: cover;
|
||||
background-image: url('../../assets/rightBottom.png');
|
||||
}
|
||||
|
||||
.header-card-title {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
font-weight: normal;
|
||||
margin-bottom: 0;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
.chinese-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-card-total {
|
||||
font-family: DIDIFD-Medium;
|
||||
margin-bottom: 18px;
|
||||
font-size: 56px;
|
||||
line-height: 56px;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-card-info {
|
||||
font-size: 13px;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 20px;
|
||||
|
||||
.card-info-item {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 20px;
|
||||
padding: 0 6px;
|
||||
> div {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
&-live {
|
||||
background: no-repeat url('../../assets/clusters-live-bg.png') bottom;
|
||||
background-size: 100% 12px;
|
||||
margin-left: -4px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
&-down {
|
||||
background: no-repeat url('../../assets/clusters-down-bg.png') bottom;
|
||||
background-size: 100% 12px;
|
||||
}
|
||||
|
||||
.info-item-value {
|
||||
position: relative;
|
||||
font-family: DIDIFD-Black;
|
||||
font-size: 22px;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 16px;
|
||||
margin-left: 3px;
|
||||
|
||||
em {
|
||||
position: relative;
|
||||
font-style: normal;
|
||||
z-index: 999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cluster-header-filter {
|
||||
flex: 1;
|
||||
|
||||
.header-filter-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&-input {
|
||||
flex: 1;
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
color: #adb5bc;
|
||||
}
|
||||
|
||||
.dcloud-input-group-wrapper {
|
||||
.dcloud-input-affix-wrapper {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.dcloud-input-group-addon {
|
||||
background: rgba(33, 37, 41, 0.04);
|
||||
|
||||
.dcloud-btn {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-divider {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
border: 1px solid #ced4da;
|
||||
margin: 0 18px;
|
||||
}
|
||||
|
||||
&-button {
|
||||
width: 108px;
|
||||
line-height: 19px;
|
||||
|
||||
.text {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
letter-spacing: 0;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-filter-bottom {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
height: 120px;
|
||||
box-sizing: border-box;
|
||||
padding: 24px 0px 0;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.01), 0 3px 6px 3px rgba(0, 0, 0, 0.01), 0 2px 6px 0 rgba(0, 0, 0, 0.03);
|
||||
border-radius: 12px;
|
||||
|
||||
&-item {
|
||||
&-checkbox {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&-slider {
|
||||
width: 298px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
margin-bottom: 24px;
|
||||
font-size: 14px;
|
||||
color: #495057;
|
||||
letter-spacing: 0;
|
||||
padding-left: 20px;
|
||||
font-family: @font-family-bold;
|
||||
&.title-right {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
&.flex {
|
||||
display: flex;
|
||||
margin-right: 43px;
|
||||
|
||||
.dcloud-checkbox + span {
|
||||
padding-right: 8px;
|
||||
padding-left: 4px;
|
||||
width: 86px;
|
||||
}
|
||||
|
||||
.check-all {
|
||||
height: 26px;
|
||||
|
||||
.dcloud-checkbox + span {
|
||||
margin-right: 12px;
|
||||
width: 40px;
|
||||
font-size: 13px;
|
||||
color: #495057;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-check-group {
|
||||
display: inherit;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
.check-all {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.more-check-group {
|
||||
position: absolute;
|
||||
top: 34px;
|
||||
float: left;
|
||||
padding-left: 88px;
|
||||
position: absolute;
|
||||
width: 42%;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.01), 0 3px 6px 3px rgba(0, 0, 0, 0.01), 0 2px 6px 0 rgba(0, 0, 0, 0.03);
|
||||
border-radius: 8px;
|
||||
border-radius: 8px;
|
||||
max-height: 92px;
|
||||
overflow-x: hidden;
|
||||
.dcloud-checkbox-group-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.more-btn {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.multi-cluster-filter {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
line-height: 38px;
|
||||
&-select {
|
||||
width: 26%;
|
||||
max-width: 350px;
|
||||
min-width: 330px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&-checkbox {
|
||||
.dcloud-checkbox-group {
|
||||
font-size: 13px;
|
||||
.dcloud-checkbox + span {
|
||||
padding-left: 4px;
|
||||
}
|
||||
&-item:nth-child(1) {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.multi-cluster-list {
|
||||
box-sizing: content-box;
|
||||
max-width: 1420px;
|
||||
min-width: 1220px;
|
||||
margin: 0 auto;
|
||||
padding: 0 30px;
|
||||
.dcloud-list-item {
|
||||
box-sizing: content-box;
|
||||
width: calc(100% - 20px);
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
margin: 8px auto;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.01), 0 3px 6px 3px rgba(0, 0, 0, 0.01), 0 2px 6px 0 rgba(0, 0, 0, 0.03);
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-list-item:hover {
|
||||
padding: 0 10px;
|
||||
box-shadow: 0 0 8px 0 rgba(101, 98, 240, 0.04), 0 6px 12px 12px rgba(101, 98, 240, 0.04), 0 6px 10px 0 rgba(101, 98, 240, 0.08);
|
||||
|
||||
.multi-cluster-list-item-btn {
|
||||
opacity: 1;
|
||||
.icon {
|
||||
color: #74788d;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.icon:hover {
|
||||
color: #495057;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.multi-cluster-list-item-btn {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 8px;
|
||||
z-index: 10;
|
||||
text-align: right;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: rgba(33, 37, 41, 0.04);
|
||||
border-radius: 14px;
|
||||
text-align: center;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.multi-cluster-list-item {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 128px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 32px 12px 32px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: 0.5s all;
|
||||
|
||||
&-healthy {
|
||||
margin-right: 24px;
|
||||
.dcloud-progress-inner {
|
||||
border-radius: 50%;
|
||||
}
|
||||
.green-circle {
|
||||
.dcloud-progress-inner {
|
||||
background: #f5fdfc;
|
||||
}
|
||||
}
|
||||
.red-circle {
|
||||
.dcloud-progress-inner {
|
||||
background: #fffafa;
|
||||
}
|
||||
}
|
||||
.healthy-percent {
|
||||
font-family: DIDIFD-Regular;
|
||||
font-size: 40px;
|
||||
text-align: center;
|
||||
line-height: 36px;
|
||||
color: #00c0a2;
|
||||
|
||||
&.less-90 {
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
&.no-info {
|
||||
color: #e9e7e7;
|
||||
}
|
||||
|
||||
&.down {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 22px;
|
||||
color: #ff7066;
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.healthy-degree {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 12px;
|
||||
color: #495057;
|
||||
line-height: 15px;
|
||||
margin-top: 6px;
|
||||
text-align: center;
|
||||
|
||||
&-status {
|
||||
margin-right: 6px;
|
||||
color: #74788d;
|
||||
}
|
||||
|
||||
&-proportion {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.multi-cluster-list-item-base {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding-top: 16px;
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 22px;
|
||||
|
||||
.base-name {
|
||||
margin-right: 8px;
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: @font-family-bold;
|
||||
font-size: 18px;
|
||||
color: #495057;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.base-version {
|
||||
height: 18px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
background: #ececf6;
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.balance-box {
|
||||
height: 18px;
|
||||
margin-right: 4px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
&.balanced {
|
||||
background: rgba(85, 110, 230, 0.1);
|
||||
color: #556ee6;
|
||||
}
|
||||
&.unbalanced {
|
||||
background: rgba(255, 136, 0, 0.1);
|
||||
color: #f58342;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-date {
|
||||
font-size: 12px;
|
||||
color: #adb5bc;
|
||||
letter-spacing: 0;
|
||||
text-align: right;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.multi-cluster-list-item-Indicator {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding-top: 12px;
|
||||
justify-content: space-between;
|
||||
|
||||
.indicator-left {
|
||||
display: flex;
|
||||
|
||||
&-item {
|
||||
margin-right: 32px;
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
color: #74788d;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 16px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin-right: 3px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&-value {
|
||||
margin-left: 10px;
|
||||
font-size: 16px;
|
||||
color: #495057;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 20px;
|
||||
font-family: @font-family-bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.indicator-right {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
margin-right: 40px;
|
||||
|
||||
&-total {
|
||||
margin-right: 10px;
|
||||
text-align: left;
|
||||
width: 96px;
|
||||
|
||||
&-name {
|
||||
font-size: 12px;
|
||||
color: #74788d;
|
||||
line-height: 16px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
&-value {
|
||||
font-size: 14px;
|
||||
color: #212529;
|
||||
letter-spacing: 0;
|
||||
line-height: 20px;
|
||||
|
||||
.total-value-unit {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
color: #74788d;
|
||||
letter-spacing: 0;
|
||||
line-height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.first-right-item {
|
||||
.indicator-right-item-total {
|
||||
width: 116px;
|
||||
}
|
||||
}
|
||||
|
||||
&-chart {
|
||||
}
|
||||
}
|
||||
|
||||
&-item:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.multi-cluster-list-load {
|
||||
font-size: 13px;
|
||||
color: #74788d;
|
||||
text-align: center;
|
||||
margin-top: 12px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
.dcloud-divider-horizontal.dcloud-divider-with-text.load-completed-tip {
|
||||
box-sizing: content-box;
|
||||
width: calc(100% - 80px);
|
||||
max-width: 1400px;
|
||||
min-width: 1200px;
|
||||
padding: 16px 40px 40px 40px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-modal {
|
||||
.form {
|
||||
padding-top: 16px;
|
||||
.dcloud-col-4 {
|
||||
max-width: 13.67%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-content {
|
||||
.dcloud-form-item-extra {
|
||||
min-height: unset;
|
||||
.error-extra-info {
|
||||
color: @error-color;
|
||||
}
|
||||
}
|
||||
.inline-item.dcloud-form-item {
|
||||
display: -webkit-inline-box;
|
||||
margin-right: 16px;
|
||||
|
||||
&.adjust-height-style{
|
||||
.dcloud-form-item-label {
|
||||
padding: 0;
|
||||
label {
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
.dcloud-form-item-control {
|
||||
&-input {
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.max-width-66 {
|
||||
.dcloud-form-item-control {
|
||||
max-width: 66%;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-form-item-label {
|
||||
margin-right: 12px;
|
||||
|
||||
label {
|
||||
font-family: @font-family;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-item-control {
|
||||
margin-bottom: 8px !important;
|
||||
.dcloud-form-item-control {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-page {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
margin-top: 100px;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
.header-filter-top-button {
|
||||
width: 156px;
|
||||
height: 36px;
|
||||
background: #556ee6;
|
||||
border-radius: 8px;
|
||||
|
||||
.text {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
letter-spacing: 0;
|
||||
padding-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 32px;
|
||||
color: #212529;
|
||||
text-align: center;
|
||||
line-height: 48px;
|
||||
}
|
||||
|
||||
.img {
|
||||
display: flex;
|
||||
margin-top: 22px;
|
||||
margin-bottom: 39px;
|
||||
|
||||
.img-card-1 {
|
||||
width: 282px;
|
||||
height: 179px;
|
||||
background-size: cover;
|
||||
background-image: url('../../assets/dashborad.png');
|
||||
}
|
||||
|
||||
.img-card-2 {
|
||||
width: 286px;
|
||||
height: 179px;
|
||||
margin-left: 76px;
|
||||
background-size: cover;
|
||||
background-image: url('../../assets/state.png');
|
||||
}
|
||||
|
||||
.img-card-3 {
|
||||
width: 286px;
|
||||
height: 179px;
|
||||
margin-left: 76px;
|
||||
background-size: cover;
|
||||
background-image: url('../../assets/chart.png');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { Result } from 'knowdesign';
|
||||
import React from 'react';
|
||||
|
||||
export default () => <Result status="403" title="No License" subTitle="很抱歉,您的 Licence 无法使用" />;
|
||||
@@ -0,0 +1,420 @@
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { Button, Form, Input, Select, message, Drawer, Space, Divider, Utils, Radio, AutoComplete, Alert } from 'knowdesign';
|
||||
import api from '@src/api';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { UsersProps } from '../SecurityUsers';
|
||||
|
||||
// 字段对应后端存储值的枚举类型
|
||||
export enum ACL_OPERATION {
|
||||
Unknown,
|
||||
Any,
|
||||
All,
|
||||
Read,
|
||||
Write,
|
||||
Create,
|
||||
Delete,
|
||||
Alter,
|
||||
Describe,
|
||||
ClusterAction,
|
||||
DescribeConfigs,
|
||||
AlterConfigs,
|
||||
IdempotentWrite,
|
||||
}
|
||||
export enum ACL_PERMISSION_TYPE {
|
||||
Unknown,
|
||||
Any,
|
||||
Deny,
|
||||
Allow,
|
||||
}
|
||||
export enum ACL_PATTERN_TYPE {
|
||||
Unknown,
|
||||
Any,
|
||||
Match,
|
||||
Literal,
|
||||
Prefixed,
|
||||
}
|
||||
export enum ACL_RESOURCE_TYPE {
|
||||
Unknown,
|
||||
Any,
|
||||
Topic,
|
||||
Group,
|
||||
Cluster,
|
||||
TransactionalId,
|
||||
DelegationToken,
|
||||
}
|
||||
|
||||
export type RESOURCE_MAP_KEYS = Exclude<keyof typeof ACL_RESOURCE_TYPE, 'Unknown' | 'Any' | 'DelegationToken'>;
|
||||
|
||||
// 资源类型和操作映射表
|
||||
export const RESOURCE_TO_OPERATIONS_MAP: {
|
||||
[P in RESOURCE_MAP_KEYS]: string[];
|
||||
} = {
|
||||
Cluster: ['Alter', 'AlterConfigs', 'ClusterAction', 'Create', 'Describe', 'DescribeConfigs', 'IdempotentWrite'],
|
||||
Topic: ['Alter', 'AlterConfigs', 'Create', 'Delete', 'Describe', 'DescribeConfigs', 'Read', 'Write'],
|
||||
Group: ['Delete', 'Describe', 'Read'],
|
||||
TransactionalId: ['Write', 'Describe'],
|
||||
};
|
||||
|
||||
// ACL 配置类型
|
||||
const CONFIG_TYPE = [
|
||||
{
|
||||
label: '配置生产权限',
|
||||
value: 'produce',
|
||||
},
|
||||
{
|
||||
label: '配置消费权限',
|
||||
value: 'consume',
|
||||
},
|
||||
{
|
||||
label: '配置自定义权限',
|
||||
value: 'custom',
|
||||
},
|
||||
];
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const AddDrawer = forwardRef((_, ref) => {
|
||||
const { clusterId } = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const [form] = Form.useForm();
|
||||
const [visible, setVisible] = useState<boolean>(false);
|
||||
const [kafkaUserOptions, setKafkaUserOptions] = useState<{ label: string; value: string }[]>([]);
|
||||
const [confirmLoading, setConfirmLoading] = useState<boolean>(false);
|
||||
const callback = useRef(() => {
|
||||
return;
|
||||
});
|
||||
const [topicMetaData, setTopicMetaData] = React.useState([]);
|
||||
|
||||
// 获取 Topic 元信息
|
||||
const getTopicMetaData = (newValue: any) => {
|
||||
Utils.request(api.getTopicMetaData(+clusterId), {
|
||||
method: 'GET',
|
||||
params: { searchKeyword: newValue },
|
||||
}).then((res: UsersProps[]) => {
|
||||
const topics = (res || []).map((item: any) => {
|
||||
return {
|
||||
label: item.topicName,
|
||||
value: item.topicName,
|
||||
};
|
||||
});
|
||||
setTopicMetaData(topics);
|
||||
});
|
||||
};
|
||||
|
||||
// 获取 kafkaUser 列表
|
||||
const getKafkaUserList = () => {
|
||||
Utils.request(api.getKafkaUsers(clusterId), {
|
||||
method: 'GET',
|
||||
}).then((res: UsersProps[]) => {
|
||||
setKafkaUserOptions(res.map(({ name }) => ({ label: name, value: name })));
|
||||
});
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const onSubmit = () => {
|
||||
form.validateFields().then((formData) => {
|
||||
const submitData = [];
|
||||
const { configType, principle, kafkaUser } = formData;
|
||||
|
||||
if (configType === 'custom') {
|
||||
// 1. 自定义权限
|
||||
const { resourceType, resourcePatternType, aclPermissionType, aclOperation, aclClientHost } = formData;
|
||||
submitData.push({
|
||||
clusterId,
|
||||
kafkaUser: principle === 'all' ? '*' : kafkaUser,
|
||||
resourceType,
|
||||
resourcePatternType,
|
||||
resourceName: '*',
|
||||
aclPermissionType,
|
||||
aclOperation,
|
||||
aclClientHost,
|
||||
});
|
||||
} else {
|
||||
// 2. 生产或者消费权限
|
||||
// 1). 配置生产权限将赋予 User 对应 Topic 的 Create、Write 权限
|
||||
// 2). 配置消费权限将赋予 User 对应 Topic的 Read 权限和 Group 的 Read 权限
|
||||
const { topicPatternType, topicPrinciple, topicName } = formData;
|
||||
submitData.push({
|
||||
clusterId,
|
||||
kafkaUser: principle === 'all' ? '*' : kafkaUser,
|
||||
resourceType: ACL_RESOURCE_TYPE.Topic,
|
||||
resourcePatternType: topicPatternType,
|
||||
resourceName: topicPrinciple === 'all' ? '*' : topicName,
|
||||
aclPermissionType: ACL_PERMISSION_TYPE.Allow,
|
||||
aclOperation: configType === 'consume' ? ACL_OPERATION.Read : ACL_OPERATION.Create,
|
||||
aclClientHost: '*',
|
||||
});
|
||||
// 消费权限
|
||||
if (configType === 'consume') {
|
||||
const { groupPatternType, groupPrinciple, groupName } = formData;
|
||||
submitData.push({
|
||||
clusterId,
|
||||
kafkaUser: principle === 'all' ? '*' : kafkaUser,
|
||||
resourceType: ACL_RESOURCE_TYPE.Group,
|
||||
resourcePatternType: groupPatternType,
|
||||
resourceName: groupPrinciple === 'all' ? '*' : groupName,
|
||||
aclPermissionType: ACL_PERMISSION_TYPE.Allow,
|
||||
aclOperation: ACL_OPERATION.Read,
|
||||
aclClientHost: '*',
|
||||
});
|
||||
} else {
|
||||
submitData.push({
|
||||
clusterId,
|
||||
kafkaUser: principle === 'all' ? '*' : kafkaUser,
|
||||
resourceType: ACL_RESOURCE_TYPE.Topic,
|
||||
resourcePatternType: topicPatternType,
|
||||
resourceName: topicPrinciple === 'all' ? '*' : topicName,
|
||||
aclPermissionType: ACL_PERMISSION_TYPE.Allow,
|
||||
aclOperation: ACL_OPERATION.Write,
|
||||
aclClientHost: '*',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setConfirmLoading(true);
|
||||
Utils.request(api.addACL, {
|
||||
method: 'POST',
|
||||
data: submitData,
|
||||
}).then(
|
||||
() => {
|
||||
// 执行回调,刷新列表数据
|
||||
callback.current();
|
||||
|
||||
onClose();
|
||||
message.success('成功新增 ACL');
|
||||
},
|
||||
() => setConfirmLoading(false)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// 展开抽屉
|
||||
const onOpen = (status: boolean, cbk: () => void) => {
|
||||
setVisible(status);
|
||||
callback.current = cbk;
|
||||
};
|
||||
|
||||
// 关闭抽屉
|
||||
const onClose = () => {
|
||||
setVisible(false);
|
||||
setConfirmLoading(false);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
onOpen,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
getKafkaUserList();
|
||||
getTopicMetaData('');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
className="acls-edit-drawer"
|
||||
title="新增ACL"
|
||||
width={480}
|
||||
visible={visible}
|
||||
maskClosable={false}
|
||||
onClose={onClose}
|
||||
extra={
|
||||
<Space>
|
||||
<Button size="small" onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" size="small" loading={confirmLoading} onClick={onSubmit}>
|
||||
确定
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Alert
|
||||
className="drawer-alert-full-screen"
|
||||
message="新增 ACL 必须在集群已开启 ACL 功能时才会生效"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 20 }}
|
||||
/>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
label="ACL用途"
|
||||
name="configType"
|
||||
rules={[{ required: true, message: 'ACL用途不能为空' }]}
|
||||
initialValue={CONFIG_TYPE[0].value}
|
||||
>
|
||||
<Select options={CONFIG_TYPE} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Principle" name="principle" rules={[{ required: true, message: 'Principle 不能为空' }]} initialValue="all">
|
||||
<Radio.Group>
|
||||
<Radio value="all">ALL</Radio>
|
||||
<Radio value="special">Special</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item dependencies={['principle']} style={{ marginBottom: 0 }}>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue('principle') === 'special' ? (
|
||||
<Form.Item name="kafkaUser" rules={[{ required: true, message: 'Kafka User 不能为空' }]}>
|
||||
<Select placeholder="请选择 Kafka User" options={kafkaUserOptions} />
|
||||
</Form.Item>
|
||||
) : null
|
||||
}
|
||||
</Form.Item>
|
||||
<Form.Item dependencies={['configType']} style={{ marginBottom: 0 }}>
|
||||
{({ getFieldValue }) => {
|
||||
const PatternTypeFormItems = (props: { type: string }) => {
|
||||
const { type } = props;
|
||||
const UpperCaseType = type[0].toUpperCase() + type.slice(1);
|
||||
return (
|
||||
<div className="form-item-group">
|
||||
<Form.Item
|
||||
label={`${UpperCaseType} Pattern Type`}
|
||||
name={`${type}PatternType`}
|
||||
rules={[{ required: true, message: `${UpperCaseType} Pattern Type 不能为空` }]}
|
||||
initialValue={ACL_PATTERN_TYPE['Literal']}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value={ACL_PATTERN_TYPE['Literal']}>Literal</Radio>
|
||||
<Radio value={ACL_PATTERN_TYPE['Prefixed']}>Prefixed</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={UpperCaseType}
|
||||
name={`${type}Principle`}
|
||||
rules={[{ required: true, message: `${UpperCaseType} 不能为空` }]}
|
||||
initialValue="all"
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value="all">ALL</Radio>
|
||||
<Radio value="special">Special</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item dependencies={[`${type}Principle`]} style={{ marginBottom: 0 }}>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue(`${type}Principle`) === 'special' ? (
|
||||
<Form.Item
|
||||
name={`${type}Name`}
|
||||
dependencies={[`${type}PatternType`]}
|
||||
validateTrigger="onBlur"
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator: (rule: any, value: string) => {
|
||||
if (!value) {
|
||||
return Promise.reject(`${UpperCaseType}Name 不能为空`);
|
||||
}
|
||||
if (type === 'topic' && getFieldValue(`${type}PatternType`) === ACL_PATTERN_TYPE['Literal']) {
|
||||
return Utils.request(api.getTopicMetadata(clusterId as any, value)).then((res: any) => {
|
||||
return res?.exist ? Promise.resolve() : Promise.reject('该 Topic 不存在');
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<AutoComplete
|
||||
filterOption={(value, option) => {
|
||||
if (option?.value.includes(value)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
options={topicMetaData}
|
||||
placeholder={`请输入 ${type}Name`}
|
||||
/>
|
||||
</Form.Item>
|
||||
) : null
|
||||
}
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomFormItems = () => {
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label="Permission Type"
|
||||
name="aclPermissionType"
|
||||
rules={[{ required: true, message: 'Permission Type 不能为空' }]}
|
||||
initialValue={ACL_PERMISSION_TYPE['Allow']}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value={ACL_PERMISSION_TYPE['Allow']}>Allow</Radio>
|
||||
<Radio value={ACL_PERMISSION_TYPE['Deny']}>Deny</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Pattern Type"
|
||||
name="resourcePatternType"
|
||||
rules={[{ required: true, message: 'Pattern Type 不能为空' }]}
|
||||
initialValue={ACL_PATTERN_TYPE['Literal']}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value={ACL_PATTERN_TYPE['Literal']}>Literal</Radio>
|
||||
<Radio value={ACL_PATTERN_TYPE['Prefixed']}>Prefixed</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Resource Type"
|
||||
name="resourceType"
|
||||
rules={[{ required: true, message: 'Resource Type 不能为空' }]}
|
||||
initialValue={ACL_RESOURCE_TYPE['Cluster']}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择 Resource Type"
|
||||
options={Object.keys(RESOURCE_TO_OPERATIONS_MAP).map((type: RESOURCE_MAP_KEYS) => ({
|
||||
label: type,
|
||||
value: ACL_RESOURCE_TYPE[type],
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item dependencies={['resourceType']} style={{ marginBottom: 0 }}>
|
||||
{({ getFieldValue }) => {
|
||||
form.resetFields(['aclOperation']);
|
||||
return (
|
||||
<Form.Item label="Operation" name="aclOperation" rules={[{ required: true, message: 'Operation 不能为空' }]}>
|
||||
<Select
|
||||
placeholder="请选择 Resource Type"
|
||||
options={RESOURCE_TO_OPERATIONS_MAP[ACL_RESOURCE_TYPE[getFieldValue('resourceType')] as RESOURCE_MAP_KEYS].map(
|
||||
(type) => ({
|
||||
label: type,
|
||||
value: ACL_OPERATION[type as keyof typeof ACL_OPERATION],
|
||||
})
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item label="Host" name="aclClientHost" initialValue="*">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const type = getFieldValue('configType');
|
||||
if (type === 'produce') {
|
||||
return <PatternTypeFormItems type="topic" />;
|
||||
} else if (type === 'consume') {
|
||||
return (
|
||||
<>
|
||||
<PatternTypeFormItems type="topic" />
|
||||
<PatternTypeFormItems type="group" />
|
||||
</>
|
||||
);
|
||||
} else if (type === 'custom') {
|
||||
return <CustomFormItems />;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Drawer>
|
||||
);
|
||||
});
|
||||
|
||||
export default AddDrawer;
|
||||
@@ -0,0 +1,30 @@
|
||||
.security-acls-page {
|
||||
.card-bar {
|
||||
margin: 12px 0;
|
||||
}
|
||||
&-list {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
padding: 16px 24px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.01), 0 3px 6px 3px rgba(0, 0, 0, 0.01), 0 2px 6px 0 rgba(0, 0, 0, 0.03);
|
||||
// border-radius: 12px;
|
||||
}
|
||||
.operate-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.acls-edit-drawer {
|
||||
.form-item-group {
|
||||
padding: 16px 20px 0 20px;
|
||||
margin-bottom: 16px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.dcloud-form-item-control-input {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Button, Form, Input, Select, Modal, message, ProTable, AppContainer, DKSBreadcrumb, Utils } from 'knowdesign';
|
||||
import ACLsCardBar from '@src/components/CardBar/ACLsCardBar';
|
||||
import api from '@src/api';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import AddACLDrawer, {
|
||||
ACL_OPERATION,
|
||||
ACL_PERMISSION_TYPE,
|
||||
ACL_PATTERN_TYPE,
|
||||
ACL_RESOURCE_TYPE,
|
||||
RESOURCE_TO_OPERATIONS_MAP,
|
||||
RESOURCE_MAP_KEYS,
|
||||
} from './EditDrawer';
|
||||
import './index.less';
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
||||
export type ACLsProps = {
|
||||
kafkaUser: string;
|
||||
resourceType: ACL_RESOURCE_TYPE;
|
||||
resourceName: string;
|
||||
resourcePatternType: ACL_PATTERN_TYPE;
|
||||
aclPermissionType: ACL_PATTERN_TYPE;
|
||||
aclOperation: ACL_OPERATION;
|
||||
aclClientHost: string;
|
||||
};
|
||||
|
||||
export const defaultPagination = {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
position: 'bottomRight',
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
};
|
||||
|
||||
const SecurityACLs = (): JSX.Element => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const { clusterId } = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [data, setData] = useState<ACLsProps[]>([]);
|
||||
const [pagination, setPagination] = useState<any>(defaultPagination);
|
||||
const [form] = Form.useForm();
|
||||
const editDrawerRef = useRef(null);
|
||||
|
||||
const getACLs = (query = {}) => {
|
||||
const formData = form.getFieldsValue();
|
||||
const queryData = {
|
||||
// 模糊查询
|
||||
fuzzySearchDTOList: [] as { fieldName: string; fieldValue: string }[],
|
||||
// 精确查询
|
||||
preciseFilterDTOList: [] as { fieldName: string; fieldValueList: (string | number)[] }[],
|
||||
};
|
||||
Object.entries(formData)
|
||||
.filter((i) => i[1])
|
||||
.forEach(([fieldName, fieldValue]: [string, any]) => {
|
||||
if (fieldName === 'resourceType') {
|
||||
queryData.preciseFilterDTOList.push({
|
||||
fieldName,
|
||||
fieldValueList: fieldValue.map((type: string) => ACL_RESOURCE_TYPE[type as RESOURCE_MAP_KEYS]),
|
||||
});
|
||||
} else {
|
||||
queryData.fuzzySearchDTOList.push({
|
||||
fieldName,
|
||||
fieldValue,
|
||||
});
|
||||
}
|
||||
});
|
||||
const queryParams = {
|
||||
pageNo: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
...queryData,
|
||||
...query,
|
||||
};
|
||||
|
||||
setLoading(true);
|
||||
Utils.request(api.getACLs(clusterId), {
|
||||
method: 'POST',
|
||||
data: queryParams,
|
||||
}).then(
|
||||
(res: any) => {
|
||||
const { pageNo, pageSize, total } = res.pagination;
|
||||
const pages = Math.ceil(total / pageSize);
|
||||
if (pageNo > pages && pages !== 0) {
|
||||
getACLs({ pageNo: pages });
|
||||
return false;
|
||||
}
|
||||
|
||||
setPagination({
|
||||
...pagination,
|
||||
current: pageNo,
|
||||
pageSize,
|
||||
total,
|
||||
});
|
||||
setData(res.bizData);
|
||||
setLoading(false);
|
||||
return true;
|
||||
},
|
||||
() => setLoading(false)
|
||||
);
|
||||
};
|
||||
|
||||
const columns = () => {
|
||||
const baseColumns = [
|
||||
{
|
||||
title: 'Principal',
|
||||
dataIndex: 'kafkaUser',
|
||||
},
|
||||
{
|
||||
title: 'Permission',
|
||||
dataIndex: 'aclPermissionType',
|
||||
render(type: number) {
|
||||
return ACL_PERMISSION_TYPE[type];
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Pattern Type',
|
||||
dataIndex: 'resourcePatternType',
|
||||
width: 180,
|
||||
render(type: number) {
|
||||
return ACL_PATTERN_TYPE[type];
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Operations',
|
||||
dataIndex: 'aclOperation',
|
||||
render(type: number) {
|
||||
return ACL_OPERATION[type];
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Resource',
|
||||
dataIndex: 'resourceType',
|
||||
render(type: number, record: ACLsProps) {
|
||||
return `${ACL_RESOURCE_TYPE[type]} ${record.resourceName}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Host',
|
||||
dataIndex: 'aclClientHost',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: '',
|
||||
width: 120,
|
||||
render(record: ACLsProps) {
|
||||
return (
|
||||
<>
|
||||
<Button type="link" size="small" style={{ paddingLeft: 0 }} onClick={() => onDelete(record)}>
|
||||
删除
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return baseColumns;
|
||||
};
|
||||
|
||||
const onDelete = (record: ACLsProps) => {
|
||||
confirm({
|
||||
title: '确定删除 ACL 吗?',
|
||||
okText: '删除',
|
||||
okType: 'primary',
|
||||
centered: true,
|
||||
okButtonProps: {
|
||||
size: 'small',
|
||||
danger: true,
|
||||
},
|
||||
cancelButtonProps: {
|
||||
size: 'small',
|
||||
},
|
||||
onOk() {
|
||||
return Utils.request(api.delACLs, {
|
||||
method: 'DELETE',
|
||||
data: { ...record, clusterId: Number(clusterId) },
|
||||
}).then((_) => {
|
||||
message.success('删除成功');
|
||||
getACLs();
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onTableChange = (curPagination: any) => {
|
||||
getACLs({ pageNo: curPagination.current, pageSize: curPagination.pageSize });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getACLs();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="security-acls-page">
|
||||
<DKSBreadcrumb
|
||||
breadcrumbs={[
|
||||
{ label: '多集群管理', aHref: '/' },
|
||||
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
|
||||
{ label: 'ACLs', aHref: `` },
|
||||
]}
|
||||
/>
|
||||
<div className="card-bar">
|
||||
<ACLsCardBar />
|
||||
</div>
|
||||
<div className="security-acls-page-list clustom-table-content">
|
||||
<div className="operate-bar">
|
||||
<Form form={form} layout="inline" onFinish={() => getACLs({ page: 1 })}>
|
||||
<Form.Item name="kafkaUser">
|
||||
<Input placeholder="请输入 Principal" />
|
||||
</Form.Item>
|
||||
<Form.Item name="resourceType">
|
||||
<Select
|
||||
placeholder="选择 ResourceType"
|
||||
options={Object.keys(RESOURCE_TO_OPERATIONS_MAP).map((key) => ({ label: key, value: key }))}
|
||||
mode="multiple"
|
||||
maxTagCount="responsive"
|
||||
allowClear
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="resourceName">
|
||||
<Input placeholder="请输入 Resource" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" ghost htmlType="submit">
|
||||
查询
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Button
|
||||
type="primary"
|
||||
// icon={<PlusOutlined />}
|
||||
onClick={() => editDrawerRef.current.onOpen(true, getACLs)}
|
||||
>
|
||||
新增ACL
|
||||
</Button>
|
||||
</div>
|
||||
<ProTable
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
loading,
|
||||
rowKey: 'id',
|
||||
dataSource: data,
|
||||
paginationProps: pagination,
|
||||
columns: columns() as any,
|
||||
lineFillColor: true,
|
||||
attrs: {
|
||||
onChange: onTableChange,
|
||||
scroll: { y: 'calc(100vh - 400px)' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 新增 ACL 抽屉 */}
|
||||
<AddACLDrawer ref={editDrawerRef} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecurityACLs;
|
||||
@@ -0,0 +1,59 @@
|
||||
.security-users-page {
|
||||
&-list {
|
||||
width: 100%;
|
||||
height: calc(100vh - 90px);
|
||||
padding: 16px 24px;
|
||||
margin-top: 12px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.01), 0 3px 6px 3px rgba(0, 0, 0, 0.01), 0 2px 6px 0 rgba(0, 0, 0, 0.03);
|
||||
border-top-left-radius: 12px;
|
||||
border-top-right-radius: 12px;
|
||||
.operate-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.users-edit-drawer {
|
||||
.dcloud-form-item {
|
||||
margin-bottom: 14px;
|
||||
|
||||
&-label {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 13px;
|
||||
color: #495057;
|
||||
letter-spacing: 0;
|
||||
line-height: 18px !important;
|
||||
label {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.complex-label {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.random-icon {
|
||||
font-size: 18px;
|
||||
height: 18px;
|
||||
color: #adb5bc;
|
||||
}
|
||||
|
||||
.random-icon:hover {
|
||||
height: 18px;
|
||||
text-align: center;
|
||||
border-radius: 9px;
|
||||
line-height: 20px;
|
||||
color: #74788d;
|
||||
background: rgba(33, 37, 41, 0.04);
|
||||
}
|
||||
|
||||
.switch {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
message,
|
||||
ProTable,
|
||||
Drawer,
|
||||
Space,
|
||||
Divider,
|
||||
AppContainer,
|
||||
DKSBreadcrumb,
|
||||
Utils,
|
||||
Checkbox,
|
||||
Tooltip,
|
||||
IconFont,
|
||||
Alert,
|
||||
} from 'knowdesign';
|
||||
import { CloseOutlined, EyeInvisibleOutlined, EyeOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||
import './index.less';
|
||||
import api from '@src/api';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { regKafkaPassword } from '@src/common/reg';
|
||||
|
||||
export const randomString = (len = 32, chars = 'abcdefghijklmnopqrstuvwxyz1234567890'): string => {
|
||||
const maxPos = chars.length;
|
||||
let str = '';
|
||||
for (let i = 0; i < len; i++) {
|
||||
str += chars.charAt(Math.floor(Math.random() * maxPos));
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
||||
enum UsersOperate {
|
||||
Add,
|
||||
ChangePassword,
|
||||
}
|
||||
export type UsersProps = {
|
||||
clusterId: number;
|
||||
name: string;
|
||||
authType: any;
|
||||
authName: string;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
};
|
||||
|
||||
export const defaultPagination = {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
position: 'bottomRight',
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
};
|
||||
|
||||
const AES_KEY = 'KnowStreamingKM$';
|
||||
const initialShowPassword = '********';
|
||||
|
||||
const AUTH_TYPES = [
|
||||
{
|
||||
label: 'SCRAM',
|
||||
value: 1300,
|
||||
},
|
||||
{
|
||||
label: 'GSSAPI',
|
||||
value: 'gssapi',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
label: 'PLAIN',
|
||||
value: 'plain',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
label: 'OAUTHBEARER',
|
||||
value: 'oauthbearer',
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
const PasswordContent = (props: { clusterId: string; name: string }) => {
|
||||
const { clusterId, name } = props;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [pw, setPw] = useState(initialShowPassword);
|
||||
|
||||
const switchPwStatus = () => {
|
||||
if (!loading) {
|
||||
setLoading(true);
|
||||
if (pw === initialShowPassword) {
|
||||
Utils.request(api.getKafkaUserToken(clusterId, name)).then(
|
||||
(res: any) => {
|
||||
let token = res.token || '';
|
||||
if (res?.decrypt) {
|
||||
token = Utils.decryptAES(token, AES_KEY);
|
||||
}
|
||||
setPw(token);
|
||||
setLoading(false);
|
||||
},
|
||||
() => setLoading(false)
|
||||
);
|
||||
} else {
|
||||
setPw(initialShowPassword);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Tooltip title={pw} placement="bottom">
|
||||
<div style={{ maxWidth: '80%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{pw}</div>
|
||||
</Tooltip>
|
||||
<span style={{ marginLeft: 6 }} onClick={switchPwStatus}>
|
||||
{loading ? <LoadingOutlined /> : pw === initialShowPassword ? <EyeInvisibleOutlined /> : <EyeOutlined />}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 新增 KafkaUser / 修改 KafkaUser 密码
|
||||
// eslint-disable-next-line react/display-name
|
||||
const EditKafkaUser = forwardRef((_, ref) => {
|
||||
const { clusterId } = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const [type, setType] = useState<UsersOperate>(UsersOperate.Add);
|
||||
const [form] = Form.useForm();
|
||||
const [visible, setVisible] = useState<boolean>(false);
|
||||
const [confirmLoading, setConfirmLoading] = useState<boolean>(false);
|
||||
const callback = useRef(() => {
|
||||
return;
|
||||
});
|
||||
|
||||
// 提交表单
|
||||
const onSubmit = () => {
|
||||
form.validateFields().then((formData) => {
|
||||
// 对密码进行加密
|
||||
formData.token = Utils.encryptAES(formData.token, AES_KEY);
|
||||
// 注意:目前 authType 字段固定传 SCRAM 认证方式的值,之后如果开通了多认证方式,这里需要做更改
|
||||
formData.authType = 1300;
|
||||
setConfirmLoading(true);
|
||||
Utils.request(type === UsersOperate.Add ? api.kafkaUser : api.updateKafkaUserToken, {
|
||||
method: type === UsersOperate.Add ? 'POST' : 'PUT',
|
||||
data: Object.assign(formData, { clusterId: Number(clusterId) }),
|
||||
}).then(
|
||||
() => {
|
||||
// 执行回调,刷新列表数据
|
||||
callback.current();
|
||||
|
||||
onClose();
|
||||
message.success(type === UsersOperate.Add ? '成功新增 KafkaUser' : '成功修改密码');
|
||||
},
|
||||
() => setConfirmLoading(false)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// 展开抽屉
|
||||
const onOpen = (status: boolean, type: UsersOperate, cbk: () => void, detail: UsersProps) => {
|
||||
detail && form.setFieldsValue({ kafkaUser: detail.name });
|
||||
setType(type);
|
||||
setVisible(status);
|
||||
callback.current = cbk;
|
||||
};
|
||||
|
||||
// 关闭抽屉
|
||||
const onClose = () => {
|
||||
setVisible(false);
|
||||
setConfirmLoading(false);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
onOpen,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
className="users-edit-drawer"
|
||||
title={type === UsersOperate.Add ? '新增 KafkaUser' : '修改密码'}
|
||||
width={480}
|
||||
visible={visible}
|
||||
maskClosable={false}
|
||||
onClose={onClose}
|
||||
extra={
|
||||
<Space>
|
||||
<Button size="small" onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" size="small" loading={confirmLoading} onClick={onSubmit}>
|
||||
确定
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{type === UsersOperate.Add && (
|
||||
<Alert
|
||||
className="drawer-alert-full-screen"
|
||||
message="新增 KafkaUser 必须在集群已开启 ACL 功能时才会生效"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 20 }}
|
||||
/>
|
||||
)}
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
label="KafkaUser"
|
||||
name="kafkaUser"
|
||||
rules={[
|
||||
{ required: true, message: 'KafkaUser 不能为空' },
|
||||
{ pattern: /^[a-zA-Z0-9]{3,128}$/, message: 'KafkaUser 支持大小写英文、数字,长度为 3~128 字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入 KakfaUser" disabled={type === UsersOperate.ChangePassword} />
|
||||
</Form.Item>
|
||||
{type === UsersOperate.Add && (
|
||||
<Form.Item
|
||||
label="认证方式"
|
||||
name="authType"
|
||||
rules={[{ required: true, message: '认证方式不能为空' }]}
|
||||
initialValue={[AUTH_TYPES[0].value]}
|
||||
>
|
||||
<Checkbox.Group>
|
||||
{AUTH_TYPES.map((type) => {
|
||||
return type.disabled ? (
|
||||
<Tooltip title="当前版本暂不支持">
|
||||
<Checkbox value={type.value} disabled>
|
||||
{type.label}
|
||||
</Checkbox>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Checkbox value={type.value}>{type.label}</Checkbox>
|
||||
);
|
||||
})}
|
||||
</Checkbox.Group>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item
|
||||
label={
|
||||
<div className="complex-label">
|
||||
<span>{type === UsersOperate.ChangePassword && 'New'} Password</span>
|
||||
<span>
|
||||
<Tooltip title={'生成随机内容'} placement="left">
|
||||
<IconFont
|
||||
type="icon-shengchengdaima"
|
||||
className="random-icon"
|
||||
onClick={() => {
|
||||
const randomStr = randomString(
|
||||
12,
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890_-!"#$%&\'()*+,./:;<=>?@[]^`{|}~'
|
||||
);
|
||||
form && form.setFieldsValue({ token: randomStr });
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
name="token"
|
||||
rules={[
|
||||
{ required: true, message: 'Password 不能为空' },
|
||||
{
|
||||
pattern: regKafkaPassword,
|
||||
message: '密码支持中英文、数字、特殊字符 ! " # $ % & \' ( ) * + , - . / : ; < = > ? @ [ ] ^ _ ` { | } ~',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder="请输入密码" maxLength={128} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Drawer>
|
||||
);
|
||||
});
|
||||
|
||||
const SecurityUsers = (): JSX.Element => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const { clusterId } = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [data, setData] = useState<UsersProps[]>([]);
|
||||
const [pagination, setPagination] = useState<any>(defaultPagination);
|
||||
const [form] = Form.useForm();
|
||||
const editDrawerRef = useRef(null);
|
||||
|
||||
const getKafkaUserList = (query = {}) => {
|
||||
const formData = form.getFieldsValue();
|
||||
const queryParams = {
|
||||
pageNo: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
...formData,
|
||||
...query,
|
||||
};
|
||||
|
||||
setLoading(true);
|
||||
Utils.request(api.getKafkaUsers(clusterId), {
|
||||
method: 'POST',
|
||||
data: queryParams,
|
||||
}).then(
|
||||
(res: any) => {
|
||||
const { pageNo, pageSize, total } = res.pagination;
|
||||
const pages = Math.ceil(total / pageSize);
|
||||
if (pageNo > pages && pages !== 0) {
|
||||
getKafkaUserList({ pageNo: pages });
|
||||
return false;
|
||||
}
|
||||
|
||||
setPagination({
|
||||
...pagination,
|
||||
current: pageNo,
|
||||
pageSize,
|
||||
total,
|
||||
});
|
||||
setData(res.bizData);
|
||||
setLoading(false);
|
||||
return true;
|
||||
},
|
||||
() => setLoading(false)
|
||||
);
|
||||
};
|
||||
|
||||
const columns = () => {
|
||||
const baseColumns = [
|
||||
{
|
||||
title: 'KafkaUser',
|
||||
dataIndex: 'name',
|
||||
width: 300,
|
||||
render(name: string) {
|
||||
return (
|
||||
<Tooltip title={name} placement="topLeft">
|
||||
<div style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{name}</div>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '认证方式',
|
||||
dataIndex: 'authName',
|
||||
},
|
||||
{
|
||||
title: 'Password',
|
||||
dataIndex: 'token',
|
||||
width: 300,
|
||||
render(_: string, record: UsersProps) {
|
||||
return <PasswordContent clusterId={clusterId} name={record.name} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: '',
|
||||
width: 240,
|
||||
render(record: UsersProps) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={{ paddingLeft: 0 }}
|
||||
onClick={() => editDrawerRef.current.onOpen(true, UsersOperate.ChangePassword, getKafkaUserList, record)}
|
||||
>
|
||||
修改密码
|
||||
</Button>
|
||||
<Button type="link" size="small" onClick={() => onDelete(record)}>
|
||||
删除
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return baseColumns;
|
||||
};
|
||||
|
||||
const onDelete = (record: UsersProps) => {
|
||||
confirm({
|
||||
title: '确定删除该 KafkaUser 吗?',
|
||||
okText: '删除',
|
||||
okType: 'primary',
|
||||
centered: true,
|
||||
okButtonProps: {
|
||||
size: 'small',
|
||||
danger: true,
|
||||
},
|
||||
cancelButtonProps: {
|
||||
size: 'small',
|
||||
},
|
||||
maskClosable: false,
|
||||
onOk() {
|
||||
return Utils.request(api.kafkaUser, {
|
||||
method: 'DELETE',
|
||||
data: {
|
||||
clusterId,
|
||||
kafkaUser: record.name,
|
||||
},
|
||||
}).then((_) => {
|
||||
message.success('删除成功');
|
||||
getKafkaUserList();
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onTableChange = (curPagination: any) => {
|
||||
getKafkaUserList({ pageNo: curPagination.current, pageSize: curPagination.pageSize });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 获取配置列表
|
||||
getKafkaUserList();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="security-users-page">
|
||||
<DKSBreadcrumb
|
||||
breadcrumbs={[
|
||||
{ label: '多集群管理', aHref: '/' },
|
||||
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
|
||||
{ label: 'Users', aHref: `` },
|
||||
]}
|
||||
/>
|
||||
<div className="security-users-page-list">
|
||||
<div className="operate-bar">
|
||||
<Form form={form} layout="inline" onFinish={() => getKafkaUserList({ pageNo: 1 })}>
|
||||
<Form.Item name="searchKeywords">
|
||||
<Input placeholder="请输入 Kafka User" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" ghost htmlType="submit">
|
||||
查询
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Button
|
||||
type="primary"
|
||||
// icon={<PlusOutlined />}
|
||||
onClick={() => editDrawerRef.current.onOpen(true, UsersOperate.Add, getKafkaUserList)}
|
||||
>
|
||||
新增KafkaUser
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ProTable
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
loading,
|
||||
rowKey: 'name',
|
||||
dataSource: data,
|
||||
paginationProps: pagination,
|
||||
columns: columns() as any,
|
||||
lineFillColor: true,
|
||||
attrs: {
|
||||
onChange: onTableChange,
|
||||
scroll: { y: 'calc(100vh - 270px)' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<EditKafkaUser ref={editDrawerRef} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecurityUsers;
|
||||
@@ -0,0 +1,151 @@
|
||||
import { Collapse, Divider, Spin, Utils } from 'knowdesign';
|
||||
import * as React from 'react';
|
||||
import API from '../../api';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import InfiniteScroll from 'react-infinite-scroll-component';
|
||||
import moment from 'moment';
|
||||
import { timeFormat } from '../../constants/common';
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import { renderToolTipValue } from './config';
|
||||
|
||||
const { Panel } = Collapse;
|
||||
|
||||
interface ILog {
|
||||
clusterPhyId: number;
|
||||
createTime: number;
|
||||
operateTime: string;
|
||||
resName: string;
|
||||
resTypeCode: number;
|
||||
resTypeName: string;
|
||||
updateTime: number;
|
||||
}
|
||||
|
||||
const ChangeLog = () => {
|
||||
const { clusterId } = useParams<{ clusterId: string }>();
|
||||
const [data, setData] = React.useState<ILog[]>([]);
|
||||
const [loading, setLoading] = React.useState<boolean>(true);
|
||||
const [pagination, setPagination] = React.useState({
|
||||
pageNo: 0,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const getChangeLog = () => {
|
||||
const promise = Utils.request(API.getClusterChangeLog(+clusterId), {
|
||||
params: {
|
||||
pageNo: pagination.pageNo + 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
});
|
||||
promise.then((res: any) => {
|
||||
setData((cur) => cur.concat(res?.bizData));
|
||||
setPagination(res?.pagination);
|
||||
});
|
||||
return promise;
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
getChangeLog().then(
|
||||
() => setLoading(false),
|
||||
() => setLoading(false)
|
||||
);
|
||||
}, []);
|
||||
|
||||
const renderEmpty = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="empty-panel">
|
||||
<div className="img" />
|
||||
<div className="text">暂无配置记录</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getHref = (item: any) => {
|
||||
if (item.resTypeName.toLowerCase().includes('topic')) return `/cluster/${clusterId}/topic/list#topicName=${item.resName}`;
|
||||
if (item.resTypeName.toLowerCase().includes('broker')) return `/cluster/${clusterId}/broker/list#brokerId=${item.resName}`;
|
||||
return '';
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="change-log-panel">
|
||||
<div className="title">历史变更记录</div>
|
||||
{!loading && !data.length ? (
|
||||
renderEmpty()
|
||||
) : (
|
||||
<div id="changelog-scroll-box">
|
||||
<Spin spinning={loading} style={{ paddingLeft: '42%', marginTop: 100 }} />
|
||||
<InfiniteScroll
|
||||
dataLength={data.length}
|
||||
next={getChangeLog as any}
|
||||
hasMore={data.length < pagination.total}
|
||||
loader={<Spin style={{ paddingLeft: '42%', paddingTop: 10 }} spinning={true} />}
|
||||
endMessage={
|
||||
!pagination.total ? (
|
||||
''
|
||||
) : (
|
||||
<Divider className="load-completed-tip" plain>
|
||||
加载完成 共 {pagination.total} 条
|
||||
</Divider>
|
||||
)
|
||||
}
|
||||
scrollableTarget="changelog-scroll-box"
|
||||
>
|
||||
<Collapse defaultActiveKey={['log-0']} accordion>
|
||||
{data.map((item, index) => {
|
||||
return (
|
||||
<Panel
|
||||
header={
|
||||
<>
|
||||
<div className="header">
|
||||
<div className="label">{renderToolTipValue(`[${item.resTypeName}] ${item.resName}`, 24)}</div>
|
||||
<span className="icon">
|
||||
<DownOutlined />
|
||||
</span>
|
||||
</div>
|
||||
<div className="header-time">{moment(item.updateTime).format(timeFormat)}</div>
|
||||
</>
|
||||
}
|
||||
key={`log-${index}`}
|
||||
showArrow={false}
|
||||
>
|
||||
<div className="log-item">
|
||||
<span>名称</span>
|
||||
<div className="value">
|
||||
{getHref(item) ? (
|
||||
<Link to={getHref(item)}>{renderToolTipValue(item.resName, 18)}</Link>
|
||||
) : (
|
||||
renderToolTipValue(item.resName, 18)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="log-item">
|
||||
<span>时间</span>
|
||||
<span className="value">{moment(item.updateTime).format(timeFormat)}</span>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="log-item">
|
||||
<span>内容</span>
|
||||
<span className="value">{'修改配置'}</span>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="log-item">
|
||||
<span>类型</span>
|
||||
<span className="value">{item.resTypeName}</span>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
})}
|
||||
</Collapse>
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangeLog;
|
||||
@@ -0,0 +1,59 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { Drawer, Form, Spin, Table, Utils } from 'knowdesign';
|
||||
import React, { useEffect, useState, forwardRef, useImperativeHandle } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { getDetailColumn } from './config';
|
||||
import API from '../../api';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
const CheckDetail = forwardRef((props: any, ref): JSX.Element => {
|
||||
const intl = useIntl();
|
||||
const [form] = Form.useForm();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
const { clusterId } = useParams<{ clusterId: string }>();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
setVisible,
|
||||
getHealthDetail,
|
||||
}));
|
||||
|
||||
const getHealthDetail = () => {
|
||||
setLoading(true);
|
||||
return Utils.request(API.getResourceListHealthDetail(+clusterId)).then((res: any) => {
|
||||
setData(res);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
form.resetFields();
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
getHealthDetail();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
className="drawer-content"
|
||||
onClose={onCancel}
|
||||
maskClosable={false}
|
||||
title="Cluster健康状态详情"
|
||||
// title={intl.formatMessage({ id: 'check.detail' })}
|
||||
visible={visible}
|
||||
placement="right"
|
||||
width={1080}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
<Table dataSource={data} columns={getDetailColumn(+clusterId)} pagination={false} />
|
||||
</Spin>
|
||||
</Drawer>
|
||||
);
|
||||
});
|
||||
|
||||
export default CheckDetail;
|
||||
@@ -0,0 +1,98 @@
|
||||
import { getBasicChartConfig } from '@src/constants/chartConfig';
|
||||
import moment from 'moment';
|
||||
|
||||
const DEFAULT_METRIC = 'MessagesIn';
|
||||
|
||||
// 图表 tooltip 展示的样式
|
||||
const messagesInTooltipFormatter = (date: any, arr: any) => {
|
||||
// 面积图只有一条线,这里直接取 arr 的第 0 项
|
||||
const params = arr[0];
|
||||
// MessageIn 的指标数据存放在 data 数组第 3 项
|
||||
const metricsData = params.data[2];
|
||||
|
||||
const str = `<div style="margin: 3px 0;">
|
||||
<div style="display:flex;align-items:center;">
|
||||
<div style="margin-right:4px;width:8px;height:2px;background-color:${params.color};"></div>
|
||||
<div style="flex:1;display:flex;justify-content:space-between;align-items:center;overflow: hidden;">
|
||||
<span style="flex: 1;font-size:12px;color:#74788D;pointer-events:auto;margin-left:2px;line-height: 18px;font-family: HelveticaNeue;overflow: hidden; text-overflow: ellipsis; white-space: no-wrap;">
|
||||
${params.seriesName}
|
||||
</span>
|
||||
<span style="font-size:12px;color:#212529;line-height:18px;font-family:HelveticaNeue-Medium;margin-left: 10px;">
|
||||
${parseFloat(Number(params.value[1]).toFixed(3))}
|
||||
<span style="font-family: PingFangSC-Regular;color: #495057;">${metricsData[DEFAULT_METRIC]?.unit || ''}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
return `<div style="margin: 0px 0 0; position: relative; z-index: 99;width: fit-content;">
|
||||
<div style="padding: 8px 0;height: 100%;">
|
||||
<div style="font-size:12px;padding: 0 12px;color:#212529;line-height:20px;font-family: HelveticaNeue;">
|
||||
${date}
|
||||
</div>
|
||||
<div style="margin: 4px 0 0 0;padding: 0 12px;">
|
||||
${str}
|
||||
<div style="width: 100%; height: 1px; background: #EFF2F7;margin: 8px 0;"></div>
|
||||
${metricsData
|
||||
.map(({ key, value, unit }: { key: string; value: number; unit: string }) => {
|
||||
if (key === DEFAULT_METRIC) return '';
|
||||
return `
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span style="font-size:12px;color:#74788D;pointer-events:auto;margin-left:2px;line-height: 18px;font-family: HelveticaNeue;margin-right: 10px;">
|
||||
${key}
|
||||
</span>
|
||||
<span style="font-size:12px;color:#212529;pointer-events:auto;margin-left:2px;line-height: 18px;font-family: HelveticaNeue;">
|
||||
${parseFloat(Number(value).toFixed(3))}
|
||||
<span style="font-family: PingFangSC-Regular;color: #495057;">${unit}</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
export const getChartConfig = (props: any) => {
|
||||
const { metricName, lineColor, isDefaultMetric = false } = props;
|
||||
return {
|
||||
option: getBasicChartConfig({
|
||||
// TODO: time 轴图表联动有问题,先切换为 category
|
||||
// xAxis: { type: 'time', boundaryGap: isDefaultMetric ? ['2%', '2%'] : ['5%', '5%'] },
|
||||
title: { show: false },
|
||||
legend: { show: false },
|
||||
grid: { top: 24, bottom: 12 },
|
||||
lineColor: [lineColor],
|
||||
tooltip: isDefaultMetric
|
||||
? {
|
||||
formatter: function (params: any) {
|
||||
let res = '';
|
||||
if (params != null && params.length > 0) {
|
||||
res += messagesInTooltipFormatter(moment(Number(params[0].axisValue)).format('YYYY-MM-DD HH:mm'), params);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
}
|
||||
: {},
|
||||
}),
|
||||
seriesCallback: (lineList: { name: string; data: [number, string | number][] }[]) => {
|
||||
// 补充线条配置
|
||||
return lineList.map((line) => {
|
||||
return {
|
||||
...line,
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
},
|
||||
symbol: 'emptyCircle',
|
||||
symbolSize: 4,
|
||||
// 面积图样式
|
||||
areaStyle: {
|
||||
color: lineColor,
|
||||
opacity: 0.06,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
.cluster-container-border {
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.01), 0 3px 6px 3px rgba(0, 0, 0, 0.01), 0 2px 6px 0 rgba(0, 0, 0, 0.03);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.cluster-detail-container {
|
||||
width: 100%;
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
|
||||
.refresh-icon-box {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
.refresh-icon {
|
||||
font-size: 14px;
|
||||
color: #74788d;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #21252904;
|
||||
|
||||
.refresh-icon {
|
||||
color: #495057;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-main {
|
||||
.header-chart-container {
|
||||
&-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
width: 100%;
|
||||
height: 244px;
|
||||
margin-bottom: 12px;
|
||||
.cluster-container-border();
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: calc(100vh - 404px);
|
||||
min-height: 526px;
|
||||
|
||||
.multiple-chart-container {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
.cluster-container-border();
|
||||
> div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
overflow: hidden auto;
|
||||
}
|
||||
&-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart-box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 244px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
|
||||
.expand-icon-box {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
top: 14px;
|
||||
right: 16px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
.expand-icon {
|
||||
color: #adb5bc;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(33, 37, 41, 0.06);
|
||||
.expand-icon {
|
||||
color: #74788d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.config-change-records-container {
|
||||
width: 240px;
|
||||
height: 100%;
|
||||
margin-left: 12px;
|
||||
.cluster-container-border();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-box-title {
|
||||
padding: 18px 0 0 20px;
|
||||
font-family: @font-family-bold;
|
||||
line-height: 16px;
|
||||
.name {
|
||||
font-size: 14px;
|
||||
color: #212529;
|
||||
}
|
||||
.unit {
|
||||
font-size: 12px;
|
||||
color: #495057;
|
||||
}
|
||||
> span {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
import { Col, Row, SingleChart, IconFont, Utils, Modal, Spin, Empty, AppContainer, Tooltip } from 'knowdesign';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import api from '@src/api';
|
||||
import { getChartConfig } from './config';
|
||||
import './index.less';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
MetricDefaultChartDataType,
|
||||
MetricChartDataType,
|
||||
formatChartData,
|
||||
supplementaryPoints,
|
||||
} from '@src/components/DashboardDragChart/config';
|
||||
import { MetricType } from '@src/api';
|
||||
import { getDataNumberUnit, getUnit } from '@src/constants/chartConfig';
|
||||
import SingleChartHeader, { KsHeaderOptions } from '@src/components/SingleChartHeader';
|
||||
import { MAX_TIME_RANGE_WITH_SMALL_POINT_INTERVAL } from '@src/constants/common';
|
||||
|
||||
type ChartFilterOptions = Omit<KsHeaderOptions, 'gridNum'>;
|
||||
interface MetricInfo {
|
||||
type: number;
|
||||
name: string;
|
||||
desc: string;
|
||||
set: boolean;
|
||||
support: boolean;
|
||||
}
|
||||
|
||||
interface MessagesInDefaultData {
|
||||
aggType: string | null;
|
||||
createTime: string | null;
|
||||
updateTime: string | null;
|
||||
timeStamp: number;
|
||||
values: {
|
||||
[metric: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
type MessagesInMetric = {
|
||||
name: 'MessagesIn';
|
||||
unit: string;
|
||||
data: (readonly [number, number | string, { key: string; value: number; unit: string }[]])[];
|
||||
};
|
||||
|
||||
const { EventBus } = Utils;
|
||||
const busInstance = new EventBus();
|
||||
|
||||
// 图表颜色定义 & 计算
|
||||
const CHART_LINE_COLORS = ['#556EE6', '#3991FF'];
|
||||
const calculateChartColor = (i: number) => {
|
||||
const isEvenRow = ((i / 2) | 0) % 2;
|
||||
const isEvenCol = i % 2;
|
||||
return CHART_LINE_COLORS[isEvenRow ^ isEvenCol];
|
||||
};
|
||||
|
||||
const DEFAULT_METRIC = 'MessagesIn';
|
||||
// 默认指标图表固定需要获取展示的指标项
|
||||
const DEFUALT_METRIC_NEED_METRICS = [DEFAULT_METRIC, 'TotalLogSize', 'TotalProduceRequests', 'Topics', 'Partitions'];
|
||||
|
||||
const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const { clusterId } = useParams<{ clusterId: string }>();
|
||||
const [metricList, setMetricList] = useState<MetricInfo[]>([]); // 指标列表
|
||||
const [selectedMetricNames, setSelectedMetricNames] = useState<(string | number)[]>([]); // 默认选中的指标的列表
|
||||
const [metricDataList, setMetricDataList] = useState<any>([]);
|
||||
const [messagesInMetricData, setMessagesInMetricData] = useState<MessagesInMetric>({
|
||||
name: 'MessagesIn',
|
||||
unit: '',
|
||||
data: [],
|
||||
});
|
||||
const [curHeaderOptions, setCurHeaderOptions] = useState<ChartFilterOptions>();
|
||||
const [defaultChartLoading, setDefaultChartLoading] = useState<boolean>(true);
|
||||
const [chartLoading, setChartLoading] = useState<boolean>(true);
|
||||
const [showChartDetailModal, setShowChartDetailModal] = useState<boolean>(false);
|
||||
const [chartDetail, setChartDetail] = useState<any>();
|
||||
const curFetchingTimestamp = useRef({
|
||||
messagesIn: 0,
|
||||
other: 0,
|
||||
});
|
||||
|
||||
// 筛选项变化或者点击刷新按钮
|
||||
const ksHeaderChange = (ksOptions: KsHeaderOptions) => {
|
||||
// 如果为相对时间,则当前时间减去 1 分钟,避免最近一分钟的数据还没采集到时前端多补一个点
|
||||
if (ksOptions.isRelativeRangeTime) {
|
||||
ksOptions.rangeTime = ksOptions.rangeTime.map((timestamp) => timestamp - 60 * 1000) as [number, number];
|
||||
}
|
||||
setCurHeaderOptions({
|
||||
isRelativeRangeTime: ksOptions.isRelativeRangeTime,
|
||||
isAutoReload: ksOptions.isAutoReload,
|
||||
rangeTime: ksOptions.rangeTime,
|
||||
});
|
||||
};
|
||||
|
||||
// 获取指标列表
|
||||
const getMetricList = () => {
|
||||
Utils.request(api.getDashboardMetricList(clusterId, MetricType.Cluster)).then((res: MetricInfo[] | null) => {
|
||||
if (!res) return;
|
||||
const showMetrics = res.filter((metric) => metric.support);
|
||||
const selectedMetrics = showMetrics.filter((metric) => metric.set).map((metric) => metric.name);
|
||||
!selectedMetrics.includes(DEFAULT_METRIC) && selectedMetrics.push(DEFAULT_METRIC);
|
||||
setMetricList(showMetrics);
|
||||
setSelectedMetricNames(selectedMetrics);
|
||||
});
|
||||
};
|
||||
|
||||
// 更新指标
|
||||
const updateMetricList = (metricsSet: { [name: string]: boolean }) => {
|
||||
return Utils.request(api.getDashboardMetricList(clusterId, MetricType.Cluster), {
|
||||
method: 'POST',
|
||||
data: {
|
||||
metricsSet,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 指标选中项更新回调
|
||||
const indicatorChangeCallback = (newMetricNames: (string | number)[]) => {
|
||||
const updateMetrics: { [name: string]: boolean } = {};
|
||||
// 需要选中的指标
|
||||
newMetricNames.forEach((name) => !selectedMetricNames.includes(name) && (updateMetrics[name] = true));
|
||||
// 取消选中的指标
|
||||
selectedMetricNames.forEach((name) => !newMetricNames.includes(name) && (updateMetrics[name] = false));
|
||||
|
||||
const requestPromise = Object.keys(updateMetrics).length ? updateMetricList(updateMetrics) : Promise.resolve();
|
||||
requestPromise.then(
|
||||
() => getMetricList(),
|
||||
() => getMetricList()
|
||||
);
|
||||
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
// 获取 metric 列表的图表数据
|
||||
const getMetricData = () => {
|
||||
if (!selectedMetricNames.length) return;
|
||||
!curHeaderOptions.isAutoReload && setChartLoading(true);
|
||||
const [startTime, endTime] = curHeaderOptions.rangeTime;
|
||||
|
||||
const curTimestamp = Date.now();
|
||||
curFetchingTimestamp.current = {
|
||||
...curFetchingTimestamp.current,
|
||||
messagesIn: curTimestamp,
|
||||
};
|
||||
Utils.request(api.getClusterMetricDataList(), {
|
||||
method: 'POST',
|
||||
data: {
|
||||
startTime,
|
||||
endTime,
|
||||
clusterPhyIds: [clusterId],
|
||||
metricsNames: selectedMetricNames.filter((name) => name !== DEFAULT_METRIC),
|
||||
},
|
||||
}).then(
|
||||
(res: MetricDefaultChartDataType[]) => {
|
||||
// 如果当前请求不是最新请求,则不做任何操作
|
||||
if (curFetchingTimestamp.current.messagesIn !== curTimestamp) {
|
||||
return;
|
||||
}
|
||||
|
||||
const supplementaryInterval = (endTime - startTime > MAX_TIME_RANGE_WITH_SMALL_POINT_INTERVAL ? 10 : 1) * 60 * 1000;
|
||||
const formattedMetricData: MetricChartDataType[] = formatChartData(
|
||||
res,
|
||||
global.getMetricDefine || {},
|
||||
MetricType.Cluster,
|
||||
curHeaderOptions.rangeTime,
|
||||
supplementaryInterval
|
||||
);
|
||||
formattedMetricData.forEach((data) => (data.metricLines[0].name = data.metricName));
|
||||
setMetricDataList(formattedMetricData);
|
||||
setChartLoading(false);
|
||||
},
|
||||
() => setChartLoading(false)
|
||||
);
|
||||
};
|
||||
|
||||
// 获取默认展示指标的图表数据
|
||||
const getDefaultMetricData = () => {
|
||||
!curHeaderOptions.isAutoReload && setDefaultChartLoading(true);
|
||||
|
||||
const curTimestamp = Date.now();
|
||||
curFetchingTimestamp.current = {
|
||||
...curFetchingTimestamp.current,
|
||||
other: curTimestamp,
|
||||
};
|
||||
Utils.request(api.getClusterDefaultMetricData(), {
|
||||
method: 'POST',
|
||||
data: {
|
||||
startTime: curHeaderOptions.rangeTime[0],
|
||||
endTime: curHeaderOptions.rangeTime[1],
|
||||
clusterPhyIds: [clusterId],
|
||||
metricsNames: DEFUALT_METRIC_NEED_METRICS,
|
||||
},
|
||||
}).then(
|
||||
(res: MessagesInDefaultData[]) => {
|
||||
// 如果当前请求不是最新请求,则不做任何操作
|
||||
if (curFetchingTimestamp.current.other !== curTimestamp) {
|
||||
return;
|
||||
}
|
||||
// TODO: 这里直接将指标数据放到数组第三项中,之后可以尝试优化,优化需要注意 tooltipFormatter 函数也要修改
|
||||
let maxValue = -1;
|
||||
const result = res.map((item) => {
|
||||
const { timeStamp, values } = item;
|
||||
let parsedValue: string | number = Number(values.MessagesIn);
|
||||
if (Number.isNaN(parsedValue)) {
|
||||
parsedValue = values.MessagesIn;
|
||||
} else {
|
||||
if (maxValue < parsedValue) maxValue = parsedValue;
|
||||
}
|
||||
const valuesWithUnit = Object.entries(values).map(([key, value]) => {
|
||||
let valueWithUnit = Number(value);
|
||||
let unit = ((global.getMetricDefine && global.getMetricDefine(MetricType.Cluster, key)?.unit) || '') as string;
|
||||
if (unit.toLowerCase().includes('byte')) {
|
||||
const [unitName, unitSize]: [string, number] = getUnit(Number(value));
|
||||
unit = unit.toLowerCase().replace('byte', unitName);
|
||||
valueWithUnit /= unitSize;
|
||||
}
|
||||
const returnValue = {
|
||||
key,
|
||||
value: valueWithUnit,
|
||||
unit,
|
||||
};
|
||||
return returnValue;
|
||||
});
|
||||
return [timeStamp, values.MessagesIn || '0', valuesWithUnit] as [number, number | string, typeof valuesWithUnit];
|
||||
});
|
||||
result.sort((a, b) => (a[0] as number) - (b[0] as number));
|
||||
const line = {
|
||||
name: 'MessagesIn' as const,
|
||||
unit: global.getMetricDefine(MetricType.Cluster, 'MessagesIn')?.unit,
|
||||
data: result as any,
|
||||
};
|
||||
if (maxValue > 0) {
|
||||
const [unitName, unitSize]: [string, number] = getDataNumberUnit(maxValue);
|
||||
line.unit = `${unitName}${line.unit}`;
|
||||
result.forEach((point) => ((point[1] as number) /= unitSize));
|
||||
}
|
||||
|
||||
// 补充缺少的图表点
|
||||
const extraMetrics = result[0][2].map((info) => ({
|
||||
...info,
|
||||
value: 0,
|
||||
}));
|
||||
const supplementaryInterval =
|
||||
(curHeaderOptions.rangeTime[1] - curHeaderOptions.rangeTime[0] > MAX_TIME_RANGE_WITH_SMALL_POINT_INTERVAL ? 10 : 1) * 60 * 1000;
|
||||
supplementaryPoints([line], curHeaderOptions.rangeTime, supplementaryInterval, (point) => {
|
||||
point.push(extraMetrics as any);
|
||||
return point;
|
||||
});
|
||||
|
||||
setMessagesInMetricData(line);
|
||||
setDefaultChartLoading(false);
|
||||
},
|
||||
() => setDefaultChartLoading(false)
|
||||
);
|
||||
};
|
||||
|
||||
// 监听盒子宽度变化,重置图表宽度
|
||||
const observeDashboardWidthChange = () => {
|
||||
const targetNode = document.getElementsByClassName('dcd-two-columns-layout-sider-footer')[0];
|
||||
targetNode && targetNode.addEventListener('click', () => busInstance.emit('chartResize'));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getMetricData();
|
||||
}, [selectedMetricNames]);
|
||||
|
||||
useEffect(() => {
|
||||
if (curHeaderOptions && curHeaderOptions?.rangeTime.join(',') !== '0,0') {
|
||||
getDefaultMetricData();
|
||||
getMetricData();
|
||||
}
|
||||
}, [curHeaderOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
getMetricList();
|
||||
setTimeout(() => observeDashboardWidthChange());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="cluster-detail-container">
|
||||
<SingleChartHeader
|
||||
onChange={ksHeaderChange}
|
||||
hideNodeScope={true}
|
||||
hideGridSelect={true}
|
||||
indicatorSelectModule={{
|
||||
hide: false,
|
||||
metricType: MetricType.Cluster,
|
||||
tableData: metricList,
|
||||
selectedRows: selectedMetricNames,
|
||||
checkboxProps: (record: MetricInfo) => {
|
||||
return record.name === DEFAULT_METRIC
|
||||
? {
|
||||
disabled: true,
|
||||
}
|
||||
: {};
|
||||
},
|
||||
submitCallback: indicatorChangeCallback,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="cluster-detail-container-main">
|
||||
{/* MessageIn 图表 */}
|
||||
<div className={`header-chart-container ${!messagesInMetricData.data.length ? 'header-chart-container-loading' : ''}`}>
|
||||
<Spin spinning={defaultChartLoading}>
|
||||
{/* TODO: 暂时通过判断是否有图表数据来修复,有时间可以查找下宽度溢出的原因 */}
|
||||
{messagesInMetricData.data.length ? (
|
||||
<>
|
||||
<div className="chart-box-title">
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
title={() => {
|
||||
let content = '';
|
||||
const metricDefine = global.getMetricDefine(MetricType.Cluster, messagesInMetricData.name);
|
||||
if (metricDefine) {
|
||||
content = metricDefine.desc;
|
||||
}
|
||||
return content;
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<span className="name">{messagesInMetricData.name}</span>
|
||||
<span className="unit">({messagesInMetricData.unit})</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<SingleChart
|
||||
chartKey="messagesIn"
|
||||
chartTypeProp="line"
|
||||
showHeader={false}
|
||||
wrapStyle={{
|
||||
width: 'auto',
|
||||
height: 210,
|
||||
}}
|
||||
connectEventName="clusterChart"
|
||||
eventBus={busInstance}
|
||||
propChartData={[messagesInMetricData]}
|
||||
{...getChartConfig({
|
||||
// metricName: `${messagesInMetricData.name}{unit|(${messagesInMetricData.unit})}`,
|
||||
lineColor: CHART_LINE_COLORS[0],
|
||||
isDefaultMetric: true,
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
{/* 其余指标图表 */}
|
||||
<div className="multiple-chart-container">
|
||||
<div className={!metricDataList.length ? 'multiple-chart-container-loading' : ''}>
|
||||
<Spin spinning={chartLoading}>
|
||||
<Row gutter={[16, 16]}>
|
||||
{metricDataList.length ? (
|
||||
metricDataList.map((data: any, i: number) => {
|
||||
const { metricName, metricUnit, metricLines } = data;
|
||||
return (
|
||||
<Col key={metricName} span={12}>
|
||||
<div className="chart-box">
|
||||
<div className="chart-box-title">
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
title={() => {
|
||||
let content = '';
|
||||
const metricDefine = global.getMetricDefine(MetricType.Cluster, metricName);
|
||||
if (metricDefine) {
|
||||
content = metricDefine.desc;
|
||||
}
|
||||
return content;
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<span className="name">{metricName}</span>
|
||||
<span className="unit">({metricUnit})</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
className="expand-icon-box"
|
||||
onClick={() => {
|
||||
setChartDetail(data);
|
||||
setShowChartDetailModal(true);
|
||||
}}
|
||||
>
|
||||
<IconFont type="icon-chuangkoufangda" className="expand-icon" />
|
||||
</div>
|
||||
<SingleChart
|
||||
chartKey={metricName}
|
||||
showHeader={false}
|
||||
chartTypeProp="line"
|
||||
wrapStyle={{
|
||||
width: 'auto',
|
||||
height: 210,
|
||||
}}
|
||||
connectEventName="clusterChart"
|
||||
eventBus={busInstance}
|
||||
propChartData={metricLines}
|
||||
{...getChartConfig({
|
||||
metricName: `${metricName}{unit|(${metricUnit})}`,
|
||||
lineColor: calculateChartColor(i),
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
);
|
||||
})
|
||||
) : chartLoading ? (
|
||||
<></>
|
||||
) : (
|
||||
<Empty description="请先选择指标或刷新" style={{ width: '100%', height: '100%' }} />
|
||||
)}
|
||||
</Row>
|
||||
</Spin>
|
||||
</div>
|
||||
</div>
|
||||
{/* 历史配置变更记录内容 */}
|
||||
<div className="config-change-records-container">{props.children}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 图表详情 */}
|
||||
<Modal
|
||||
width={1080}
|
||||
visible={showChartDetailModal}
|
||||
centered={true}
|
||||
footer={null}
|
||||
closable={false}
|
||||
onCancel={() => setShowChartDetailModal(false)}
|
||||
>
|
||||
<div className="chart-detail-modal-container">
|
||||
<div className="expand-icon-box" onClick={() => setShowChartDetailModal(false)}>
|
||||
<IconFont type="icon-chuangkousuoxiao" className="expand-icon" />
|
||||
</div>
|
||||
{chartDetail && (
|
||||
<SingleChart
|
||||
chartTypeProp="line"
|
||||
wrapStyle={{
|
||||
width: 'auto',
|
||||
height: 462,
|
||||
}}
|
||||
propChartData={chartDetail.metricLines}
|
||||
{...getChartConfig({
|
||||
metricName: `${chartDetail.metricName}{unit|(${chartDetail.metricUnit})}`,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailChart;
|
||||
@@ -0,0 +1,135 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { Button, Divider, Drawer, Form, message, ProTable, Table, Utils } from 'knowdesign';
|
||||
import React, { useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { getHealthySettingColumn } from './config';
|
||||
import API from '../../api';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
const HealthySetting = React.forwardRef((props: any, ref): JSX.Element => {
|
||||
const intl = useIntl();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [initialValues, setInitialValues] = useState({} as any);
|
||||
|
||||
const [data, setData] = React.useState([]);
|
||||
const { clusterId } = useParams<{ clusterId: string }>();
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
setVisible,
|
||||
getHealthconfig,
|
||||
}));
|
||||
|
||||
const getHealthconfig = () => {
|
||||
return Utils.request(API.getClusterHealthyConfigs(+clusterId)).then((res: any) => {
|
||||
const values = {} as any;
|
||||
|
||||
try {
|
||||
res = res.map((item: any) => {
|
||||
const itemValue = JSON.parse(item.value);
|
||||
item.weight = itemValue?.weight;
|
||||
|
||||
item.configItemName =
|
||||
item.configItem.indexOf('Group Re-Balance') > -1
|
||||
? 'ReBalance'
|
||||
: item.configItem.includes('副本未同步')
|
||||
? 'UNDER_REPLICA'
|
||||
: item.configItem;
|
||||
|
||||
values[`weight_${item.configItemName}`] = itemValue?.weight;
|
||||
values[`value_${item.configItemName}`] = itemValue?.value;
|
||||
values[`latestMinutes_${item.configItemName}`] = itemValue?.latestMinutes;
|
||||
values[`detectedTimes_${item.configItemName}`] = itemValue?.detectedTimes;
|
||||
return item;
|
||||
});
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
const formItemsValue = {
|
||||
...initialValues,
|
||||
...values,
|
||||
};
|
||||
setInitialValues(formItemsValue);
|
||||
form.setFieldsValue(formItemsValue);
|
||||
setData(res);
|
||||
});
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
form.resetFields();
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
form.validateFields().then((res) => {
|
||||
const params = [] as any;
|
||||
data.map((item) => {
|
||||
params.push({
|
||||
clusterId: +clusterId,
|
||||
value: JSON.stringify({
|
||||
clusterPhyId: +clusterId,
|
||||
detectedTimes: res[`detectedTimes_${item.configItemName}`],
|
||||
latestMinutes: res[`latestMinutes_${item.configItemName}`],
|
||||
weight: res[`weight_${item.configItemName}`],
|
||||
value: item.configItemName === 'Controller' ? 1 : res[`value_${item.configItemName}`],
|
||||
}),
|
||||
valueGroup: item.configGroup,
|
||||
valueName: item.configName,
|
||||
});
|
||||
});
|
||||
Utils.put(API.putPlatformConfigs, params)
|
||||
.then((res) => {
|
||||
message.success('修改成功');
|
||||
form.resetFields();
|
||||
setVisible(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error('操作失败' + err.message);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const onHandleValuesChange = (value: any, allValues: any) => {
|
||||
//
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
className="drawer-content healthy-drawer-content"
|
||||
onClose={onCancel}
|
||||
maskClosable={false}
|
||||
extra={
|
||||
<div className="operate-wrap">
|
||||
<Button size="small" style={{ marginRight: 8 }} onClick={onCancel}>
|
||||
取消
|
||||
</Button>
|
||||
<Button size="small" type="primary" onClick={onSubmit}>
|
||||
确定
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
</div>
|
||||
}
|
||||
title={intl.formatMessage({ id: 'healthy.setting' })}
|
||||
visible={visible}
|
||||
placement="right"
|
||||
width={1080}
|
||||
>
|
||||
<Form form={form} layout="vertical" onValuesChange={onHandleValuesChange}>
|
||||
<ProTable
|
||||
tableProps={{
|
||||
rowKey: 'dimensionCode',
|
||||
showHeader: false,
|
||||
dataSource: data,
|
||||
columns: getHealthySettingColumn(form, data, clusterId),
|
||||
noPagination: true,
|
||||
}}
|
||||
/>
|
||||
</Form>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default HealthySetting;
|
||||
@@ -0,0 +1,253 @@
|
||||
import { AppContainer, Divider, IconFont, Progress, Tooltip, Utils } from 'knowdesign';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import AccessClusters from '../MutliClusterPage/AccessCluster';
|
||||
import './index.less';
|
||||
import API from '../../api';
|
||||
import HealthySetting from './HealthySetting';
|
||||
import CheckDetail from './CheckDetail';
|
||||
import { Link, useHistory, useParams } from 'react-router-dom';
|
||||
import { getHealthClassName, getHealthProcessColor, getHealthState, getHealthText, renderToolTipValue } from './config';
|
||||
import { ClustersPermissionMap } from '../CommonConfig';
|
||||
|
||||
const LeftSider = () => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const history = useHistory();
|
||||
const [kafkaVersion, setKafkaVersion] = useState({});
|
||||
const [clusterInfo, setClusterInfo] = useState({} as any);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [clusterMetrics, setClusterMetrics] = useState({} as any);
|
||||
const [brokerState, setBrokerState] = useState({} as any);
|
||||
|
||||
const detailDrawerRef: any = React.createRef();
|
||||
const healthyDrawerRef: any = React.createRef();
|
||||
const { clusterId } = useParams<{ clusterId: string }>();
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
|
||||
const getSupportKafkaVersion = () => {
|
||||
Utils.request(API.supportKafkaVersion).then((res) => {
|
||||
setKafkaVersion(res || {});
|
||||
});
|
||||
};
|
||||
|
||||
const getBrokerState = () => {
|
||||
return Utils.request(API.getBrokersState(clusterId)).then((res) => {
|
||||
setBrokerState(res);
|
||||
});
|
||||
};
|
||||
|
||||
const getPhyClusterMetrics = () => {
|
||||
return Utils.post(
|
||||
API.getPhyClusterMetrics(+clusterId),
|
||||
[
|
||||
'HealthScore',
|
||||
'HealthCheckPassed',
|
||||
'HealthCheckTotal',
|
||||
'Topics',
|
||||
'PartitionURP',
|
||||
'PartitionNoLeader', // > 0 error
|
||||
'PartitionMinISR_S', // > 0 error
|
||||
'Groups',
|
||||
'GroupDeads',
|
||||
'Alive',
|
||||
].concat(process.env.BUSINESS_VERSION ? ['LoadReBalanceEnable', 'LoadReBalanceNwIn', 'LoadReBalanceNwOut', 'LoadReBalanceDisk'] : [])
|
||||
).then((res: any) => {
|
||||
setClusterMetrics(res?.metrics || {});
|
||||
});
|
||||
};
|
||||
|
||||
const getPhyClusterInfo = () => {
|
||||
setLoading(true);
|
||||
Utils.request(API.getPhyClusterBasic(+clusterId))
|
||||
.then((res: any) => {
|
||||
let jmxProperties = null;
|
||||
try {
|
||||
jmxProperties = JSON.parse(res?.jmxProperties);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
// 转化值对应成表单值
|
||||
if (jmxProperties?.openSSL) {
|
||||
jmxProperties.security = 'Password';
|
||||
}
|
||||
|
||||
if (jmxProperties) {
|
||||
res = Object.assign({}, res || {}, jmxProperties);
|
||||
}
|
||||
setClusterInfo(res);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getBrokerState();
|
||||
getPhyClusterMetrics();
|
||||
getSupportKafkaVersion();
|
||||
getPhyClusterInfo();
|
||||
}, []);
|
||||
|
||||
const renderIcon = (type: string) => {
|
||||
return (
|
||||
<span className={`icon`}>
|
||||
<IconFont type={type === 'green' ? 'icon-zhengchang' : type === 'warning' ? 'icon-yichang' : 'icon-warning'} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="left-sider">
|
||||
<div className="state-card">
|
||||
<Progress
|
||||
type="circle"
|
||||
status="active"
|
||||
strokeWidth={4}
|
||||
strokeColor={getHealthProcessColor(clusterMetrics?.HealthScore, clusterMetrics?.Alive)}
|
||||
percent={clusterMetrics?.HealthScore ?? '-'}
|
||||
format={() => (
|
||||
<div className={`healthy-percent ${getHealthClassName(clusterMetrics?.HealthScore, clusterMetrics?.Alive)}`}>
|
||||
{getHealthText(clusterMetrics?.HealthScore, clusterMetrics?.Alive)}
|
||||
</div>
|
||||
)}
|
||||
width={75}
|
||||
/>
|
||||
<div className="healthy-state">
|
||||
<div className="healthy-state-status">
|
||||
<span>{getHealthState(clusterMetrics?.HealthScore, clusterMetrics?.Alive)}</span>
|
||||
{/* 健康分设置 */}
|
||||
{global.hasPermission && global.hasPermission(ClustersPermissionMap.CLUSTER_CHANGE_HEALTHY) ? (
|
||||
<span
|
||||
className="icon"
|
||||
onClick={() => {
|
||||
healthyDrawerRef.current.getHealthconfig().then(() => {
|
||||
healthyDrawerRef.current.setVisible(true);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconFont type="icon-shezhi" size={13} />
|
||||
</span>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="healthy-state-num">
|
||||
{clusterMetrics?.HealthCheckPassed}/{clusterMetrics?.HealthCheckTotal}
|
||||
</span>
|
||||
{/* 健康度详情 */}
|
||||
<span
|
||||
className="healthy-state-btn"
|
||||
onClick={() => {
|
||||
detailDrawerRef.current.setVisible(true);
|
||||
}}
|
||||
>
|
||||
查看详情
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="title">
|
||||
<div className="name">{renderToolTipValue(clusterInfo?.name, 35)}</div>
|
||||
{global.hasPermission && global.hasPermission(ClustersPermissionMap.CLUSTER_CHANGE_INFO) ? (
|
||||
<div className="edit-icon-box" onClick={() => setVisible(true)}>
|
||||
<IconFont className="edit-icon" type="icon-bianji2" />
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
<div className="tag-panel">
|
||||
<div className="tag default">{clusterInfo?.kafkaVersion ?? '-'}</div>
|
||||
{clusterMetrics?.LoadReBalanceEnable !== undefined &&
|
||||
[
|
||||
['BytesIn', clusterMetrics?.LoadReBalanceEnable && clusterMetrics?.LoadReBalanceNwIn],
|
||||
['BytesOut', clusterMetrics?.LoadReBalanceEnable && clusterMetrics?.LoadReBalanceNwOut],
|
||||
['Disk', clusterMetrics?.LoadReBalanceEnable && clusterMetrics?.LoadReBalanceDisk],
|
||||
].map(([name, isBalanced]) => {
|
||||
return isBalanced ? (
|
||||
<div className="tag balanced">{name} 已均衡</div>
|
||||
) : clusterMetrics?.LoadReBalanceEnable ? (
|
||||
<div className="tag unbalanced">{name} 未均衡</div>
|
||||
) : (
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
尚未开启 {name} 均衡策略,
|
||||
<Link to={`/cluster/${clusterId}/cluster/balance`}>前往开启</Link>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="tag unbalanced">{name} 未均衡</div>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="desc">{renderToolTipValue(clusterInfo?.description, 35)}</div>
|
||||
<div className="card-panel">
|
||||
<div className="card-item" onClick={() => history.push(`/cluster/${clusterId}/broker`)}>
|
||||
<div className="title">Brokers总数</div>
|
||||
<div className="count">
|
||||
<span className="num">{brokerState?.brokerCount ?? '-'}</span>
|
||||
<span className="unit">个</span>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<span className="type">Controller</span>
|
||||
{renderIcon(brokerState?.kafkaControllerAlive ? 'green' : 'red')}
|
||||
</div>
|
||||
<div className="metric">
|
||||
<span className="type">Similar Config</span>
|
||||
{renderIcon(brokerState?.configSimilar ? 'green' : 'warning')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-item" onClick={() => history.push(`/cluster/${clusterId}/topic`)}>
|
||||
<div className="title">Topics总数</div>
|
||||
<div className="count">
|
||||
<span className="num">{clusterMetrics?.Topics ?? '-'}</span>
|
||||
<span className="unit">个</span>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<span className="type">No leader</span>
|
||||
{renderIcon(clusterMetrics?.PartitionNoLeader === 0 ? 'green' : 'red')}
|
||||
</div>
|
||||
<div className="metric">
|
||||
<span className="type">{'< Min ISR'}</span>
|
||||
{renderIcon(clusterMetrics?.PartitionMinISR_S === 0 ? 'green' : 'red')}
|
||||
</div>
|
||||
<div className="metric">
|
||||
<span className="type">URP</span>
|
||||
{renderIcon(clusterMetrics?.PartitionURP === 0 ? 'green' : 'red')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-item" onClick={() => history.push(`/cluster/${clusterId}/consumers`)}>
|
||||
<div className="title">ConsumerGroup总数</div>
|
||||
<div className="count">
|
||||
<span className="num">{clusterMetrics?.Groups ?? '-'}</span>
|
||||
<span className="unit">个</span>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<span className="type">Dead</span>
|
||||
{renderIcon(clusterMetrics?.GroupDeads === 0 ? 'green' : 'red')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AccessClusters
|
||||
visible={visible}
|
||||
setVisible={setVisible}
|
||||
title={'edit.cluster'}
|
||||
infoLoading={loading}
|
||||
afterSubmitSuccess={getPhyClusterInfo}
|
||||
clusterInfo={clusterInfo}
|
||||
kafkaVersion={Object.keys(kafkaVersion)}
|
||||
/>
|
||||
<HealthySetting ref={healthyDrawerRef} />
|
||||
<CheckDetail ref={detailDrawerRef} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LeftSider;
|
||||
@@ -0,0 +1,334 @@
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { timeFormat } from '../../constants/common';
|
||||
import TagsWithHide from '../../components/TagsWithHide/index';
|
||||
import { Form, IconFont, InputNumber, Tooltip } from 'knowdesign';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { systemKey } from '../../constants/menu';
|
||||
|
||||
const statusTxtEmojiMap = {
|
||||
success: {
|
||||
emoji: '👍',
|
||||
txt: '优异',
|
||||
},
|
||||
normal: {
|
||||
emoji: '😊',
|
||||
txt: '正常',
|
||||
},
|
||||
exception: {
|
||||
emoji: '👻',
|
||||
txt: '异常',
|
||||
},
|
||||
};
|
||||
|
||||
export const dimensionMap = {
|
||||
'-1': {
|
||||
label: 'Unknown',
|
||||
href: ``,
|
||||
},
|
||||
0: {
|
||||
label: 'Cluster',
|
||||
href: ``,
|
||||
},
|
||||
1: {
|
||||
label: 'Broker',
|
||||
href: `/broker`,
|
||||
},
|
||||
2: {
|
||||
label: 'Topic',
|
||||
href: `/topic`,
|
||||
},
|
||||
3: {
|
||||
label: 'ConsumerGroup',
|
||||
href: `/consumers`,
|
||||
},
|
||||
} as any;
|
||||
|
||||
export const getHealthState = (value: number, down: number) => {
|
||||
if (value === undefined) return '-';
|
||||
const progressStatus = +down <= 0 ? 'exception' : value >= 90 ? 'success' : 'normal';
|
||||
return (
|
||||
<span>
|
||||
{statusTxtEmojiMap[progressStatus].emoji} 集群状态{statusTxtEmojiMap[progressStatus].txt}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export const getHealthText = (value: number, down: number) => {
|
||||
return +down <= 0 ? 'Down' : value ? value.toFixed(0) : '-';
|
||||
};
|
||||
|
||||
export const getHealthProcessColor = (value: number, down: number) => {
|
||||
return +down <= 0 ? '#FF7066' : +value < 90 ? '#556EE6' : '#00C0A2';
|
||||
};
|
||||
|
||||
export const getHealthClassName = (value: number, down: number) => {
|
||||
return +down <= 0 ? 'down' : value === undefined ? 'no-info' : +value < 90 ? 'less-90' : '';
|
||||
};
|
||||
|
||||
export const renderToolTipValue = (value: string, num: number) => {
|
||||
return (
|
||||
<>
|
||||
{value?.length > num ? (
|
||||
<>
|
||||
<Tooltip placement="topLeft" title={value}>
|
||||
{value ?? '-'}
|
||||
</Tooltip>
|
||||
</>
|
||||
) : (
|
||||
value ?? '-'
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const getDetailColumn = (clusterId: number) => [
|
||||
{
|
||||
title: '检查模块',
|
||||
dataIndex: 'dimension',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (text: number) => {
|
||||
if (text === 0 || text === -1) return dimensionMap[text]?.label;
|
||||
return <Link to={`/${systemKey}/${clusterId}${dimensionMap[text]?.href}`}>{dimensionMap[text]?.label}</Link>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '检查项',
|
||||
dataIndex: 'checkConfig',
|
||||
render(config: any, record: any) {
|
||||
const valueGroup = JSON.parse(config.value);
|
||||
if (record.configItem === 'Controller') {
|
||||
return '集群 Controller 数等于 1';
|
||||
} else if (record.configItem === 'RequestQueueSize') {
|
||||
return `Broker-RequestQueueSize 小于 ${valueGroup.value}`;
|
||||
} else if (record.configItem === 'NoLeader') {
|
||||
return `Topic 无 Leader 数小于 ${valueGroup.value}`;
|
||||
} else if (record.configItem === 'NetworkProcessorAvgIdlePercent') {
|
||||
return `Broker-NetworkProcessorAvgIdlePercent 的 idle 大于 ${valueGroup.value}%`;
|
||||
} else if (record.configItem === 'UnderReplicaTooLong') {
|
||||
return `Topic 小于 ${parseFloat(((valueGroup.detectedTimes / valueGroup.latestMinutes) * 100).toFixed(2))}% 周期处于未同步状态`;
|
||||
} else if (record.configItem === 'Group Re-Balance') {
|
||||
return `Consumer Group 小于 ${parseFloat(
|
||||
((valueGroup.detectedTimes / valueGroup.latestMinutes) * 100).toFixed(2)
|
||||
)}% 周期处于 Re-balance 状态`;
|
||||
}
|
||||
|
||||
return <></>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '权重',
|
||||
dataIndex: 'weightPercent',
|
||||
width: 80,
|
||||
render(value: number) {
|
||||
return `${value}%`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '得分',
|
||||
dataIndex: 'score',
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
title: '检查时间',
|
||||
width: 190,
|
||||
dataIndex: 'updateTime',
|
||||
render: (text: string) => {
|
||||
return moment(text).format(timeFormat);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '检查结果',
|
||||
dataIndex: 'passed',
|
||||
width: 280,
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (passed: boolean, record: any) => {
|
||||
if (passed) {
|
||||
return (
|
||||
<>
|
||||
<IconFont type="icon-zhengchang" />
|
||||
<span style={{ marginLeft: 4 }}>通过</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', width: '240px' }}>
|
||||
<IconFont type="icon-yichang" />
|
||||
<div style={{ marginLeft: 4, marginRight: 6, flexShrink: 0 }}>未通过</div>
|
||||
<TagsWithHide list={record.notPassedResNameList || []} expandTagContent="更多" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const getHealthySettingColumn = (form: any, data: any, clusterId: string) =>
|
||||
[
|
||||
{
|
||||
title: '检查模块',
|
||||
dataIndex: 'dimensionCode',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (text: number) => {
|
||||
if (text === 0 || text === -1) return dimensionMap[text]?.label;
|
||||
return <Link to={`/${systemKey}/${clusterId}${dimensionMap[text]?.href}`}>{dimensionMap[text]?.label}</Link>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '检查项',
|
||||
dataIndex: 'configItem',
|
||||
width: 200,
|
||||
needTooltip: true,
|
||||
},
|
||||
{
|
||||
title: '检查项描述',
|
||||
dataIndex: 'configDesc',
|
||||
width: 240,
|
||||
needToolTip: true,
|
||||
},
|
||||
{
|
||||
title: '权重',
|
||||
dataIndex: 'weight',
|
||||
// width: 180,
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (text: number, record: any) => {
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name={`weight_${record.configItemName}`}
|
||||
label=""
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
validator: async (rule: any, value: string) => {
|
||||
const otherWeightCongigName: string[] = [];
|
||||
let totalPercent = 0;
|
||||
data.map((item: any) => {
|
||||
if (item.configItemName !== record.configItemName) {
|
||||
otherWeightCongigName.push(`weight_${item.configItemName}`);
|
||||
totalPercent += form.getFieldValue(`weight_${item.configItemName}`) ?? 0;
|
||||
}
|
||||
});
|
||||
if (!value) {
|
||||
return Promise.reject('请输入权重');
|
||||
}
|
||||
if (+value < 0) {
|
||||
return Promise.reject('最小为0');
|
||||
}
|
||||
if (+value + totalPercent !== 100) {
|
||||
return Promise.reject('总和应为100%');
|
||||
}
|
||||
form.setFields(otherWeightCongigName.map((i) => ({ name: i, errors: [] })));
|
||||
return Promise.resolve('');
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
size="small"
|
||||
min={0}
|
||||
max={100}
|
||||
formatter={(value) => `${value}%`}
|
||||
parser={(value: any) => value.replace('%', '')}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '检查规则',
|
||||
// width: 350,
|
||||
dataIndex: 'passed',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (text: any, record: any) => {
|
||||
const getFormItem = (params: { type?: string; percent?: boolean; attrs?: any; validator?: any }) => {
|
||||
const { validator, percent, type = 'value', attrs = { min: 0 } } = params;
|
||||
return (
|
||||
<Form.Item
|
||||
name={`${type}_${record.configItemName}`}
|
||||
label=""
|
||||
rules={
|
||||
validator
|
||||
? [
|
||||
{
|
||||
required: true,
|
||||
validator: validator,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入',
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
{percent ? (
|
||||
<InputNumber
|
||||
size="small"
|
||||
min={0}
|
||||
max={100}
|
||||
style={{ width: 86 }}
|
||||
formatter={(value) => `${value}%`}
|
||||
parser={(value: any) => value.replace('%', '')}
|
||||
/>
|
||||
) : (
|
||||
<InputNumber style={{ width: 86 }} size="small" {...attrs} />
|
||||
)}
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
|
||||
if (record.configItemName === 'Controller') {
|
||||
return <div className="table-form-item">不等于 1 则不通过</div>;
|
||||
}
|
||||
if (record.configItemName === 'RequestQueueSize' || record.configItemName === 'NoLeader') {
|
||||
return (
|
||||
<div className="table-form-item">
|
||||
<span className="left-text">≥</span>
|
||||
{getFormItem({ attrs: { min: 0, max: 99998 } })}
|
||||
<span className="right-text">则不通过</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (record.configItemName === 'NetworkProcessorAvgIdlePercent') {
|
||||
return (
|
||||
<div className="table-form-item">
|
||||
<span className="left-text">≤</span>
|
||||
{getFormItem({ percent: true })}
|
||||
<span className="right-text">则不通过</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (record.configItemName === 'UnderReplicaTooLong' || record.configItemName === 'ReBalance') {
|
||||
return (
|
||||
<div className="table-form-item">
|
||||
{getFormItem({ type: 'latestMinutes', attrs: { min: 1, max: 10080 } })}
|
||||
<span className="right-text left-text">周期内,≥</span>
|
||||
{getFormItem({
|
||||
type: 'detectedTimes',
|
||||
attrs: { min: 1, max: 10080 },
|
||||
validator: async (rule: any, value: string) => {
|
||||
const latestMinutesValue = form.getFieldValue(`latestMinutes_${record.configItemName}`);
|
||||
|
||||
if (!value) {
|
||||
return Promise.reject('请输入');
|
||||
}
|
||||
if (+value < 1) {
|
||||
return Promise.reject('最小为1');
|
||||
}
|
||||
if (+value > +latestMinutesValue) {
|
||||
return Promise.reject('值不能大于周期');
|
||||
}
|
||||
return Promise.resolve('');
|
||||
},
|
||||
})}
|
||||
<span className="right-text">则不通过</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
},
|
||||
},
|
||||
] as any;
|
||||
@@ -0,0 +1,413 @@
|
||||
.single-cluster-detail {
|
||||
width: 100%;
|
||||
padding-bottom: 10px;
|
||||
|
||||
.cluster-detail {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
|
||||
.left-sider {
|
||||
width: 240px;
|
||||
background: #fff;
|
||||
// height: calc(100vh - 128px);
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.01), 0 3px 6px 3px rgba(0, 0, 0, 0.01), 0 2px 6px 0 rgba(0, 0, 0, 0.03);
|
||||
border-radius: 12px;
|
||||
padding: 24px 16px;
|
||||
|
||||
.title {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 18px;
|
||||
color: #212529;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 20px;
|
||||
display: flex;
|
||||
|
||||
.name {
|
||||
max-width: 188px;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
}
|
||||
.edit-icon-box {
|
||||
position: relative;
|
||||
width: 20px;
|
||||
cursor: pointer;
|
||||
.edit-icon {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
margin-left: 4px;
|
||||
font-size: 16px;
|
||||
color: #74788d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tag-panel {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0;
|
||||
line-height: 12px;
|
||||
margin-top: 10px;
|
||||
|
||||
.tag {
|
||||
height: 18px;
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
|
||||
&.default {
|
||||
background: #ececf6;
|
||||
color: #495057;
|
||||
}
|
||||
&.balanced {
|
||||
background: rgba(85, 110, 230, 0.1);
|
||||
color: #556ee6;
|
||||
}
|
||||
&.unbalanced {
|
||||
background: rgba(255, 136, 0, 0.1);
|
||||
color: #f58342;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 11px;
|
||||
color: #74788d;
|
||||
text-align: left;
|
||||
line-height: 16px;
|
||||
margin: 12px 0px 16px;
|
||||
max-width: 208px;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.card-panel {
|
||||
.card-item {
|
||||
width: 208px;
|
||||
background: #f8f9fe;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
.title {
|
||||
font-size: 14px;
|
||||
color: #212529;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 22px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.count {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.num {
|
||||
font-family: DIDIFD-Regular;
|
||||
font-size: 40px;
|
||||
color: #212529;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: 16px;
|
||||
color: #212529;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.metric {
|
||||
font-size: 11px;
|
||||
color: #74788d;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 4px;
|
||||
|
||||
.icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
|
||||
&.green {
|
||||
color: #34c38f;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: #ffe4c6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.state-card {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
.healthy-percent {
|
||||
font-family: DIDIFD-Regular;
|
||||
font-size: 38px;
|
||||
color: #00c0a2;
|
||||
text-align: center;
|
||||
|
||||
&.less-90 {
|
||||
color: #556ee6;
|
||||
}
|
||||
|
||||
&.no-info {
|
||||
color: #e9e7e7;
|
||||
}
|
||||
|
||||
&.down {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 22px;
|
||||
color: #ff7066;
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.healthy-state {
|
||||
margin-left: 14px;
|
||||
margin-top: 8px;
|
||||
|
||||
&-status {
|
||||
font-size: 13px;
|
||||
color: #495057;
|
||||
letter-spacing: 0;
|
||||
line-height: 20px;
|
||||
margin-bottom: 13px;
|
||||
|
||||
.icon {
|
||||
margin-left: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&-btn {
|
||||
font-size: 10px;
|
||||
color: #495057;
|
||||
line-height: 15px;
|
||||
background: #ececf6;
|
||||
border-radius: 4px;
|
||||
padding: 0px 6px;
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-divider-horizontal {
|
||||
margin: 16px 4px;
|
||||
padding: 0px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-panel {
|
||||
flex: 1;
|
||||
margin-left: 12px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.change-log-panel {
|
||||
height: 100%;
|
||||
padding: 12px 16px;
|
||||
|
||||
.title {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 14px;
|
||||
color: #212529;
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 22px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
#changelog-scroll-box {
|
||||
height: calc(100% - 34px);
|
||||
overflow: hidden auto;
|
||||
}
|
||||
|
||||
.dcloud-collapse {
|
||||
border: none;
|
||||
background-color: #fff;
|
||||
|
||||
.dcloud-collapse-header {
|
||||
background: #fff;
|
||||
height: 40px;
|
||||
display: block;
|
||||
padding: 0px 0px 8px !important;
|
||||
|
||||
.header {
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
.label {
|
||||
font-size: 13px;
|
||||
color: #212529;
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 9px;
|
||||
height: 5px;
|
||||
color: #74788d;
|
||||
|
||||
.anticon {
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-time {
|
||||
height: 20px;
|
||||
font-size: 10px;
|
||||
color: #74788d;
|
||||
text-align: left;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-collapse-header:hover {
|
||||
.label {
|
||||
color: #556ee6;
|
||||
font-family: @font-family-bold;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-collapse-content {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.dcloud-collapse-item {
|
||||
border: none;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dcloud-collapse-item-active {
|
||||
.dcloud-collapse-header {
|
||||
.header {
|
||||
.anticon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-collapse-content > .dcloud-collapse-content-box {
|
||||
padding: 7px 11px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
|
||||
.log-item {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 10px;
|
||||
color: #74788d;
|
||||
|
||||
.value {
|
||||
color: #212529;
|
||||
max-width: 124px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-divider-horizontal {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.page-panel {
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
|
||||
.dcloud-pagination-simple-pager {
|
||||
font-size: 13px;
|
||||
color: #495057;
|
||||
text-align: center;
|
||||
line-height: 22px;
|
||||
|
||||
input {
|
||||
width: 48px;
|
||||
height: 24px;
|
||||
background: #ececf6;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-pagination-simple {
|
||||
.anticon {
|
||||
font-size: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-panel {
|
||||
margin-top: 96px;
|
||||
text-align: center;
|
||||
|
||||
.img {
|
||||
width: 51px;
|
||||
height: 34px;
|
||||
margin-left: 80px;
|
||||
margin-bottom: 7px;
|
||||
background-size: cover;
|
||||
background-image: url('../../assets/empty.png');
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 10px;
|
||||
color: #919aac;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.healthy-drawer-content {
|
||||
.table-form-item {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-start;
|
||||
.left-text,
|
||||
.right-text {
|
||||
line-height: 27px;
|
||||
}
|
||||
|
||||
.left-text {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.right-text {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-form-item {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import DBreadcrumb from 'knowdesign/lib/extend/d-breadcrumb';
|
||||
import React from 'react';
|
||||
import TourGuide, { ClusterDetailSteps } from '@src/components/TourGuide';
|
||||
import './index.less';
|
||||
import LeftSider from './LeftSider';
|
||||
import ChartPanel from './DetailChart';
|
||||
import ChangeLog from './ChangeLog';
|
||||
|
||||
const SingleClusterDetail = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<TourGuide guide={ClusterDetailSteps} run={true} />
|
||||
<div className="single-cluster-detail">
|
||||
<div className="breadcrumb">
|
||||
<DBreadcrumb
|
||||
breadcrumbs={[
|
||||
{ label: '多集群管理', aHref: '/' },
|
||||
{ label: '集群详情', aHref: '' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="cluster-detail">
|
||||
<LeftSider />
|
||||
<div className="chart-panel">
|
||||
<ChartPanel>
|
||||
<ChangeLog />
|
||||
</ChartPanel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SingleClusterDetail;
|
||||
@@ -0,0 +1,418 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import { DownloadOutlined } from '@ant-design/icons';
|
||||
import { AppContainer, Divider, IconFont, message, Tooltip, Utils } from 'knowdesign';
|
||||
import * as React from 'react';
|
||||
import moment from 'moment';
|
||||
import { timeFormat } from '../../constants/common';
|
||||
import ConfigForm from './component/ConfigForm';
|
||||
import TestResult from './component/Result';
|
||||
import { getFormConfig, getTableColumns, startFromMap } from './config';
|
||||
import './index.less';
|
||||
import API from '../../api';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
interface IPartition {
|
||||
consumedOffset: number;
|
||||
logEndOffset: number;
|
||||
partitionId: number;
|
||||
recordCount: number;
|
||||
recordSizeUnitB: number;
|
||||
}
|
||||
|
||||
const ConsumeClientTest = () => {
|
||||
const formRef: any = React.createRef();
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [configInfo, setConfigInfo] = React.useState({});
|
||||
const [topicMetaData, setTopicMetaData] = React.useState([]);
|
||||
const [partitionProcess, setPartitionProcess] = React.useState([]);
|
||||
const partitionProcessRef = React.useRef<any[]>([]);
|
||||
const [tableData, setTableData] = React.useState([]);
|
||||
const [tableDataRes, setTableDataRes] = React.useState([]);
|
||||
const recordCountCur = React.useRef(0);
|
||||
const recordSizeCur = React.useRef(0);
|
||||
const [isStop, setIsStop] = React.useState(true);
|
||||
const { clusterId } = useParams<{ clusterId: string }>();
|
||||
const isStopStatus = React.useRef(true);
|
||||
const curPartitionList = React.useRef<IPartition[]>([]);
|
||||
const lastPartitionList = React.useRef<IPartition[]>([]);
|
||||
const [topicName, setTopicName] = React.useState<string>('');
|
||||
const [partitionLists, setPartitionLists] = React.useState([]);
|
||||
const [consumerGroupList, setConsumerGroupList] = React.useState([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
Utils.request(API.getTopicMetaData(+clusterId))
|
||||
.then((res: any) => {
|
||||
const topics = (res || []).map((item: any) => {
|
||||
return {
|
||||
label: item.topicName,
|
||||
value: item.topicName,
|
||||
};
|
||||
});
|
||||
setTopicMetaData(topics);
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error(err);
|
||||
});
|
||||
return () => {
|
||||
stop();
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (topicName) {
|
||||
Utils.request(API.getConsumerGroup(topicName, +clusterId))
|
||||
.then((res: any) => {
|
||||
const consumers = (res || []).map((item: any) => {
|
||||
return {
|
||||
label: item.groupName,
|
||||
value: item.groupName,
|
||||
};
|
||||
});
|
||||
setConsumerGroupList(consumers);
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error(err);
|
||||
});
|
||||
|
||||
Utils.request(API.getTopicsMetaData(topicName, +clusterId))
|
||||
.then((res: any) => {
|
||||
const partitionLists = (res?.partitionIdList || []).map((item: any) => {
|
||||
return {
|
||||
label: item,
|
||||
value: item,
|
||||
};
|
||||
});
|
||||
setPartitionLists(partitionLists);
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error(err);
|
||||
});
|
||||
}
|
||||
}, [topicName]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// TODO: 临时修改,tableData 长度为 0 时,直接退出
|
||||
if (tableData.length !== 0) {
|
||||
const res = tableData.map((item, index) => {
|
||||
//Partition,offset,Timestamp,key,value,Header
|
||||
return {
|
||||
...item,
|
||||
Partition: item.Partition,
|
||||
offset: item.offset,
|
||||
Timestamp: item.Timestamp,
|
||||
key: index + '_' + item.offset,
|
||||
keydata: item.key,
|
||||
};
|
||||
});
|
||||
setTableDataRes(res);
|
||||
}
|
||||
}, [tableData]);
|
||||
|
||||
const onHandleValuesChange = (value: any, allValues: any) => {
|
||||
Object.keys(value).forEach((key) => {
|
||||
let changeValue = null;
|
||||
switch (key) {
|
||||
case 'topic':
|
||||
AppContainer.eventBus.emit('ConsumeTopicChange', value[key]);
|
||||
setTopicName(value[key]);
|
||||
break;
|
||||
case 'start':
|
||||
changeValue = value[key][1];
|
||||
setConfigInfo({
|
||||
...configInfo,
|
||||
needStartFromDate: changeValue === 'a specific date',
|
||||
needConsumerGroup: changeValue === 'a consumer group',
|
||||
needOffset: changeValue === 'latest x offsets' || changeValue === 'an offset',
|
||||
needOffsetMax: changeValue === 'latest x offsets',
|
||||
needPartitionList: changeValue === 'an offset',
|
||||
});
|
||||
break;
|
||||
case 'until':
|
||||
changeValue = value[key];
|
||||
setConfigInfo({
|
||||
...configInfo,
|
||||
needMsgNum: changeValue === 'number of messages' || changeValue === 'number of messages per partition',
|
||||
needMsgSize: changeValue === 'max size per partition' || changeValue === 'max size',
|
||||
needUntilDate: changeValue === 'timestamp',
|
||||
});
|
||||
break;
|
||||
case 'filter':
|
||||
changeValue = value[key];
|
||||
setConfigInfo({
|
||||
...configInfo,
|
||||
needFilterKeyValue: changeValue === 1 || changeValue === 2,
|
||||
needFilterSize: changeValue === 3 || changeValue === 4 || changeValue === 5,
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const loopUntilTask = (values: any, res: any) => {
|
||||
const { until, untilDate, untilMsgNum, unitMsgSize } = values;
|
||||
const currentTime = new Date().getTime();
|
||||
const partitionConsumedList: IPartition[] = res.partitionConsumedList || [];
|
||||
lastPartitionList.current = res.partitionConsumedList || [];
|
||||
const _partitionList = curPartitionList.current.length ? Array.from(curPartitionList.current) : Array.from(partitionConsumedList);
|
||||
|
||||
for (const item of partitionConsumedList) {
|
||||
const index = _partitionList.findIndex((row) => row.partitionId === item.partitionId);
|
||||
if (index < 0) {
|
||||
_partitionList.push({
|
||||
...item,
|
||||
}); // 不存在则加入
|
||||
} else {
|
||||
// 存在则累加
|
||||
_partitionList[index].recordCount = _partitionList[index].recordCount + item.recordCount;
|
||||
_partitionList[index].recordSizeUnitB = _partitionList[index].recordSizeUnitB + item.recordSizeUnitB;
|
||||
_partitionList.splice(index, 1, item);
|
||||
}
|
||||
}
|
||||
const processList = _partitionList.map((row) => ({
|
||||
label: `P${row.partitionId}`,
|
||||
totalNumber: row.logEndOffset,
|
||||
currentNumber: row.consumedOffset,
|
||||
key: row.partitionId,
|
||||
}));
|
||||
setPartitionProcess(processList as any); // 用于进度条渲染
|
||||
partitionProcessRef.current = processList;
|
||||
|
||||
curPartitionList.current = _partitionList;
|
||||
|
||||
switch (until) {
|
||||
case 'timestamp':
|
||||
setIsStop(currentTime >= untilDate);
|
||||
isStopStatus.current = currentTime >= untilDate;
|
||||
break;
|
||||
case 'number of messages':
|
||||
setIsStop(+recordCountCur.current >= untilMsgNum);
|
||||
isStopStatus.current = +recordCountCur.current >= untilMsgNum;
|
||||
break;
|
||||
case 'number of messages per partition': // 所有分区都达到了设定值
|
||||
// 过滤出消费数量不足设定值的partition
|
||||
const filtersPartition = _partitionList.filter((item: any) => item.recordCount < untilMsgNum);
|
||||
curPartitionList.current = filtersPartition; // 用作下一次请求的入参
|
||||
setIsStop(filtersPartition.length < 1);
|
||||
isStopStatus.current = filtersPartition.length < 1;
|
||||
break;
|
||||
case 'max size':
|
||||
setIsStop(+recordSizeCur.current >= unitMsgSize);
|
||||
isStopStatus.current = +recordSizeCur.current >= unitMsgSize;
|
||||
break;
|
||||
case 'max size per partition':
|
||||
// 过滤出消费size不足设定值的partition
|
||||
const filters = partitionConsumedList.filter((item: any) => item.recordSizeUnitB < unitMsgSize);
|
||||
setIsStop(filters.length < 1);
|
||||
isStopStatus.current = filters.length < 1;
|
||||
curPartitionList.current = filters;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const runClientConsumer = (values: any, params: any = {}, isFirst = false) => {
|
||||
if (isStopStatus.current && !isFirst) return;
|
||||
const offsetLists = [
|
||||
{
|
||||
offset: values?.offset,
|
||||
partitionId: values?.partitionId,
|
||||
},
|
||||
];
|
||||
const _params = {
|
||||
clusterId,
|
||||
maxDurationUnitMs: 8000, // 前端超时时间为10s,这里接口超时设置8s
|
||||
clientProperties: {},
|
||||
maxRecords: 10,
|
||||
topicName: values.topic,
|
||||
recordOperate: isFirst,
|
||||
filter: {
|
||||
filterCompareKey: values.filterKey,
|
||||
filterCompareSizeUnitB: values.filterSize,
|
||||
filterCompareValue: values.filterValue,
|
||||
filterType: values.filter,
|
||||
},
|
||||
startFrom: {
|
||||
startFromType: isFirst ? startFromMap[values.start[1]].type : 3,
|
||||
consumerGroup: startFromMap[values.start[1]].type === 4 ? values.consumerGroup : undefined,
|
||||
latestMinusX: startFromMap[values.start[1]].type === 5 ? values.offset : undefined,
|
||||
offsetList: isFirst
|
||||
? startFromMap[values.start[1]].type === 3
|
||||
? offsetLists
|
||||
: undefined
|
||||
: lastPartitionList.current?.map((item) => ({
|
||||
offset: item.consumedOffset,
|
||||
partitionId: item.partitionId,
|
||||
})),
|
||||
timestampUnitMs:
|
||||
values.start[1] === 'a specific date'
|
||||
? startFromMap[values.start[1]].getDate(values.startDate)
|
||||
: startFromMap[values.start[1]].getDate && startFromMap[values.start[1]].getDate(),
|
||||
},
|
||||
...params,
|
||||
};
|
||||
|
||||
if (!isStopStatus.current) {
|
||||
Utils.post(API.postClientConsumer, _params)
|
||||
.then((res: any) => {
|
||||
// 累计
|
||||
recordCountCur.current = recordCountCur.current + res.totalRecordCount;
|
||||
recordSizeCur.current = recordSizeCur.current + res.totalRecordSizeUnitB;
|
||||
|
||||
setTableData(res.recordList || []); // TODO:这里累加是不是需要按partition进行累加
|
||||
loopUntilTask(values, res);
|
||||
if (!isStopStatus.current) {
|
||||
runClientConsumer(values, {
|
||||
partitionConsumedList: curPartitionList.current,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsStop(true);
|
||||
isStopStatus.current = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const initValues = () => {
|
||||
curPartitionList.current = [];
|
||||
lastPartitionList.current = [];
|
||||
setPartitionProcess([]);
|
||||
partitionProcessRef.current = [];
|
||||
// setRecordCount(0);
|
||||
recordCountCur.current = 0;
|
||||
// setRecordSize(0);
|
||||
recordSizeCur.current = 0;
|
||||
};
|
||||
|
||||
const clearForm = () => {
|
||||
initValues();
|
||||
setTableData([]);
|
||||
setIsStop(true);
|
||||
isStopStatus.current = true;
|
||||
setConfigInfo({});
|
||||
AppContainer.eventBus.emit('ConsumeTopicChange', '');
|
||||
formRef.current.resetFields();
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
const values = await formRef.current.validateFields();
|
||||
initValues();
|
||||
setIsStop(false);
|
||||
isStopStatus.current = false;
|
||||
setTableDataRes([]);
|
||||
runClientConsumer(values, {}, true);
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
setIsStop(true);
|
||||
isStopStatus.current = true;
|
||||
};
|
||||
|
||||
const download = () => {
|
||||
let str = `Partition,offset,Timestamp,key,value,Header,\n`;
|
||||
const tableRes: any[] = tableData.map((item, index) => {
|
||||
return {
|
||||
Partition: item.partitionId,
|
||||
offset: item.offset,
|
||||
Timestamp: moment(item.timestampUnitMs).format(timeFormat),
|
||||
key: item.key || '-',
|
||||
value: item.value,
|
||||
Header: item.headerList?.join('、'),
|
||||
};
|
||||
});
|
||||
// 增加\t为了不让表格显示科学计数法或者其他格式
|
||||
for (let i = 0; i < tableRes.length; i++) {
|
||||
// for(let item in tableRes[i]){
|
||||
// str += `${tableRes[i][item] + '\t'}${i < (Object.keys(tableRes[i]).length-1) ? ',' : ''}`;
|
||||
// }
|
||||
const keyArrs = Object.keys(tableRes[i]);
|
||||
for (let j = 0; j < keyArrs.length; j++) {
|
||||
const item = keyArrs[j];
|
||||
str += `${tableRes[i][item] + '\t'},`;
|
||||
}
|
||||
str += '\n';
|
||||
}
|
||||
|
||||
let uri = 'data:text/csv;charset=utf-8,\ufeff' + encodeURIComponent(str);
|
||||
|
||||
var link = document.createElement('a');
|
||||
link.href = uri;
|
||||
|
||||
link.download = `consume-client-test${new Date().getTime()}.csv`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
const TableHeaderInfo = (
|
||||
<div className="consume-table-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
|
||||
<div>
|
||||
<span className="title">消费详情</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="info">
|
||||
消息条数 <span className="num">{recordCountCur.current}</span> 条
|
||||
</span>
|
||||
<span className="info" style={{ marginLeft: 10 }}>
|
||||
消息大小 <span className="num">{Utils.getSizeAndUnit(recordSizeCur.current).value}</span>{' '}
|
||||
{Utils.getSizeAndUnit(recordSizeCur.current, 'B').unit}
|
||||
</span>
|
||||
<Divider type="vertical" />
|
||||
<span className="info info-download" onClick={download}>
|
||||
<DownloadOutlined style={{ marginRight: 6 }} size={12} color="#74788D" />
|
||||
{/* <IconFont type="icon-xiazai" /> */}
|
||||
<span>下载</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const processTitle = () => {
|
||||
return (
|
||||
<>
|
||||
<span className="">
|
||||
offset{' '}
|
||||
<Tooltip title="每个partition消费的offset信息">
|
||||
<IconFont type="icon-zhushi" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="client-test-panel">
|
||||
<ConfigForm
|
||||
title="消费配置"
|
||||
formConfig={getFormConfig(topicMetaData, configInfo, partitionLists, consumerGroupList)}
|
||||
formData={{}}
|
||||
formRef={formRef}
|
||||
onHandleValuesChange={onHandleValuesChange}
|
||||
clearForm={clearForm}
|
||||
submit={run}
|
||||
running={!isStop}
|
||||
stop={stop}
|
||||
/>
|
||||
<TestResult
|
||||
showProcessList={true}
|
||||
processTitle={processTitle()}
|
||||
processList={partitionProcess}
|
||||
tableTitle={TableHeaderInfo}
|
||||
tableProps={{
|
||||
columns: getTableColumns({ isShowControl: global.isShowControl }),
|
||||
dataSource: tableDataRes,
|
||||
lineFillColor: true,
|
||||
rowKey: 'offset',
|
||||
showHeader: false,
|
||||
attrs: {
|
||||
id: 'timestampUnitMs',
|
||||
},
|
||||
}}
|
||||
showCardList={false}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConsumeClientTest;
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Button, XForm } from 'knowdesign';
|
||||
import { IFormItem } from 'knowdesign/lib/extend/x-form';
|
||||
import * as React from 'react';
|
||||
import './style/form.less';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const prefixTesting = 'config-form-panel-consume';
|
||||
interface IProps {
|
||||
formConfig: IFormItem[];
|
||||
formData: any;
|
||||
formRef: any;
|
||||
onHandleValuesChange?: any;
|
||||
title: string;
|
||||
customContent?: React.ReactNode;
|
||||
customForm?: React.ReactNode;
|
||||
clearForm: any;
|
||||
submit: any;
|
||||
stop: any;
|
||||
running?: boolean;
|
||||
}
|
||||
|
||||
const ConfigForm = (props: IProps): JSX.Element => {
|
||||
const { formConfig, formRef, formData, onHandleValuesChange, title, clearForm, submit, stop, running, customContent, customForm } = props;
|
||||
const intl = useIntl();
|
||||
const onSubmit = () => {
|
||||
running ? stop() : submit();
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className={prefixTesting}>
|
||||
<div className={`${prefixTesting}-title`}>{title}</div>
|
||||
<div className={`${prefixTesting}-content`}>
|
||||
<div>{customContent}</div>
|
||||
{customForm ? (
|
||||
customForm
|
||||
) : (
|
||||
<XForm
|
||||
onHandleValuesChange={onHandleValuesChange}
|
||||
formData={formData}
|
||||
formMap={formConfig}
|
||||
wrappedComponentRef={formRef}
|
||||
layout={'vertical'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${prefixTesting}-footer-btn`}>
|
||||
<Button loading={running} onClick={clearForm}>
|
||||
{intl.formatMessage({ id: 'test.client.clear' })}
|
||||
</Button>
|
||||
<Button onClick={onSubmit} type="primary">
|
||||
{intl.formatMessage({ id: running ? 'test.client.stop' : 'test.client.run' })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigForm;
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Progress, Table, TableProps, Tooltip, ProTable } from 'knowdesign';
|
||||
import * as React from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import './style/result.less';
|
||||
|
||||
interface ICard {
|
||||
label: string;
|
||||
value: number;
|
||||
key: string;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
interface ITableCol {
|
||||
partition: number;
|
||||
offset: number;
|
||||
time?: string;
|
||||
timestamp: string;
|
||||
key?: number;
|
||||
value?: number;
|
||||
header?: string;
|
||||
}
|
||||
|
||||
interface IProcess {
|
||||
label: string;
|
||||
totalNumber: number;
|
||||
currentNumber: number;
|
||||
key: string;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
cardList?: ICard[];
|
||||
showCardList: boolean;
|
||||
processList?: IProcess[];
|
||||
processTitle?: React.ReactNode | string;
|
||||
showProcessList: boolean;
|
||||
tableTitle?: React.ReactNode;
|
||||
tableProps: any;
|
||||
}
|
||||
|
||||
const getValueWithUnit = (value: number) => {
|
||||
if (value === undefined || value === null) return '-';
|
||||
|
||||
if (value < 10000) {
|
||||
return Number.isInteger(value) ? value : value.toFixed(2) + '';
|
||||
}
|
||||
// 9999万
|
||||
if (value < 99990000) {
|
||||
return (value / 10000).toFixed(2) + '万';
|
||||
}
|
||||
|
||||
if (value < 999999990000) {
|
||||
return (value / 10000 / 10000).toFixed(2) + '亿';
|
||||
}
|
||||
|
||||
return (value / (10000 * 10000 * 1000)).toFixed(2) + '千亿';
|
||||
};
|
||||
|
||||
const TestResult = (props: IProps): JSX.Element => {
|
||||
const intl = useIntl();
|
||||
const { cardList, showCardList, processList, showProcessList, processTitle, tableTitle, tableProps } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="test-result-panel">
|
||||
<div className="page-title">{intl.formatMessage({ id: 'test.result' })}</div>
|
||||
{showCardList && (
|
||||
<div className="card-panel">
|
||||
{cardList?.map((item, index) => (
|
||||
<div className="card-item" key={`card-${index}`}>
|
||||
<h4 className="label">{item.label}</h4>
|
||||
<div className="value">
|
||||
{item.value ?? '-'}
|
||||
<span className="unit">{item.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{showProcessList && processList.length > 0 && (
|
||||
<div className="process-panel">
|
||||
{
|
||||
<>
|
||||
<div className="title">{processTitle}</div>
|
||||
{processList?.map((item, index) => (
|
||||
<div className="process-item" key={`process-${index}`}>
|
||||
<Tooltip title={`${item.currentNumber}/${item.totalNumber}`}>
|
||||
<div className="content">
|
||||
<span className="label">{item.label}</span>
|
||||
<span className="info">{`${getValueWithUnit(item.currentNumber)}/${getValueWithUnit(item.totalNumber)}`}</span>
|
||||
</div>
|
||||
<Progress
|
||||
className="value"
|
||||
percent={item.totalNumber ? +((item.currentNumber / item.totalNumber) * 100) : 0}
|
||||
showInfo={false}
|
||||
trailColor={'#ECECF1'}
|
||||
strokeColor={'#556EE6'}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
<div className="table-panel">
|
||||
{tableTitle ? <div className="table-title">{tableTitle}</div> : null}
|
||||
<ProTable tableProps={tableProps} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestResult;
|
||||
@@ -0,0 +1,264 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { LeftOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import { Button, Tabs } from 'knowdesign';
|
||||
import _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import './style/tabs.less';
|
||||
|
||||
interface ITab {
|
||||
label: string;
|
||||
tabpane: any;
|
||||
key: string;
|
||||
closable?: boolean;
|
||||
}
|
||||
|
||||
interface IPagation {
|
||||
pageSize: number;
|
||||
total: number;
|
||||
pageNo: number;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
title?: string;
|
||||
initial: ITab;
|
||||
}
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
const TaskTabs = React.forwardRef((props: IProps, ref): JSX.Element => {
|
||||
const intl = useIntl();
|
||||
const { initial } = props;
|
||||
|
||||
const [tabs, setTabs] = React.useState([{ ...initial }]);
|
||||
const [activeKey, setActiveKey] = React.useState('tab-1');
|
||||
const [pagination, setPagination] = React.useState<IPagation>({
|
||||
pageSize: 10,
|
||||
total: 1,
|
||||
pageNo: 1,
|
||||
});
|
||||
|
||||
const onClickArrow = _.throttle((type = '') => {
|
||||
const navWrap = document.querySelector('#tabs-list .dcloud-tabs-nav-wrap') as HTMLElement;
|
||||
const navList = document.querySelector('#tabs-list .dcloud-tabs-nav-list') as HTMLElement;
|
||||
const navTab = document.querySelector('#tabs-list .dcloud-tabs-tab') as HTMLElement;
|
||||
|
||||
const navWrapWidth = Number(navWrap.offsetWidth);
|
||||
const navListWidth = Number(navList.offsetWidth);
|
||||
const tagWidth = navTab ? Number(navTab.offsetWidth) + 12 : 139 + 12;
|
||||
const translateLen = Number((navList as any).style.transform.split('(')[1].split(',')[0].replace('px', ''));
|
||||
const navListScrollLeft = Number(translateLen * -1) + (pagination.pageNo === pagination.total ? 180 : 0);
|
||||
const navListScrollRight = Number(navListWidth - navWrapWidth - navListScrollLeft);
|
||||
const tabVW = tagWidth * pagination.pageSize;
|
||||
|
||||
if (type === 'left') {
|
||||
if (navListScrollLeft <= 0) {
|
||||
return;
|
||||
}
|
||||
if (navListScrollLeft > tabVW) {
|
||||
navList.style.transform = `translate(${translateLen + tabVW}px, 0)`;
|
||||
} else {
|
||||
navList.style.transform = `translate(0, 0)`;
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'right') {
|
||||
if (navListScrollRight <= 0) {
|
||||
return;
|
||||
}
|
||||
if (navListScrollRight >= tabVW) {
|
||||
navList.style.transform = `translate(${translateLen - tabVW}px, 0)`;
|
||||
} else {
|
||||
navList.style.transform = `translate(${navWrapWidth - navListWidth - 80}px, 0)`;
|
||||
}
|
||||
}
|
||||
|
||||
setPaginationAfterClick(type);
|
||||
}, 300);
|
||||
|
||||
const setPaginationAfterClick = (type: string) => {
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageNo: type === 'left' ? pagination.pageNo - 1 : pagination.pageNo + 1,
|
||||
});
|
||||
};
|
||||
|
||||
const countPagination = () => {
|
||||
const navWrap = document.querySelector('#tabs-list .dcloud-tabs-nav-wrap') as HTMLElement;
|
||||
const navList = document.querySelector('#tabs-list .dcloud-tabs-nav-list') as HTMLElement;
|
||||
const navWrapWidth = Number(navWrap?.offsetWidth);
|
||||
const navListWidth = Number(navList?.offsetWidth);
|
||||
const tabNum = (navWrapWidth / (139 + 12)) | 0; // 每页可展示的数量
|
||||
const index = tabs.findIndex((tab) => tab.key === activeKey); // 当前tab所在位置
|
||||
|
||||
if (navListWidth >= navWrapWidth) {
|
||||
setPagination({
|
||||
pageNo: Math.ceil((index + 1) / tabNum),
|
||||
pageSize: tabNum,
|
||||
total: Math.ceil(tabs.length / tabNum),
|
||||
});
|
||||
} else {
|
||||
setPagination({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
total: 1,
|
||||
});
|
||||
navList.style.transform = `translate(0, 0)`;
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
countPagination();
|
||||
const resize = _.throttle(() => {
|
||||
countPagination();
|
||||
}, 300);
|
||||
|
||||
const navWrap = document.querySelector('#tabs-list .dcloud-tabs-nav-wrap') as HTMLElement;
|
||||
const onMouseOverOrOut = _.throttle((event, type) => {
|
||||
if (
|
||||
!event.target?.className ||
|
||||
typeof event.target?.className !== 'string' ||
|
||||
!event.target.className.includes('dcloud-tabs-tab') ||
|
||||
event.target.className.includes('dcloud-tabs-tab-active')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target?.firstChild?.className === 'dcloud-tabs-tab-btn') {
|
||||
const keys = event.target.firstChild.id?.split('-') || [];
|
||||
const key = `tab-${keys[keys.length - 1]}`;
|
||||
let _tabs = [].concat(tabs);
|
||||
|
||||
_tabs = _tabs.map((tab) => {
|
||||
return {
|
||||
...tab,
|
||||
closable: tab.key === key ? type === 'over' : tab.key === activeKey,
|
||||
};
|
||||
});
|
||||
|
||||
setTabs([..._tabs]);
|
||||
}
|
||||
}, 300);
|
||||
|
||||
const onMouseOver = (event: any) => onMouseOverOrOut(event, 'over');
|
||||
const onMouseOut = (event: any) => onMouseOverOrOut(event, 'out');
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
navWrap.addEventListener('mouseover', onMouseOver);
|
||||
navWrap.addEventListener('mouseout', onMouseOut);
|
||||
return () => {
|
||||
navWrap.removeEventListener('mouseover', onMouseOver);
|
||||
navWrap.removeEventListener('mouseout', onMouseOut);
|
||||
window.removeEventListener('resize', resize);
|
||||
};
|
||||
}, [tabs, activeKey]);
|
||||
|
||||
const setTabsTitle = (title: string) => {
|
||||
const _tabs = [].concat(tabs);
|
||||
const index = _tabs.findIndex((item) => item.key === activeKey);
|
||||
|
||||
if (index > -1) {
|
||||
_tabs[index].label = `${title}`;
|
||||
setTabs(_tabs);
|
||||
}
|
||||
};
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
setTabsTitle,
|
||||
}));
|
||||
|
||||
const addTask = async () => {
|
||||
const _tabs = Array.from(tabs).map((tab) => ({
|
||||
...tab,
|
||||
closable: false,
|
||||
}));
|
||||
const lastKey = _tabs[_tabs.length - 1].key?.split('-')[1];
|
||||
await setTabs([..._tabs, { ...initial, key: `tab-${+lastKey + 1}`, closable: true }]);
|
||||
setActiveKey(`tab-${+lastKey + 1}`);
|
||||
};
|
||||
|
||||
const removeTask = async (targetKey: string) => {
|
||||
let _tabs = Array.from(tabs).filter((tab) => tab.key !== targetKey);
|
||||
|
||||
_tabs = _tabs.map((row, index) => ({
|
||||
...row,
|
||||
closable: _tabs.length !== 1 && index === _tabs.length - 1,
|
||||
}));
|
||||
|
||||
await setTabs([..._tabs]);
|
||||
// 设置最后一个tab为当前tab
|
||||
setActiveKey(_tabs[_tabs.length - 1].key);
|
||||
};
|
||||
|
||||
const onTabChange = async (key: string) => {
|
||||
const _tabs = [].concat(tabs).map((tab) => ({
|
||||
...tab,
|
||||
closable: tab.key === key,
|
||||
}));
|
||||
|
||||
await setTabs([..._tabs]);
|
||||
setActiveKey(key);
|
||||
};
|
||||
|
||||
const onTabScroll = ({ direction }: { direction: string }) => {
|
||||
};
|
||||
|
||||
const onEdit = (targetKey: string, action: string) => {
|
||||
if (action === 'remove') {
|
||||
removeTask(targetKey);
|
||||
}
|
||||
};
|
||||
|
||||
const renderTabOpsSlot = () => {
|
||||
return {
|
||||
left: (
|
||||
<Button className="add-task" onClick={addTask} type="dashed" icon={<PlusOutlined color="#74788D" />}>
|
||||
{intl.formatMessage({ id: 'add.task' })}
|
||||
</Button>
|
||||
),
|
||||
right: null as any,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="tabs-panel">
|
||||
<Tabs
|
||||
tabBarExtraContent={renderTabOpsSlot()}
|
||||
animated={false}
|
||||
onEdit={onEdit}
|
||||
hideAdd={true}
|
||||
type="editable-card"
|
||||
id="tabs-list"
|
||||
defaultActiveKey="tab-1"
|
||||
activeKey={activeKey}
|
||||
onChange={onTabChange}
|
||||
onTabScroll={onTabScroll}
|
||||
>
|
||||
{tabs.map((row) => (
|
||||
<TabPane tab={row.label} key={row.key} closable={row.closable}>
|
||||
{row.tabpane}
|
||||
</TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
{pagination.total > 1 && (
|
||||
<div className="tab-nav-right">
|
||||
<LeftOutlined
|
||||
className={`tab-nav-right-icon ${pagination.pageNo <= 1 ? 'disabled' : ''}`}
|
||||
onClick={() => onClickArrow('left')}
|
||||
/>
|
||||
<span className="tab-nav-right-text">{pagination.pageNo}</span>
|
||||
<span className="tab-nav-right-text-divider">/</span>
|
||||
<span className="tab-nav-right-text">{pagination.total}</span>
|
||||
<RightOutlined
|
||||
className={`tab-nav-right-icon ${pagination.pageNo >= pagination.total ? 'disabled' : ''}`}
|
||||
onClick={() => onClickArrow('right')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default TaskTabs;
|
||||
@@ -0,0 +1,37 @@
|
||||
.config-form-panel-consume {
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
margin-right: 12px;
|
||||
padding: 16px 24px 60px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.01), 0 3px 6px 3px rgba(0, 0, 0, 0.01), 0 2px 6px 0 rgba(0, 0, 0, 0.03);
|
||||
border-radius: 12px 12px 0 0;
|
||||
position: relative;
|
||||
.dcloud-row {
|
||||
display: block;
|
||||
}
|
||||
&-title {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 18px;
|
||||
color: #212529;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 20px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
&-footer-btn {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
width: 240px;
|
||||
text-align: right;
|
||||
border-top: 1px solid #EBEDEF;
|
||||
padding-top: 5px;
|
||||
|
||||
button+button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
.test-result-panel {
|
||||
flex: 1;
|
||||
height: calc(100vh - 180px);
|
||||
padding: 16px 24px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.01), 0 3px 6px 3px rgba(0, 0, 0, 0.01), 0 2px 6px 0 rgba(0, 0, 0, 0.03);
|
||||
border-radius: 12px 12px 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 156px);
|
||||
.page-title {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 18px;
|
||||
color: #212529;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-panel {
|
||||
background: #f8f9fe;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.card-item {
|
||||
height: 56px;
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #495057;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-family: DIDIFD-Medium;
|
||||
font-size: 32px;
|
||||
color: #212529;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: 16px;
|
||||
color: #74788D;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 14px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.process-panel {
|
||||
margin-bottom: 32px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
.title {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 14px;
|
||||
color: #495057;
|
||||
letter-spacing: 0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.process-item:not(:last-child) {
|
||||
margin-right: 55px;
|
||||
}
|
||||
|
||||
.process-item {
|
||||
width: 140px;
|
||||
height: 25px;
|
||||
display: inline-block;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
line-height: 16px;
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
color: #74788D;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: 10px;
|
||||
color: #ADB5BC;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-progress-inner {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.dcloud-progress-status-success .dcloud-progress-bg {
|
||||
background-color: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
.table-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 13px;
|
||||
|
||||
.consume-table-header{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title{
|
||||
font-family: @font-family-bold;
|
||||
font-size: 14px;
|
||||
color: #495057;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: 12px;
|
||||
color: #74788D;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.num {
|
||||
font-size: 14px;
|
||||
color: #212529;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.info-download{
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.pro-table-container {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
.tabs-panel {
|
||||
margin: 10px 0 0;
|
||||
position: relative;
|
||||
|
||||
.dcloud-tabs-nav {
|
||||
height: 50px;
|
||||
background: rgba(33, 37, 41, 0.04);
|
||||
border-radius: 8px;
|
||||
line-height: 50px;
|
||||
padding: 0px 100px 0 12px;
|
||||
margin-bottom: 12px;
|
||||
.dcloud-tabs-tab {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-task {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.dcloud-btn-dashed {
|
||||
border-color: #74788D;
|
||||
background: none;
|
||||
border-radius: 6px;
|
||||
border-style: dashed;
|
||||
font-size: 12px;
|
||||
color: #74788D;
|
||||
letter-spacing: 0;
|
||||
height: 30px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.dcloud-btn-dashed:hover,
|
||||
.dcloud-btn-dashed:focus,
|
||||
.dcloud-btn-dashed:active {
|
||||
color: #556EE6;
|
||||
border-color: #556EE6;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.dcloud-btn>.anticon+span,
|
||||
.dcloud-btn>span+.anticon {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.dcloud-tabs-card.dcloud-tabs-top>.dcloud-tabs-nav .dcloud-tabs-tab+.dcloud-tabs-tab,
|
||||
.dcloud-tabs-card.dcloud-tabs-bottom>.dcloud-tabs-nav .dcloud-tabs-tab+.dcloud-tabs-tab,
|
||||
.dcloud-tabs-card.dcloud-tabs-top>div>.dcloud-tabs-nav .dcloud-tabs-tab+.dcloud-tabs-tab,
|
||||
.dcloud-tabs-card.dcloud-tabs-bottom>div>.dcloud-tabs-nav .dcloud-tabs-tab+.dcloud-tabs-tab {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.dcloud-tabs-card.dcloud-tabs-top>.dcloud-tabs-nav .dcloud-tabs-tab,
|
||||
.dcloud-tabs-card.dcloud-tabs-top>div>.dcloud-tabs-nav .dcloud-tabs-tab {
|
||||
padding: 0px 10px;
|
||||
border-radius: 6px;
|
||||
background: #d7d7e2;
|
||||
height: 30px;
|
||||
margin-top: 10px;
|
||||
border: none;
|
||||
transition: none;
|
||||
width: 139px;
|
||||
justify-content: space-between;
|
||||
|
||||
.dcloud-tabs-tab-btn {
|
||||
outline: none;
|
||||
transition: none;
|
||||
font-size: 12px;
|
||||
color: #74788D;
|
||||
letter-spacing: 0;
|
||||
width: 120px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-tabs-card.dcloud-tabs-top>.dcloud-tabs-nav .dcloud-tabs-tab-active,
|
||||
.dcloud-tabs-card.dcloud-tabs-top>div>.dcloud-tabs-nav .dcloud-tabs-tab-active {
|
||||
background: #556EE6;
|
||||
opacity: 1;
|
||||
|
||||
.dcloud-tabs-tab-btn {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dcloud-tabs-tab-remove {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-tabs>.dcloud-tabs-nav .dcloud-tabs-nav-operations,
|
||||
.dcloud-tabs>div>.dcloud-tabs-nav .dcloud-tabs-nav-operations {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dcloud-tabs-extra-content {
|
||||
|
||||
.tab-nav-right {
|
||||
text-align: center;
|
||||
padding-left: 14px;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
|
||||
&-icon {
|
||||
color: #495057;
|
||||
font-size: 7px;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&-text {
|
||||
font-size: 11px;
|
||||
color: #495057;
|
||||
text-align: center;
|
||||
line-height: 16px;
|
||||
padding: 0px 6px;
|
||||
|
||||
&-divider {
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-nav-right {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
background: none;
|
||||
background-image: linear-gradient(90deg, rgba(226, 226, 235, 0.00) 0%, #E2E2EB 29%);
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
padding: 0px 20px;
|
||||
border-radius: 8px;
|
||||
|
||||
&-icon {
|
||||
color: #495057;
|
||||
font-size: 7px;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&-text {
|
||||
font-size: 11px;
|
||||
color: #495057;
|
||||
text-align: center;
|
||||
line-height: 16px;
|
||||
padding: 0px 6px;
|
||||
|
||||
&-divider {
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
import { FormItemType, IFormItem } from 'knowdesign/lib/extend/x-form';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { timeFormat } from '../../constants/common';
|
||||
import { Select } from 'knowdesign';
|
||||
import { ControlStatusMap } from '../CommonRoute';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
export const cardList = [
|
||||
{ label: 'Records Average', value: 1, key: 'string', unit: 'B' },
|
||||
{ label: 'Fetch Requests', value: 1, key: 'string', unit: 'B' },
|
||||
{ label: 'Fetch Size Average', value: 1, key: 'string', unit: 'B' },
|
||||
{ label: 'Fetch Letency Average', value: 1, key: 'string', unit: 'B' },
|
||||
];
|
||||
|
||||
export const filterList = [
|
||||
{
|
||||
label: 'none',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: 'contains',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: 'does not contains',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
label: 'equals',
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
label: 'Above Size',
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
label: 'Under Size',
|
||||
value: 5,
|
||||
},
|
||||
];
|
||||
|
||||
export const untilList = [
|
||||
{
|
||||
label: 'forever',
|
||||
value: 'forever',
|
||||
},
|
||||
{
|
||||
label: 'number of messages',
|
||||
value: 'number of messages',
|
||||
},
|
||||
{
|
||||
label: 'number of messages per partition',
|
||||
value: 'number of messages per partition',
|
||||
},
|
||||
{
|
||||
label: 'max size',
|
||||
value: 'max size',
|
||||
},
|
||||
{
|
||||
label: 'max size per partition',
|
||||
value: 'max size per partition',
|
||||
},
|
||||
{
|
||||
label: 'timestamp',
|
||||
value: 'timestamp',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* LATEST(0, "最新位置开始消费"): now(latest)
|
||||
* EARLIEST(1, "最旧位置开始消费"): the beginning
|
||||
* PRECISE_TIMESTAMP(2, "指定时间开始消费"): last hour、today、yesterday、a specific date
|
||||
* PRECISE_OFFSET(3, "指定位置开始消费"),
|
||||
* CONSUMER_GROUP(4, "指定消费组进行消费"),
|
||||
* LATEST_MINUS_X_OFFSET(5, "近X条数据开始消费"),
|
||||
*/
|
||||
|
||||
export const startFromMap = {
|
||||
'an offset': {
|
||||
type: 3,
|
||||
},
|
||||
'now(latest)': {
|
||||
type: 0,
|
||||
},
|
||||
today: {
|
||||
type: 2,
|
||||
getDate: () => new Date(new Date().setHours(0, 0, 0, 0)).getTime(),
|
||||
},
|
||||
'last hour': {
|
||||
type: 2,
|
||||
getDate: () => new Date().getTime() - 60 * 60 * 1000,
|
||||
},
|
||||
yesterday: {
|
||||
type: 2,
|
||||
getDate: () => new Date().getTime() - 24 * 60 * 60 * 1000,
|
||||
},
|
||||
'a specific date': {
|
||||
type: 2,
|
||||
getDate: (date: any) => new Date(date).getTime(),
|
||||
},
|
||||
'the beginning': {
|
||||
type: 1,
|
||||
},
|
||||
'a consumer group': {
|
||||
type: 4,
|
||||
},
|
||||
'latest x offsets': {
|
||||
type: 5,
|
||||
},
|
||||
} as any;
|
||||
|
||||
const startFromOptions = [
|
||||
{
|
||||
value: 'Time Base',
|
||||
label: 'Time Base',
|
||||
children: [
|
||||
{
|
||||
value: 'now(latest)',
|
||||
label: 'now(latest)',
|
||||
},
|
||||
{
|
||||
value: 'last hour',
|
||||
label: 'last hour',
|
||||
},
|
||||
{
|
||||
value: 'today',
|
||||
label: 'today',
|
||||
},
|
||||
{
|
||||
value: 'yesterday',
|
||||
label: 'yesterday',
|
||||
},
|
||||
{
|
||||
value: 'the beginning',
|
||||
label: 'the beginning',
|
||||
},
|
||||
{
|
||||
value: 'a specific date',
|
||||
label: 'a specific date',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'Offset based',
|
||||
label: 'Offset based',
|
||||
children: [
|
||||
{
|
||||
value: 'a consumer group',
|
||||
label: 'a consumer group',
|
||||
},
|
||||
{
|
||||
value: 'latest x offsets',
|
||||
label: 'latest x offsets',
|
||||
},
|
||||
{
|
||||
value: 'an offset',
|
||||
label: 'an offset',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const getFormConfig = (topicMetaData: any, info = {} as any, partitionLists: any, consumerGroupList: any) => {
|
||||
return [
|
||||
{
|
||||
key: 'topic',
|
||||
label: 'Topic',
|
||||
type: FormItemType.select,
|
||||
attrs: {
|
||||
placeholder: '请选择 Topic',
|
||||
showSearch: true,
|
||||
filterOption: (input: string, option: any) => option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0,
|
||||
},
|
||||
rules: [{ required: true, message: '请选择Topic' }],
|
||||
options: topicMetaData,
|
||||
},
|
||||
{
|
||||
key: 'start',
|
||||
label: 'Start From',
|
||||
attrs: {
|
||||
placeholder: '请选择',
|
||||
},
|
||||
type: FormItemType.cascader,
|
||||
options: startFromOptions,
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择Start From',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'startDate',
|
||||
label: 'Date',
|
||||
type: FormItemType.datePicker,
|
||||
invisible: !info?.needStartFromDate,
|
||||
attrs: {
|
||||
showTime: true,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
required: info?.needStartFromDate,
|
||||
message: '请选择Date',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'consumerGroup',
|
||||
label: 'Consumer Group',
|
||||
type: FormItemType.select,
|
||||
options: consumerGroupList || [],
|
||||
invisible: !info?.needConsumerGroup,
|
||||
rules: [
|
||||
{
|
||||
required: info?.needConsumerGroup,
|
||||
message: '请选择Consumer Group',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'partitionId',
|
||||
label: 'Partition',
|
||||
type: FormItemType.select,
|
||||
options: partitionLists || [],
|
||||
colSpan: 12,
|
||||
invisible: !info?.needPartitionList,
|
||||
rules: [
|
||||
{
|
||||
required: info?.needPartitionList,
|
||||
message: '请选择Partition',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'offset',
|
||||
label: 'Offset',
|
||||
type: FormItemType.inputNumber,
|
||||
colSpan: 12,
|
||||
attrs: {
|
||||
min: 1,
|
||||
max: info?.needOffsetMax ? 99999 : Math.pow(2, 53) - 1,
|
||||
},
|
||||
invisible: !info?.needOffset,
|
||||
rules: [
|
||||
{
|
||||
required: info?.needOffset,
|
||||
message: '请输入Offset',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'until',
|
||||
label: 'Until',
|
||||
type: FormItemType.select,
|
||||
options: untilList,
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择Until',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'untilMsgNum',
|
||||
label: 'Message Number',
|
||||
type: FormItemType.inputNumber,
|
||||
attrs: {
|
||||
min: 1,
|
||||
},
|
||||
invisible: !info?.needMsgNum,
|
||||
rules: [
|
||||
{
|
||||
required: info?.needMsgNum,
|
||||
message: '请输入Message Number',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'unitMsgSize',
|
||||
label: 'Message Size',
|
||||
type: FormItemType.inputNumber,
|
||||
attrs: {
|
||||
min: 1,
|
||||
},
|
||||
invisible: !info?.needMsgSize,
|
||||
rules: [
|
||||
{
|
||||
required: info?.needMsgSize,
|
||||
message: '请输入Message Size',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'untilDate',
|
||||
label: 'Date',
|
||||
type: FormItemType.datePicker,
|
||||
invisible: !info?.needUntilDate,
|
||||
attrs: {
|
||||
showTime: true,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
required: info?.needUntilDate,
|
||||
message: '请选择Date',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'filter',
|
||||
label: 'Filter',
|
||||
type: FormItemType.select,
|
||||
options: filterList,
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择Filter',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'filterKey',
|
||||
label: 'Key',
|
||||
type: FormItemType.input,
|
||||
invisible: !info?.needFilterKeyValue,
|
||||
rules: [
|
||||
{
|
||||
required: info?.needFilterKeyValue,
|
||||
message: '请输入Key',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'filterValue',
|
||||
label: 'Value',
|
||||
type: FormItemType.input,
|
||||
invisible: !info?.needFilterKeyValue,
|
||||
rules: [
|
||||
{
|
||||
required: info?.needFilterKeyValue,
|
||||
message: '请输入Value',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'filterSize',
|
||||
label: 'Size',
|
||||
type: FormItemType.inputNumber,
|
||||
attrs: {
|
||||
min: 1,
|
||||
},
|
||||
invisible: !info?.needFilterSize,
|
||||
rules: [
|
||||
{
|
||||
required: info?.needFilterSize,
|
||||
message: '请输入Size',
|
||||
},
|
||||
],
|
||||
},
|
||||
] as unknown as IFormItem[];
|
||||
};
|
||||
|
||||
export const getTableColumns = (props: any) => {
|
||||
const { isShowControl } = props;
|
||||
const columns = [
|
||||
{
|
||||
title: 'Partition',
|
||||
dataIndex: 'partitionId',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: 'Offset',
|
||||
dataIndex: 'offset',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: 'Timestamp',
|
||||
dataIndex: 'timestampUnitMs',
|
||||
width: 180,
|
||||
render: (text: number) => {
|
||||
return text ? moment(text).format(timeFormat) : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Key',
|
||||
dataIndex: 'keydata',
|
||||
render: (text: string) => {
|
||||
return text || '-';
|
||||
},
|
||||
width: 180,
|
||||
needTooltip: true,
|
||||
lineClampTwo: true,
|
||||
},
|
||||
{
|
||||
title: 'Value',
|
||||
dataIndex: 'value',
|
||||
width: 180,
|
||||
needTooltip: true,
|
||||
lineClampTwo: true,
|
||||
},
|
||||
];
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
if (isShowControl && isShowControl(ControlStatusMap.TESTING_CONSUMER_HEADER)) {
|
||||
columns.push({
|
||||
title: 'Header',
|
||||
dataIndex: 'headerList',
|
||||
// render: (text: any) => (text ? text?.join('、') : '-'),
|
||||
render: (text: any) => {
|
||||
if (text && Array.isArray(text)) {
|
||||
const newText = text.map((item: any) => {
|
||||
try {
|
||||
return JSON.stringify(item);
|
||||
} catch (error) {
|
||||
return item;
|
||||
}
|
||||
});
|
||||
return newText?.join('、') || '-';
|
||||
}
|
||||
return text || '-';
|
||||
},
|
||||
width: 180,
|
||||
needTooltip: true,
|
||||
lineClampTwo: true,
|
||||
});
|
||||
}
|
||||
|
||||
return columns;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
.client-test-panel {
|
||||
display: flex;
|
||||
// padding: 12px 20px 0px 20px;
|
||||
// height: calc(100vh - 68px)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { AppContainer } from 'knowdesign';
|
||||
import DBreadcrumb from 'knowdesign/lib/extend/d-breadcrumb';
|
||||
import * as React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import TaskTabs from './component/TaskTabs';
|
||||
import ConsumeClientTest from './Consume';
|
||||
import './index.less';
|
||||
|
||||
const Consume = () => {
|
||||
const initial = {
|
||||
label: '消费',
|
||||
key: 'tab-1',
|
||||
closable: false,
|
||||
tabpane: <ConsumeClientTest />,
|
||||
};
|
||||
|
||||
const ref: any = React.useRef();
|
||||
const { clusterId } = useParams<{ clusterId: string }>();
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
React.useEffect(() => {
|
||||
AppContainer.eventBus.on('ConsumeTopicChange', (args: string) => {
|
||||
ref.current && ref.current.setTabsTitle && ref.current.setTabsTitle(`消费 ${args}`);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="breadcrumb">
|
||||
<DBreadcrumb
|
||||
breadcrumbs={[
|
||||
{ label: '多集群管理', aHref: '/' },
|
||||
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
|
||||
{ label: 'Consume', aHref: '' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<TaskTabs initial={initial} ref={ref} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Consume;
|
||||
@@ -0,0 +1,245 @@
|
||||
import { AppContainer, Form, message, Tabs, Utils } from 'knowdesign';
|
||||
import * as React from 'react';
|
||||
import ConfigForm from './component/ConfigFrom';
|
||||
import TestResult from '../TestingConsumer/component/Result';
|
||||
import API from '../../api';
|
||||
import { getFormConfig, getTableColumns, tabsConfig } from './config';
|
||||
import './index.less';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
const ProduceClientTest = () => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [form] = Form.useForm();
|
||||
const customFormRef: any = React.createRef();
|
||||
const [configInfo, setConfigInfo] = React.useState({});
|
||||
const [activeKey, setActiveKey] = React.useState(tabsConfig[0].name);
|
||||
const [tableData, setTableData] = React.useState([]);
|
||||
const [topicMetaData, setTopicMetaData] = React.useState([]);
|
||||
const [running, setRunning] = React.useState(false);
|
||||
const [isKeyOn, setIsKeyOn] = React.useState(true);
|
||||
const { clusterId } = useParams<{ clusterId: string }>();
|
||||
|
||||
const currentInterval = React.useRef(null);
|
||||
|
||||
let currentMsgNum = 0;
|
||||
let startTime = 0;
|
||||
|
||||
React.useEffect(() => {
|
||||
Utils.request(API.getTopicMetaData(+clusterId))
|
||||
.then((res: any) => {
|
||||
const filterRes = res.filter((item: any) => item.type !== 1);
|
||||
const topics = (filterRes || []).map((item: any) => {
|
||||
return {
|
||||
label: item.topicName,
|
||||
value: item.topicName,
|
||||
partitionIdList: item.partitionIdList,
|
||||
};
|
||||
});
|
||||
setTopicMetaData(topics);
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error(err);
|
||||
});
|
||||
|
||||
return () => {
|
||||
clearInterval();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const runProduceClient = (values: any, isFirst = false) => {
|
||||
// 记录每一次请求发送的条数
|
||||
currentMsgNum = currentMsgNum ? currentMsgNum + values.chunks : values.chunks;
|
||||
|
||||
const params = {
|
||||
clientProperties: {
|
||||
acks: values.acks,
|
||||
'compression.type': values.compressionType,
|
||||
},
|
||||
clusterId,
|
||||
partitionIdList: values.frocePartition,
|
||||
recordCount: values.chunks,
|
||||
recordHeader: values.recordHeader,
|
||||
recordKey: isKeyOn ? values.key || '' : undefined,
|
||||
recordOperate: isFirst,
|
||||
recordValue: values.value,
|
||||
topicName: values.topicName,
|
||||
};
|
||||
|
||||
Utils.post(API.postClientProducer, params)
|
||||
.then((res: any) => {
|
||||
if (isFirst) {
|
||||
startTime = new Date().getTime();
|
||||
}
|
||||
setTableData(res || []);
|
||||
if (values.producerMode === 'timed') {
|
||||
if (values.interval && !currentInterval.current) {
|
||||
// const randomValue = Math.random() < 0.5 ? -1 : 1;
|
||||
// const random = +values.jitter ? +values.interval + randomValue * (Math.random() * +values.jitter) : values.interval;
|
||||
const random = values.interval;
|
||||
|
||||
currentInterval.current = window.setInterval(() => {
|
||||
runProduceClient(values);
|
||||
}, random);
|
||||
}
|
||||
// 第一次接口返回到现在的时间大于运行时间 当前发送条数大于总条数停止请求
|
||||
const currentTime = new Date().getTime();
|
||||
// if (currentTime - startTime >= +values.elapsed || currentMsgNum >= values.messageProduced) {
|
||||
if (currentTime - startTime >= +values.elapsed) {
|
||||
clearInterval();
|
||||
setRunning(false);
|
||||
}
|
||||
} else {
|
||||
// manual 方式
|
||||
setRunning(false);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
// manual 方式
|
||||
if (values.producerMode !== 'timed') {
|
||||
setRunning(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
values.elapsed = values.elapsed * 60 * 1000;
|
||||
const data = customFormRef.current.getTableData();
|
||||
values.recordHeader = {};
|
||||
for (const item of data) {
|
||||
values.recordHeader[item.key] = item.value;
|
||||
}
|
||||
// 点击按钮重新定时器清空
|
||||
clearInterval();
|
||||
setRunning(true);
|
||||
runProduceClient(values, true);
|
||||
})
|
||||
.catch((error) => {
|
||||
const { values } = error;
|
||||
if (!values.chunks || (values.producerMode === 'timed' && (!values.interval || !values.elapsed))) {
|
||||
setActiveKey('Flow');
|
||||
}
|
||||
if (!values.topicName || !values.value) {
|
||||
setActiveKey('Data');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const clearInterval = () => {
|
||||
currentInterval.current && window.clearInterval(currentInterval.current);
|
||||
currentInterval.current = null;
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
currentInterval.current && window.clearInterval(currentInterval.current);
|
||||
setRunning(false);
|
||||
};
|
||||
|
||||
const onHandleValuesChange = (value: any, allValues: any) => {
|
||||
Object.keys(value).forEach((key) => {
|
||||
let changeValue: any = null;
|
||||
let partitionIdList = [];
|
||||
switch (key) {
|
||||
case 'producerMode':
|
||||
changeValue = value[key];
|
||||
setConfigInfo({
|
||||
...configInfo,
|
||||
needTimeOption: changeValue === 'timed',
|
||||
});
|
||||
break;
|
||||
case 'interval':
|
||||
changeValue = value[key];
|
||||
setConfigInfo({
|
||||
...configInfo,
|
||||
maxJitter: +value[key] - 1,
|
||||
});
|
||||
break;
|
||||
case 'topicName':
|
||||
changeValue = value[key] as string;
|
||||
AppContainer.eventBus.emit('ProduceTopicChange', value[key]);
|
||||
partitionIdList = topicMetaData.find((item) => item.label === changeValue)?.partitionIdList || [];
|
||||
partitionIdList = partitionIdList.map((item: number) => ({
|
||||
label: item,
|
||||
value: item,
|
||||
}));
|
||||
setConfigInfo({
|
||||
...configInfo,
|
||||
partitionIdList,
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const clearForm = () => {
|
||||
setConfigInfo({});
|
||||
clearInterval();
|
||||
setTableData([]);
|
||||
form.resetFields();
|
||||
AppContainer.eventBus.emit('ProduceTopicChange', '');
|
||||
setIsKeyOn(true);
|
||||
customFormRef.current.resetTable();
|
||||
};
|
||||
|
||||
const onTabChange = (key: string) => {
|
||||
setActiveKey(key);
|
||||
};
|
||||
|
||||
const onKeySwitchChange = (checked: boolean) => {
|
||||
setIsKeyOn(checked);
|
||||
};
|
||||
|
||||
const TabsContent = (
|
||||
<div className="form-tabs">
|
||||
<Tabs defaultActiveKey={tabsConfig[0].name} activeKey={activeKey} onChange={onTabChange}>
|
||||
{tabsConfig.map(({ name, control }) =>
|
||||
!control || (global.isShowControl && global.isShowControl(control)) ? <TabPane tab={name} key={name} /> : <></>
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div className="client-test-panel">
|
||||
<ConfigForm
|
||||
title="生产配置"
|
||||
customContent={TabsContent}
|
||||
customFormRef={customFormRef}
|
||||
activeKey={activeKey}
|
||||
formConfig={getFormConfig({
|
||||
topicMetaData,
|
||||
activeKey,
|
||||
configInfo,
|
||||
form,
|
||||
onKeySwitchChange,
|
||||
isKeyOn,
|
||||
isShowControl: global.isShowControl,
|
||||
})}
|
||||
formData={{}}
|
||||
form={form}
|
||||
onHandleValuesChange={onHandleValuesChange}
|
||||
clearForm={clearForm}
|
||||
submit={run}
|
||||
running={running}
|
||||
stop={stop}
|
||||
/>
|
||||
<TestResult
|
||||
showProcessList={false}
|
||||
tableProps={{
|
||||
scroll: { y: 600 },
|
||||
columns: getTableColumns(),
|
||||
dataSource: tableData,
|
||||
pagination: false,
|
||||
}}
|
||||
showCardList={false}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProduceClientTest;
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Button, Col, Form, Row } from 'knowdesign';
|
||||
import { FormItemType, handleFormItem, IFormItem, renderFormItem } from 'knowdesign/lib/extend/x-form';
|
||||
import * as React from 'react';
|
||||
import './style/form.less';
|
||||
import EditTable from './EditTable';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const prefixTesting = 'config-form-panel';
|
||||
interface IProps {
|
||||
formConfig: IFormItem[];
|
||||
formData: any;
|
||||
form: any;
|
||||
customFormRef: any;
|
||||
onHandleValuesChange?: any;
|
||||
title: string;
|
||||
customContent?: React.ReactNode;
|
||||
customForm?: React.ReactNode;
|
||||
clearForm: any;
|
||||
submit: any;
|
||||
stop: any;
|
||||
activeKey: string;
|
||||
running?: boolean;
|
||||
}
|
||||
|
||||
export const renderFormContent = ({ formMap, formData, layout, formLayout, formItemColSpan = 24 }: any) => {
|
||||
return (
|
||||
<Row gutter={10}>
|
||||
{formMap.map((formItem: IFormItem) => {
|
||||
const { initialValue = undefined, valuePropName } = handleFormItem(formItem, formData);
|
||||
if (formItem.type === FormItemType.text)
|
||||
return (
|
||||
<Col style={{ display: formItem.invisible ? 'none' : '' }} key={formItem.key} span={formItem.colSpan || formItemColSpan}>
|
||||
{layout === 'vertical' ? (
|
||||
<>
|
||||
<span className="dcloud-form-item-label" style={{ padding: '0 0 7px', display: 'block' }}>
|
||||
{formItem.label}
|
||||
</span>
|
||||
{(formItem as any).customFormItem ? (
|
||||
<span style={{ fontSize: '14px', padding: '0 0 16px', display: 'block' }}>{(formItem as any).customFormItem}</span>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<Row style={{ padding: '6px 0 10px' }}>
|
||||
<Col span={formLayout?.labelCol.span || 4} style={{ textAlign: 'right' }}>
|
||||
<span className="dcloud-form-item-label" style={{ padding: '0 10px 0 0', display: 'inline-block' }}>
|
||||
{formItem.label}:
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={formLayout?.wrapperCol.span || 20}>
|
||||
<span>{(formItem as any).customFormItem}</span>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
);
|
||||
return (
|
||||
<Col style={{ display: formItem.invisible ? 'none' : '' }} key={formItem.key} span={formItem.colSpan || formItemColSpan}>
|
||||
<Form.Item
|
||||
name={formItem.key}
|
||||
key={formItem.key}
|
||||
label={formItem.label}
|
||||
rules={formItem.rules || [{ required: false, message: '' }]}
|
||||
initialValue={initialValue}
|
||||
valuePropName={valuePropName}
|
||||
{...formItem.formAttrs}
|
||||
>
|
||||
{renderFormItem(formItem)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
const ConfigForm = (props: IProps): JSX.Element => {
|
||||
const {
|
||||
formConfig,
|
||||
activeKey,
|
||||
customFormRef,
|
||||
form,
|
||||
formData,
|
||||
onHandleValuesChange,
|
||||
title,
|
||||
clearForm,
|
||||
submit,
|
||||
stop,
|
||||
running,
|
||||
customContent,
|
||||
} = props;
|
||||
const intl = useIntl();
|
||||
|
||||
const onSubmit = () => {
|
||||
running ? stop() : submit();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={prefixTesting}>
|
||||
<div className={`${prefixTesting}-title`}>{title}</div>
|
||||
<div>{customContent}</div>
|
||||
<div className={`${prefixTesting}-content`}>
|
||||
<div style={{ display: activeKey === 'Header' ? '' : 'none' }}>
|
||||
<EditTable ref={customFormRef} />
|
||||
</div>
|
||||
<Form form={form} layout={'vertical'} onValuesChange={onHandleValuesChange}>
|
||||
{renderFormContent({ formMap: formConfig, formData, layout: 'vertical' })}
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<div className={`${prefixTesting}-footer-btn`}>
|
||||
<Button loading={running} onClick={clearForm}>
|
||||
{intl.formatMessage({ id: 'test.client.clear' })}
|
||||
</Button>
|
||||
<Button onClick={onSubmit} type="primary">
|
||||
{intl.formatMessage({ id: running ? 'test.client.stop' : 'test.client.run' })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigForm;
|
||||
@@ -0,0 +1,40 @@
|
||||
import { PicLeftOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { Button, Input, Tooltip } from 'knowdesign';
|
||||
import * as React from 'react';
|
||||
|
||||
interface IProps {
|
||||
value?: string;
|
||||
onChange?: any;
|
||||
tooltip?: string;
|
||||
}
|
||||
const CustomTextArea = (props: IProps) => {
|
||||
const { tooltip, value, onChange } = props;
|
||||
|
||||
const genRandomContent = () => {
|
||||
onChange && onChange(value.trim());
|
||||
};
|
||||
|
||||
const onInputChange = ({ target: { value } }: { target: { value: string } }) => {
|
||||
onChange && onChange(value.trim());
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<span>
|
||||
<Tooltip title={tooltip}>
|
||||
<QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
</span>
|
||||
<span>
|
||||
<Tooltip title={'生成随机内容'}>
|
||||
<PicLeftOutlined onClick={genRandomContent} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
<Input.TextArea value={value} onChange={onInputChange} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomTextArea;
|
||||
@@ -0,0 +1,217 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import React, { useState } from 'react';
|
||||
import { Table, Input, InputNumber, Popconfirm, Form, Typography, Button, message, IconFont } from 'knowdesign';
|
||||
import './style/edit-table.less';
|
||||
import { CheckOutlined, CloseOutlined, PlusSquareOutlined } from '@ant-design/icons';
|
||||
|
||||
const EditableCell = ({ editing, dataIndex, title, inputType, placeholder, record, index, children, ...restProps }: any) => {
|
||||
const inputNode =
|
||||
inputType === 'number' ? (
|
||||
<InputNumber style={{ width: '130px' }} autoComplete="off" placeholder={placeholder} />
|
||||
) : (
|
||||
<Input autoComplete="off" placeholder={placeholder} />
|
||||
);
|
||||
return (
|
||||
<td {...restProps}>
|
||||
{editing ? (
|
||||
<Form.Item
|
||||
name={dataIndex}
|
||||
style={{
|
||||
margin: 0,
|
||||
}}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `请输入!`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{inputNode}
|
||||
</Form.Item>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
||||
const EditTable = React.forwardRef((props: any, ref: any) => {
|
||||
const { colCustomConfigs } = props;
|
||||
const [form] = Form.useForm();
|
||||
const [data, setData] = useState([]);
|
||||
const [editingKey, setEditingKey] = useState(0);
|
||||
|
||||
const isEditing = (record: any) => record.editKey === editingKey;
|
||||
|
||||
const getTableData = () => {
|
||||
return data;
|
||||
};
|
||||
|
||||
const resetTable = () => {
|
||||
setData([]);
|
||||
};
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
getTableData,
|
||||
resetTable,
|
||||
}));
|
||||
|
||||
const edit = (record: any) => {
|
||||
form.setFieldsValue({
|
||||
key: '',
|
||||
value: '',
|
||||
...record,
|
||||
});
|
||||
setEditingKey(record.editKey);
|
||||
};
|
||||
const deleteRow = (record: any) => {
|
||||
const _data = data.filter((item) => item.editKey !== record.editKey);
|
||||
setData(_data);
|
||||
};
|
||||
|
||||
const cancel = (record: any) => {
|
||||
if (record.key === '') {
|
||||
const _data = data.filter((item) => item.editKey !== record.editKey);
|
||||
setData(_data);
|
||||
}
|
||||
setEditingKey(0);
|
||||
};
|
||||
|
||||
const add = () => {
|
||||
if (editingKey !== 0) {
|
||||
message.error('请先保存当前编辑项');
|
||||
return;
|
||||
}
|
||||
form.resetFields();
|
||||
const editKey = data.length ? data[data.length - 1].editKey + 1 : 1;
|
||||
setData([...data, { key: '', value: '', editKey }]);
|
||||
setEditingKey(editKey);
|
||||
};
|
||||
const save = async (editKey: number) => {
|
||||
try {
|
||||
const row = await form.validateFields();
|
||||
const newData = [...data];
|
||||
const index = newData.findIndex((item) => editKey === item.editKey);
|
||||
|
||||
if (index > -1) {
|
||||
const item = newData[index];
|
||||
newData.splice(index, 1, { ...item, ...row });
|
||||
setData(newData);
|
||||
setEditingKey(0);
|
||||
} else {
|
||||
newData.push(row);
|
||||
setData(newData);
|
||||
setEditingKey(0);
|
||||
}
|
||||
} catch (errInfo) {
|
||||
console.log('Validate Failed:', errInfo);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'key',
|
||||
dataIndex: 'key',
|
||||
width: 40,
|
||||
editable: true,
|
||||
},
|
||||
{
|
||||
title: 'value',
|
||||
dataIndex: 'value',
|
||||
width: 40,
|
||||
editable: true,
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'operation',
|
||||
width: 20,
|
||||
className: 'no-padding',
|
||||
render: (text: any, record: any) => {
|
||||
const editable = isEditing(record);
|
||||
return editable ? (
|
||||
<span>
|
||||
<Typography.Link
|
||||
onClick={() => save(record.editKey)}
|
||||
style={{
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
<CheckOutlined />
|
||||
</Typography.Link>
|
||||
{/* <Popconfirm title="确认取消?" onConfirm={() => cancel(record)}> */}
|
||||
<a>
|
||||
<CloseOutlined onClick={() => cancel(record)} />
|
||||
</a>
|
||||
{/* </Popconfirm> */}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<Typography.Link
|
||||
className="custom-typography"
|
||||
disabled={editingKey !== 0}
|
||||
style={{
|
||||
marginRight: 8,
|
||||
}}
|
||||
onClick={() => edit(record)}
|
||||
>
|
||||
<IconFont type={'icon-bianji1'} size={11} />
|
||||
</Typography.Link>
|
||||
<Typography.Link className="custom-typography" disabled={editingKey !== 0} onClick={() => deleteRow(record)}>
|
||||
<IconFont type={'icon-shanchu1'} size={11} />
|
||||
</Typography.Link>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mergedColumns = columns.map((col, index) => {
|
||||
if (!col.editable) {
|
||||
return col;
|
||||
}
|
||||
|
||||
return {
|
||||
...col,
|
||||
onCell: (record: any) =>
|
||||
Object.assign(
|
||||
{
|
||||
record,
|
||||
inputType: 'text',
|
||||
dataIndex: col.dataIndex,
|
||||
title: col.title,
|
||||
editing: isEditing(record),
|
||||
},
|
||||
colCustomConfigs?.[index]
|
||||
),
|
||||
title: colCustomConfigs?.[index]?.title || col.title,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="edit-table-form">
|
||||
<Form form={form} component={false}>
|
||||
<Table
|
||||
components={{
|
||||
body: {
|
||||
cell: EditableCell,
|
||||
},
|
||||
}}
|
||||
// scroll={{
|
||||
// x: true,
|
||||
// }}
|
||||
dataSource={data}
|
||||
columns={mergedColumns}
|
||||
rowClassName="editable-row"
|
||||
pagination={false}
|
||||
/>
|
||||
</Form>
|
||||
<div className="add-btn">
|
||||
<Button type="link" onClick={add}>
|
||||
<PlusSquareOutlined /> 新增条件
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default EditTable;
|
||||
@@ -0,0 +1,42 @@
|
||||
.edit-table-form {
|
||||
.dcloud-table-thead>tr>th:not(:last-child):not(.dcloud-table-selection-column):not(.dcloud-table-row-expand-icon-cell):not([colspan])::before {
|
||||
width: 0px;
|
||||
}
|
||||
table {
|
||||
table-layout: fixed !important;
|
||||
}
|
||||
.dcloud-table .dcloud-table-content .dcloud-table-cell {
|
||||
padding: 4px 12px;
|
||||
height: 36px;
|
||||
font-family: HelveticaNeue-Bold;
|
||||
font-size: 13px;
|
||||
color: #495057;
|
||||
letter-spacing: 0;
|
||||
|
||||
.dcloud-form-item-control-input {
|
||||
min-height: 27px;
|
||||
}
|
||||
|
||||
.dcloud-input {
|
||||
padding: 4px 12px;
|
||||
height: 27px;
|
||||
line-height: 27px;
|
||||
background: rgba(33, 37, 41, 0.04);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.custom-typography {
|
||||
color: #74788D;
|
||||
}
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
border-bottom: 1px solid #EFF2F7;
|
||||
}
|
||||
|
||||
.no-padding {
|
||||
padding: 0px !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
.config-form-panel {
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
min-height: 260px;
|
||||
margin-right: 12px;
|
||||
padding: 16px 24px 60px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.01), 0 3px 6px 3px rgba(0, 0, 0, 0.01), 0 2px 6px 0 rgba(0, 0, 0, 0.03);
|
||||
border-radius: 12px 12px 0 0;
|
||||
position: relative;
|
||||
height: calc(100vh - 156px);
|
||||
.dcloud-radio-wrapper.dcloud-radio-wrapper-checked {
|
||||
.dcloud-radio-inner {
|
||||
background-color: #526ecc;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-radio-inner {
|
||||
border-color: #ADB5BD;
|
||||
}
|
||||
|
||||
.dcloud-radio-inner::after {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
|
||||
&-title {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 18px;
|
||||
color: #212529;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 20px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
&-content{
|
||||
overflow-y: auto;
|
||||
height: calc(100vh - 326px);
|
||||
min-height: 90px;
|
||||
}
|
||||
|
||||
&-footer-btn {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
width: 240px;
|
||||
text-align: right;
|
||||
border-top: 1px solid #EBEDEF;
|
||||
padding-top: 5px;
|
||||
|
||||
button+button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { IconFont, Switch, Tooltip } from 'knowdesign';
|
||||
import { FormItemType, IFormItem } from 'knowdesign/lib/extend/x-form';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { timeFormat } from '../../constants/common';
|
||||
import { getRandomStr } from '../../lib/utils';
|
||||
import { ControlStatusMap } from '../CommonRoute';
|
||||
|
||||
export const filterList = [
|
||||
{
|
||||
label: 'none',
|
||||
value: 'none',
|
||||
},
|
||||
{
|
||||
label: 'contains',
|
||||
value: 'contains',
|
||||
},
|
||||
{
|
||||
label: 'does not contains',
|
||||
value: 'does not contains',
|
||||
},
|
||||
{
|
||||
label: 'equals',
|
||||
value: 'equals',
|
||||
},
|
||||
{
|
||||
label: 'Above Size',
|
||||
value: 'aboveSize',
|
||||
},
|
||||
{
|
||||
label: 'Under Size',
|
||||
value: 'underSize',
|
||||
},
|
||||
];
|
||||
|
||||
export const untilList = [
|
||||
{
|
||||
label: 'Forever',
|
||||
value: 'forever',
|
||||
},
|
||||
{
|
||||
label: 'number of messages',
|
||||
value: 'number of messages',
|
||||
},
|
||||
{
|
||||
label: 'number of messages per partition',
|
||||
value: 'number of messages per partition',
|
||||
},
|
||||
{
|
||||
label: 'max size',
|
||||
value: 'max size',
|
||||
},
|
||||
{
|
||||
label: 'max size per partition',
|
||||
value: 'max size per partition',
|
||||
},
|
||||
{
|
||||
label: 'timestamp',
|
||||
value: 'timestamp',
|
||||
},
|
||||
];
|
||||
|
||||
export const tabsConfig = [
|
||||
{ name: 'Data', control: '' },
|
||||
{ name: 'Flow', control: '' },
|
||||
{ name: 'Header', control: ControlStatusMap.TESTING_PRODUCER_HEADER },
|
||||
{ name: 'Options', control: '' },
|
||||
];
|
||||
|
||||
const getComplexLabel = (key: string, label: string, form: any, onSwitchChange?: any, needSwitch = false) => (
|
||||
<div className="complex-label">
|
||||
<span>
|
||||
<span>{label}</span>
|
||||
<Tooltip title="暂支持string格式">
|
||||
<QuestionCircleOutlined size={12} style={{ marginLeft: 2 }} />
|
||||
</Tooltip>
|
||||
:
|
||||
{needSwitch ? (
|
||||
<span>
|
||||
<Switch onClick={onSwitchChange} size="small" defaultChecked className="switch" />
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span>
|
||||
<Tooltip title={'生成随机内容'}>
|
||||
<IconFont
|
||||
type="icon-shengchengdaima"
|
||||
className="random-icon"
|
||||
onClick={() => {
|
||||
const randomStr = getRandomStr(key === 'key' ? 30 : 128);
|
||||
form && form.setFieldsValue({ [key]: randomStr });
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const getFormConfig = (params: any) => {
|
||||
const { topicMetaData, activeKey: type, configInfo: info, form, onKeySwitchChange, isKeyOn, isShowControl } = params;
|
||||
const formConfig = [
|
||||
{
|
||||
key: 'topicName',
|
||||
label: 'Topic',
|
||||
type: FormItemType.select,
|
||||
invisible: type !== 'Data',
|
||||
rules: [{ required: true, message: '请选择Topic' }],
|
||||
options: topicMetaData,
|
||||
attrs: {
|
||||
showSearch: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'key',
|
||||
label: getComplexLabel('key', 'Key', form, onKeySwitchChange, true),
|
||||
type: FormItemType.textArea,
|
||||
invisible: type !== 'Data',
|
||||
rules: [
|
||||
{
|
||||
required: false,
|
||||
message: '请输入Key',
|
||||
},
|
||||
],
|
||||
attrs: {
|
||||
disabled: !isKeyOn,
|
||||
rows: 5,
|
||||
placeholder: '暂支持string类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'value',
|
||||
label: getComplexLabel('value', 'Value', form),
|
||||
type: FormItemType.textArea,
|
||||
invisible: type !== 'Data',
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入Value',
|
||||
},
|
||||
],
|
||||
attrs: {
|
||||
rows: 5,
|
||||
placeholder: '暂支持string类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'chunks',
|
||||
label: '单次发送消息数',
|
||||
type: FormItemType.inputNumber,
|
||||
invisible: type !== 'Flow',
|
||||
defaultValue: 1,
|
||||
rules: [{ required: true, message: '请输入' }],
|
||||
attrs: {
|
||||
min: 0,
|
||||
style: { width: 232 },
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'producerMode',
|
||||
label: '生产模式',
|
||||
type: FormItemType.radioGroup,
|
||||
invisible: type !== 'Flow',
|
||||
defaultValue: 'manual',
|
||||
options: [
|
||||
{
|
||||
label: '手动',
|
||||
value: 'manual',
|
||||
},
|
||||
{
|
||||
label: '周期',
|
||||
value: 'timed',
|
||||
},
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择生产模式',
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// key: 'timeOptions',
|
||||
// label: 'Timer options',
|
||||
// type: FormItemType.text,
|
||||
// invisible: type !== 'Flow' || !info?.needTimeOption,
|
||||
// customFormItem: null,
|
||||
// rules: [
|
||||
// {
|
||||
// required: false,
|
||||
// message: '请选择Producer Mode',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
key: 'elapsed',
|
||||
label: '运行总时间(min)',
|
||||
type: FormItemType.inputNumber,
|
||||
invisible: type !== 'Flow' || !info?.needTimeOption,
|
||||
rules: [
|
||||
{
|
||||
required: info?.needTimeOption,
|
||||
message: '请输入',
|
||||
},
|
||||
],
|
||||
attrs: {
|
||||
min: 0,
|
||||
max: 300,
|
||||
style: { width: '100%' },
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'interval',
|
||||
label: '时间间隔(ms)',
|
||||
type: FormItemType.inputNumber,
|
||||
invisible: type !== 'Flow' || !info?.needTimeOption,
|
||||
rules: [
|
||||
{
|
||||
required: info?.needTimeOption,
|
||||
message: '请输入',
|
||||
},
|
||||
],
|
||||
attrs: {
|
||||
min: 300, // 300ms间隔保证请求不太频繁
|
||||
style: { width: '100%' },
|
||||
},
|
||||
},
|
||||
// {
|
||||
// key: 'jitter',
|
||||
// label: 'Max jitter(ms)',
|
||||
// type: FormItemType.inputNumber,
|
||||
// invisible: type !== 'Flow' || !info?.needTimeOption,
|
||||
// rules: [
|
||||
// {
|
||||
// required: info?.needTimeOption,
|
||||
// message: '请输入Max jitter',
|
||||
// },
|
||||
// ],
|
||||
// attrs: {
|
||||
// max: info.maxJitter || 0,
|
||||
// size: 'small',
|
||||
// style: { width: 216 },
|
||||
// },
|
||||
// formAttrs: {
|
||||
// className: 'inner-item',
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// key: 'lifecycle',
|
||||
// label: (
|
||||
// <>
|
||||
// Lifecycle options
|
||||
// <Tooltip title="Shutdown the producer automatically">
|
||||
// <QuestionCircleOutlined style={{ marginLeft: 8 }} />
|
||||
// </Tooltip>
|
||||
// </>
|
||||
// ),
|
||||
// type: FormItemType.text,
|
||||
// invisible: type !== 'Flow' || !info?.needTimeOption,
|
||||
// customFormItem: null,
|
||||
// rules: [
|
||||
// {
|
||||
// required: false,
|
||||
// message: '',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// key: 'messageProduced',
|
||||
// label: 'Number of message produced',
|
||||
// type: FormItemType.inputNumber,
|
||||
// invisible: type !== 'Flow' || !info?.needTimeOption,
|
||||
// rules: [
|
||||
// {
|
||||
// required: info?.needTimeOption,
|
||||
// message: '请输入Number of message produced',
|
||||
// },
|
||||
// ],
|
||||
// attrs: {
|
||||
// min: 0,
|
||||
// size: 'small',
|
||||
// style: { width: 216 },
|
||||
// },
|
||||
// formAttrs: {
|
||||
// className: 'inner-item',
|
||||
// },
|
||||
// },
|
||||
{
|
||||
key: 'frocePartition',
|
||||
label: 'Froce Partition',
|
||||
invisible: type !== 'Options',
|
||||
type: FormItemType.select,
|
||||
attrs: {
|
||||
mode: 'multiple',
|
||||
},
|
||||
options: info.partitionIdList || [],
|
||||
rules: [{ required: false, message: '请选择' }],
|
||||
},
|
||||
{
|
||||
key: 'compressionType',
|
||||
label: 'Compression Type',
|
||||
type: FormItemType.radioGroup,
|
||||
defaultValue: 'none',
|
||||
invisible: type !== 'Options',
|
||||
options: (() => {
|
||||
const options = [
|
||||
{
|
||||
label: 'none',
|
||||
value: 'none',
|
||||
},
|
||||
{
|
||||
label: 'gzip',
|
||||
value: 'gzip',
|
||||
},
|
||||
{
|
||||
label: 'snappy',
|
||||
value: 'snappy',
|
||||
},
|
||||
{
|
||||
label: 'lz4',
|
||||
value: 'lz4',
|
||||
},
|
||||
];
|
||||
|
||||
if (isShowControl && isShowControl(ControlStatusMap.TESTING_PRODUCER_COMPRESSION_TYPE_ZSTD)) {
|
||||
options.push({
|
||||
label: 'zstd',
|
||||
value: 'zstd',
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
})(),
|
||||
rules: [
|
||||
{
|
||||
required: false,
|
||||
message: '请选择Compression Type',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'acks',
|
||||
label: 'Acks',
|
||||
type: FormItemType.radioGroup,
|
||||
defaultValue: '0',
|
||||
invisible: type !== 'Options',
|
||||
options: [
|
||||
{
|
||||
label: 'none',
|
||||
value: '0',
|
||||
},
|
||||
{
|
||||
label: 'leader',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: 'all',
|
||||
value: 'all',
|
||||
},
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
required: false,
|
||||
message: '请选择Acks',
|
||||
},
|
||||
],
|
||||
},
|
||||
] as unknown as IFormItem[];
|
||||
|
||||
return formConfig;
|
||||
};
|
||||
|
||||
export const getTableColumns = () => {
|
||||
return [
|
||||
{
|
||||
title: 'Partition',
|
||||
dataIndex: 'partitionId',
|
||||
},
|
||||
{
|
||||
title: 'offset',
|
||||
dataIndex: 'offset',
|
||||
},
|
||||
{
|
||||
title: 'Timestamp',
|
||||
dataIndex: 'timestampUnitMs',
|
||||
render: (text: number) => {
|
||||
return moment(text).format(timeFormat);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'time',
|
||||
dataIndex: 'costTimeUnitMs',
|
||||
width: 60,
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
.client-test-panel {
|
||||
display: flex;
|
||||
|
||||
.form-tabs {
|
||||
background: transparent;
|
||||
|
||||
.dcloud-tabs-nav {
|
||||
height: 32px;
|
||||
background: none;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.dcloud-tabs-top>.dcloud-tabs-nav,
|
||||
.dcloud-tabs-bottom>.dcloud-tabs-nav,
|
||||
.dcloud-tabs-top>div>.dcloud-tabs-nav,
|
||||
.dcloud-tabs-bottom>div>.dcloud-tabs-nav {
|
||||
margin-bottom: 18px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.dcloud-tabs-tab+.dcloud-tabs-tab {
|
||||
margin: 0 0 0 16px;
|
||||
}
|
||||
|
||||
.dcloud-tabs-top>.dcloud-tabs-nav::before,
|
||||
.dcloud-tabs-bottom>.dcloud-tabs-nav::before,
|
||||
.dcloud-tabs-top>div>.dcloud-tabs-nav::before,
|
||||
.dcloud-tabs-bottom>div>.dcloud-tabs-nav::before {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-form-item {
|
||||
margin-bottom: 14px;
|
||||
|
||||
&-label {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 13px;
|
||||
color: #495057;
|
||||
letter-spacing: 0;
|
||||
line-height: 18px !important;
|
||||
|
||||
.complex-label {
|
||||
width: 228px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.random-icon {
|
||||
font-size: 18px;
|
||||
height: 18px;
|
||||
color: #ADB5BC;
|
||||
}
|
||||
|
||||
.random-icon:hover {
|
||||
height: 18px;
|
||||
text-align: center;
|
||||
border-radius: 9px;
|
||||
line-height: 20px;
|
||||
color: #74788D;
|
||||
background: rgba(33, 37, 41, 0.04);
|
||||
}
|
||||
|
||||
.switch {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-form-item-control-input {
|
||||
textarea {
|
||||
height: 110px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.dcloud-form-item.inner-item {
|
||||
margin-left: 16px;
|
||||
margin-bottom: 7px;
|
||||
|
||||
.dcloud-form-item-label {
|
||||
padding: 0 0 4px;
|
||||
|
||||
>label {
|
||||
font-size: 13px;
|
||||
color: #74788D;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.dcloud-radio-group{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.dcloud-radio-wrapper{
|
||||
width: 33.33%;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { AppContainer } from 'knowdesign';
|
||||
import * as React from 'react';
|
||||
import ProduceClientTest from './Produce';
|
||||
import './index.less';
|
||||
import TaskTabs from '../TestingConsumer/component/TaskTabs';
|
||||
import DBreadcrumb from 'knowdesign/lib/extend/d-breadcrumb';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
const Produce = () => {
|
||||
const initial = {
|
||||
label: '生产',
|
||||
key: 'tab-1',
|
||||
closable: false,
|
||||
tabpane: <ProduceClientTest />,
|
||||
};
|
||||
const { clusterId } = useParams<{ clusterId: string }>();
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const ref: any = React.useRef();
|
||||
|
||||
React.useEffect(() => {
|
||||
AppContainer.eventBus.on('ProduceTopicChange', (args: string) => {
|
||||
ref.current && ref.current.setTabsTitle && ref.current.setTabsTitle(`生产 ${args}`);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="breadcrumb">
|
||||
<DBreadcrumb
|
||||
breadcrumbs={[
|
||||
{ label: '多集群管理', aHref: '/' },
|
||||
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
|
||||
{ label: 'Produce', aHref: '' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<TaskTabs initial={initial} ref={ref} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Produce;
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { MetricType } from '@src/api';
|
||||
import TopicHealthCheck from '@src/components/CardBar/TopicHealthCheck';
|
||||
import DashboardDragChart from '@src/components/DashboardDragChart';
|
||||
import { AppContainer } from 'knowdesign';
|
||||
import DBreadcrumb from 'knowdesign/lib/extend/d-breadcrumb';
|
||||
|
||||
const TopicDashboard = () => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
return (
|
||||
<>
|
||||
<div className="breadcrumb" style={{ marginBottom: '10px' }}>
|
||||
<DBreadcrumb
|
||||
breadcrumbs={[
|
||||
{ label: '多集群管理', aHref: '/' },
|
||||
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
|
||||
{ label: 'Topic', aHref: `` },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<TopicHealthCheck />
|
||||
<DashboardDragChart type={MetricType.Topic} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopicDashboard;
|
||||
@@ -0,0 +1,97 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ProTable, Utils } from 'knowdesign';
|
||||
import Api from '@src/api';
|
||||
import { useParams } from 'react-router-dom';
|
||||
// import { dealTableRequestParams } from '../../constants/common';
|
||||
import { getTopicACLsColmns } from './config';
|
||||
const { request } = Utils;
|
||||
|
||||
const TopicACLs = (props: any) => {
|
||||
const { hashData } = props;
|
||||
const urlParams = useParams<any>(); // 获取地址栏参数
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
const [pagination, setPagination] = useState<any>({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
position: 'bottomRight',
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
|
||||
showTotal: (total: number) => `共 ${total} 条目`,
|
||||
// locale: {
|
||||
// items_per_page: '条',
|
||||
// },
|
||||
// selectComponentClass: CustomSelect,
|
||||
});
|
||||
// 默认排序
|
||||
const defaultSorter = {
|
||||
sortField: 'kafkaUser',
|
||||
sortType: 'asc',
|
||||
};
|
||||
|
||||
// 请求接口获取数据
|
||||
const genData = async ({ pageNo, pageSize, filters = null, sorter = null }: any) => {
|
||||
if (urlParams?.clusterId === undefined || hashData?.topicName === undefined) return;
|
||||
|
||||
// filters = filters || filteredInfo;
|
||||
|
||||
setLoading(true);
|
||||
const params = {
|
||||
searchKeywords: props.searchKeywords ? props.searchKeywords.slice(0, 128) : '',
|
||||
pageNo,
|
||||
pageSize,
|
||||
...defaultSorter,
|
||||
};
|
||||
|
||||
request(Api.getTopicACLsList(hashData?.topicName, urlParams?.clusterId), { params })
|
||||
.then((res: any) => {
|
||||
setPagination({
|
||||
current: res.pagination?.pageNo,
|
||||
pageSize: res.pagination?.pageSize,
|
||||
total: res.pagination?.total,
|
||||
});
|
||||
setData(res?.bizData || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
// setPagination(pagination);
|
||||
// const asc = sorter?.order && sorter?.order === 'ascend' ? true : false;
|
||||
// const sortColumn = sorter.field && toLine(sorter.field);
|
||||
genData({ pageNo: pagination.current, pageSize: pagination.pageSize });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
props.positionType === 'ACLs' &&
|
||||
genData({
|
||||
pageNo: 1,
|
||||
pageSize: pagination.pageSize,
|
||||
// sorter: defaultSorter
|
||||
});
|
||||
}, [props.searchKeywords, props.positionType]);
|
||||
|
||||
return (
|
||||
<ProTable
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
rowKey: 'path',
|
||||
loading: loading,
|
||||
columns: getTopicACLsColmns(),
|
||||
dataSource: data,
|
||||
paginationProps: { ...pagination },
|
||||
attrs: {
|
||||
// className: 'frameless-table', // 纯无边框表格类名
|
||||
bordered: false,
|
||||
onChange: onTableChange,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopicACLs;
|
||||
@@ -0,0 +1,462 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AppContainer, Button, Empty, IconFont, List, Popover, ProTable, Radio, Spin, Utils } from 'knowdesign';
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
import api, { MetricType } from '@src/api';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import TagsWithHide from '@src/components/TagsWithHide';
|
||||
import SwitchTab from '@src/components/SwitchTab';
|
||||
|
||||
interface PropsType {
|
||||
hashData: any;
|
||||
// searchKeywords: string;
|
||||
}
|
||||
|
||||
interface PartitionsSummary {
|
||||
brokerCount: number;
|
||||
deadBrokerCount: number;
|
||||
liveBrokerCount: number;
|
||||
noLeaderPartitionCount: number;
|
||||
partitionCount: number;
|
||||
underReplicatedPartitionCount: number;
|
||||
}
|
||||
|
||||
const PARTITION_DETAIL_METRICS = ['LogEndOffset', 'LogStartOffset', 'Messages', 'LogSize'] as const;
|
||||
|
||||
interface PartitionDetail {
|
||||
partitionId: number;
|
||||
topicName: string;
|
||||
leaderBrokerId: number;
|
||||
assignReplicas: number[];
|
||||
inSyncReplicas: number[];
|
||||
latestMetrics: {
|
||||
clusterPhyId: number;
|
||||
metrics: {
|
||||
[K in typeof PARTITION_DETAIL_METRICS[number]]: number;
|
||||
};
|
||||
timestamp: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface BrokersDetail {
|
||||
live: number;
|
||||
dead: number;
|
||||
total: number;
|
||||
partitionCount: number;
|
||||
noLeaderPartitionIdList: number[];
|
||||
underReplicatedPartitionIdList: number[];
|
||||
brokerPartitionStateList: brokerPartitionState[];
|
||||
}
|
||||
|
||||
interface brokerPartitionState {
|
||||
alive: boolean;
|
||||
brokerId: number;
|
||||
bytesInOneMinuteRate: number;
|
||||
bytesOutOneMinuteRate: number;
|
||||
host: string;
|
||||
replicaList: brokerReplica[];
|
||||
}
|
||||
|
||||
interface brokerReplica {
|
||||
leaderBrokerId: number;
|
||||
partitionId: number;
|
||||
topicName: string;
|
||||
isLeaderReplace: boolean;
|
||||
inSync: boolean;
|
||||
}
|
||||
|
||||
function getTranformedBytes(bytes: number) {
|
||||
const unit = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let i = 0;
|
||||
let isUp = true,
|
||||
outBytes = bytes;
|
||||
if (typeof outBytes !== 'number') {
|
||||
outBytes = Number(outBytes);
|
||||
if (isNaN(outBytes)) return [outBytes, unit[i]];
|
||||
}
|
||||
|
||||
while (isUp) {
|
||||
if (outBytes / 1024 >= 1) {
|
||||
outBytes /= 1024;
|
||||
i++;
|
||||
} else {
|
||||
isUp = false;
|
||||
}
|
||||
}
|
||||
return [outBytes.toFixed(2), unit[i]];
|
||||
}
|
||||
|
||||
const RenderEmpty = (props: { message: string }) => {
|
||||
const { message } = props;
|
||||
return (
|
||||
<>
|
||||
<div className="empty-panel">
|
||||
<div className="img" />
|
||||
<div className="text">{message}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const PartitionPopoverContent = (props: {
|
||||
clusterId: string;
|
||||
hashData: any;
|
||||
record: brokerReplica;
|
||||
brokerId: brokerPartitionState['brokerId'];
|
||||
close: () => void;
|
||||
}) => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const { clusterId, hashData, record, brokerId, close } = props;
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [partitionInfo, setPartitionInfo] = useState<{ label: string; value: string | number }[]>([]);
|
||||
|
||||
// 获取单个 Partition 详情
|
||||
const getDetail = () => {
|
||||
const { partitionId, leaderBrokerId, inSync } = record;
|
||||
|
||||
Utils.request(api.getPartitionMetricInfo(clusterId, hashData.topicName, brokerId, partitionId), {
|
||||
method: 'POST',
|
||||
data: PARTITION_DETAIL_METRICS,
|
||||
}).then((res: any) => {
|
||||
const type = MetricType.Replication;
|
||||
const metricsData = res?.metrics || {};
|
||||
|
||||
const partitionInfo = [
|
||||
{ label: 'LeaderBroker', value: leaderBrokerId },
|
||||
{
|
||||
label: 'BeginningOffset',
|
||||
value: `${metricsData.LogStartOffset === undefined ? '-' : metricsData.LogStartOffset} ${global.getMetricDefine(type, 'LogStartOffset')?.unit || ''
|
||||
}`,
|
||||
},
|
||||
{
|
||||
label: 'EndOffset',
|
||||
value: `${metricsData.LogEndOffset === undefined ? '-' : metricsData.LogEndOffset} ${global.getMetricDefine(type, 'LogEndOffset')?.unit || ''
|
||||
}`,
|
||||
},
|
||||
{
|
||||
label: 'MsgNum',
|
||||
value: `${metricsData.Messages === undefined ? '-' : metricsData.Messages} ${global.getMetricDefine(type, 'Messages')?.unit || ''
|
||||
}`,
|
||||
},
|
||||
{
|
||||
label: 'LogSize',
|
||||
value: `${metricsData.LogSize === undefined ? '-' : Utils.formatAssignSize(metricsData.LogSize, 'MB')} MB`,
|
||||
},
|
||||
{ label: '是否同步', value: inSync ? '是' : '否' },
|
||||
];
|
||||
setPartitionInfo(partitionInfo);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getDetail();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
<div className="header">
|
||||
<div className="title">分区详情</div>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CloseOutlined className="close-icon" />}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="main">
|
||||
<List
|
||||
itemLayout="vertical"
|
||||
size="small"
|
||||
dataSource={partitionInfo}
|
||||
renderItem={(item) => (
|
||||
<List.Item key={item.label} extra={item.value}>
|
||||
{item.label}
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Spin>
|
||||
);
|
||||
};
|
||||
|
||||
const PartitionSummary = (props: { clusterId: string; topicName: string }) => {
|
||||
const { clusterId, topicName } = props;
|
||||
const [partitionsSummary, setPartitionsSummary] = useState<PartitionsSummary>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// 获取统计信息
|
||||
Utils.request(api.getTopicPartitionsSummary(clusterId, topicName)).then((res: PartitionsSummary) => {
|
||||
setPartitionsSummary(res);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="info-box">
|
||||
<div className="info-detail info-detail-title">
|
||||
<span className="desc">Brokers 总数</span>
|
||||
<span className="num">{partitionsSummary?.brokerCount}</span>
|
||||
</div>
|
||||
<div className="info-detail">
|
||||
<span className="desc">live</span>
|
||||
<span className="num">{partitionsSummary?.liveBrokerCount}</span>
|
||||
</div>
|
||||
<div className="info-detail">
|
||||
<span className="desc">down</span>
|
||||
<span className="num">{partitionsSummary?.deadBrokerCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="info-box">
|
||||
<div className="info-detail info-detail-title">
|
||||
<span className="desc">Partition 总数</span>
|
||||
<span className="num">{partitionsSummary?.partitionCount}</span>
|
||||
</div>
|
||||
<div className="info-detail">
|
||||
<span className="desc">No Leader</span>
|
||||
<span className="num">{partitionsSummary?.noLeaderPartitionCount}</span>
|
||||
</div>
|
||||
<div className="info-detail">
|
||||
<span className="desc">URP</span>
|
||||
<span className="num">{partitionsSummary?.underReplicatedPartitionCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const PartitionCard = (props: { clusterId: string; hashData: any }) => {
|
||||
const { clusterId, hashData } = props;
|
||||
const [brokersDetail, setBrokersDetail] = useState<BrokersDetail>();
|
||||
const [hoverPartitionId, setHoverPartitionId] = useState<brokerReplica['partitionId']>(-1);
|
||||
const [clickPartition, setClickPartition] = useState<string>('');
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
const closePartitionDetail = useCallback(() => setClickPartition(''), []);
|
||||
|
||||
useEffect(() => {
|
||||
Utils.request(api.getTopicBrokersList(clusterId, hashData.topicName)).then(
|
||||
(res: any) => {
|
||||
setBrokersDetail(res);
|
||||
setLoading(false);
|
||||
},
|
||||
() => setLoading(false)
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
<div className="broker-container">
|
||||
{brokersDetail?.brokerPartitionStateList?.length ? (
|
||||
brokersDetail?.brokerPartitionStateList.map((partitionState) => {
|
||||
return (
|
||||
<div className="broker-container-box" key={partitionState.brokerId}>
|
||||
<div className="broker-container-box-header">
|
||||
<div className="header-info">
|
||||
<div className="label">Broker</div>
|
||||
<div className="value">{partitionState.brokerId}</div>
|
||||
</div>
|
||||
<div className="header-info">
|
||||
<div className="label">Host ID</div>
|
||||
<div className="value">{partitionState.host || '-'}</div>
|
||||
</div>
|
||||
{['BytesIn', 'BytesOut'].map((type) => {
|
||||
return (
|
||||
<div className="header-info" key={type}>
|
||||
<div className="label">{type}</div>
|
||||
<div className="value">
|
||||
{getTranformedBytes(
|
||||
type === 'BytesIn' ? partitionState.bytesInOneMinuteRate : partitionState.bytesOutOneMinuteRate
|
||||
).map((val, i) => {
|
||||
return i ? <span className="unit">{val}/s</span> : <span>{val}</span>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="broker-container-box-detail">
|
||||
{partitionState.alive ? (
|
||||
partitionState?.replicaList?.length ? (
|
||||
<div className="partition-list">
|
||||
{partitionState?.replicaList?.map((partition) => {
|
||||
return (
|
||||
<div
|
||||
key={partition.partitionId}
|
||||
className={`partition-list-item partition-list-item-${partition.isLeaderReplace ? 'leader' : partition.inSync ? 'isr' : 'osr'
|
||||
} ${partition.partitionId === hoverPartitionId ? 'partition-active' : ''}`}
|
||||
onMouseEnter={() => setHoverPartitionId(partition.partitionId)}
|
||||
onMouseLeave={() => setHoverPartitionId(-1)}
|
||||
onClick={() => setClickPartition(`${partitionState.brokerId}&${partition.partitionId}`)}
|
||||
>
|
||||
<Popover
|
||||
visible={clickPartition === `${partitionState.brokerId}&${partition.partitionId}`}
|
||||
onVisibleChange={(v) => !v && closePartitionDetail()}
|
||||
overlayClassName="broker-partition-popover"
|
||||
content={
|
||||
<PartitionPopoverContent
|
||||
clusterId={clusterId}
|
||||
hashData={hashData}
|
||||
record={partition}
|
||||
brokerId={partitionState.brokerId}
|
||||
close={closePartitionDetail}
|
||||
/>
|
||||
}
|
||||
destroyTooltipOnHide={true}
|
||||
trigger="click"
|
||||
placement="rightTop"
|
||||
>
|
||||
{partition.partitionId}
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<RenderEmpty message="暂无数据" />
|
||||
)
|
||||
) : (
|
||||
<RenderEmpty message="暂无数据" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : loading ? (
|
||||
<></>
|
||||
) : (
|
||||
<Empty style={{ margin: '0 auto', marginTop: 100 }} />
|
||||
)}
|
||||
</div>
|
||||
</Spin>
|
||||
);
|
||||
};
|
||||
|
||||
const PartitionTable = (props: { clusterId: string; hashData: any }) => {
|
||||
const { clusterId, hashData } = props;
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [partitionsDetail, setPartitionsDetail] = useState<PartitionDetail[]>([]);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'Partition ID',
|
||||
dataIndex: 'partitionId',
|
||||
},
|
||||
{
|
||||
title: 'StartOffset',
|
||||
dataIndex: ['latestMetrics', 'metrics', 'LogStartOffset'],
|
||||
},
|
||||
{
|
||||
title: 'EndOffset',
|
||||
dataIndex: ['latestMetrics', 'metrics', 'LogEndOffset'],
|
||||
},
|
||||
{
|
||||
title: 'MsgNum',
|
||||
dataIndex: ['latestMetrics', 'metrics', 'Messages'],
|
||||
},
|
||||
{
|
||||
title: 'Leader Broker',
|
||||
dataIndex: 'leaderBrokerId',
|
||||
},
|
||||
{
|
||||
title: 'LogSize(MB)',
|
||||
dataIndex: ['latestMetrics', 'metrics', 'LogSize'],
|
||||
render: (size: number | undefined) => (size === undefined ? '-' : Utils.formatAssignSize(size, 'MB')),
|
||||
},
|
||||
{
|
||||
title: 'AR',
|
||||
dataIndex: 'assignReplicas',
|
||||
width: 180,
|
||||
render: (arr: PartitionDetail['assignReplicas']) => <TagsWithHide list={arr} expandTagContent={(len) => `共有${len}个`} />,
|
||||
},
|
||||
{
|
||||
title: 'ISR',
|
||||
dataIndex: 'inSyncReplicas',
|
||||
width: 180,
|
||||
render: (arr: PartitionDetail['inSyncReplicas']) => <TagsWithHide list={arr} expandTagContent={(len) => `共有${len}个`} />,
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
Utils.request(api.getTopicPartitionsDetail(clusterId, hashData.topicName), {
|
||||
method: 'POST',
|
||||
data: PARTITION_DETAIL_METRICS,
|
||||
}).then((res: PartitionDetail[]) => {
|
||||
setPartitionsDetail(res);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<ProTable
|
||||
tableProps={{
|
||||
loading,
|
||||
rowKey: 'partitionId',
|
||||
columns: columns as any,
|
||||
dataSource: partitionsDetail,
|
||||
showHeader: false,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default (props: PropsType) => {
|
||||
const { clusterId } = useParams<{ clusterId: string }>();
|
||||
const { hashData } = props;
|
||||
const [showMode, setShowMode] = useState<string>('card');
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="brokers-tab-container">
|
||||
<div className="overview">
|
||||
<div className="left">
|
||||
<PartitionSummary clusterId={clusterId} topicName={hashData.topicName} />
|
||||
</div>
|
||||
<div className="cases-box">
|
||||
{showMode === 'card' && (
|
||||
<div className="broker-cases">
|
||||
<div className="case case-leader">
|
||||
<div className="icon"></div>
|
||||
<div className="desc">Leader</div>
|
||||
</div>
|
||||
<div className="case case-isr">
|
||||
<div className="icon"></div>
|
||||
<div className="desc">ISR</div>
|
||||
</div>
|
||||
<div className="case case-osr">
|
||||
<div className="icon"></div>
|
||||
<div className="desc">OSR</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<SwitchTab
|
||||
defaultKey="card"
|
||||
onChange={(key) => {
|
||||
setShowMode(key);
|
||||
}}
|
||||
>
|
||||
<SwitchTab.TabItem key="card">
|
||||
<div style={{ width: 34, height: 23 }}>
|
||||
<IconFont type="icon-tubiao" />
|
||||
</div>
|
||||
</SwitchTab.TabItem>
|
||||
<SwitchTab.TabItem key="table">
|
||||
<div style={{ width: 34, height: 23 }}>
|
||||
<IconFont type="icon-biaoge" />
|
||||
</div>
|
||||
</SwitchTab.TabItem>
|
||||
</SwitchTab>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showMode === 'card' ? (
|
||||
<PartitionCard clusterId={clusterId} hashData={hashData} />
|
||||
) : (
|
||||
<PartitionTable clusterId={clusterId} hashData={hashData} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,160 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { AppContainer, Checkbox, ProTable, Utils } from 'knowdesign';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Api from '@src/api';
|
||||
import { getTopicConfigurationColmns } from './config';
|
||||
import { ConfigurationEdit } from './ConfigurationEdit';
|
||||
import { ClustersPermissionMap } from '../CommonConfig';
|
||||
const { request } = Utils;
|
||||
|
||||
const BrokerConfiguration = (props: any) => {
|
||||
const { hashData } = props;
|
||||
const urlParams = useParams<any>(); // 获取地址栏参数
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
// const [filterType, setFilterType] = useState<number>(0); // 多选框的筛选结果 filterType
|
||||
const [checkedBoxList, setCheckedBoxList] = useState<string[]>([]); // 多选框的选中的列表
|
||||
const [pagination, setPagination] = useState<any>({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
position: 'bottomRight',
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
|
||||
showTotal: (total: number) => `共 ${total} 条目`,
|
||||
});
|
||||
const [editVisible, setEditVisible] = useState(false);
|
||||
const [record, setRecord] = useState(null); // 获取当前点击行的数据;
|
||||
const [readOnlyVisible, setReadOnlyVisible] = useState(null);
|
||||
const [readOnlyRecord, setReadOnlyRecord] = useState(null);
|
||||
|
||||
// 请求接口获取数据
|
||||
const genData = async ({ pageNo, pageSize }: any) => {
|
||||
if (urlParams?.clusterId === undefined || hashData?.topicName === undefined) return;
|
||||
|
||||
setLoading(true);
|
||||
// const params = dealTableRequestParams({
|
||||
// searchKeywords: props.searchKeywords ? props.searchKeywords : '',
|
||||
// pageNo,
|
||||
// pageSize,
|
||||
// });
|
||||
const params = {
|
||||
searchKeywords: props.searchKeywords ? props.searchKeywords.slice(0, 128) : undefined,
|
||||
pageNo,
|
||||
pageSize,
|
||||
preciseFilterDTOList:
|
||||
checkedBoxList.length > 0
|
||||
? checkedBoxList.map((item) => {
|
||||
return {
|
||||
fieldName: item,
|
||||
fieldValueList: [item === 'readOnly' ? false : true],
|
||||
include: true,
|
||||
};
|
||||
})
|
||||
: undefined,
|
||||
};
|
||||
|
||||
request(Api.getTopicConfigs(hashData?.topicName, urlParams?.clusterId), { data: params, method: 'POST' })
|
||||
.then((res: any) => {
|
||||
setPagination({
|
||||
current: res.pagination?.pageNo,
|
||||
pageSize: res.pagination?.pageSize,
|
||||
total: res.pagination?.total,
|
||||
});
|
||||
setData(res?.bizData || []);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
// setPagination(pagination);
|
||||
// const asc = sorter?.order && sorter?.order === 'ascend' ? true : false;
|
||||
// const sortColumn = sorter.field && toLine(sorter.field);
|
||||
genData({ pageNo: pagination.current, pageSize: pagination.pageSize });
|
||||
};
|
||||
|
||||
// 多选配置
|
||||
const checkedBoxOptions = [
|
||||
{ label: 'Hide read-only', value: 'readOnly' },
|
||||
{ label: 'Show Overrides Only', value: 'override' },
|
||||
];
|
||||
|
||||
const checkedBoxChange = (e: any) => {
|
||||
// 通过checked转换filterType
|
||||
// const newfilterType =
|
||||
// e.includes('readOnly') && e.includes('override')
|
||||
// ? 0
|
||||
// : e.includes('readOnly') && !e.includes('override')
|
||||
// ? 1
|
||||
// : !e.includes('readOnly') && e.includes('override')
|
||||
// ? 2
|
||||
// : 3;
|
||||
|
||||
// setFilterType(newfilterType);
|
||||
setCheckedBoxList(e);
|
||||
// 调用接口
|
||||
};
|
||||
|
||||
const setEditOp = (record: any) => {
|
||||
setEditVisible(true);
|
||||
setRecord(record);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
props.positionType === 'Configuration' &&
|
||||
genData({
|
||||
pageNo: 1,
|
||||
pageSize: pagination.pageSize,
|
||||
// sorter: defaultSorter
|
||||
});
|
||||
}, [props.searchKeywords, checkedBoxList]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'detail-header-cases'} style={{ padding: '0 0 12px' }}>
|
||||
<Checkbox.Group options={checkedBoxOptions} value={checkedBoxList} onChange={checkedBoxChange} />
|
||||
</div>
|
||||
<ProTable
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
rowKey: 'path',
|
||||
loading: loading,
|
||||
columns: getTopicConfigurationColmns({
|
||||
setEditOp,
|
||||
readOnlyRecord,
|
||||
readOnlyVisible,
|
||||
allowEdit: global.hasPermission && global.hasPermission(ClustersPermissionMap.TOPIC_CHANGE_CONFIG),
|
||||
}),
|
||||
dataSource: data,
|
||||
paginationProps: { ...pagination },
|
||||
attrs: {
|
||||
// className: 'frameless-table', // 纯无边框表格类名
|
||||
bordered: false,
|
||||
onChange: onTableChange,
|
||||
onRow: (record: any) => {
|
||||
if (!!record?.readOnly) {
|
||||
return {
|
||||
onMouseEnter: () => {
|
||||
setReadOnlyVisible(true);
|
||||
setReadOnlyRecord(record);
|
||||
},
|
||||
onMouseLeave: () => {
|
||||
setReadOnlyVisible(false);
|
||||
setReadOnlyRecord(null);
|
||||
},
|
||||
};
|
||||
}
|
||||
return {};
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ConfigurationEdit record={record} hashData={hashData} visible={editVisible} setVisible={setEditVisible} genData={genData} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrokerConfiguration;
|
||||
@@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
import { Drawer, Form, Input, Space, Button, Utils, Row, Col, IconFont, Divider, message } from 'knowdesign';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Api from '@src/api';
|
||||
export const ConfigurationEdit = (props: any) => {
|
||||
const urlParams = useParams<any>();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const onClose = () => {
|
||||
props.setVisible(false);
|
||||
};
|
||||
|
||||
const onOk = () => {
|
||||
form.validateFields().then((res: any) => {
|
||||
const data = {
|
||||
changedProps: {
|
||||
[props.record?.name]: res.newValue,
|
||||
// value: res.newValue,
|
||||
},
|
||||
clusterId: Number(urlParams.clusterId),
|
||||
topicName: props.hashData?.topicName,
|
||||
};
|
||||
Utils.put(Api.getTopicEditConfig(), data)
|
||||
.then((res: any) => {
|
||||
message.success('编辑配置成功');
|
||||
props.setVisible(false);
|
||||
props.genData({ pageNo: 1, pageSize: 10 });
|
||||
})
|
||||
.catch((err: any) => { });
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={
|
||||
<Space size={0}>
|
||||
<Button className="drawer-title-left-button" type="text" size="small" icon={<IconFont type="icon-fanhui1" />} onClick={onClose} />
|
||||
<Divider type="vertical" />
|
||||
<span style={{ paddingLeft: '5px' }}>编辑配置</span>
|
||||
</Space>
|
||||
}
|
||||
width={580}
|
||||
visible={props.visible}
|
||||
onClose={() => props.setVisible(false)}
|
||||
maskClosable={false}
|
||||
extra={
|
||||
<Space>
|
||||
<Button size="small" onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button size="small" type="primary" onClick={onOk}>
|
||||
确认
|
||||
</Button>
|
||||
<div
|
||||
style={{
|
||||
width: '1px',
|
||||
height: '17px',
|
||||
background: 'rgba(0, 0, 0, 0.13)',
|
||||
margin: '0 16px 0 10px',
|
||||
}}
|
||||
></div>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Row gutter={[12, 12]} className="desc-row">
|
||||
<Col span={3} className="label-col">
|
||||
配置名:
|
||||
</Col>
|
||||
<Col span={21} className="value-col">
|
||||
{props.record?.name || '-'}
|
||||
</Col>
|
||||
<Col span={3} className="label-col">
|
||||
描述:
|
||||
</Col>
|
||||
<Col span={21} className="value-col">
|
||||
{props.record?.documentation || '-'}
|
||||
</Col>
|
||||
</Row>
|
||||
<Form form={form} layout={'vertical'} initialValues={props.record}>
|
||||
<Form.Item name="defaultValue" label="Kafka默认配置">
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
<Form.Item name="value" label="当前配置">
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
<Form.Item name="newValue" label="新配置" rules={[{ required: true, message: '请输入新的配置值!!!' }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
{/* <Form.Item name="applyAll" valuePropName="checked">
|
||||
<Checkbox>应用到全部Broker</Checkbox>
|
||||
</Form.Item> */}
|
||||
</Form>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ProTable, Utils } from 'knowdesign';
|
||||
import Api from '@src/api';
|
||||
const { request } = Utils;
|
||||
const getColmns = (solveClick: any) => {
|
||||
const columns = [
|
||||
{
|
||||
title: 'ConsumerGroup',
|
||||
dataIndex: 'ConsumerGroup',
|
||||
key: 'ConsumerGroup',
|
||||
},
|
||||
{
|
||||
title: '关联KafkaUser',
|
||||
dataIndex: 'kafkaUser',
|
||||
key: 'kafkaUser',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
},
|
||||
{
|
||||
title: 'Max Lag',
|
||||
dataIndex: 'maxLag',
|
||||
key: 'maxLag',
|
||||
},
|
||||
{
|
||||
title: 'Member数',
|
||||
dataIndex: 'member',
|
||||
key: 'member',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'option',
|
||||
key: 'option',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (_t: any, r: any) => {
|
||||
return <a onClick={() => solveClick(r)}>解决</a>;
|
||||
},
|
||||
},
|
||||
];
|
||||
return columns;
|
||||
};
|
||||
|
||||
const TopicGroup = (props: any) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
const [pagination, setPagination] = useState<any>({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
position: 'bottomRight',
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
|
||||
showTotal: (total: number) => `共 ${total} 条目`,
|
||||
// locale: {
|
||||
// items_per_page: '条',
|
||||
// },
|
||||
// selectComponentClass: CustomSelect,
|
||||
});
|
||||
const solveClick = (record: any) => {};
|
||||
|
||||
// 请求接口获取数据
|
||||
const genData = async ({ pageNo, pageSize, filters = null, sorter = null }: any) => {
|
||||
// if (clusterId === undefined) return;
|
||||
|
||||
// filters = filters || filteredInfo;
|
||||
|
||||
setLoading(true);
|
||||
// const params = dealTableRequestParams({ searchKeywords, pageNo, pageSize, sorter, filters, isPhyId: true });
|
||||
|
||||
const params = {
|
||||
filterKey: 'string',
|
||||
filterPartitionId: 1,
|
||||
filterValue: 'string',
|
||||
maxRecords: 100,
|
||||
pullTimeoutUnitMs: 10000,
|
||||
truncate: true,
|
||||
};
|
||||
|
||||
request(Api.getTopicMessagesList('你好', 2), { params })
|
||||
.then((res: any) => {
|
||||
setPagination({
|
||||
current: res.pagination?.pageNo,
|
||||
pageSize: res.pagination?.pageSize,
|
||||
total: res.pagination?.total,
|
||||
});
|
||||
setData(res?.bizData || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setPagination({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 20,
|
||||
});
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
setPagination(pagination);
|
||||
// const asc = sorter?.order && sorter?.order === 'ascend' ? true : false;
|
||||
// const sortColumn = sorter.field && toLine(sorter.field);
|
||||
// genData({ pageNo: pagination.current, pageSize: pagination.pageSize, filters, asc, sortColumn, queryTerm: searchResult, ...allParams });
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// genData({
|
||||
// pageNo: 1,
|
||||
// pageSize: pagination.pageSize,
|
||||
// // sorter: defaultSorter
|
||||
// });
|
||||
// }, [props]);
|
||||
|
||||
return (
|
||||
<ProTable
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
rowKey: 'path',
|
||||
loading: loading,
|
||||
columns: getColmns(solveClick),
|
||||
dataSource: data,
|
||||
paginationProps: { ...pagination },
|
||||
attrs: {
|
||||
// className: 'frameless-table', // 纯无边框表格类名
|
||||
bordered: false,
|
||||
onChange: onTableChange,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopicGroup;
|
||||
@@ -0,0 +1,179 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Alert, Button, Checkbox, Form, IconFont, Input, ProTable, Select, Tooltip, Utils } from 'knowdesign';
|
||||
import Api from '@src/api';
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
import { getTopicMessagesColmns } from './config';
|
||||
|
||||
const { request } = Utils;
|
||||
const defaultParams: any = {
|
||||
truncate: true,
|
||||
maxRecords: 100,
|
||||
pullTimeoutUnitMs: 8000,
|
||||
// filterPartitionId: 1,
|
||||
};
|
||||
const defaultpaPagination = {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
position: 'bottomRight',
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
};
|
||||
const TopicMessages = (props: any) => {
|
||||
const { hashData } = props;
|
||||
const urlParams = useParams<any>(); // 获取地址栏参数
|
||||
const history = useHistory();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
const [params, setParams] = useState(defaultParams);
|
||||
const [partitionIdList, setPartitionIdList] = useState([]);
|
||||
const [pagination, setPagination] = useState<any>(defaultpaPagination);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// 默认排序
|
||||
const defaultSorter = {
|
||||
sortField: 'timestampUnitMs',
|
||||
sortType: 'desc',
|
||||
};
|
||||
|
||||
// 请求接口获取数据
|
||||
const genData = async () => {
|
||||
if (urlParams?.clusterId === undefined || hashData?.topicName === undefined) return;
|
||||
setLoading(true);
|
||||
request(Api.getTopicMessagesMetadata(hashData?.topicName, urlParams?.clusterId)).then((res: any) => {
|
||||
// console.log(res, 'metadata');
|
||||
const newPartitionIdList = res?.partitionIdList.map((item: any) => {
|
||||
return {
|
||||
label: item,
|
||||
value: item,
|
||||
};
|
||||
});
|
||||
setPartitionIdList(newPartitionIdList || []);
|
||||
});
|
||||
request(Api.getTopicMessagesList(hashData?.topicName, urlParams?.clusterId), { data: { ...params, ...defaultSorter }, method: 'POST' })
|
||||
.then((res: any) => {
|
||||
// setPagination({
|
||||
// current: res.pagination?.pageNo,
|
||||
// pageSize: res.pagination?.pageSize,
|
||||
// total: res.pagination?.total,
|
||||
// });
|
||||
setData(res || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
// 查询
|
||||
const onFinish = (formData: any) => {
|
||||
setParams({ ...params, ...formData, filterKey: formData?.filterKey ? formData?.filterKey : undefined });
|
||||
};
|
||||
|
||||
// 截断
|
||||
const checkBoxChange = (e: any) => {
|
||||
setParams({ ...params, truncate: e.target.checked });
|
||||
};
|
||||
|
||||
// 刷新
|
||||
const refreshClick = () => {
|
||||
// genData();
|
||||
// form.resetFields();
|
||||
setPagination(defaultpaPagination);
|
||||
genData();
|
||||
};
|
||||
|
||||
// 跳转Consume
|
||||
const jumpConsume = () => {
|
||||
history.push(`/cluster/${urlParams?.clusterId}/testing/consumer`);
|
||||
};
|
||||
|
||||
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
setPagination(pagination);
|
||||
// const asc = sorter?.order && sorter?.order === 'ascend' ? true : false;
|
||||
// const sortColumn = sorter.field && toLine(sorter.field);
|
||||
// genData({ pageNo: pagination.current, pageSize: pagination.pageSize, filters, asc, sortColumn, queryTerm: searchResult, ...allParams });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
props.positionType === 'Messages' && genData();
|
||||
}, [props, params]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<span
|
||||
style={{ display: 'inline-block', padding: '0 10px', marginRight: '10px', borderRight: '1px solid #ccc', fontSize: '13px' }}
|
||||
onClick={refreshClick}
|
||||
>
|
||||
<i className="iconfont icon-shuaxin1" style={{ fontSize: '13px', cursor: 'pointer' }} />
|
||||
</span>
|
||||
<span style={{ fontSize: '13px' }}>
|
||||
<Checkbox checked={params.truncate} onChange={checkBoxChange}>
|
||||
是否要截断数据
|
||||
</Checkbox>
|
||||
</span>
|
||||
<Tooltip title={'截断数据后只展示前1024字符的数据'}>
|
||||
<IconFont style={{ fontSize: '14px' }} type="icon-zhushi" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="messages-query">
|
||||
<Form form={form} layout="inline" onFinish={onFinish}>
|
||||
<Form.Item name="filterPartitionId">
|
||||
<Select
|
||||
options={partitionIdList}
|
||||
size="small"
|
||||
style={{ width: '140px' }}
|
||||
className={'detail-table-select'}
|
||||
placeholder="请选择分区"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="filterKey">
|
||||
<Input size="small" style={{ width: '140px' }} className={'detail-table-input'} placeholder="请输入key" />
|
||||
</Form.Item>
|
||||
<Form.Item name="filterValue">
|
||||
<Input size="small" style={{ width: '140px' }} className={'detail-table-input'} placeholder="请输入value" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button size="small" type="primary" ghost htmlType="submit">
|
||||
查询
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Alert
|
||||
style={{ margin: '12px 0 4px', padding: '7px 12px', background: '#FFF9E6' }}
|
||||
message={
|
||||
<div>
|
||||
此处展示Topic最近的100条messages,若想获取其他messages,可前往<a onClick={jumpConsume}>Produce&Consume</a>进行操作
|
||||
</div>
|
||||
}
|
||||
type="warning"
|
||||
closable
|
||||
/>
|
||||
</div>
|
||||
<ProTable
|
||||
showQueryForm={false}
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
rowKey: 'path',
|
||||
loading: loading,
|
||||
columns: getTopicMessagesColmns(),
|
||||
dataSource: data,
|
||||
paginationProps: pagination,
|
||||
// noPagination: true,
|
||||
attrs: {
|
||||
// className: 'frameless-table', // 纯无边框表格类名
|
||||
bordered: false,
|
||||
onChange: onTableChange,
|
||||
scroll: { x: 'max-content' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopicMessages;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user