mirror of
https://github.com/didi/KnowStreaming.git
synced 2026-01-11 02:13:28 +08:00
V3.2
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
const themeConfig = {
|
const themeConfig = {
|
||||||
primaryColor: '#556ee6',
|
primaryColor: '#5664FF',
|
||||||
theme: {
|
theme: {
|
||||||
'primary-color': '#556ee6',
|
'primary-color': '#5664FF',
|
||||||
'border-radius-base': '2px',
|
'border-radius-base': '2px',
|
||||||
'border-radius-sm': '2px',
|
'border-radius-sm': '2px',
|
||||||
'font-size-base': '12px',
|
'font-size-base': '12px',
|
||||||
|
|||||||
@@ -423,7 +423,7 @@ export default (props: { curTabKey: string }): JSX.Element => {
|
|||||||
dataIndex: 'authedUserCnt',
|
dataIndex: 'authedUserCnt',
|
||||||
width: 100,
|
width: 100,
|
||||||
render(cnt: Pick<RoleProps, 'authedUserCnt'>, record: RoleProps) {
|
render(cnt: Pick<RoleProps, 'authedUserCnt'>, record: RoleProps) {
|
||||||
return (
|
return cnt ? (
|
||||||
<Popover
|
<Popover
|
||||||
placement="right"
|
placement="right"
|
||||||
overlayClassName="tags-with-hide-popover"
|
overlayClassName="tags-with-hide-popover"
|
||||||
@@ -441,6 +441,10 @@ export default (props: { curTabKey: string }): JSX.Element => {
|
|||||||
{cnt}
|
{cnt}
|
||||||
</Button>
|
</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
) : (
|
||||||
|
<Button size="small" type="link">
|
||||||
|
{cnt}
|
||||||
|
</Button>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const EditUserDrawer = forwardRef((props, ref) => {
|
|||||||
})
|
})
|
||||||
: request(api.editUser, {
|
: request(api.editUser, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: { ...formData },
|
data: { ...formData, phone: Date.now() },
|
||||||
});
|
});
|
||||||
requestPromise.then(
|
requestPromise.then(
|
||||||
(res) => {
|
(res) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const themeConfig = {
|
const themeConfig = {
|
||||||
primaryColor: '#556ee6',
|
primaryColor: '#5664FF',
|
||||||
theme: {
|
theme: {
|
||||||
'primary-color': '#556ee6',
|
'primary-color': '#5664FF',
|
||||||
'border-radius-base': '2px',
|
'border-radius-base': '2px',
|
||||||
'border-radius-sm': '2px',
|
'border-radius-sm': '2px',
|
||||||
'font-size-base': '12px',
|
'font-size-base': '12px',
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ module.exports = {
|
|||||||
proxy: {
|
proxy: {
|
||||||
'/ks-km/api/v3': {
|
'/ks-km/api/v3': {
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
target: 'http://localhost:8080/',
|
target: 'https://api-kylin-xg02.intra.xiaojukeji.com/ks-km/',
|
||||||
},
|
},
|
||||||
'/logi-security/api/v1': {
|
'/logi-security/api/v1': {
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
target: 'http://localhost:8080/',
|
target: 'https://api-kylin-xg02.intra.xiaojukeji.com/ks-km/',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
21119
km-console/packages/layout-clusters-fe/package-lock.json
generated
21119
km-console/packages/layout-clusters-fe/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -50,7 +50,7 @@
|
|||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.0.1",
|
||||||
"html-webpack-plugin": "^4.0.0",
|
"html-webpack-plugin": "^4.0.0",
|
||||||
"knowdesign": "^1.3.7",
|
"knowdesign": "1.3.7",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"react": "16.12.0",
|
"react": "16.12.0",
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export enum MetricType {
|
|||||||
Partition = 104,
|
Partition = 104,
|
||||||
Replication = 105,
|
Replication = 105,
|
||||||
Zookeeper = 110,
|
Zookeeper = 110,
|
||||||
|
Connect = 120,
|
||||||
|
Connectors = 121,
|
||||||
Controls = 901,
|
Controls = 901,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +165,7 @@ const api = {
|
|||||||
getApi(`/clusters/${clusterPhyId}/${MetricType[type].toLowerCase()}s-metadata`), // 集群节点信息
|
getApi(`/clusters/${clusterPhyId}/${MetricType[type].toLowerCase()}s-metadata`), // 集群节点信息
|
||||||
getDashboardMetricList: (clusterPhyId: string, type: MetricType) => getApi(`/clusters/${clusterPhyId}/types/${type}/user-metric-config`), // 默认选中的指标项
|
getDashboardMetricList: (clusterPhyId: string, type: MetricType) => getApi(`/clusters/${clusterPhyId}/types/${type}/user-metric-config`), // 默认选中的指标项
|
||||||
getDashboardMetricChartData: (clusterPhyId: string, type: MetricType) =>
|
getDashboardMetricChartData: (clusterPhyId: string, type: MetricType) =>
|
||||||
getApi(`/clusters/${clusterPhyId}/${MetricType[type].toLowerCase()}-metrics`), // 图表数据Z
|
getApi(`/clusters/${clusterPhyId}/${MetricType[type].toLowerCase()}-metrics`), // 图表数据
|
||||||
|
|
||||||
// ! Jobs 集群任务相关接口
|
// ! Jobs 集群任务相关接口
|
||||||
getJobsList: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/jobs-overview`),
|
getJobsList: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/jobs-overview`),
|
||||||
@@ -211,6 +213,44 @@ const api = {
|
|||||||
getZookeeperNodeData: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/znode-data`),
|
getZookeeperNodeData: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/znode-data`),
|
||||||
getZookeeperMetricsInfo: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/zookeeper-latest-metrics`),
|
getZookeeperMetricsInfo: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/zookeeper-latest-metrics`),
|
||||||
getZookeeperMetrics: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/zookeeper-metrics`),
|
getZookeeperMetrics: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/zookeeper-metrics`),
|
||||||
|
|
||||||
|
// Connector 接口
|
||||||
|
getConnectState: (clusterPhyId: string) => getApi(`/kafka-clusters/${clusterPhyId}/connect-state`),
|
||||||
|
getConnectorsList: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/connectors-overview`),
|
||||||
|
// Connector 详情
|
||||||
|
getConnectDetailMetricPoints: (connectorName: number | string, connectClusterId: number | string) =>
|
||||||
|
getApi(`/kafka-connect/clusters/${connectClusterId}/connectors/${connectorName}/latest-metrics`),
|
||||||
|
getConnectDetailTasks: (connectorName: number | string, connectClusterId: number | string) =>
|
||||||
|
getApi(`/kafka-connect/clusters/${connectClusterId}/connectors/${connectorName}/tasks`),
|
||||||
|
getConnectDetailState: (connectorName: number | string, connectClusterId: number | string) =>
|
||||||
|
getApi(`/kafka-connect/clusters/${connectClusterId}/connectors/${connectorName}/state`),
|
||||||
|
optionTasks: () => getApi(`/kafka-connect/tasks`),
|
||||||
|
// Workers 接口
|
||||||
|
getWorkersList: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/workers-overview`),
|
||||||
|
// Connector
|
||||||
|
getConnectClusters: (clusterPhyId: string) => getApi(`/kafka-clusters/${clusterPhyId}/connect-clusters-basic`),
|
||||||
|
getConnectClusterMetrics: (clusterPhyId: string) => getApi(`/kafka-clusters/${clusterPhyId}/connect-cluster-metrics`),
|
||||||
|
getConnectors: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/connectors-basic`),
|
||||||
|
getConnectorMetrics: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/connectors-metrics`),
|
||||||
|
getConnectorPlugins: (connectClusterId: number) => getApi(`/kafka-connect/clusters/${connectClusterId}/connector-plugins`),
|
||||||
|
getConnectorPluginConfig: (connectClusterId: number, pluginName: string) =>
|
||||||
|
getApi(`/kafka-connect/clusters/${connectClusterId}/connector-plugins/${pluginName}/config`),
|
||||||
|
getCurPluginConfig: (connectClusterId: number, connectorName: string) =>
|
||||||
|
getApi(`/kafka-connect/clusters/${connectClusterId}/connectors/${connectorName}/config`),
|
||||||
|
isConnectorExist: (connectClusterId: number, connectorName: string) =>
|
||||||
|
getApi(`/kafka-connect/clusters/${connectClusterId}/connectors/${connectorName}/basic-combine-exist`),
|
||||||
|
validateConnectorConfig: getApi('/kafka-connect/connectors-config/validate'),
|
||||||
|
// Connector 操作接口 新增、暂停、重启、删除
|
||||||
|
connectorsOperates: getApi('/kafka-connect/connectors'),
|
||||||
|
// 修改 Connector 配置
|
||||||
|
updateConnectorConfig: getApi('/kafka-connect/connectors-config'),
|
||||||
|
// Cluster首页修改Connect集群
|
||||||
|
batchConnectClusters: getApi(`/kafka-connect/batch-connect-clusters`),
|
||||||
|
// Cluster首页删除Connect集群
|
||||||
|
deleteConnectClusters: getApi(`/kafka-connect/connect-clusters`),
|
||||||
|
|
||||||
|
getConnectClusterBasicExit: (clusterPhyId: string, clusterPhyName: string) =>
|
||||||
|
getApi(`/kafka-clusters/${clusterPhyId}/connect-clusters/${clusterPhyName}/basic-combine-exist`),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
BIN
km-console/packages/layout-clusters-fe/src/assets/no-data.png
Normal file
BIN
km-console/packages/layout-clusters-fe/src/assets/no-data.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
@@ -111,5 +111,5 @@ export default () => {
|
|||||||
});
|
});
|
||||||
}, [routeParams.clusterId]);
|
}, [routeParams.clusterId]);
|
||||||
|
|
||||||
return <CardBar scene="broker" healthData={healthData} cardColumns={cardData} loading={loading}></CardBar>;
|
return <CardBar scene="brokers" healthData={healthData} cardColumns={cardData} loading={loading}></CardBar>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import CardBar, { healthDataProps } from './index';
|
||||||
|
import { Tooltip, Utils } from 'knowdesign';
|
||||||
|
import api from '@src/api';
|
||||||
|
import { HealthStateEnum } from '../HealthState';
|
||||||
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
interface ConnectState {
|
||||||
|
connectClusterCount: number;
|
||||||
|
workerCount: number;
|
||||||
|
aliveConnectorCount: number;
|
||||||
|
aliveTaskCount: number;
|
||||||
|
healthCheckPassed: number;
|
||||||
|
healthCheckTotal: number;
|
||||||
|
healthState: number;
|
||||||
|
totalConnectorCount: string;
|
||||||
|
totalTaskCount: number;
|
||||||
|
totalServerCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getVal = (val: string | number | undefined | null) => {
|
||||||
|
return val === undefined || val === null || val === '' ? '0' : val;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConnectCard = ({ state }: { state?: boolean }) => {
|
||||||
|
const { clusterId } = useParams<{
|
||||||
|
clusterId: string;
|
||||||
|
}>();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [cardData, setCardData] = useState([]);
|
||||||
|
const [healthData, setHealthData] = useState<healthDataProps>({
|
||||||
|
state: HealthStateEnum.UNKNOWN,
|
||||||
|
passed: 0,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getHealthData = () => {
|
||||||
|
return Utils.post(api.getMetricPointsLatest(Number(clusterId)), [
|
||||||
|
'HealthCheckPassed_Connector',
|
||||||
|
'HealthCheckTotal_Connector',
|
||||||
|
'HealthState_Connector',
|
||||||
|
]).then((data: any) => {
|
||||||
|
setHealthData({
|
||||||
|
state: data?.metrics?.['HealthState_Connector'],
|
||||||
|
passed: data?.metrics?.['HealthCheckPassed_Connector'] || 0,
|
||||||
|
total: data?.metrics?.['HealthCheckTotal_Connector'] || 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCardInfo = () => {
|
||||||
|
return Utils.request(api.getConnectState(clusterId)).then((res: ConnectState) => {
|
||||||
|
const { connectClusterCount, aliveConnectorCount, aliveTaskCount, totalConnectorCount, totalTaskCount, workerCount } = res || {};
|
||||||
|
const cardMap = [
|
||||||
|
{
|
||||||
|
title: 'Connect集群数',
|
||||||
|
value: getVal(connectClusterCount),
|
||||||
|
customStyle: {
|
||||||
|
// 自定义cardbar样式
|
||||||
|
marginLeft: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Workers',
|
||||||
|
value: getVal(workerCount),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span style={{ display: 'inline-block', marginRight: '8px' }}>Connectors</span>
|
||||||
|
<Tooltip overlayClassName="rebalance-tooltip" title="conector运行数/总数">
|
||||||
|
<InfoCircleOutlined />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
value() {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{getVal(aliveConnectorCount)}/{getVal(totalConnectorCount)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span style={{ display: 'inline-block', marginRight: '8px' }}>Tasks</span>
|
||||||
|
<Tooltip overlayClassName="rebalance-tooltip" title="Task运行数/总数">
|
||||||
|
<InfoCircleOutlined />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
value() {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{getVal(aliveTaskCount)}/{getVal(totalTaskCount)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
setCardData(cardMap);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
Promise.all([getHealthData(), getCardInfo()]).finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [clusterId, state]);
|
||||||
|
return <CardBar scene="connect" healthData={healthData} cardColumns={cardData} loading={loading}></CardBar>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConnectCard;
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/* eslint-disable react/display-name */
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
|
import CardBar from '@src/components/CardBar';
|
||||||
|
import { healthDataProps } from '.';
|
||||||
|
import { Tooltip, Utils } from 'knowdesign';
|
||||||
|
import Api from '@src/api';
|
||||||
|
import { hashDataParse } from '@src/constants/common';
|
||||||
|
import { HealthStateEnum } from '../HealthState';
|
||||||
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
|
import { stateEnum } from '@src/pages/Connect/config';
|
||||||
|
const getVal = (val: string | number | undefined | null) => {
|
||||||
|
return val === undefined || val === null || val === '' ? '0' : val;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConnectDetailCard = (props: { record: any }) => {
|
||||||
|
const { record } = props;
|
||||||
|
const urlParams = useParams<{ clusterId: string; brokerId: string }>();
|
||||||
|
const urlLocation = useLocation<any>();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [cardData, setCardData] = useState([]);
|
||||||
|
const [healthData, setHealthData] = useState<healthDataProps>({
|
||||||
|
state: HealthStateEnum.UNKNOWN,
|
||||||
|
passed: 0,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getHealthData = () => {
|
||||||
|
return Utils.post(Api.getConnectDetailMetricPoints(record.connectorName, record?.connectClusterId), [
|
||||||
|
'HealthCheckPassed',
|
||||||
|
'HealthCheckTotal',
|
||||||
|
'HealthState',
|
||||||
|
]).then((data: any) => {
|
||||||
|
setHealthData({
|
||||||
|
state: data?.metrics?.['HealthState'],
|
||||||
|
passed: data?.metrics?.['HealthCheckPassed'] || 0,
|
||||||
|
total: data?.metrics?.['HealthCheckTotal'] || 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCardInfo = () => {
|
||||||
|
return Utils.request(Api.getConnectDetailState(record.connectorName, record?.connectClusterId)).then((res: any) => {
|
||||||
|
const { type, aliveTaskCount, state, totalTaskCount, totalWorkerCount } = res || {};
|
||||||
|
const cordRightMap = [
|
||||||
|
{
|
||||||
|
title: 'Type',
|
||||||
|
value: () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
<span style={{ fontFamily: 'HelveticaNeue-Medium', fontSize: 32, color: '#212529' }}>
|
||||||
|
{Utils.firstCharUppercase(type) || '-'}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Status',
|
||||||
|
// value: Utils.firstCharUppercase(state) || '-',
|
||||||
|
value: () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
<span style={{ fontFamily: 'HelveticaNeue-Medium', fontSize: 32, color: stateEnum[state].color }}>
|
||||||
|
{Utils.firstCharUppercase(state) || '-'}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span style={{ display: 'inline-block', marginRight: '8px' }}>Tasks</span>
|
||||||
|
<Tooltip overlayClassName="rebalance-tooltip" title="Task运行数/总数">
|
||||||
|
<InfoCircleOutlined />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
value() {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{getVal(aliveTaskCount)}/{getVal(totalTaskCount)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Workers',
|
||||||
|
value: getVal(totalWorkerCount),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
setCardData(cordRightMap);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
Promise.all([getHealthData(), getCardInfo()]).finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [record]);
|
||||||
|
return (
|
||||||
|
<CardBar
|
||||||
|
record={record}
|
||||||
|
scene="connector"
|
||||||
|
healthData={healthData}
|
||||||
|
cardColumns={cardData}
|
||||||
|
showCardBg={false}
|
||||||
|
loading={loading}
|
||||||
|
></CardBar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConnectDetailCard;
|
||||||
@@ -66,5 +66,5 @@ export default () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
return <CardBar scene="topic" healthData={healthData} cardColumns={cardData} loading={loading}></CardBar>;
|
return <CardBar scene="topics" healthData={healthData} cardColumns={cardData} loading={loading}></CardBar>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
.card-bar-health {
|
.card-bar-health {
|
||||||
width: 240px;
|
// width: 240px; // 去掉固定宽度自适应
|
||||||
|
margin-right: 10px;
|
||||||
height: 70px;
|
height: 70px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export interface CardBarProps {
|
|||||||
cardColumns?: any[];
|
cardColumns?: any[];
|
||||||
healthData?: healthDataProps;
|
healthData?: healthDataProps;
|
||||||
showCardBg?: boolean;
|
showCardBg?: boolean;
|
||||||
scene: 'topic' | 'broker' | 'group' | 'zookeeper';
|
scene: 'topics' | 'brokers' | 'topic' | 'broker' | 'group' | 'zookeeper' | 'connect' | 'connector';
|
||||||
record?: any;
|
record?: any;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
needProgress?: boolean;
|
needProgress?: boolean;
|
||||||
@@ -27,16 +27,26 @@ const renderValue = (v: string | number | ((visibleType?: boolean) => JSX.Elemen
|
|||||||
return typeof v === 'function' ? v(visibleType) : v;
|
return typeof v === 'function' ? v(visibleType) : v;
|
||||||
};
|
};
|
||||||
const sceneCodeMap = {
|
const sceneCodeMap = {
|
||||||
broker: {
|
brokers: {
|
||||||
code: 1,
|
code: 1,
|
||||||
fieldName: 'brokerId',
|
fieldName: 'brokerId',
|
||||||
alias: 'Brokers',
|
alias: 'Brokers',
|
||||||
},
|
},
|
||||||
topic: {
|
broker: {
|
||||||
|
code: 1,
|
||||||
|
fieldName: 'brokerId',
|
||||||
|
alias: 'Broker',
|
||||||
|
},
|
||||||
|
topics: {
|
||||||
code: 2,
|
code: 2,
|
||||||
fieldName: 'topicName',
|
fieldName: 'topicName',
|
||||||
alias: 'Topics',
|
alias: 'Topics',
|
||||||
},
|
},
|
||||||
|
topic: {
|
||||||
|
code: 2,
|
||||||
|
fieldName: 'topicName',
|
||||||
|
alias: 'Topic',
|
||||||
|
},
|
||||||
group: {
|
group: {
|
||||||
code: 3,
|
code: 3,
|
||||||
fieldName: 'groupName',
|
fieldName: 'groupName',
|
||||||
@@ -47,6 +57,16 @@ const sceneCodeMap = {
|
|||||||
fieldName: 'zookeeperId',
|
fieldName: 'zookeeperId',
|
||||||
alias: 'Zookeeper',
|
alias: 'Zookeeper',
|
||||||
},
|
},
|
||||||
|
connect: {
|
||||||
|
code: 5,
|
||||||
|
fieldName: 'connectClusterId',
|
||||||
|
alias: 'Connect',
|
||||||
|
},
|
||||||
|
connector: {
|
||||||
|
code: 6,
|
||||||
|
fieldName: 'connectorName',
|
||||||
|
alias: 'Connector',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const CardColumnsItem: any = (cardItem: any) => {
|
const CardColumnsItem: any = (cardItem: any) => {
|
||||||
const { cardColumnsItemData, showCardBg } = cardItem;
|
const { cardColumnsItemData, showCardBg } = cardItem;
|
||||||
@@ -87,12 +107,17 @@ const CardBar = (props: CardBarProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sceneObj = sceneCodeMap[scene];
|
const sceneObj = sceneCodeMap[scene];
|
||||||
const path = record
|
const path = record
|
||||||
? api.getResourceHealthDetail(Number(routeParams.clusterId), sceneObj.code, record[sceneObj.fieldName])
|
? api.getResourceHealthDetail(
|
||||||
|
scene === 'connector' ? Number(record?.connectClusterId) : Number(routeParams.clusterId),
|
||||||
|
sceneObj.code,
|
||||||
|
record[sceneObj.fieldName]
|
||||||
|
)
|
||||||
: api.getResourceListHealthDetail(Number(routeParams.clusterId));
|
: api.getResourceListHealthDetail(Number(routeParams.clusterId));
|
||||||
const promise = record
|
const promise = record
|
||||||
? Utils.request(path)
|
? Utils.request(path)
|
||||||
: Utils.request(path, {
|
: Utils.request(path, {
|
||||||
params: { dimensionCode: sceneObj.code },
|
method: 'POST',
|
||||||
|
data: scene === 'connect' ? JSON.parse(JSON.stringify([5, 6])) : JSON.parse(JSON.stringify([sceneObj.code])),
|
||||||
});
|
});
|
||||||
promise.then((data: any[]) => {
|
promise.then((data: any[]) => {
|
||||||
setHealthCheckDetailList(data);
|
setHealthCheckDetailList(data);
|
||||||
@@ -102,6 +127,7 @@ const CardBar = (props: CardBarProps) => {
|
|||||||
{
|
{
|
||||||
title: '检查项',
|
title: '检查项',
|
||||||
dataIndex: 'checkConfig',
|
dataIndex: 'checkConfig',
|
||||||
|
width: '40%',
|
||||||
render(config: any, record: any) {
|
render(config: any, record: any) {
|
||||||
let valueGroup = {};
|
let valueGroup = {};
|
||||||
try {
|
try {
|
||||||
@@ -109,7 +135,12 @@ const CardBar = (props: CardBarProps) => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
return getConfigItemDetailDesc(record.configItem, valueGroup) || record.configDesc || '-';
|
return (
|
||||||
|
getConfigItemDetailDesc(record.configItem, valueGroup) ||
|
||||||
|
getConfigItemDetailDesc(config.configItem, valueGroup) ||
|
||||||
|
record.configDesc ||
|
||||||
|
'-'
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
@@ -119,13 +150,15 @@ const CardBar = (props: CardBarProps) => {
|
|||||||
{
|
{
|
||||||
title: '检查时间',
|
title: '检查时间',
|
||||||
dataIndex: 'updateTime',
|
dataIndex: 'updateTime',
|
||||||
|
width: '30%',
|
||||||
render: (value: number) => {
|
render: (value: number) => {
|
||||||
return moment(value).format('YYYY-MM-DD HH:mm:ss');
|
return value ? moment(value).format('YYYY-MM-DD HH:mm:ss') : '-';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '检查结果',
|
title: '检查结果',
|
||||||
dataIndex: 'passed',
|
dataIndex: 'passed',
|
||||||
|
width: '30%',
|
||||||
render(value: boolean, record: any) {
|
render(value: boolean, record: any) {
|
||||||
const icon = value ? <IconFont type="icon-zhengchang"></IconFont> : <IconFont type="icon-yichang"></IconFont>;
|
const icon = value ? <IconFont type="icon-zhengchang"></IconFont> : <IconFont type="icon-yichang"></IconFont>;
|
||||||
const txt = value ? '已通过' : '未通过';
|
const txt = value ? '已通过' : '未通过';
|
||||||
@@ -145,7 +178,7 @@ const CardBar = (props: CardBarProps) => {
|
|||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
<div className="card-bar-container">
|
<div className="card-bar-container">
|
||||||
<div className="card-bar-content">
|
<div className="card-bar-content">
|
||||||
{!loading && healthData && needProgress && (
|
{healthData && needProgress && (
|
||||||
<div className="card-bar-health">
|
<div className="card-bar-health">
|
||||||
<div className="card-bar-health-process">
|
<div className="card-bar-health-process">
|
||||||
<HealthState state={healthData?.state} width={74} height={74} />
|
<HealthState state={healthData?.state} width={74} height={74} />
|
||||||
@@ -181,7 +214,7 @@ const CardBar = (props: CardBarProps) => {
|
|||||||
onClose={(_) => setDetailDrawerVisible(false)}
|
onClose={(_) => setDetailDrawerVisible(false)}
|
||||||
visible={detailDrawerVisible}
|
visible={detailDrawerVisible}
|
||||||
>
|
>
|
||||||
<Table rowKey={'topicName'} columns={columns} dataSource={healthCheckDetailList} pagination={false} />
|
<Table rowKey={'configName'} columns={columns} dataSource={healthCheckDetailList} pagination={false} />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</Spin>
|
</Spin>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
import React, { useState, useEffect, forwardRef, useImperativeHandle, useRef } from 'react';
|
||||||
import { Drawer, Button, Space, Divider, AppContainer, ProTable, Utils } from 'knowdesign';
|
import { Drawer, Button, Space, Divider, AppContainer, ProTable, Utils } from 'knowdesign';
|
||||||
import { IconFont } from '@knowdesign/icons';
|
import { IconFont } from '@knowdesign/icons';
|
||||||
import { MetricSelect } from './index';
|
import { arrayMoveImmutable } from 'array-move';
|
||||||
import './style/indicator-drawer.less';
|
import './style/indicator-drawer.less';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
|
import api, { MetricType } from '@src/api';
|
||||||
|
import { MetricInfo, resolveMetricsRank } from '@src/constants/chartConfig';
|
||||||
|
|
||||||
interface PropsType extends React.HTMLAttributes<HTMLDivElement> {
|
export interface Inode {
|
||||||
metricSelect: MetricSelect;
|
name: string;
|
||||||
|
desc: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MetricInfo {
|
export interface MetricSelectProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
name: string;
|
metricType: MetricType;
|
||||||
unit: string;
|
hide?: boolean;
|
||||||
desc: string;
|
drawerTitle?: string;
|
||||||
|
selectedRows: (string | number)[];
|
||||||
|
checkboxProps?: (record: any) => { [props: string]: any };
|
||||||
|
tableData?: Inode[];
|
||||||
|
submitCallback?: (value: (string | number)[]) => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectedMetrics {
|
interface SelectedMetrics {
|
||||||
@@ -21,10 +28,14 @@ interface SelectedMetrics {
|
|||||||
|
|
||||||
type CategoryData = {
|
type CategoryData = {
|
||||||
category: string;
|
category: string;
|
||||||
metrics: MetricInfo[];
|
metrics: {
|
||||||
|
name: string;
|
||||||
|
unit: string;
|
||||||
|
desc: string;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const expandedRowColumns = [
|
export const expandedRowColumns = [
|
||||||
{
|
{
|
||||||
title: '指标名称',
|
title: '指标名称',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
@@ -44,16 +55,7 @@ const expandedRowColumns = [
|
|||||||
|
|
||||||
const ExpandedRow = ({ metrics, category, selectedMetrics, selectedMetricChange }: any) => {
|
const ExpandedRow = ({ metrics, category, selectedMetrics, selectedMetricChange }: any) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div>
|
||||||
style={{
|
|
||||||
position: 'relative',
|
|
||||||
padding: '12px 16px',
|
|
||||||
margin: '0 7px',
|
|
||||||
border: '1px solid #EFF2F7',
|
|
||||||
borderRadius: '8px',
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProTable
|
<ProTable
|
||||||
tableProps={{
|
tableProps={{
|
||||||
showHeader: false,
|
showHeader: false,
|
||||||
@@ -76,7 +78,7 @@ const ExpandedRow = ({ metrics, category, selectedMetrics, selectedMetricChange
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MetricSelect = forwardRef(({ metricSelect }: PropsType, ref) => {
|
export const MetricSelect = forwardRef((metricSelect: MetricSelectProps, ref) => {
|
||||||
const [global] = AppContainer.useGlobalValue();
|
const [global] = AppContainer.useGlobalValue();
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const [confirmLoading, setConfirmLoading] = useState<boolean>(false);
|
const [confirmLoading, setConfirmLoading] = useState<boolean>(false);
|
||||||
@@ -96,7 +98,11 @@ const MetricSelect = forwardRef(({ metricSelect }: PropsType, ref) => {
|
|||||||
const formateTableData = () => {
|
const formateTableData = () => {
|
||||||
const tableData = metricSelect.tableData;
|
const tableData = metricSelect.tableData;
|
||||||
const categoryData: {
|
const categoryData: {
|
||||||
[category: string]: MetricInfo[];
|
[category: string]: {
|
||||||
|
name: string;
|
||||||
|
unit: string;
|
||||||
|
desc: string;
|
||||||
|
}[];
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
tableData.forEach(({ name, desc }) => {
|
tableData.forEach(({ name, desc }) => {
|
||||||
@@ -328,4 +334,106 @@ const MetricSelect = forwardRef(({ metricSelect }: PropsType, ref) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default MetricSelect;
|
interface MetricsFilterProps {
|
||||||
|
metricType: MetricType;
|
||||||
|
onSelectChange: (list: (string | number)[], rankList: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MetricsFilter = forwardRef((props: MetricsFilterProps, ref) => {
|
||||||
|
const { metricType, onSelectChange } = props;
|
||||||
|
const { clusterId } = useParams<{
|
||||||
|
clusterId: string;
|
||||||
|
}>();
|
||||||
|
const [metricsList, setMetricsList] = useState<MetricInfo[]>([]); // 指标列表
|
||||||
|
const [metricRankList, setMetricRankList] = useState<string[]>([]);
|
||||||
|
const [selectedMetricNames, setSelectedMetricNames] = useState<(string | number)[]>(undefined); // 默认选中的指标的列表
|
||||||
|
const metricSelectRef = useRef(null);
|
||||||
|
|
||||||
|
// 更新指标
|
||||||
|
const setMetricList = (metricDetailDTOList: { metric: string; rank: number; set: boolean }[]) => {
|
||||||
|
return Utils.request(api.getDashboardMetricList(clusterId, metricType), {
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
metricDetailDTOList,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 图表展示顺序变更
|
||||||
|
const rankChange = (oldIndex: number, newIndex: number) => {
|
||||||
|
const newList = arrayMoveImmutable(metricRankList, oldIndex, newIndex);
|
||||||
|
setMetricRankList(newList);
|
||||||
|
setMetricList(newList.map((metric, rank) => ({ metric, rank, set: metricsList.find(({ name }) => metric === name)?.set || false })));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新 rank
|
||||||
|
const updateRank = (metricList: MetricInfo[]) => {
|
||||||
|
const { list, listInfo, shouldUpdate } = resolveMetricsRank(metricList);
|
||||||
|
setMetricRankList(list);
|
||||||
|
if (shouldUpdate) {
|
||||||
|
setMetricList(listInfo);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取指标列表
|
||||||
|
const getMetricList = () => {
|
||||||
|
Utils.request(api.getDashboardMetricList(clusterId, metricType)).then((res: MetricInfo[] | null) => {
|
||||||
|
if (!res) return;
|
||||||
|
const supportMetrics = res.filter((metric) => metric.support);
|
||||||
|
const selectedMetrics = supportMetrics.filter((metric) => metric.set).map((metric) => metric.name);
|
||||||
|
updateRank([...supportMetrics]);
|
||||||
|
setMetricsList(supportMetrics);
|
||||||
|
setSelectedMetricNames(selectedMetrics);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 指标选中项更新回调
|
||||||
|
const metricSelectCallback = (newMetricNames: (string | number)[]) => {
|
||||||
|
const updateMetrics: { metric: string; set: boolean; rank: number }[] = [];
|
||||||
|
// 需要选中的指标
|
||||||
|
newMetricNames.forEach(
|
||||||
|
(name) =>
|
||||||
|
!selectedMetricNames.includes(name) &&
|
||||||
|
updateMetrics.push({ metric: name as string, set: true, rank: metricsList.find(({ name: metric }) => metric === name)?.rank })
|
||||||
|
);
|
||||||
|
// 取消选中的指标
|
||||||
|
selectedMetricNames.forEach(
|
||||||
|
(name) =>
|
||||||
|
!newMetricNames.includes(name) &&
|
||||||
|
updateMetrics.push({ metric: name as string, set: false, rank: metricsList.find(({ name: metric }) => metric === name)?.rank })
|
||||||
|
);
|
||||||
|
|
||||||
|
const requestPromise = Object.keys(updateMetrics).length ? setMetricList(updateMetrics) : Promise.resolve();
|
||||||
|
requestPromise.then(
|
||||||
|
() => getMetricList(),
|
||||||
|
() => getMetricList()
|
||||||
|
);
|
||||||
|
|
||||||
|
return requestPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onSelectChange(selectedMetricNames, metricRankList);
|
||||||
|
}, [selectedMetricNames, metricRankList]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getMetricList();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
rankChange,
|
||||||
|
open: () => metricSelectRef.current?.open(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MetricSelect
|
||||||
|
ref={metricSelectRef}
|
||||||
|
metricType={metricType}
|
||||||
|
tableData={metricsList}
|
||||||
|
selectedRows={selectedMetricNames}
|
||||||
|
submitCallback={metricSelectCallback}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default MetricsFilter;
|
||||||
|
|||||||
@@ -1,203 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Radio, Input, Popover, Space, Checkbox, Row, Col, Button } from 'knowdesign';
|
|
||||||
import { IconFont } from '@knowdesign/icons';
|
|
||||||
import { InodeScopeModule } from './index';
|
|
||||||
import './style/node-scope.less';
|
|
||||||
|
|
||||||
interface propsType {
|
|
||||||
change: Function;
|
|
||||||
nodeScopeModule: InodeScopeModule;
|
|
||||||
}
|
|
||||||
|
|
||||||
const OptionsDefault = [
|
|
||||||
{
|
|
||||||
label: 'Top 5',
|
|
||||||
value: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Top 10',
|
|
||||||
value: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Top 15',
|
|
||||||
value: 15,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const NodeScope = ({ nodeScopeModule, change }: propsType) => {
|
|
||||||
const {
|
|
||||||
customScopeList: customList,
|
|
||||||
scopeName = '',
|
|
||||||
scopeLabel = '自定义范围',
|
|
||||||
searchPlaceholder = '输入内容进行搜索',
|
|
||||||
} = nodeScopeModule;
|
|
||||||
const [topNum, setTopNum] = useState<number>(5);
|
|
||||||
const [isTop, setIsTop] = useState(true);
|
|
||||||
const [audioOptions, setAudioOptions] = useState(OptionsDefault);
|
|
||||||
const [scopeSearchValue, setScopeSearchValue] = useState('');
|
|
||||||
const [inputValue, setInputValue] = useState<string>(null);
|
|
||||||
const [indeterminate, setIndeterminate] = useState(false);
|
|
||||||
const [popVisible, setPopVisible] = useState(false);
|
|
||||||
const [checkAll, setCheckAll] = useState(false);
|
|
||||||
const [checkedListTemp, setCheckedListTemp] = useState([]);
|
|
||||||
const [checkedList, setCheckedList] = useState([]);
|
|
||||||
const [allCheckedList, setAllCheckedList] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const all = customList?.map((item) => item.value) || [];
|
|
||||||
setAllCheckedList(all);
|
|
||||||
}, [customList]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (topNum) {
|
|
||||||
const timeOption = audioOptions.find((item) => item.value === topNum);
|
|
||||||
|
|
||||||
setInputValue(timeOption?.label);
|
|
||||||
setCheckedListTemp([]);
|
|
||||||
setCheckedList([]);
|
|
||||||
|
|
||||||
setPopVisible(false);
|
|
||||||
}
|
|
||||||
}, [topNum]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIndeterminate(!!checkedListTemp.length && checkedListTemp.length < allCheckedList.length);
|
|
||||||
setCheckAll(checkedListTemp?.length === allCheckedList.length);
|
|
||||||
}, [checkedListTemp]);
|
|
||||||
|
|
||||||
const customSure = () => {
|
|
||||||
if (checkedListTemp?.length > 0) {
|
|
||||||
setCheckedList(checkedListTemp);
|
|
||||||
change(checkedListTemp, false);
|
|
||||||
setIsTop(false);
|
|
||||||
setTopNum(null);
|
|
||||||
setInputValue(`${checkedListTemp?.length}项`);
|
|
||||||
setPopVisible(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const customCancel = () => {
|
|
||||||
setCheckedListTemp(checkedList);
|
|
||||||
setPopVisible(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const visibleChange = (visible: any) => {
|
|
||||||
setCheckedListTemp(checkedList);
|
|
||||||
setPopVisible(visible);
|
|
||||||
};
|
|
||||||
|
|
||||||
const periodtimeChange = (e: any) => {
|
|
||||||
const topNum = e.target.value;
|
|
||||||
setTopNum(topNum);
|
|
||||||
change(topNum, true);
|
|
||||||
setIsTop(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCheckAllChange = (e: any) => {
|
|
||||||
setCheckedListTemp(e.target.checked ? allCheckedList : []);
|
|
||||||
setIndeterminate(false);
|
|
||||||
setCheckAll(e.target.checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkChange = (val: any) => {
|
|
||||||
setCheckedListTemp(val);
|
|
||||||
// setIndeterminate(!!val.length && val.length < allCheckedList.length);
|
|
||||||
// setCheckAll(val?.length === allCheckedList.length);
|
|
||||||
};
|
|
||||||
|
|
||||||
const clickContent = (
|
|
||||||
<div className="dd-node-scope-module">
|
|
||||||
{/* <span>时间:</span> */}
|
|
||||||
<div className="flx_con">
|
|
||||||
<div className="flx_l">
|
|
||||||
<h6 className="time_title">选择 top 范围</h6>
|
|
||||||
<Radio.Group
|
|
||||||
optionType="button"
|
|
||||||
buttonStyle="solid"
|
|
||||||
className="topNum-radio-group"
|
|
||||||
// options={audioOptions}
|
|
||||||
onChange={periodtimeChange}
|
|
||||||
value={topNum}
|
|
||||||
>
|
|
||||||
<Space direction="vertical" size={16}>
|
|
||||||
{audioOptions.map((item, index) => (
|
|
||||||
<Radio value={item.value} key={index}>
|
|
||||||
{item.label}
|
|
||||||
</Radio>
|
|
||||||
))}
|
|
||||||
</Space>
|
|
||||||
</Radio.Group>
|
|
||||||
</div>
|
|
||||||
<div className="flx_r">
|
|
||||||
<h6 className="time_title">{scopeLabel}</h6>
|
|
||||||
<div className="custom-scope">
|
|
||||||
<div className="check-row">
|
|
||||||
<Checkbox className="check-all" indeterminate={indeterminate} onChange={onCheckAllChange} checked={checkAll}>
|
|
||||||
全选
|
|
||||||
</Checkbox>
|
|
||||||
<Input
|
|
||||||
className="search-input"
|
|
||||||
suffix={<IconFont type="icon-fangdajing" style={{ fontSize: '16px' }} />}
|
|
||||||
size="small"
|
|
||||||
placeholder={searchPlaceholder}
|
|
||||||
onChange={(e) => setScopeSearchValue(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="fixed-height">
|
|
||||||
<Checkbox.Group style={{ width: '100%' }} onChange={checkChange} value={checkedListTemp}>
|
|
||||||
<Row gutter={[10, 12]}>
|
|
||||||
{customList
|
|
||||||
.filter((item) => item.label.includes(scopeSearchValue))
|
|
||||||
.map((item) => (
|
|
||||||
<Col span={12} key={item.value}>
|
|
||||||
<Checkbox value={item.value}>{item.label}</Checkbox>
|
|
||||||
</Col>
|
|
||||||
))}
|
|
||||||
</Row>
|
|
||||||
</Checkbox.Group>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="btn-con">
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
className="btn-sure"
|
|
||||||
onClick={customSure}
|
|
||||||
disabled={checkedListTemp?.length > 0 ? false : true}
|
|
||||||
>
|
|
||||||
确定
|
|
||||||
</Button>
|
|
||||||
<Button size="small" onClick={customCancel}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div id="d-node-scope">
|
|
||||||
<div className="scope-title">{scopeName}筛选:</div>
|
|
||||||
<Popover
|
|
||||||
trigger={['click']}
|
|
||||||
visible={popVisible}
|
|
||||||
content={clickContent}
|
|
||||||
placement="bottomRight"
|
|
||||||
overlayClassName="d-node-scope-popover large-size"
|
|
||||||
onVisibleChange={visibleChange}
|
|
||||||
>
|
|
||||||
<span className="input-span">
|
|
||||||
<Input
|
|
||||||
className={isTop ? 'relativeTime d-node-scope-input' : 'absoluteTime d-node-scope-input'}
|
|
||||||
value={inputValue}
|
|
||||||
readOnly={true}
|
|
||||||
suffix={<IconFont type="icon-jiantou1" rotate={90} style={{ color: '#74788D' }}></IconFont>}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NodeScope;
|
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import React, { useState, useEffect, useRef, PropsWithChildren } from 'react';
|
||||||
|
import { Radio, Input, Popover, Space, Checkbox, Row, Col, Button } from 'knowdesign';
|
||||||
|
import { IconFont } from '@knowdesign/icons';
|
||||||
|
import './style/node-scope.less';
|
||||||
|
|
||||||
|
interface NodeSelectProps {
|
||||||
|
name?: string;
|
||||||
|
onChange: (data: any, isTop: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOP_SELECT_OPTIONS = [
|
||||||
|
{
|
||||||
|
label: 'Top 5',
|
||||||
|
value: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Top 10',
|
||||||
|
value: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Top 15',
|
||||||
|
value: 15,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const NodeSelect = ({ name, onChange, children }: PropsWithChildren<NodeSelectProps>) => {
|
||||||
|
const [topNum, setTopNum] = useState<number>(5);
|
||||||
|
const [isTop, setIsTop] = useState(true);
|
||||||
|
const [audioOptions] = useState(TOP_SELECT_OPTIONS);
|
||||||
|
const [inputValue, setInputValue] = useState<string>(null);
|
||||||
|
const [popVisible, setPopVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (topNum) {
|
||||||
|
const timeOption = audioOptions.find((item) => item.value === topNum);
|
||||||
|
|
||||||
|
setInputValue(timeOption?.label);
|
||||||
|
|
||||||
|
setPopVisible(false);
|
||||||
|
}
|
||||||
|
}, [topNum]);
|
||||||
|
|
||||||
|
const visibleChange = (visible: any) => {
|
||||||
|
setPopVisible(visible);
|
||||||
|
};
|
||||||
|
|
||||||
|
const periodtimeChange = (e: any) => {
|
||||||
|
const topNum = e.target.value;
|
||||||
|
setTopNum(topNum);
|
||||||
|
onChange(topNum, true);
|
||||||
|
setIsTop(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clickContent = (
|
||||||
|
<div className="dd-node-scope-module">
|
||||||
|
<div className="flx_con">
|
||||||
|
<div className="flx_l">
|
||||||
|
<h6 className="time_title">选择 top 范围</h6>
|
||||||
|
<Radio.Group optionType="button" buttonStyle="solid" className="topNum-radio-group" onChange={periodtimeChange} value={topNum}>
|
||||||
|
<Space direction="vertical" size={16}>
|
||||||
|
{audioOptions.map((item, index) => (
|
||||||
|
<Radio value={item.value} key={index}>
|
||||||
|
{item.label}
|
||||||
|
</Radio>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</Radio.Group>
|
||||||
|
</div>
|
||||||
|
<div className="flx_r">
|
||||||
|
{children ? (
|
||||||
|
React.Children.map(children, (child) => {
|
||||||
|
return React.cloneElement(child as React.ReactElement<any>, {
|
||||||
|
isTop,
|
||||||
|
visibleChange: visibleChange,
|
||||||
|
onChange: (data: any, inputValue: string) => {
|
||||||
|
onChange(data, false);
|
||||||
|
setIsTop(false);
|
||||||
|
setTopNum(null);
|
||||||
|
setInputValue(inputValue);
|
||||||
|
setPopVisible(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div id="d-node-scope">
|
||||||
|
<div className="scope-title">{name}筛选:</div>
|
||||||
|
<Popover
|
||||||
|
trigger={['click']}
|
||||||
|
visible={popVisible}
|
||||||
|
content={clickContent}
|
||||||
|
placement="bottomRight"
|
||||||
|
overlayClassName="d-node-scope-popover large-size"
|
||||||
|
onVisibleChange={visibleChange}
|
||||||
|
>
|
||||||
|
<span className="input-span">
|
||||||
|
<Input
|
||||||
|
className={isTop ? 'relativeTime d-node-scope-input' : 'absoluteTime d-node-scope-input'}
|
||||||
|
value={inputValue}
|
||||||
|
readOnly={true}
|
||||||
|
suffix={<IconFont type="icon-jiantou1" rotate={90} style={{ color: '#74788D' }}></IconFont>}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NodeSelect;
|
||||||
@@ -3,16 +3,8 @@ import { Select, Divider, Button } from 'knowdesign';
|
|||||||
import { IconFont } from '@knowdesign/icons';
|
import { IconFont } from '@knowdesign/icons';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { DRangeTime } from 'knowdesign';
|
import { DRangeTime } from 'knowdesign';
|
||||||
import MetricSelect from './MetricSelect';
|
import NodeSelect from './NodeSelect';
|
||||||
import NodeScope from './NodeScope';
|
|
||||||
|
|
||||||
import './style/index.less';
|
import './style/index.less';
|
||||||
import { MetricType } from 'src/api';
|
|
||||||
|
|
||||||
export interface Inode {
|
|
||||||
name: string;
|
|
||||||
desc: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface KsHeaderOptions {
|
export interface KsHeaderOptions {
|
||||||
rangeTime: [number, number];
|
rangeTime: [number, number];
|
||||||
@@ -21,18 +13,9 @@ export interface KsHeaderOptions {
|
|||||||
gridNum?: number;
|
gridNum?: number;
|
||||||
scopeData?: {
|
scopeData?: {
|
||||||
isTop: boolean;
|
isTop: boolean;
|
||||||
data: number | number[];
|
data: any;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export interface MetricSelect {
|
|
||||||
metricType: MetricType;
|
|
||||||
hide?: boolean;
|
|
||||||
drawerTitle?: string;
|
|
||||||
selectedRows: (string | number)[];
|
|
||||||
checkboxProps?: (record: any) => { [props: string]: any };
|
|
||||||
tableData?: Inode[];
|
|
||||||
submitCallback?: (value: (string | number)[]) => Promise<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IfilterData {
|
export interface IfilterData {
|
||||||
hostName?: string;
|
hostName?: string;
|
||||||
@@ -41,25 +24,15 @@ export interface IfilterData {
|
|||||||
agent?: string;
|
agent?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IcustomScope {
|
|
||||||
label: string;
|
|
||||||
value: string | number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InodeScopeModule {
|
|
||||||
customScopeList: IcustomScope[];
|
|
||||||
scopeName?: string;
|
|
||||||
scopeLabel?: string;
|
|
||||||
searchPlaceholder?: string;
|
|
||||||
change?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PropsType {
|
interface PropsType {
|
||||||
metricSelect?: MetricSelect;
|
|
||||||
hideNodeScope?: boolean;
|
hideNodeScope?: boolean;
|
||||||
hideGridSelect?: boolean;
|
hideGridSelect?: boolean;
|
||||||
nodeScopeModule?: InodeScopeModule;
|
nodeSelect?: {
|
||||||
|
name?: string;
|
||||||
|
customContent?: React.ReactElement<any>;
|
||||||
|
};
|
||||||
onChange: (options: KsHeaderOptions) => void;
|
onChange: (options: KsHeaderOptions) => void;
|
||||||
|
openMetricFilter: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ScopeData {
|
interface ScopeData {
|
||||||
@@ -84,15 +57,12 @@ const GRID_SIZE_OPTIONS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const MetricOperateBar = ({
|
const MetricOperateBar = ({
|
||||||
metricSelect,
|
nodeSelect = {},
|
||||||
nodeScopeModule = {
|
|
||||||
customScopeList: [],
|
|
||||||
},
|
|
||||||
hideNodeScope = false,
|
hideNodeScope = false,
|
||||||
hideGridSelect = false,
|
hideGridSelect = false,
|
||||||
onChange: onChangeCallback,
|
onChange: onChangeCallback,
|
||||||
|
openMetricFilter,
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
const metricSelectRef = useRef(null);
|
|
||||||
const [gridNum, setGridNum] = useState<number>(GRID_SIZE_OPTIONS[1].value);
|
const [gridNum, setGridNum] = useState<number>(GRID_SIZE_OPTIONS[1].value);
|
||||||
const [rangeTime, setRangeTime] = useState<[number, number]>(() => {
|
const [rangeTime, setRangeTime] = useState<[number, number]>(() => {
|
||||||
const curTimeStamp = moment().valueOf();
|
const curTimeStamp = moment().valueOf();
|
||||||
@@ -170,20 +140,22 @@ const MetricOperateBar = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="header-right">
|
<div className="header-right">
|
||||||
{/* 节点范围 */}
|
{/* 节点范围 */}
|
||||||
{!hideNodeScope && <NodeScope nodeScopeModule={nodeScopeModule} change={nodeScopeChange} />}
|
{!hideNodeScope && (
|
||||||
|
<NodeSelect name={nodeSelect.name || ''} onChange={nodeScopeChange}>
|
||||||
|
{nodeSelect.customContent}
|
||||||
|
</NodeSelect>
|
||||||
|
)}
|
||||||
{/* 分栏 */}
|
{/* 分栏 */}
|
||||||
{!hideGridSelect && (
|
{!hideGridSelect && (
|
||||||
<Select className="grid-select" style={{ width: 70 }} value={gridNum} options={GRID_SIZE_OPTIONS} onChange={sizeChange} />
|
<Select className="grid-select" style={{ width: 70 }} value={gridNum} options={GRID_SIZE_OPTIONS} onChange={sizeChange} />
|
||||||
)}
|
)}
|
||||||
{(!hideNodeScope || !hideGridSelect) && <Divider type="vertical" style={{ height: 20, top: 0 }} />}
|
{(!hideNodeScope || !hideGridSelect) && <Divider type="vertical" style={{ height: 20, top: 0 }} />}
|
||||||
<Button type="primary" onClick={() => metricSelectRef.current.open()}>
|
<Button type="primary" onClick={() => openMetricFilter()}>
|
||||||
指标筛选
|
指标筛选
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* 指标筛选 */}
|
|
||||||
{!metricSelect?.hide && <MetricSelect ref={metricSelectRef} metricSelect={metricSelect} />}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,135 +2,15 @@
|
|||||||
.dcloud-drawer-body {
|
.dcloud-drawer-body {
|
||||||
padding-top: 2px !important;
|
padding-top: 2px !important;
|
||||||
}
|
}
|
||||||
|
.dcloud-table-cell.dcloud-table-selection-column {
|
||||||
|
padding: 11px 0;
|
||||||
|
}
|
||||||
|
.dcloud-table .dcloud-table-tbody .dcloud-table-cell:nth-child(1) {
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
.dcloud-table-expanded-row {
|
||||||
|
> td {
|
||||||
|
padding: 0 0 0 56px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// .dd-indicator-drawer {
|
|
||||||
// @drawerItemH: 27px;
|
|
||||||
// @primary-color: #556ee6;
|
|
||||||
// &.contain-tab {
|
|
||||||
// .@{ant-prefix}-drawer-body {
|
|
||||||
// padding: 0 20px 20px 20px;
|
|
||||||
// }
|
|
||||||
// .@{ant-prefix}-layout-sider {
|
|
||||||
// height: ~'calc(100vh - 268px)';
|
|
||||||
// }
|
|
||||||
// .@{ant-prefix}-spin-container {
|
|
||||||
// height: ~'calc(100vh - 268px - 12px)';
|
|
||||||
// .@{ant-prefix}-table {
|
|
||||||
// height: ~'calc(100% - 36px)';
|
|
||||||
// overflow: auto;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .hide {
|
|
||||||
// display: none;
|
|
||||||
// }
|
|
||||||
// .@{ant-prefix}-drawer-body {
|
|
||||||
// padding: 12px 20px;
|
|
||||||
// .label-name {
|
|
||||||
// height: 34px;
|
|
||||||
// line-height: 34px;
|
|
||||||
// font-size: 13px;
|
|
||||||
// color: #495057;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .@{ant-prefix}-spin-container {
|
|
||||||
// height: ~'calc(100vh - 218px - 12px)';
|
|
||||||
// .@{ant-prefix}-table {
|
|
||||||
// height: ~'calc(100% - 36px)';
|
|
||||||
// overflow: auto;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .@{ant-prefix}-layout-sider {
|
|
||||||
// height: ~'calc(100vh - 218px)';
|
|
||||||
// overflow: auto;
|
|
||||||
// }
|
|
||||||
// .@{ant-prefix}-menu-overflow {
|
|
||||||
// // display: inline-flex;
|
|
||||||
// &::before,
|
|
||||||
// &::after {
|
|
||||||
// width: 0;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// .@{ant-prefix}-tree {
|
|
||||||
// padding: 20px;
|
|
||||||
// &.@{ant-prefix}-tree-directory {
|
|
||||||
// .@{ant-prefix}-tree-treenode {
|
|
||||||
// padding-right: 24px;
|
|
||||||
// &:not(.@{ant-prefix}-tree-treenode-switcher-open) {
|
|
||||||
// padding-right: 0;
|
|
||||||
// }
|
|
||||||
// &:not(.@{ant-prefix}-tree-treenode-switcher-close) {
|
|
||||||
// padding-right: 0;
|
|
||||||
// }
|
|
||||||
// &.@{ant-prefix}-tree-treenode-selected {
|
|
||||||
// &::before {
|
|
||||||
// background: transparent;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// height: 27px;
|
|
||||||
// &::before {
|
|
||||||
// bottom: 0;
|
|
||||||
// }
|
|
||||||
// .@{ant-prefix}-tree-switcher {
|
|
||||||
// position: absolute;
|
|
||||||
// right: 0;
|
|
||||||
// z-index: 8;
|
|
||||||
// line-height: @drawerItemH;
|
|
||||||
// color: #495057;
|
|
||||||
// .anticon {
|
|
||||||
// vertical-align: middle;
|
|
||||||
// font-size: 16px;
|
|
||||||
// transform: rotate(90deg);
|
|
||||||
// }
|
|
||||||
// &_close {
|
|
||||||
// .@{ant-prefix}-tree-switcher-icon svg {
|
|
||||||
// transform: rotate(-180deg);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .@{ant-prefix}-tree-node-content-wrapper {
|
|
||||||
// padding-left: 4px;
|
|
||||||
// width: 100%;
|
|
||||||
// height: @drawerItemH;
|
|
||||||
// line-height: @drawerItemH;
|
|
||||||
// min-height: @drawerItemH;
|
|
||||||
// font-weight: bold;
|
|
||||||
// color: #495057;
|
|
||||||
// &.@{ant-prefix}-tree-node-content-wrapper-normal {
|
|
||||||
// font-weight: normal;
|
|
||||||
// color: #74788d;
|
|
||||||
// .@{ant-prefix}-tree-title {
|
|
||||||
// width: ~'calc(100%)';
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// &.@{ant-prefix}-tree-node-selected {
|
|
||||||
// color: @primary-color;
|
|
||||||
// }
|
|
||||||
// .@{ant-prefix}-tree-icon__customize {
|
|
||||||
// line-height: 22px;
|
|
||||||
// .anticon {
|
|
||||||
// vertical-align: middle;
|
|
||||||
// font-size: 16px;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .@{ant-prefix}-tree-iconEle {
|
|
||||||
// width: auto;
|
|
||||||
// margin-right: 8px;
|
|
||||||
// }
|
|
||||||
// .@{ant-prefix}-tree-title {
|
|
||||||
// display: inline-block;
|
|
||||||
// width: ~'calc(100% - 24px)';
|
|
||||||
// overflow: hidden;
|
|
||||||
// overflow: hidden;
|
|
||||||
// text-overflow: ellipsis;
|
|
||||||
// white-space: nowrap;
|
|
||||||
// & > span {
|
|
||||||
// padding-left: 3px;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { MetricType } from '@src/api';
|
||||||
|
import { FormattedMetricData } from '@src/constants/chartConfig';
|
||||||
|
import { AppContainer, Empty, SingleChart, Spin, Tooltip } from 'knowdesign';
|
||||||
|
import { arrayMoveImmutable } from 'array-move';
|
||||||
|
import DragGroup from '../DragGroup';
|
||||||
|
import { IconFont } from '@knowdesign/icons';
|
||||||
|
import { getChartConfig } from './config';
|
||||||
|
import { EventBus } from 'knowdesign/lib/utils/event-bus';
|
||||||
|
|
||||||
|
const DRAG_GROUP_GUTTER_NUM: [number, number] = [16, 16];
|
||||||
|
|
||||||
|
interface ChartListProps {
|
||||||
|
busInstance: EventBus;
|
||||||
|
loading: boolean;
|
||||||
|
gridNum: number;
|
||||||
|
data: FormattedMetricData[];
|
||||||
|
autoReload: boolean;
|
||||||
|
dragCallback: (oldIndex: number, newIndex: number) => void;
|
||||||
|
onExpand: (metricName: string, metricType: MetricType) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChartList = (props: ChartListProps) => {
|
||||||
|
const { loading, gridNum, data, autoReload, busInstance, dragCallback, onExpand } = props;
|
||||||
|
const [global] = AppContainer.useGlobalValue();
|
||||||
|
const [chartData, setChartData] = useState(data);
|
||||||
|
|
||||||
|
// 拖拽开始回调,触发图表的 onDrag 事件( 设置为 true ),禁止同步展示图表的 tooltip
|
||||||
|
const dragStart = () => {
|
||||||
|
busInstance.emit('onDrag', true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 拖拽结束回调,更新图表顺序,并触发图表的 onDrag 事件( 设置为 false ),允许同步展示图表的 tooltip
|
||||||
|
const dragEnd = ({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) => {
|
||||||
|
dragCallback(oldIndex, newIndex);
|
||||||
|
busInstance.emit('onDrag', false);
|
||||||
|
setChartData(arrayMoveImmutable(chartData, oldIndex, newIndex));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听盒子宽度变化,重置图表宽度
|
||||||
|
const observeDashboardWidthChange = () => {
|
||||||
|
const targetNode = document.getElementsByClassName('dcd-two-columns-layout-sider-footer')[0];
|
||||||
|
targetNode && targetNode.addEventListener('click', () => busInstance.emit('chartResize'));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setChartData(data);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => observeDashboardWidthChange());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="topic-dashboard-container">
|
||||||
|
<Spin spinning={loading} style={{ height: 400 }}>
|
||||||
|
{chartData && chartData.length ? (
|
||||||
|
<div className="no-group-con">
|
||||||
|
<DragGroup
|
||||||
|
sortableContainerProps={{
|
||||||
|
onSortStart: dragStart,
|
||||||
|
onSortEnd: dragEnd,
|
||||||
|
axis: 'xy',
|
||||||
|
useDragHandle: true,
|
||||||
|
}}
|
||||||
|
gridProps={{
|
||||||
|
span: gridNum,
|
||||||
|
gutter: DRAG_GROUP_GUTTER_NUM,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{chartData.map((data) => {
|
||||||
|
const { metricName, metricType, metricUnit, metricLines, showLegend } = data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={metricName} className="dashboard-drag-item-box">
|
||||||
|
<div className="dashboard-drag-item-box-title">
|
||||||
|
<Tooltip
|
||||||
|
placement="topLeft"
|
||||||
|
title={() => {
|
||||||
|
let content = '';
|
||||||
|
const metricDefine = global.getMetricDefine(metricType, metricName);
|
||||||
|
if (metricDefine) {
|
||||||
|
content = metricDefine.desc;
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<span className="name">{metricName}</span>
|
||||||
|
<span className="unit">({metricUnit})</span>
|
||||||
|
{(metricType === MetricType.Connect || metricType === MetricType.Connectors) && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
padding: '2px 8px',
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: 12,
|
||||||
|
background: metricType === MetricType.Connect ? '#ECECF6' : 'rgba(85,110,230,0.10)',
|
||||||
|
color: metricType === MetricType.Connect ? '#495057' : '#5664FF',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{metricType === MetricType.Connect ? 'Cluster' : 'Connector'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className="expand-icon-box" onClick={() => onExpand(metricName, metricType)}>
|
||||||
|
<IconFont type="icon-chuangkoufangda" className="expand-icon" />
|
||||||
|
</div>
|
||||||
|
<SingleChart
|
||||||
|
chartKey={metricName}
|
||||||
|
chartTypeProp="line"
|
||||||
|
showHeader={false}
|
||||||
|
wrapStyle={{
|
||||||
|
width: 'auto',
|
||||||
|
height: 222,
|
||||||
|
}}
|
||||||
|
connectEventName={`${metricType}BoardDragChart`}
|
||||||
|
eventBus={busInstance}
|
||||||
|
propChartData={metricLines}
|
||||||
|
optionMergeProps={{ replaceMerge: autoReload ? ['xAxis'] : ['series'] }}
|
||||||
|
{...getChartConfig(`${metricName}{unit|(${metricUnit})}`, metricLines.length, showLegend)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DragGroup>
|
||||||
|
</div>
|
||||||
|
) : loading ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
|
<Empty description="数据为空,请选择指标或刷新" image={Empty.PRESENTED_IMAGE_CUSTOM} style={{ padding: '100px 0' }} />
|
||||||
|
)}
|
||||||
|
</Spin>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChartList;
|
||||||
@@ -165,12 +165,28 @@ const ChartDetail = (props: ChartDetailProps) => {
|
|||||||
|
|
||||||
// 请求图表数据
|
// 请求图表数据
|
||||||
const getMetricChartData = ([startTime, endTime]: readonly [number, number]) => {
|
const getMetricChartData = ([startTime, endTime]: readonly [number, number]) => {
|
||||||
return Utils.post(api.getDashboardMetricChartData(clusterId, metricType), {
|
const getQueryUrl = () => {
|
||||||
|
switch (metricType) {
|
||||||
|
case MetricType.Connect: {
|
||||||
|
return api.getConnectClusterMetrics(clusterId);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return api.getDashboardMetricChartData(clusterId, metricType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const queryMap = {
|
||||||
|
[MetricType.Broker]: 'brokerIds',
|
||||||
|
[MetricType.Topic]: 'topics',
|
||||||
|
[MetricType.Connect]: 'connectClusterIdList',
|
||||||
|
[MetricType.Connectors]: 'connectorNameList',
|
||||||
|
};
|
||||||
|
return Utils.post(getQueryUrl(), {
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
metricsNames: [metricName],
|
metricsNames: [metricName],
|
||||||
topNu: null,
|
topNu: null,
|
||||||
[metricType === MetricType.Broker ? 'brokerIds' : 'topics']: queryLines,
|
[queryMap[metricType as keyof typeof queryMap]]: queryLines,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { arrayMoveImmutable } from 'array-move';
|
import { Utils, AppContainer, Checkbox, Input, Row, Col, Button } from 'knowdesign';
|
||||||
import { Utils, Empty, Spin, AppContainer, SingleChart, Tooltip } from 'knowdesign';
|
|
||||||
import { IconFont } from '@knowdesign/icons';
|
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import api, { MetricType } from '@src/api';
|
import api, { MetricType } from '@src/api';
|
||||||
import { MetricInfo, OriginMetricData, FormattedMetricData, formatChartData, resolveMetricsRank } from '@src/constants/chartConfig';
|
import { OriginMetricData, FormattedMetricData, formatChartData } from '@src/constants/chartConfig';
|
||||||
import ChartOperateBar, { KsHeaderOptions } from '../ChartOperateBar';
|
import ChartOperateBar, { KsHeaderOptions } from '../ChartOperateBar';
|
||||||
import DragGroup from '../DragGroup';
|
|
||||||
import ChartDetail from './Detail';
|
import ChartDetail from './Detail';
|
||||||
import { getChartConfig, getMetricDashboardReq } from './config';
|
import { getMetricDashboardReq } from './config';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
|
import MetricsFilter from '../ChartOperateBar/MetricSelect';
|
||||||
|
import ChartList from './ChartList';
|
||||||
|
import { IconFont } from '@knowdesign/icons';
|
||||||
|
|
||||||
interface IcustomScope {
|
interface IcustomScope {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -25,7 +25,111 @@ type PropsType = {
|
|||||||
const { EventBus } = Utils;
|
const { EventBus } = Utils;
|
||||||
const busInstance = new EventBus();
|
const busInstance = new EventBus();
|
||||||
|
|
||||||
const DRAG_GROUP_GUTTER_NUM: [number, number] = [16, 16];
|
interface SelectContentProps {
|
||||||
|
title: string;
|
||||||
|
list: {
|
||||||
|
label: string;
|
||||||
|
value: string | number;
|
||||||
|
}[];
|
||||||
|
isTop?: boolean;
|
||||||
|
visibleChange?: (v: boolean) => void;
|
||||||
|
onChange?: (list: any[], inputValue: string) => void;
|
||||||
|
searchPlaceholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SelectContent = (props: SelectContentProps) => {
|
||||||
|
const { searchPlaceholder = '输入内容进行搜索', list, isTop, visibleChange, onChange } = props;
|
||||||
|
const [scopeSearchValue, setScopeSearchValue] = useState('');
|
||||||
|
// 全选属性
|
||||||
|
const [indeterminate, setIndeterminate] = useState(false);
|
||||||
|
const [checkAll, setCheckAll] = useState(false);
|
||||||
|
const [checkedListTemp, setCheckedListTemp] = useState([]);
|
||||||
|
const [allCheckedList, setAllCheckedList] = useState([]);
|
||||||
|
const defaultChecked = useRef([]);
|
||||||
|
|
||||||
|
const customSure = () => {
|
||||||
|
defaultChecked.current = checkedListTemp;
|
||||||
|
onChange?.(checkedListTemp, `${checkedListTemp?.length}项`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const customCancel = () => {
|
||||||
|
setCheckedListTemp(defaultChecked.current);
|
||||||
|
visibleChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCheckAllChange = (e: any) => {
|
||||||
|
setCheckedListTemp(e.target.checked ? allCheckedList : []);
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkChange = (val: any) => {
|
||||||
|
setCheckedListTemp(val);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIndeterminate(!!checkedListTemp.length && checkedListTemp.length < allCheckedList.length);
|
||||||
|
setCheckAll(checkedListTemp?.length === allCheckedList.length);
|
||||||
|
}, [checkedListTemp]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const all = list?.map((item) => item.value) || [];
|
||||||
|
setAllCheckedList(all);
|
||||||
|
}, [list]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isTop) {
|
||||||
|
setCheckedListTemp([]);
|
||||||
|
defaultChecked.current = [];
|
||||||
|
}
|
||||||
|
}, [isTop]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h6 className="time_title">{props.title}</h6>
|
||||||
|
<div className="custom-scope">
|
||||||
|
<div className="check-row">
|
||||||
|
<Checkbox className="check-all" indeterminate={indeterminate} checked={checkAll} onChange={onCheckAllChange}>
|
||||||
|
全选
|
||||||
|
</Checkbox>
|
||||||
|
<Input
|
||||||
|
className="search-input"
|
||||||
|
suffix={<IconFont type="icon-fangdajing" style={{ fontSize: '16px' }} />}
|
||||||
|
size="small"
|
||||||
|
placeholder={searchPlaceholder}
|
||||||
|
onChange={(e) => setScopeSearchValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="fixed-height">
|
||||||
|
<Checkbox.Group style={{ width: '100%' }} onChange={checkChange} value={checkedListTemp}>
|
||||||
|
<Row gutter={[10, 12]}>
|
||||||
|
{list
|
||||||
|
.filter((item) => item.label.includes(scopeSearchValue))
|
||||||
|
.map((item) => (
|
||||||
|
<Col span={12} key={item.value}>
|
||||||
|
<Checkbox value={item.value}>{item.label}</Checkbox>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</Checkbox.Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="btn-con">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
className="btn-sure"
|
||||||
|
onClick={customSure}
|
||||||
|
disabled={checkedListTemp?.length > 0 ? false : true}
|
||||||
|
>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
<Button size="small" onClick={customCancel}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const DraggableCharts = (props: PropsType): JSX.Element => {
|
const DraggableCharts = (props: PropsType): JSX.Element => {
|
||||||
const [global] = AppContainer.useGlobalValue();
|
const [global] = AppContainer.useGlobalValue();
|
||||||
@@ -35,14 +139,14 @@ const DraggableCharts = (props: PropsType): JSX.Element => {
|
|||||||
}>();
|
}>();
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [scopeList, setScopeList] = useState<IcustomScope[]>([]); // 节点范围列表
|
const [scopeList, setScopeList] = useState<IcustomScope[]>([]); // 节点范围列表
|
||||||
const [metricsList, setMetricsList] = useState<MetricInfo[]>([]); // 指标列表
|
|
||||||
const [selectedMetricNames, setSelectedMetricNames] = useState<(string | number)[]>([]); // 默认选中的指标的列表
|
|
||||||
const [curHeaderOptions, setCurHeaderOptions] = useState<ChartFilterOptions>();
|
const [curHeaderOptions, setCurHeaderOptions] = useState<ChartFilterOptions>();
|
||||||
|
const [metricList, setMetricList] = useState<(string | number)[]>([]);
|
||||||
const [metricChartData, setMetricChartData] = useState<FormattedMetricData[]>([]); // 指标图表数据列表
|
const [metricChartData, setMetricChartData] = useState<FormattedMetricData[]>([]); // 指标图表数据列表
|
||||||
const [gridNum, setGridNum] = useState<number>(12); // 图表列布局
|
const [gridNum, setGridNum] = useState<number>(12); // 图表列布局
|
||||||
const metricRankList = useRef<string[]>([]);
|
|
||||||
const chartDetailRef = useRef(null);
|
|
||||||
const curFetchingTimestamp = useRef(0);
|
const curFetchingTimestamp = useRef(0);
|
||||||
|
const metricRankList = useRef<string[]>([]);
|
||||||
|
const metricFilterRef = useRef(null);
|
||||||
|
const chartDetailRef = useRef(null);
|
||||||
|
|
||||||
// 获取节点范围列表
|
// 获取节点范围列表
|
||||||
const getScopeList = async () => {
|
const getScopeList = async () => {
|
||||||
@@ -61,40 +165,6 @@ const DraggableCharts = (props: PropsType): JSX.Element => {
|
|||||||
setScopeList(list);
|
setScopeList(list);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新 rank
|
|
||||||
const updateRank = (metricList: MetricInfo[]) => {
|
|
||||||
const { list, listInfo, shouldUpdate } = resolveMetricsRank(metricList);
|
|
||||||
metricRankList.current = list;
|
|
||||||
if (shouldUpdate) {
|
|
||||||
setMetricList(listInfo);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取指标列表
|
|
||||||
const getMetricList = () => {
|
|
||||||
Utils.request(api.getDashboardMetricList(clusterId, dashboardType)).then((res: MetricInfo[] | null) => {
|
|
||||||
if (!res) return;
|
|
||||||
const supportMetrics = res.filter((metric) => metric.support);
|
|
||||||
const selectedMetrics = supportMetrics.filter((metric) => metric.set).map((metric) => metric.name);
|
|
||||||
updateRank([...supportMetrics]);
|
|
||||||
setMetricsList(supportMetrics);
|
|
||||||
setSelectedMetricNames(selectedMetrics);
|
|
||||||
if (!selectedMetrics.length) {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 更新指标
|
|
||||||
const setMetricList = (metricDetailDTOList: { metric: string; rank: number; set: boolean }[]) => {
|
|
||||||
return Utils.request(api.getDashboardMetricList(clusterId, dashboardType), {
|
|
||||||
method: 'POST',
|
|
||||||
data: {
|
|
||||||
metricDetailDTOList,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 根据筛选项获取图表信息
|
// 根据筛选项获取图表信息
|
||||||
const getMetricChartData = () => {
|
const getMetricChartData = () => {
|
||||||
!curHeaderOptions.isAutoReload && setLoading(true);
|
!curHeaderOptions.isAutoReload && setLoading(true);
|
||||||
@@ -107,7 +177,7 @@ const DraggableCharts = (props: PropsType): JSX.Element => {
|
|||||||
{
|
{
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
metricsNames: selectedMetricNames,
|
metricsNames: metricList || [],
|
||||||
},
|
},
|
||||||
dashboardType === MetricType.Broker || dashboardType === MetricType.Topic
|
dashboardType === MetricType.Broker || dashboardType === MetricType.Topic
|
||||||
? {
|
? {
|
||||||
@@ -168,65 +238,29 @@ const DraggableCharts = (props: PropsType): JSX.Element => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 指标选中项更新回调
|
// 图表拖拽
|
||||||
const metricSelectCallback = (newMetricNames: (string | number)[]) => {
|
const dragCallback = (oldIndex: number, newIndex: number) => {
|
||||||
const updateMetrics: { metric: string; set: boolean; rank: number }[] = [];
|
|
||||||
// 需要选中的指标
|
|
||||||
newMetricNames.forEach(
|
|
||||||
(name) =>
|
|
||||||
!selectedMetricNames.includes(name) &&
|
|
||||||
updateMetrics.push({ metric: name as string, set: true, rank: metricsList.find(({ name: metric }) => metric === name)?.rank })
|
|
||||||
);
|
|
||||||
// 取消选中的指标
|
|
||||||
selectedMetricNames.forEach(
|
|
||||||
(name) =>
|
|
||||||
!newMetricNames.includes(name) &&
|
|
||||||
updateMetrics.push({ metric: name as string, set: false, rank: metricsList.find(({ name: metric }) => metric === name)?.rank })
|
|
||||||
);
|
|
||||||
|
|
||||||
const requestPromise = Object.keys(updateMetrics).length ? setMetricList(updateMetrics) : Promise.resolve();
|
|
||||||
requestPromise.then(
|
|
||||||
() => getMetricList(),
|
|
||||||
() => getMetricList()
|
|
||||||
);
|
|
||||||
|
|
||||||
return requestPromise;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 拖拽开始回调,触发图表的 onDrag 事件( 设置为 true ),禁止同步展示图表的 tooltip
|
|
||||||
const dragStart = () => {
|
|
||||||
busInstance.emit('onDrag', true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 拖拽结束回调,更新图表顺序,并触发图表的 onDrag 事件( 设置为 false ),允许同步展示图表的 tooltip
|
|
||||||
const dragEnd = ({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) => {
|
|
||||||
busInstance.emit('onDrag', false);
|
|
||||||
const originFrom = metricRankList.current.indexOf(metricChartData[oldIndex].metricName);
|
const originFrom = metricRankList.current.indexOf(metricChartData[oldIndex].metricName);
|
||||||
const originTarget = metricRankList.current.indexOf(metricChartData[newIndex].metricName);
|
const originTarget = metricRankList.current.indexOf(metricChartData[newIndex].metricName);
|
||||||
const newList = arrayMoveImmutable(metricRankList.current, originFrom, originTarget);
|
metricFilterRef.current?.rankChange(originFrom, originTarget);
|
||||||
metricRankList.current = newList;
|
|
||||||
setMetricList(newList.map((metric, rank) => ({ metric, rank, set: metricsList.find(({ name }) => metric === name)?.set || false })));
|
|
||||||
setMetricChartData(arrayMoveImmutable(metricChartData, oldIndex, newIndex));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听盒子宽度变化,重置图表宽度
|
// 展开图表详情
|
||||||
const observeDashboardWidthChange = () => {
|
const onExpand = (metricName: string) => {
|
||||||
const targetNode = document.getElementsByClassName('dcd-two-columns-layout-sider-footer')[0];
|
const linesName = scopeList.map((item) => item.value);
|
||||||
targetNode && targetNode.addEventListener('click', () => busInstance.emit('chartResize'));
|
chartDetailRef.current.onOpen(dashboardType, metricName, linesName);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取图表指标
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedMetricNames.length && curHeaderOptions) {
|
if (metricList?.length && curHeaderOptions) {
|
||||||
getMetricChartData();
|
getMetricChartData();
|
||||||
}
|
}
|
||||||
}, [curHeaderOptions, selectedMetricNames]);
|
}, [curHeaderOptions, metricList]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 初始化页面,获取 scope 和 metric 信息
|
// 初始化页面,获取 scope 和 metric 信息
|
||||||
(dashboardType === MetricType.Broker || dashboardType === MetricType.Topic) && getScopeList();
|
(dashboardType === MetricType.Broker || dashboardType === MetricType.Topic) && getScopeList();
|
||||||
getMetricList();
|
|
||||||
|
|
||||||
setTimeout(() => observeDashboardWidthChange());
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -234,95 +268,36 @@ const DraggableCharts = (props: PropsType): JSX.Element => {
|
|||||||
<ChartOperateBar
|
<ChartOperateBar
|
||||||
onChange={ksHeaderChange}
|
onChange={ksHeaderChange}
|
||||||
hideNodeScope={dashboardType === MetricType.Zookeeper}
|
hideNodeScope={dashboardType === MetricType.Zookeeper}
|
||||||
nodeScopeModule={{
|
openMetricFilter={() => metricFilterRef.current?.open()}
|
||||||
customScopeList: scopeList,
|
nodeSelect={{
|
||||||
scopeName: dashboardType === MetricType.Broker ? 'Broker' : dashboardType === MetricType.Topic ? 'Topic' : 'Zookeeper',
|
name: dashboardType === MetricType.Broker ? 'Broker' : dashboardType === MetricType.Topic ? 'Topic' : 'Zookeeper',
|
||||||
scopeLabel: `自定义 ${
|
customContent: (
|
||||||
dashboardType === MetricType.Broker ? 'Broker' : dashboardType === MetricType.Topic ? 'Topic' : 'Zookeeper'
|
<SelectContent
|
||||||
} 范围`,
|
title={`自定义 ${
|
||||||
}}
|
dashboardType === MetricType.Broker ? 'Broker' : dashboardType === MetricType.Topic ? 'Topic' : 'Zookeeper'
|
||||||
metricSelect={{
|
} 范围`}
|
||||||
hide: false,
|
list={scopeList}
|
||||||
metricType: dashboardType,
|
/>
|
||||||
tableData: metricsList,
|
),
|
||||||
selectedRows: selectedMetricNames,
|
|
||||||
submitCallback: metricSelectCallback,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="topic-dashboard-container">
|
<MetricsFilter
|
||||||
<Spin spinning={loading} style={{ height: 400 }}>
|
ref={metricFilterRef}
|
||||||
{metricChartData && metricChartData.length ? (
|
metricType={dashboardType}
|
||||||
<div className="no-group-con">
|
onSelectChange={(list, rankList) => {
|
||||||
<DragGroup
|
metricRankList.current = rankList;
|
||||||
sortableContainerProps={{
|
setMetricList(list);
|
||||||
onSortStart: dragStart,
|
}}
|
||||||
onSortEnd: dragEnd,
|
/>
|
||||||
axis: 'xy',
|
<ChartList
|
||||||
useDragHandle: true,
|
busInstance={busInstance}
|
||||||
}}
|
loading={loading}
|
||||||
gridProps={{
|
gridNum={gridNum}
|
||||||
span: gridNum,
|
data={metricChartData}
|
||||||
gutter: DRAG_GROUP_GUTTER_NUM,
|
autoReload={curHeaderOptions?.isAutoReload}
|
||||||
}}
|
dragCallback={dragCallback}
|
||||||
>
|
onExpand={onExpand}
|
||||||
{metricChartData.map((data) => {
|
/>
|
||||||
const { metricName, metricUnit, metricLines, showLegend } = data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={metricName} className="dashboard-drag-item-box">
|
|
||||||
<div className="dashboard-drag-item-box-title">
|
|
||||||
<Tooltip
|
|
||||||
placement="topLeft"
|
|
||||||
title={() => {
|
|
||||||
let content = '';
|
|
||||||
const metricDefine = global.getMetricDefine(dashboardType, 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={() => {
|
|
||||||
const linesName = scopeList.map((item) => item.value);
|
|
||||||
chartDetailRef.current.onOpen(dashboardType, metricName, linesName);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconFont type="icon-chuangkoufangda" className="expand-icon" />
|
|
||||||
</div>
|
|
||||||
<SingleChart
|
|
||||||
chartKey={metricName}
|
|
||||||
chartTypeProp="line"
|
|
||||||
showHeader={false}
|
|
||||||
wrapStyle={{
|
|
||||||
width: 'auto',
|
|
||||||
height: 222,
|
|
||||||
}}
|
|
||||||
connectEventName={`${dashboardType}BoardDragChart`}
|
|
||||||
eventBus={busInstance}
|
|
||||||
propChartData={metricLines}
|
|
||||||
optionMergeProps={{ replaceMerge: curHeaderOptions.isAutoReload ? ['xAxis'] : ['series'] }}
|
|
||||||
{...getChartConfig(`${metricName}{unit|(${metricUnit})}`, metricLines.length, showLegend)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</DragGroup>
|
|
||||||
</div>
|
|
||||||
) : loading ? (
|
|
||||||
<></>
|
|
||||||
) : (
|
|
||||||
<Empty description="数据为空,请选择指标或刷新" image={Empty.PRESENTED_IMAGE_CUSTOM} style={{ padding: '100px 0' }} />
|
|
||||||
)}
|
|
||||||
</Spin>
|
|
||||||
</div>
|
|
||||||
{/* 图表详情 */}
|
{/* 图表详情 */}
|
||||||
<ChartDetail ref={chartDetailRef} />
|
<ChartDetail ref={chartDetailRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import React, { useLayoutEffect, useRef, useState } from 'react';
|
|||||||
import './index.less';
|
import './index.less';
|
||||||
|
|
||||||
interface SwitchTabProps {
|
interface SwitchTabProps {
|
||||||
defaultKey: string;
|
defaultKey?: string;
|
||||||
|
activeKey?: string | number;
|
||||||
onChange: (key: string) => void;
|
onChange: (key: string) => void;
|
||||||
children: any;
|
children: any;
|
||||||
}
|
}
|
||||||
@@ -18,9 +19,9 @@ const TabItem = (props: TabItemProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SwitchTab = (props: SwitchTabProps) => {
|
const SwitchTab = (props: SwitchTabProps) => {
|
||||||
const { defaultKey, onChange, children } = props;
|
const { defaultKey, activeKey, onChange, children } = props;
|
||||||
const tabRef = useRef();
|
const tabRef = useRef();
|
||||||
const [activeKey, setActiveKey] = useState<string>(defaultKey);
|
const [active, setActive] = useState<string | number>(activeKey || defaultKey);
|
||||||
const [pos, setPos] = useState({
|
const [pos, setPos] = useState({
|
||||||
left: 0,
|
left: 0,
|
||||||
width: 0,
|
width: 0,
|
||||||
@@ -39,6 +40,10 @@ const SwitchTab = (props: SwitchTabProps) => {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}, [active]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
activeKey && setActive(activeKey);
|
||||||
}, [activeKey]);
|
}, [activeKey]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -48,9 +53,10 @@ const SwitchTab = (props: SwitchTabProps) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={key}
|
key={key}
|
||||||
className={`d-switch-tab-content d-switch-tab-content-${activeKey === key ? 'active' : ''}`}
|
className={`d-switch-tab-content d-switch-tab-content-${active === key ? 'active' : ''}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveKey(key);
|
// 受控模式下不自动更新状态
|
||||||
|
!activeKey && setActive(key);
|
||||||
onChange(key);
|
onChange(key);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface OriginMetricData {
|
|||||||
export interface FormattedMetricData {
|
export interface FormattedMetricData {
|
||||||
metricName: string;
|
metricName: string;
|
||||||
metricUnit: string;
|
metricUnit: string;
|
||||||
|
metricType: MetricType;
|
||||||
metricLines: {
|
metricLines: {
|
||||||
name: string;
|
name: string;
|
||||||
data: (string | number)[][];
|
data: (string | number)[][];
|
||||||
@@ -240,6 +241,7 @@ export const formatChartData = (
|
|||||||
// 初始化返回结构
|
// 初始化返回结构
|
||||||
const chartData: FormattedMetricData = {
|
const chartData: FormattedMetricData = {
|
||||||
metricName,
|
metricName,
|
||||||
|
metricType,
|
||||||
metricUnit: curMetricInfo?.unit || '',
|
metricUnit: curMetricInfo?.unit || '',
|
||||||
metricLines: metricLines
|
metricLines: metricLines
|
||||||
.sort((a, b) => Number(a.name < b.name) - 0.5)
|
.sort((a, b) => Number(a.name < b.name) - 0.5)
|
||||||
|
|||||||
@@ -56,12 +56,7 @@ export const leftMenus = (clusterId?: string, clusterRunState?: number) => ({
|
|||||||
clusterRunState && clusterRunState !== ClusterRunState.Raft
|
clusterRunState && clusterRunState !== ClusterRunState.Raft
|
||||||
? {
|
? {
|
||||||
name: (intl: any) => {
|
name: (intl: any) => {
|
||||||
return (
|
return <span>{intl.formatMessage({ id: 'menu.cluster.zookeeper' })}</span>;
|
||||||
<div className="menu-item-with-beta-tag">
|
|
||||||
<span>{intl.formatMessage({ id: 'menu.cluster.zookeeper' })}</span>
|
|
||||||
<div className="beta-tag"></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
path: 'zookeeper',
|
path: 'zookeeper',
|
||||||
icon: 'icon-Zookeeper',
|
icon: 'icon-Zookeeper',
|
||||||
@@ -79,6 +74,35 @@ export const leftMenus = (clusterId?: string, clusterRunState?: number) => ({
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
{
|
||||||
|
name: (intl: any) => {
|
||||||
|
return (
|
||||||
|
<div className="menu-item-with-beta-tag">
|
||||||
|
<span>{intl.formatMessage({ id: 'menu.cluster.connect' })}</span>
|
||||||
|
<div className="beta-tag"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
path: 'connect',
|
||||||
|
icon: 'icon-Operation',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: (intl: any) => <span>{intl.formatMessage({ id: 'menu.cluster.connect.dashboard' })}</span>,
|
||||||
|
path: '',
|
||||||
|
icon: 'icon-luoji',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: (intl: any) => <span>{intl.formatMessage({ id: 'menu.cluster.connect.connectors' })}</span>,
|
||||||
|
path: 'connectors',
|
||||||
|
icon: '#icon-luoji',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: (intl: any) => <span>{intl.formatMessage({ id: 'menu.cluster.connect.workers' })}</span>,
|
||||||
|
path: 'workers',
|
||||||
|
icon: 'icon-Jobs',
|
||||||
|
},
|
||||||
|
].filter((m) => m),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'consumer-group',
|
name: 'consumer-group',
|
||||||
path: 'consumers',
|
path: 'consumers',
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ export const regOddNumber = /^\d*[13579]$/; //奇数
|
|||||||
export const regClusterName = /^[\u4E00-\u9FA5A-Za-z0-9\_\-\!\"\#\$\%&'()\*\+,./\:\;\<=\>?\@\[\\\]^\`\{\|\}~]*$/im; // 大、小写字母、数字、-、_ new RegExp('\[a-z0-9_-]$', 'g')
|
export const regClusterName = /^[\u4E00-\u9FA5A-Za-z0-9\_\-\!\"\#\$\%&'()\*\+,./\:\;\<=\>?\@\[\\\]^\`\{\|\}~]*$/im; // 大、小写字母、数字、-、_ new RegExp('\[a-z0-9_-]$', 'g')
|
||||||
export const regUsername = /^[_a-zA-Z-]*$/; // 大、小写字母、数字、-、_ new RegExp('\[a-z0-9_-]$', 'g')
|
export const regUsername = /^[_a-zA-Z-]*$/; // 大、小写字母、数字、-、_ new RegExp('\[a-z0-9_-]$', 'g')
|
||||||
|
|
||||||
|
export const regIpAddress = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/;
|
||||||
|
export const regIpPort = /^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{1,5})|([0-9]{1,4}))$/;
|
||||||
|
export const regIpAndPort =
|
||||||
|
/^http(s|):\/\/(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(:((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{1,5})|([0-9]{1,4})))?$/;
|
||||||
|
export const regHttpOrHttpsAddress =
|
||||||
|
/^http(s|):\/\/(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/;
|
||||||
export const regExp = /^[ ]+$/; // 不能为空
|
export const regExp = /^[ ]+$/; // 不能为空
|
||||||
|
|
||||||
export const regNonnegativeNumber = /^[+]{0,1}(\d+)$|^[+]{0,1}(\d+\.\d+)$/; // 非负数
|
export const regNonnegativeNumber = /^[+]{0,1}(\d+)$|^[+]{0,1}(\d+\.\d+)$/; // 非负数
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ export default {
|
|||||||
[`menu.${systemKey}.operation.balance`]: 'Rebalance',
|
[`menu.${systemKey}.operation.balance`]: 'Rebalance',
|
||||||
[`menu.${systemKey}.operation.jobs`]: 'Job',
|
[`menu.${systemKey}.operation.jobs`]: 'Job',
|
||||||
|
|
||||||
|
[`menu.${systemKey}.connect`]: 'Connect',
|
||||||
|
[`menu.${systemKey}.connect.dashboard`]: 'Overview',
|
||||||
|
[`menu.${systemKey}.connect.connectors`]: 'Connectors',
|
||||||
|
[`menu.${systemKey}.connect.workers`]: 'Workers',
|
||||||
|
|
||||||
[`menu.${systemKey}.acls`]: 'ACLs',
|
[`menu.${systemKey}.acls`]: 'ACLs',
|
||||||
|
|
||||||
[`menu.${systemKey}.jobs`]: 'Job',
|
[`menu.${systemKey}.jobs`]: 'Job',
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ const ControllerChangeLogList: React.FC = (props: any) => {
|
|||||||
<div style={{ margin: '12px 0' }}>
|
<div style={{ margin: '12px 0' }}>
|
||||||
<BrokerHealthCheck />
|
<BrokerHealthCheck />
|
||||||
</div>
|
</div>
|
||||||
<div className="clustom-table-content">
|
<div className="custom-table-content">
|
||||||
<div className={tableHeaderPrefix}>
|
<div className={tableHeaderPrefix}>
|
||||||
<div className={`${tableHeaderPrefix}-left`}>
|
<div className={`${tableHeaderPrefix}-left`}>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ const BrokerList: React.FC = (props: any) => {
|
|||||||
<div style={{ margin: '12px 0' }}>
|
<div style={{ margin: '12px 0' }}>
|
||||||
<BrokerHealthCheck />
|
<BrokerHealthCheck />
|
||||||
</div>
|
</div>
|
||||||
<div className="clustom-table-content">
|
<div className="custom-table-content">
|
||||||
<div className={tableHeaderPrefix}>
|
<div className={tableHeaderPrefix}>
|
||||||
<div className={`${tableHeaderPrefix}-left`}>
|
<div className={`${tableHeaderPrefix}-left`}>
|
||||||
<div
|
<div
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,262 @@
|
|||||||
|
import api from '@src/api';
|
||||||
|
import CodeMirrorFormItem from '@src/components/CodeMirrorFormItem';
|
||||||
|
import customMessage from '@src/components/Message';
|
||||||
|
import { Button, Divider, Drawer, Form, message, Space, Utils } from 'knowdesign';
|
||||||
|
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { ConnectCluster, ConnectorPlugin, ConnectorPluginConfig, OperateInfo } from './AddConnector';
|
||||||
|
|
||||||
|
const PLACEHOLDER = `配置格式如下
|
||||||
|
|
||||||
|
{
|
||||||
|
"connectClusterName": "", // Connect Cluster 名称
|
||||||
|
"configs": { // 具体配置项
|
||||||
|
"name": "",
|
||||||
|
"connector.class": "",
|
||||||
|
"tasks.max": 1,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export default forwardRef((props: any, ref) => {
|
||||||
|
const { clusterId } = useParams<{
|
||||||
|
clusterId: string;
|
||||||
|
}>();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [type, setType] = useState('create');
|
||||||
|
const [connectClusters, setConnectClusters] = useState<{ label: string; value: number }[]>([]);
|
||||||
|
const [defaultConfigs, setDefaultConfigs] = useState<{ [key: string]: any }>({});
|
||||||
|
const [submitLoading, setSubmitLoading] = useState(false);
|
||||||
|
|
||||||
|
const getConnectClusters = () => {
|
||||||
|
return Utils.request(api.getConnectClusters(clusterId)).then((res: ConnectCluster[]) => {
|
||||||
|
setConnectClusters(
|
||||||
|
res.map(({ name, id }) => ({
|
||||||
|
label: name || '-',
|
||||||
|
value: id,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOpen = (type: 'create' | 'edit', connectClusterName?: string, defaultConfigs?: { [key: string]: any }) => {
|
||||||
|
if (defaultConfigs) {
|
||||||
|
setDefaultConfigs({ ...defaultConfigs, connectClusterName });
|
||||||
|
form.setFieldsValue({
|
||||||
|
configs: JSON.stringify(
|
||||||
|
{
|
||||||
|
connectClusterName,
|
||||||
|
configs: defaultConfigs,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setType(type);
|
||||||
|
setVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
setSubmitLoading(true);
|
||||||
|
form.validateFields().then(
|
||||||
|
(data) => {
|
||||||
|
const postData = JSON.parse(data.configs);
|
||||||
|
postData.connectorName = postData.configs.name;
|
||||||
|
postData.connectClusterId = connectClusters.find((cluster) => cluster.label === postData.connectClusterName).value;
|
||||||
|
delete postData.connectClusterName;
|
||||||
|
|
||||||
|
Object.entries(postData.configs).forEach(([key, val]) => {
|
||||||
|
if (val === null) {
|
||||||
|
delete postData.configs[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Utils.put(api.validateConnectorConfig, postData).then(
|
||||||
|
(res: ConnectorPluginConfig) => {
|
||||||
|
if (res) {
|
||||||
|
if (res?.errorCount > 0) {
|
||||||
|
const errors: OperateInfo['errors'] = {};
|
||||||
|
res?.configs
|
||||||
|
?.filter((config) => config.value.errors.length !== 0)
|
||||||
|
.forEach(({ value }) => {
|
||||||
|
if (value.name.includes('transforms.')) {
|
||||||
|
errors['transforms'] = (errors['transforms'] || []).concat(value.errors);
|
||||||
|
} else {
|
||||||
|
errors[value.name] = value.errors;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
form.setFields([
|
||||||
|
{
|
||||||
|
name: 'configs',
|
||||||
|
errors: Object.entries(errors).map(([name, errorArr]) => `${name}: ${errorArr.join('; ')}\n`),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
setSubmitLoading(false);
|
||||||
|
} else {
|
||||||
|
if (type === 'create') {
|
||||||
|
Utils.post(api.connectorsOperates, postData)
|
||||||
|
.then(() => {
|
||||||
|
customMessage.success('新建成功');
|
||||||
|
onClose();
|
||||||
|
props?.refresh();
|
||||||
|
})
|
||||||
|
.finally(() => setSubmitLoading(false));
|
||||||
|
} else {
|
||||||
|
Utils.put(api.updateConnectorConfig, postData)
|
||||||
|
.then(() => {
|
||||||
|
customMessage.success('编辑成功');
|
||||||
|
props?.refresh();
|
||||||
|
onClose();
|
||||||
|
})
|
||||||
|
.finally(() => setSubmitLoading(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setSubmitLoading(false);
|
||||||
|
message.error('接口校验出错,请重新提交');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => setSubmitLoading(false)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
() => setSubmitLoading(false)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
setVisible(false);
|
||||||
|
form.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getConnectClusters();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
onOpen,
|
||||||
|
onClose,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
title={`${type === 'create' ? '新建' : '编辑'} Connector`}
|
||||||
|
className="operate-connector-drawer-use-json"
|
||||||
|
width={800}
|
||||||
|
visible={visible}
|
||||||
|
onClose={onClose}
|
||||||
|
maskClosable={false}
|
||||||
|
extra={
|
||||||
|
<div className="operate-wrap">
|
||||||
|
<Space>
|
||||||
|
<Button size="small" onClick={onClose}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button size="small" type="primary" onClick={onSubmit} loading={submitLoading}>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
<Divider type="vertical" />
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
name="configs"
|
||||||
|
validateTrigger="onBlur"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
validator(rule, value) {
|
||||||
|
if (!value) {
|
||||||
|
return Promise.reject('配置不能为空');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const v = JSON.parse(value);
|
||||||
|
if (typeof v !== 'object') {
|
||||||
|
return Promise.reject('输入内容必须为 JSON');
|
||||||
|
}
|
||||||
|
let connectClusterId = -1;
|
||||||
|
// 校验 connectClusterName 字段
|
||||||
|
if (!v.connectClusterName) {
|
||||||
|
return Promise.reject('内容缺少 connectClusterName 字段或字段内容为空');
|
||||||
|
} else {
|
||||||
|
if (type === 'edit') {
|
||||||
|
if (v.connectClusterName !== defaultConfigs.connectClusterName) {
|
||||||
|
return Promise.reject('编辑模式下不允许修改 connectClusterName 字段');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!connectClusters.length) {
|
||||||
|
getConnectClusters();
|
||||||
|
return Promise.reject('connectClusterName 列表获取失败,请重试');
|
||||||
|
}
|
||||||
|
const targetConnectCluster = connectClusters.find((cluster) => cluster.label === v.connectClusterName);
|
||||||
|
if (!targetConnectCluster) {
|
||||||
|
return Promise.reject('connectClusterName 不存在,请检查');
|
||||||
|
} else {
|
||||||
|
connectClusterId = targetConnectCluster.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!v.configs || typeof v.configs !== 'object') {
|
||||||
|
return Promise.reject('内容缺少 configs 字段或字段格式错误');
|
||||||
|
} else {
|
||||||
|
// 校验 connectorName 字段
|
||||||
|
if (!v.configs.name) {
|
||||||
|
return Promise.reject('configs 字段下缺少 name 项');
|
||||||
|
} else {
|
||||||
|
if (type === 'edit' && v.configs.name !== defaultConfigs.name) {
|
||||||
|
return Promise.reject('编辑模式下不允许修改 name 字段');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!v.configs['connector.class']) {
|
||||||
|
return Promise.reject('configs 字段下缺少 connector.class 项');
|
||||||
|
} else if (type === 'edit' && v.configs['connector.class'] !== defaultConfigs['connector.class']) {
|
||||||
|
return Promise.reject('编辑模式下不允许修改 connector.class 字段');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'create') {
|
||||||
|
// 异步校验 connector 名称是否重复 以及 className 是否存在
|
||||||
|
return Promise.all([
|
||||||
|
Utils.request(api.isConnectorExist(connectClusterId, v.configs.name)),
|
||||||
|
Utils.request(api.getConnectorPlugins(connectClusterId)),
|
||||||
|
]).then(
|
||||||
|
([data, plugins]: [any, ConnectorPlugin[]]) => {
|
||||||
|
return data?.exist
|
||||||
|
? Promise.reject('name 与已有 Connector 重复')
|
||||||
|
: plugins.every((plugin) => plugin.className !== v.configs['connector.class'])
|
||||||
|
? Promise.reject('该 connectCluster 下不存在 connector.class 项配置的插件')
|
||||||
|
: Promise.resolve();
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
return Promise.reject('接口校验出错,请重试');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return Promise.reject('输入内容必须为 JSON');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{visible && (
|
||||||
|
<div>
|
||||||
|
<CodeMirrorFormItem
|
||||||
|
resize
|
||||||
|
defaultInput={form.getFieldValue('configs')}
|
||||||
|
placeholder={PLACEHOLDER}
|
||||||
|
onBeforeChange={(configs: string) => {
|
||||||
|
form.setFieldsValue({ configs });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { Button, Form, Input, Modal, Utils } from 'knowdesign';
|
||||||
|
import notification from '@src/components/Notification';
|
||||||
|
import { IconFont } from '@knowdesign/icons';
|
||||||
|
import Api from '@src/api/index';
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/display-name
|
||||||
|
const DeleteConnector = (props: { record: any; onConfirm?: () => void }) => {
|
||||||
|
const { record, onConfirm } = props;
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [delDialogVisible, setDelDialogVisble] = useState(false);
|
||||||
|
const handleDelOk = () => {
|
||||||
|
form.validateFields().then((e) => {
|
||||||
|
const formVal = form.getFieldsValue();
|
||||||
|
formVal.connectClusterId = Number(record.connectClusterId);
|
||||||
|
Utils.delete(Api.connectorsOperates, { data: formVal }).then((res: any) => {
|
||||||
|
if (res === null) {
|
||||||
|
notification.success({
|
||||||
|
message: '删除成功',
|
||||||
|
});
|
||||||
|
setDelDialogVisble(false);
|
||||||
|
onConfirm && onConfirm();
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: '删除失败',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
onClick={(_) => {
|
||||||
|
setDelDialogVisble(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
className="custom-modal"
|
||||||
|
title="确定删除此Connector吗?"
|
||||||
|
centered={true}
|
||||||
|
visible={delDialogVisible}
|
||||||
|
wrapClassName="del-connect-modal"
|
||||||
|
destroyOnClose={true}
|
||||||
|
maskClosable={false}
|
||||||
|
onOk={handleDelOk}
|
||||||
|
onCancel={(_) => {
|
||||||
|
setDelDialogVisble(false);
|
||||||
|
}}
|
||||||
|
okText="删除"
|
||||||
|
okButtonProps={{
|
||||||
|
danger: true,
|
||||||
|
size: 'small',
|
||||||
|
style: {
|
||||||
|
paddingLeft: '16px',
|
||||||
|
paddingRight: '16px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
cancelButtonProps={{
|
||||||
|
size: 'small',
|
||||||
|
style: {
|
||||||
|
paddingLeft: '16px',
|
||||||
|
paddingRight: '16px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form form={form} labelCol={{ span: 6 }} style={{ marginTop: 17 }}>
|
||||||
|
<Form.Item label="ConnectorName">{record.connectorName}</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="connectorName"
|
||||||
|
label="ConnectorName"
|
||||||
|
rules={[
|
||||||
|
// { required: true },
|
||||||
|
() => ({
|
||||||
|
validator(_, value) {
|
||||||
|
if (!value) {
|
||||||
|
return Promise.reject(new Error('请输入ConnectorName名称'));
|
||||||
|
} else if (value !== record.connectorName) {
|
||||||
|
return Promise.reject(new Error('请输入正确的ConnectorName名称'));
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入" size="small"></Input>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteConnector;
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Drawer, Utils, AppContainer, ProTable } from 'knowdesign';
|
||||||
|
import API from '@src/api';
|
||||||
|
import ConnectDetailCard from '@src/components/CardBar/ConnectDetailCard';
|
||||||
|
import { defaultPagination, getConnectorsDetailColumns } from './config';
|
||||||
|
import notification from '@src/components/Notification';
|
||||||
|
import './index.less';
|
||||||
|
|
||||||
|
const prefix = 'connect-detail';
|
||||||
|
const { request } = Utils;
|
||||||
|
const ConnectorDetail = (props: any) => {
|
||||||
|
const { visible, setVisible, record } = props;
|
||||||
|
const [global] = AppContainer.useGlobalValue();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [data, setData] = useState([]);
|
||||||
|
const [pagination, setPagination] = useState<any>(defaultPagination);
|
||||||
|
const onClose = () => {
|
||||||
|
setVisible(false);
|
||||||
|
setPagination(defaultPagination);
|
||||||
|
// clean hash
|
||||||
|
};
|
||||||
|
|
||||||
|
// 请求接口获取数据
|
||||||
|
const genData = async () => {
|
||||||
|
if (global?.clusterInfo?.id === undefined) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
request(API.getConnectDetailTasks(record.connectorName, record.connectClusterId))
|
||||||
|
.then((res: any) => {
|
||||||
|
setData(res || []);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||||
|
setPagination(pagination);
|
||||||
|
};
|
||||||
|
|
||||||
|
const optionFn: any = (taskId: any) => {
|
||||||
|
const params = {
|
||||||
|
action: 'restart',
|
||||||
|
connectClusterId: record?.connectClusterId,
|
||||||
|
connectorName: record?.connectorName,
|
||||||
|
taskId,
|
||||||
|
};
|
||||||
|
|
||||||
|
request(API.optionTasks(), { method: 'PUT', data: params }).then((res: any) => {
|
||||||
|
if (res === null) {
|
||||||
|
notification.success({
|
||||||
|
message: `任务重试成功`,
|
||||||
|
});
|
||||||
|
genData();
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: `任务重试失败`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const retryOption = Utils.useDebounce(optionFn, 500);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
visible && record && genData();
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
// push={false}
|
||||||
|
title={
|
||||||
|
<span>
|
||||||
|
<span style={{ fontSize: '18px', fontFamily: 'PingFangSC-Semibold', color: '#495057' }}>{record.connectorName ?? '-'}</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
width={1080}
|
||||||
|
placement="right"
|
||||||
|
onClose={onClose}
|
||||||
|
visible={visible}
|
||||||
|
className={`${prefix}-drawer`}
|
||||||
|
destroyOnClose
|
||||||
|
maskClosable={false}
|
||||||
|
>
|
||||||
|
<ConnectDetailCard record={record} />
|
||||||
|
<div className={`${prefix}-drawer-title`}>Tasks</div>
|
||||||
|
<ProTable
|
||||||
|
key="connector-detail-table"
|
||||||
|
showQueryForm={false}
|
||||||
|
tableProps={{
|
||||||
|
showHeader: false,
|
||||||
|
rowKey: 'taskId',
|
||||||
|
loading: loading,
|
||||||
|
columns: getConnectorsDetailColumns({ retryOption }),
|
||||||
|
dataSource: data,
|
||||||
|
paginationProps: { ...pagination },
|
||||||
|
attrs: {
|
||||||
|
onChange: onTableChange,
|
||||||
|
// scroll: { x: 'max-content' },
|
||||||
|
bordered: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* <BrokerDetailHealthCheck record={{ brokerId: hashData?.brokerId }} /> */}
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConnectorDetail;
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import React, { useLayoutEffect, useState } from 'react';
|
||||||
|
import api from '@src/api';
|
||||||
|
import { Spin, Utils } from 'knowdesign';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import NodataImg from '@src/assets/no-data.png';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NoConnector = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: 'calc(100vh - 118px)',
|
||||||
|
boxShadow: '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)',
|
||||||
|
borderRadius: 12,
|
||||||
|
background: '#fff',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={NodataImg} style={{ width: 100, height: 162 }} />
|
||||||
|
<span style={{ fontSize: 13, color: '#919AAC', paddingTop: 16 }}>暂无数据,请先接入 Connect 集群</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default (props: Props) => {
|
||||||
|
const { clusterId } = useParams<{
|
||||||
|
clusterId: string;
|
||||||
|
}>();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [disabled, setDisabled] = useState(true);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
Utils.request(api.getConnectors(clusterId))
|
||||||
|
.then((res: any[]) => {
|
||||||
|
res?.length && setDisabled(false);
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return disabled ? (
|
||||||
|
<Spin spinning={loading}>{loading ? <div style={{ height: 'calc(100vh - 118px)' }} /> : <NoConnector />}</Spin>
|
||||||
|
) : (
|
||||||
|
props.children
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import React, { useState, useEffect, memo } from 'react';
|
||||||
|
import { useParams, useHistory, useLocation } from 'react-router-dom';
|
||||||
|
import { ProTable, Button, Utils, AppContainer, SearchInput } from 'knowdesign';
|
||||||
|
import { IconFont } from '@knowdesign/icons';
|
||||||
|
import API from '../../api';
|
||||||
|
import { getWorkersColumns, defaultPagination } from './config';
|
||||||
|
import { tableHeaderPrefix } from '@src/constants/common';
|
||||||
|
import ConnectCard from '@src/components/CardBar/ConnectCard';
|
||||||
|
import DBreadcrumb from 'knowdesign/es/extend/d-breadcrumb';
|
||||||
|
import './index.less';
|
||||||
|
import HasConnector from './HasConnector';
|
||||||
|
const { request } = Utils;
|
||||||
|
|
||||||
|
const Workers: React.FC = () => {
|
||||||
|
const [global] = AppContainer.useGlobalValue();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [data, setData] = useState([]);
|
||||||
|
const [searchKeywords, setSearchKeywords] = useState('');
|
||||||
|
const [pagination, setPagination] = useState<any>(defaultPagination);
|
||||||
|
|
||||||
|
// 请求接口获取数据
|
||||||
|
const genData = async ({ pageNo, pageSize, filters, sorter }: any) => {
|
||||||
|
if (global?.clusterInfo?.id === undefined) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
const params = {
|
||||||
|
searchKeywords: searchKeywords.slice(0, 128),
|
||||||
|
pageNo,
|
||||||
|
pageSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
request(API.getWorkersList(global?.clusterInfo?.id), { 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) => {
|
||||||
|
genData({ pageNo: pagination.current, pageSize: pagination.pageSize, filters, sorter });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
genData({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
});
|
||||||
|
}, [searchKeywords]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="breadcrumb" style={{ marginBottom: '10px' }}>
|
||||||
|
<DBreadcrumb
|
||||||
|
breadcrumbs={[
|
||||||
|
{ label: '多集群管理', aHref: '/' },
|
||||||
|
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
|
||||||
|
{ label: 'Connect', aHref: `/cluster/${global?.clusterInfo?.id}/connect` },
|
||||||
|
{ label: 'Workers', aHref: `` },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<HasConnector>
|
||||||
|
<>
|
||||||
|
<div style={{ margin: '12px 0' }}>
|
||||||
|
<ConnectCard />
|
||||||
|
</div>
|
||||||
|
<div className="custom-table-content">
|
||||||
|
<div className={tableHeaderPrefix}>
|
||||||
|
<div className={`${tableHeaderPrefix}-left`}>
|
||||||
|
<div
|
||||||
|
className={`${tableHeaderPrefix}-left-refresh`}
|
||||||
|
onClick={() => genData({ pageNo: pagination.current, pageSize: pagination.pageSize })}
|
||||||
|
>
|
||||||
|
<IconFont className={`${tableHeaderPrefix}-left-refresh-icon`} type="icon-shuaxin1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${tableHeaderPrefix}-right`}>
|
||||||
|
<SearchInput
|
||||||
|
onSearch={setSearchKeywords}
|
||||||
|
attrs={{
|
||||||
|
placeholder: '请输入Host',
|
||||||
|
style: { width: '248px', borderRiadus: '8px' },
|
||||||
|
maxLength: 128,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ProTable
|
||||||
|
key="workers-table"
|
||||||
|
showQueryForm={false}
|
||||||
|
tableProps={{
|
||||||
|
showHeader: false,
|
||||||
|
rowKey: 'workers_list',
|
||||||
|
loading: loading,
|
||||||
|
columns: getWorkersColumns(),
|
||||||
|
dataSource: data,
|
||||||
|
paginationProps: { ...pagination },
|
||||||
|
attrs: {
|
||||||
|
onChange: onTableChange,
|
||||||
|
scroll: { y: 'calc(100vh - 400px)' },
|
||||||
|
bordered: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</HasConnector>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Workers;
|
||||||
@@ -0,0 +1,364 @@
|
|||||||
|
import SmallChart from '@src/components/SmallChart';
|
||||||
|
import TagsWithHide from '@src/components/TagsWithHide';
|
||||||
|
import { Button, Tag, Tooltip, Utils, Popconfirm } from 'knowdesign';
|
||||||
|
import React from 'react';
|
||||||
|
import Delete from './Delete';
|
||||||
|
export const defaultPagination = {
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
position: 'bottomRight',
|
||||||
|
showSizeChanger: true,
|
||||||
|
pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const optionType: { [name: string]: string } = {
|
||||||
|
['stop']: '暂停',
|
||||||
|
['restart']: '重启',
|
||||||
|
['resume']: '继续',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stateEnum: any = {
|
||||||
|
['UNASSIGNED']: {
|
||||||
|
// 未分配
|
||||||
|
name: 'Unassigned',
|
||||||
|
color: '#556EE6',
|
||||||
|
bgColor: '#EBEEFA',
|
||||||
|
},
|
||||||
|
['RUNNING']: {
|
||||||
|
// 运行
|
||||||
|
name: 'Running',
|
||||||
|
color: '#00C0A2',
|
||||||
|
bgColor: 'rgba(0,192,162,0.10)',
|
||||||
|
},
|
||||||
|
['PAUSED']: {
|
||||||
|
// 暂停
|
||||||
|
name: 'Paused',
|
||||||
|
color: '#495057',
|
||||||
|
bgColor: '#ECECF6',
|
||||||
|
},
|
||||||
|
['FAILED']: {
|
||||||
|
// 失败
|
||||||
|
name: 'Failed',
|
||||||
|
color: '#F58342',
|
||||||
|
bgColor: '#fef3e5',
|
||||||
|
},
|
||||||
|
['DESTROYED']: {
|
||||||
|
// 销毁
|
||||||
|
name: 'Destroyed',
|
||||||
|
color: '#FF7066',
|
||||||
|
bgColor: '#fdefee',
|
||||||
|
},
|
||||||
|
['RESTARTING']: {
|
||||||
|
// 重新启动
|
||||||
|
name: 'Restarting',
|
||||||
|
color: '#3991FF',
|
||||||
|
bgColor: '#e9f5ff',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const calcCurValue = (record: any, metricName: string) => {
|
||||||
|
// const item = (record.metricPoints || []).find((item: any) => item.metricName === metricName);
|
||||||
|
// return item?.value || '';
|
||||||
|
// TODO 替换record
|
||||||
|
const orgVal = record?.latestMetrics?.metrics?.[metricName];
|
||||||
|
if (orgVal !== undefined) {
|
||||||
|
if (metricName === 'TotalRecordErrors') {
|
||||||
|
return Math.round(orgVal).toLocaleString();
|
||||||
|
} else {
|
||||||
|
return Number(Utils.formatAssignSize(orgVal, 'KB', orgVal > 1000 ? 2 : 3)).toLocaleString();
|
||||||
|
// return Utils.formatAssignSize(orgVal, 'KB');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
// return orgVal !== undefined ? (metricName !== 'HealthScore' ? formatAssignSize(orgVal, 'KB') : orgVal) : '-';
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderLine = (record: any, metricName: string) => {
|
||||||
|
const points = record.metricLines?.find((item: any) => item.metricName === metricName)?.metricPoints || [];
|
||||||
|
return points.length ? (
|
||||||
|
<div className="metric-data-wrap">
|
||||||
|
<SmallChart
|
||||||
|
width={'100%'}
|
||||||
|
height={30}
|
||||||
|
chartData={{
|
||||||
|
name: record.metricName,
|
||||||
|
data: points.map((item: any) => ({ time: item.timeStamp, value: item.value })),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="cur-val">{calcCurValue(record, metricName)}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="cur-val">{calcCurValue(record, metricName)}</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getConnectorsColumns = (arg?: any) => {
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'Connect集群',
|
||||||
|
dataIndex: 'connectClusterName',
|
||||||
|
key: 'connectClusterName',
|
||||||
|
width: 200,
|
||||||
|
fixed: 'left',
|
||||||
|
lineClampOne: true,
|
||||||
|
needTooltip: true,
|
||||||
|
// render: (t: string, r: any) => {
|
||||||
|
// return (
|
||||||
|
// <span>
|
||||||
|
// {t}
|
||||||
|
// {r?.status ? <Tag className="tag-success">Live</Tag> : <Tag className="tag-error">Down</Tag>}
|
||||||
|
// </span>
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Connector Name',
|
||||||
|
dataIndex: 'connectorName',
|
||||||
|
key: 'connectorName',
|
||||||
|
width: 160,
|
||||||
|
lineClampOne: true,
|
||||||
|
render: (t: string, r: any) => {
|
||||||
|
return t ? (
|
||||||
|
<>
|
||||||
|
<Tooltip placement="bottom" title={t}>
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
arg.getDetailInfo(r);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t}
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'State',
|
||||||
|
dataIndex: 'state',
|
||||||
|
key: 'state',
|
||||||
|
width: 120,
|
||||||
|
render: (t: string, r: any) => {
|
||||||
|
return t ? (
|
||||||
|
<Tag
|
||||||
|
style={{
|
||||||
|
background: stateEnum[t]?.bgColor,
|
||||||
|
color: stateEnum[t]?.color,
|
||||||
|
padding: '3px 6px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stateEnum[t]?.name}
|
||||||
|
</Tag>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Class',
|
||||||
|
dataIndex: 'connectorClassName',
|
||||||
|
key: 'connectorClassName',
|
||||||
|
width: 150,
|
||||||
|
lineClampOne: true,
|
||||||
|
needTooltip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Type',
|
||||||
|
dataIndex: 'connectorType',
|
||||||
|
key: 'connectorType',
|
||||||
|
width: 100,
|
||||||
|
render: (value: any, record: any) => Utils.firstCharUppercase(value),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tasks',
|
||||||
|
dataIndex: 'taskCount',
|
||||||
|
key: 'taskCount',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '消息读取速率(KB/s)',
|
||||||
|
dataIndex: 'readRate',
|
||||||
|
key: 'readRate',
|
||||||
|
sorter: true,
|
||||||
|
width: 170,
|
||||||
|
render: (value: any, record: any) =>
|
||||||
|
renderLine(record, record.connectorType === 'SINK' ? 'SinkRecordReadRate' : 'SourceRecordPollRate'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '消息写入速率(KB/s)',
|
||||||
|
dataIndex: 'writeRate',
|
||||||
|
key: 'writeRate',
|
||||||
|
sorter: true,
|
||||||
|
width: 170,
|
||||||
|
render: (value: any, record: any) =>
|
||||||
|
renderLine(record, record.connectorType === 'SINK' ? 'SinkRecordSendRate' : 'SourceRecordWriteRate'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '消息处理错误次数(次)',
|
||||||
|
dataIndex: 'recordErrors',
|
||||||
|
key: 'recordErrors',
|
||||||
|
sorter: true,
|
||||||
|
width: 170,
|
||||||
|
render: (value: any, record: any) => renderLine(record, 'TotalRecordErrors'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Topics',
|
||||||
|
dataIndex: 'topicNameList',
|
||||||
|
key: 'topicNameList',
|
||||||
|
width: 200,
|
||||||
|
render(t: any, r: any) {
|
||||||
|
return t && t.length > 0 ? <TagsWithHide placement="bottom" list={t} expandTagContent={(num: any) => `共有${num}个`} /> : '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
dataIndex: 'options',
|
||||||
|
key: 'options',
|
||||||
|
width: 200,
|
||||||
|
filterTitle: true,
|
||||||
|
fixed: 'right',
|
||||||
|
// eslint-disable-next-line react/display-name
|
||||||
|
render: (_t: any, r: any) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Popconfirm
|
||||||
|
title="是否重启当前任务?"
|
||||||
|
onConfirm={() => arg?.optionConnect(r, 'restart')}
|
||||||
|
// onCancel={cancel}
|
||||||
|
okText="是"
|
||||||
|
cancelText="否"
|
||||||
|
overlayClassName="connect-popconfirm"
|
||||||
|
>
|
||||||
|
<Button key="restart" type="link" size="small">
|
||||||
|
重启
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
|
||||||
|
{(r.state === 'RUNNING' || r.state === 'PAUSED') && (
|
||||||
|
<Popconfirm
|
||||||
|
title={`是否${r.state === 'RUNNING' ? '暂停' : '继续'}当前任务?`}
|
||||||
|
onConfirm={() => arg?.optionConnect(r, r.state === 'RUNNING' ? 'stop' : 'resume')}
|
||||||
|
// onCancel={cancel}
|
||||||
|
okText="是"
|
||||||
|
cancelText="否"
|
||||||
|
overlayClassName="connect-popconfirm"
|
||||||
|
>
|
||||||
|
<Button key="stopResume" type="link" size="small">
|
||||||
|
{/* {r?.state !== 1 ? '继续' : '暂停'} */}
|
||||||
|
{r.state === 'RUNNING' ? '暂停' : '继续'}
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button type="link" size="small" onClick={() => arg?.editConnector(r)}>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Delete record={r} onConfirm={arg?.deleteTesk}></Delete>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return columns;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Workers
|
||||||
|
export const getWorkersColumns = (arg?: any) => {
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'Worker Host',
|
||||||
|
dataIndex: 'workerHost',
|
||||||
|
key: 'workerHost',
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '所属集群',
|
||||||
|
dataIndex: 'connectClusterName',
|
||||||
|
key: 'connectClusterName',
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Connectors',
|
||||||
|
dataIndex: 'connectorCount',
|
||||||
|
key: 'connectorCount',
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tasks',
|
||||||
|
dataIndex: 'taskCount',
|
||||||
|
key: 'taskCount',
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return columns;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Detail
|
||||||
|
export const getConnectorsDetailColumns = (arg?: any) => {
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'Task ID',
|
||||||
|
dataIndex: 'taskId',
|
||||||
|
key: 'taskId',
|
||||||
|
width: 240,
|
||||||
|
render: (t: any, r: any) => {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{t}
|
||||||
|
{
|
||||||
|
<Tag
|
||||||
|
style={{
|
||||||
|
background: stateEnum[r?.state]?.bgColor,
|
||||||
|
color: stateEnum[r?.state]?.color,
|
||||||
|
padding: '3px 6px',
|
||||||
|
marginLeft: '5px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Utils.firstCharUppercase(r?.state as string)}
|
||||||
|
</Tag>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Worker',
|
||||||
|
dataIndex: 'workerId',
|
||||||
|
key: 'workerId',
|
||||||
|
width: 240,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '错误原因',
|
||||||
|
dataIndex: 'trace',
|
||||||
|
key: 'trace',
|
||||||
|
width: 400,
|
||||||
|
needTooltip: true,
|
||||||
|
lineClampOne: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
dataIndex: 'role',
|
||||||
|
key: 'role',
|
||||||
|
width: 100,
|
||||||
|
render: (_t: any, r: any) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Popconfirm
|
||||||
|
title="是否重试当前任务?"
|
||||||
|
onConfirm={() => arg?.retryOption(r.taskId)}
|
||||||
|
// onCancel={cancel}
|
||||||
|
okText="是"
|
||||||
|
cancelText="否"
|
||||||
|
overlayClassName="connect-popconfirm"
|
||||||
|
>
|
||||||
|
<a>重试</a>
|
||||||
|
</Popconfirm>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return columns;
|
||||||
|
};
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
// connect列表 图表
|
||||||
|
.metric-data-wrap {
|
||||||
|
// display: flex;
|
||||||
|
// align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
.cur-val {
|
||||||
|
display: block;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.dcloud-spin-nested-loading {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增按钮
|
||||||
|
.add-connect {
|
||||||
|
.dcloud-btn-primary:hover,
|
||||||
|
.dcloud-btn-primary:focus {
|
||||||
|
// 可以控制新增按钮的hover和focus的样式
|
||||||
|
// background: #556ee6;
|
||||||
|
// border-color: #556ee6;
|
||||||
|
}
|
||||||
|
&-btn {
|
||||||
|
border-top-right-radius: 0 !important;
|
||||||
|
border-bottom-right-radius: 0 !important;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
&-dropdown-menu {
|
||||||
|
.dcloud-dropdown-menu {
|
||||||
|
border-radius: 8px;
|
||||||
|
&-item {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-json {
|
||||||
|
padding: 8px 12px !important;
|
||||||
|
border-top-left-radius: 0 !important;
|
||||||
|
border-bottom-left-radius: 0 !important;
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect详情
|
||||||
|
.connect-detail-drawer {
|
||||||
|
.card-bar-container {
|
||||||
|
background: rgba(86, 110, 230, 0.04) !important;
|
||||||
|
|
||||||
|
.card-bar-colunms {
|
||||||
|
background-color: rgba(86, 110, 230, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-title {
|
||||||
|
margin: 20px 0 8px;
|
||||||
|
font-family: @font-family-bold;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
.del-connect-modal {
|
||||||
|
.tip-info {
|
||||||
|
display: flex;
|
||||||
|
color: #592d00;
|
||||||
|
padding: 6px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
background: #fffae0;
|
||||||
|
border-radius: 4px;
|
||||||
|
.anticon {
|
||||||
|
color: #ffc300;
|
||||||
|
margin-right: 4px;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
.test-right-away {
|
||||||
|
color: #556ee6;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.dcloud-alert-content {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重启、继续/暂停 气泡卡片
|
||||||
|
.connect-popconfirm {
|
||||||
|
.dcloud-popover-inner-content {
|
||||||
|
padding: 6px 16px;
|
||||||
|
}
|
||||||
|
.dcloud-popover-inner {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.dcloud-popover-message,
|
||||||
|
.dcloud-btn {
|
||||||
|
font-size: 12px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.operate-connector-drawer {
|
||||||
|
.connector-plugin-desc {
|
||||||
|
font-size: 13px;
|
||||||
|
.connector-plugin-title {
|
||||||
|
font-family: @font-family-bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dcloud-collapse.add-connector-collapse {
|
||||||
|
.add-connector-collapse-panel,
|
||||||
|
.add-connector-collapse-panel:last-child {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 0px;
|
||||||
|
border-radius: 8px;
|
||||||
|
.dcloud-collapse-header {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #495057;
|
||||||
|
.dcloud-collapse-arrow {
|
||||||
|
margin-right: 8px !important;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover .dcloud-collapse-extra {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
&:not(.dcloud-collapse-item-active) {
|
||||||
|
.dcloud-collapse-header:hover {
|
||||||
|
background: #f1f3ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dcloud-collapse-content-box {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 20px 14px 0 14px;
|
||||||
|
.dcloud-form-item {
|
||||||
|
flex: 1 0 50%;
|
||||||
|
.dcloud-input-number {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
&:nth-child(2n) {
|
||||||
|
padding-left: 6px;
|
||||||
|
}
|
||||||
|
&:nth-child(2n + 1) {
|
||||||
|
padding-right: 6px;
|
||||||
|
}
|
||||||
|
.dcloud-form-item-control-input {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-container-plugin-select {
|
||||||
|
height: 27px;
|
||||||
|
margin: 4px 0;
|
||||||
|
position: relative;
|
||||||
|
.dcloud-form-item {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
.dcloud-form-item-control-input {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcloud-alert {
|
||||||
|
margin: 16px 0 24px 0;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: unset;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fffae0;
|
||||||
|
.dcloud-alert-message {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #592d00;
|
||||||
|
.dcloud-btn {
|
||||||
|
padding: 0 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.operate-connector-drawer-use-json {
|
||||||
|
.CodeMirror.cm-s-default {
|
||||||
|
height: calc(100vh - 146px);
|
||||||
|
}
|
||||||
|
.dcloud-form-item {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { ProTable, Dropdown, Button, Utils, AppContainer, SearchInput, Menu } from 'knowdesign';
|
||||||
|
import { IconFont } from '@knowdesign/icons';
|
||||||
|
import API from '../../api';
|
||||||
|
import { getConnectorsColumns, defaultPagination, optionType } from './config';
|
||||||
|
import { tableHeaderPrefix } from '@src/constants/common';
|
||||||
|
import ConnectCard from '@src/components/CardBar/ConnectCard';
|
||||||
|
import DBreadcrumb from 'knowdesign/es/extend/d-breadcrumb';
|
||||||
|
import AddConnector, { OperateInfo } from './AddConnector';
|
||||||
|
import ConnectorDetail from './Detail';
|
||||||
|
import notification from '@src/components/Notification';
|
||||||
|
import './index.less';
|
||||||
|
import AddConnectorUseJSON from './AddConnectorUseJSON';
|
||||||
|
import HasConnector from './HasConnector';
|
||||||
|
const { request } = Utils;
|
||||||
|
|
||||||
|
const rateMap: any = {
|
||||||
|
readRate: ['SinkRecordReadRate', 'SourceRecordPollRate'],
|
||||||
|
writeRate: ['SinkRecordSendRate', 'SourceRecordWriteRate'],
|
||||||
|
recordErrors: ['TotalRecordErrors'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const Connectors: React.FC = () => {
|
||||||
|
const [global] = AppContainer.useGlobalValue();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [detailVisible, setDetailVisible] = useState(false);
|
||||||
|
const [data, setData] = useState([]);
|
||||||
|
const [searchKeywords, setSearchKeywords] = useState('');
|
||||||
|
const [pagination, setPagination] = useState<any>(defaultPagination);
|
||||||
|
const [sortInfo, setSortInfo] = useState({});
|
||||||
|
const [detailRecord, setDetailRecord] = useState('');
|
||||||
|
const [healthType, setHealthType] = useState(true);
|
||||||
|
const addConnectorRef = useRef(null);
|
||||||
|
const addConnectorJsonRef = useRef(null);
|
||||||
|
|
||||||
|
const getRecent1DayTimeStamp = () => [Date.now() - 24 * 60 * 60 * 1000, Date.now()];
|
||||||
|
// 请求接口获取数据
|
||||||
|
const genData = async ({ pageNo, pageSize, filters, sorter }: any) => {
|
||||||
|
const [startStamp, endStamp] = getRecent1DayTimeStamp();
|
||||||
|
if (global?.clusterInfo?.id === undefined) return;
|
||||||
|
setLoading(true);
|
||||||
|
const params = {
|
||||||
|
metricLines: {
|
||||||
|
aggType: 'avg',
|
||||||
|
endTime: endStamp,
|
||||||
|
metricsNames: ['SourceRecordPollRate', 'SourceRecordWriteRate', 'SinkRecordReadRate', 'SinkRecordSendRate', 'TotalRecordErrors'],
|
||||||
|
startTime: startStamp,
|
||||||
|
topNu: 0,
|
||||||
|
},
|
||||||
|
searchKeywords: searchKeywords.slice(0, 128),
|
||||||
|
pageNo,
|
||||||
|
pageSize,
|
||||||
|
latestMetricNames: ['SourceRecordPollRate', 'SourceRecordWriteRate', 'SinkRecordReadRate', 'SinkRecordSendRate', 'TotalRecordErrors'],
|
||||||
|
sortType: sorter?.order ? sorter.order.substring(0, sorter.order.indexOf('end')) : 'desc',
|
||||||
|
sortMetricNameList: rateMap[sorter?.field] || [],
|
||||||
|
};
|
||||||
|
|
||||||
|
request(API.getConnectorsList(global?.clusterInfo?.id), { 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,
|
||||||
|
key: item.connectClusterName + item.connectorName,
|
||||||
|
};
|
||||||
|
}) || [];
|
||||||
|
setData(newData);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||||
|
setSortInfo(sorter);
|
||||||
|
genData({ pageNo: pagination.current, pageSize: pagination.pageSize, filters, sorter });
|
||||||
|
};
|
||||||
|
|
||||||
|
const menu = (
|
||||||
|
<Menu className="">
|
||||||
|
<Menu.Item>
|
||||||
|
<span onClick={() => addConnectorJsonRef.current?.onOpen('create')}>JSON 新增Connector</span>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
const getDetailInfo = (record: any) => {
|
||||||
|
setDetailRecord(record);
|
||||||
|
setDetailVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
const editConnector = (detail: OperateInfo['detail']) => {
|
||||||
|
addConnectorRef.current?.onOpen('edit', addConnectorJsonRef.current, detail);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重启、暂停/继续 操作
|
||||||
|
const optionConnect = (record: any, action: string) => {
|
||||||
|
const params = {
|
||||||
|
action,
|
||||||
|
connectClusterId: record?.connectClusterId,
|
||||||
|
connectorName: record?.connectorName,
|
||||||
|
};
|
||||||
|
|
||||||
|
request(API.connectorsOperates, { method: 'PUT', data: params }).then((res: any) => {
|
||||||
|
if (res === null) {
|
||||||
|
notification.success({
|
||||||
|
message: `任务已${optionType[params.action]}`,
|
||||||
|
description: `任务状态更新会有至多1min延迟`,
|
||||||
|
});
|
||||||
|
genData({ pageNo: pagination.current, pageSize: pagination.pageSize, sorter: sortInfo });
|
||||||
|
setHealthType(!healthType);
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: `${optionType[params.action]}任务失败`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除任务
|
||||||
|
const deleteTesk = () => {
|
||||||
|
genData({ pageNo: 1, pageSize: pagination.pageSize });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
genData({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
sorter: sortInfo,
|
||||||
|
});
|
||||||
|
}, [searchKeywords]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="breadcrumb" style={{ marginBottom: '10px' }}>
|
||||||
|
<DBreadcrumb
|
||||||
|
breadcrumbs={[
|
||||||
|
{ label: '多集群管理', aHref: '/' },
|
||||||
|
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
|
||||||
|
{ label: 'Connect', aHref: `/cluster/${global?.clusterInfo?.id}/connect` },
|
||||||
|
{ label: 'Connectors', aHref: `` },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<HasConnector>
|
||||||
|
<>
|
||||||
|
<div style={{ margin: '12px 0' }}>
|
||||||
|
<ConnectCard state={healthType} />
|
||||||
|
</div>
|
||||||
|
<div className="custom-table-content">
|
||||||
|
<div className={tableHeaderPrefix}>
|
||||||
|
<div className={`${tableHeaderPrefix}-left`}>
|
||||||
|
<div
|
||||||
|
className={`${tableHeaderPrefix}-left-refresh`}
|
||||||
|
onClick={() => genData({ pageNo: pagination.current, pageSize: pagination.pageSize })}
|
||||||
|
>
|
||||||
|
<IconFont className={`${tableHeaderPrefix}-left-refresh-icon`} type="icon-shuaxin1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${tableHeaderPrefix}-right`}>
|
||||||
|
<SearchInput
|
||||||
|
onSearch={setSearchKeywords}
|
||||||
|
attrs={{
|
||||||
|
placeholder: '请输入Connector',
|
||||||
|
style: { width: '248px', borderRiadus: '8px' },
|
||||||
|
maxLength: 128,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="add-connect">
|
||||||
|
<Button
|
||||||
|
className="add-connect-btn"
|
||||||
|
icon={<IconFont type="icon-jiahao" />}
|
||||||
|
type="primary"
|
||||||
|
onClick={() => addConnectorRef.current?.onOpen('create', addConnectorJsonRef.current)}
|
||||||
|
>
|
||||||
|
新增Connector
|
||||||
|
</Button>
|
||||||
|
<Dropdown overlayClassName="add-connect-dropdown-menu" overlay={menu}>
|
||||||
|
<Button className="add-connect-json" type="primary">
|
||||||
|
<IconFont type="icon-guanwangxiala" />
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ProTable
|
||||||
|
key="connector-table"
|
||||||
|
showQueryForm={false}
|
||||||
|
tableProps={{
|
||||||
|
showHeader: false,
|
||||||
|
rowKey: 'key',
|
||||||
|
loading: loading,
|
||||||
|
columns: getConnectorsColumns({ getDetailInfo, deleteTesk, optionConnect, editConnector }),
|
||||||
|
dataSource: data,
|
||||||
|
paginationProps: { ...pagination },
|
||||||
|
attrs: {
|
||||||
|
onChange: onTableChange,
|
||||||
|
scroll: { x: 'max-content', y: 'calc(100vh - 400px)' },
|
||||||
|
bordered: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</HasConnector>
|
||||||
|
|
||||||
|
<ConnectorDetail visible={detailVisible} setVisible={setDetailVisible} record={detailRecord} />
|
||||||
|
<AddConnector
|
||||||
|
ref={addConnectorRef}
|
||||||
|
refresh={() => genData({ pageNo: pagination.current, pageSize: pagination.pageSize, sorter: sortInfo })}
|
||||||
|
/>
|
||||||
|
<AddConnectorUseJSON
|
||||||
|
ref={addConnectorJsonRef}
|
||||||
|
refresh={() => genData({ pageNo: pagination.current, pageSize: pagination.pageSize, sorter: sortInfo })}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Connectors;
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
import React, { useState, useEffect, forwardRef, useImperativeHandle, useRef } from 'react';
|
||||||
|
import { Drawer, Button, Space, Divider, ProTable } from 'knowdesign';
|
||||||
|
import { IconFont } from '@knowdesign/icons';
|
||||||
|
import { ITableColumnsType } from 'knowdesign/lib/extend/d-table';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
import { MetricType } from '@src/api';
|
||||||
|
import '../../components/ChartOperateBar/style/indicator-drawer.less';
|
||||||
|
|
||||||
|
export interface TreeTableDataSourceType {
|
||||||
|
set?: boolean;
|
||||||
|
children?: TreeTableData;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TreeTableData {
|
||||||
|
showHeader: boolean;
|
||||||
|
rowKey: string;
|
||||||
|
columns: ITableColumnsType[];
|
||||||
|
dataSource: TreeTableDataSourceType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckboxStatus {
|
||||||
|
type: MetricType;
|
||||||
|
key: string;
|
||||||
|
status: boolean | 'indeterminate';
|
||||||
|
children?: CheckboxStatus[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TreeTableProps {
|
||||||
|
tree: TreeTableData;
|
||||||
|
checkField: string;
|
||||||
|
submitCallback: ([leafs, checkboxData]: [any[], CheckboxStatus]) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TreeTableItem = (props: {
|
||||||
|
treeData: TreeTableData;
|
||||||
|
checkboxData: CheckboxStatus[];
|
||||||
|
keyTrace: (string | number)[];
|
||||||
|
onKeyChange: any;
|
||||||
|
}) => {
|
||||||
|
const { treeData, checkboxData: data, keyTrace, onKeyChange } = props;
|
||||||
|
return (
|
||||||
|
<ProTable
|
||||||
|
tableProps={{
|
||||||
|
noPagination: true,
|
||||||
|
showHeader: false,
|
||||||
|
rowKey: treeData.rowKey,
|
||||||
|
columns: treeData.columns,
|
||||||
|
dataSource: treeData.dataSource,
|
||||||
|
attrs: Object.assign(
|
||||||
|
{
|
||||||
|
showHeader: treeData.showHeader,
|
||||||
|
rowSelection: {
|
||||||
|
hideSelectAll: keyTrace.length !== 0,
|
||||||
|
selectedRowKeys: data.filter((item) => item.status === true).map((item) => item.key),
|
||||||
|
onChange: (keys: string[]) => {
|
||||||
|
onKeyChange(keyTrace, keys);
|
||||||
|
},
|
||||||
|
getCheckboxProps: (record: TreeTableDataSourceType) => {
|
||||||
|
return {
|
||||||
|
indeterminate: data.some((item) => item.key === record[treeData.rowKey] && item.status === 'indeterminate'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
treeData.dataSource.some((item) => item.children)
|
||||||
|
? {
|
||||||
|
expandable: {
|
||||||
|
childrenColumnName: 'notExist',
|
||||||
|
expandRowByClick: true,
|
||||||
|
expandedRowRender: (record: TreeTableDataSourceType) => {
|
||||||
|
return record?.children ? (
|
||||||
|
<TreeTableItem
|
||||||
|
keyTrace={[...keyTrace, record[treeData.rowKey]]}
|
||||||
|
treeData={record.children}
|
||||||
|
checkboxData={data.find((item) => record[treeData.rowKey] === item.key)?.children}
|
||||||
|
onKeyChange={onKeyChange}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
rowExpandable: (record: TreeTableDataSourceType) => {
|
||||||
|
return !!record?.children;
|
||||||
|
},
|
||||||
|
expandIcon: ({ expanded, onExpand, record }: { expanded: boolean; onExpand: any; record: TreeTableDataSourceType }) => {
|
||||||
|
return record?.children ? (
|
||||||
|
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);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TreeTable = forwardRef((props: TreeTableProps, ref) => {
|
||||||
|
const [treeData, setTreeData] = useState(props.tree);
|
||||||
|
const [checkboxData, setCheckboxData] = useState<CheckboxStatus>();
|
||||||
|
|
||||||
|
// 根据链条查找指定选择框
|
||||||
|
const findTargetCheckbox = (checkboxData: CheckboxStatus, keyTrace: (string | number)[]) => {
|
||||||
|
if (keyTrace.length === 0) {
|
||||||
|
return checkboxData;
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetCheckbox: CheckboxStatus = checkboxData;
|
||||||
|
keyTrace.forEach((key) => {
|
||||||
|
Object.values(targetCheckbox?.children).some((checkbox) => {
|
||||||
|
if (key === checkbox.key) {
|
||||||
|
targetCheckbox = checkbox;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return targetCheckbox;
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeStatus = (checkbox: CheckboxStatus, type: CheckboxStatus['status']) => {
|
||||||
|
checkbox.status = type;
|
||||||
|
(checkbox?.children || []).forEach((c) => changeStatus(c, type));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新节点状态
|
||||||
|
const updateCheckStatus = (data: CheckboxStatus, keyTrace: (string | number)[], keys: string[]) => {
|
||||||
|
// 1. 设置更新元素及其子元素的状态
|
||||||
|
const targetCheckbox = findTargetCheckbox(data, keyTrace);
|
||||||
|
(targetCheckbox?.children || []).forEach((checkbox) => {
|
||||||
|
if (keys.includes(checkbox.key)) {
|
||||||
|
changeStatus(checkbox, true);
|
||||||
|
} else if (checkbox.status === true) {
|
||||||
|
changeStatus(checkbox, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 更新自身状态
|
||||||
|
targetCheckbox.status = keys?.length ? (keys.length === targetCheckbox?.children?.length ? true : 'indeterminate') : false;
|
||||||
|
|
||||||
|
// 3. 更新父级选中状态
|
||||||
|
for (let i = 1; i < keyTrace.length; i++) {
|
||||||
|
const newTrace = keyTrace.slice(0, keyTrace.length - i);
|
||||||
|
const newCheckbox = findTargetCheckbox(data, newTrace);
|
||||||
|
const trueLen = newCheckbox?.children.map((item) => item.status === true).filter((s) => s).length;
|
||||||
|
newCheckbox.status = trueLen === 0 ? false : trueLen === newCheckbox?.children.length ? true : 'indeterminate';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyChange = (keyTrace: (string | number)[], keys: string[]) => {
|
||||||
|
const clonedCheckboxData = cloneDeep(checkboxData);
|
||||||
|
updateCheckStatus(clonedCheckboxData, keyTrace, keys);
|
||||||
|
setCheckboxData(clonedCheckboxData);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成初始选中结构
|
||||||
|
const renderInitCheckboxData = (
|
||||||
|
tree: TreeTableData,
|
||||||
|
curCheckboxData: CheckboxStatus,
|
||||||
|
wholeCheckboxData: CheckboxStatus,
|
||||||
|
keyTrace: (string | number)[],
|
||||||
|
callbacks: (() => void)[]
|
||||||
|
) => {
|
||||||
|
curCheckboxData.children = [];
|
||||||
|
const selectedKeys: string[] = [];
|
||||||
|
|
||||||
|
tree.dataSource.forEach((item) => {
|
||||||
|
const data = {
|
||||||
|
type: item.type,
|
||||||
|
key: item[tree.rowKey],
|
||||||
|
status: false,
|
||||||
|
};
|
||||||
|
curCheckboxData.children.push(data);
|
||||||
|
if (item.children) {
|
||||||
|
renderInitCheckboxData(item.children, data, wholeCheckboxData, [...keyTrace, item[tree.rowKey]], callbacks);
|
||||||
|
} else if (item.set) {
|
||||||
|
selectedKeys.push(item[tree.rowKey]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selectedKeys.length) {
|
||||||
|
callbacks.push(() => updateCheckStatus(wholeCheckboxData, keyTrace, selectedKeys));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取叶子节点状态数组
|
||||||
|
const getLeafNodes = (checkboxData: CheckboxStatus, arr: any[]) => {
|
||||||
|
checkboxData?.children.forEach((item) => {
|
||||||
|
if (item?.children) {
|
||||||
|
getLeafNodes(item, arr);
|
||||||
|
} else {
|
||||||
|
arr.push({ ...item });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLeafCheckboxData = () => {
|
||||||
|
const leafs: any[] = [];
|
||||||
|
getLeafNodes(checkboxData, leafs);
|
||||||
|
return [leafs, checkboxData];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化选中状态数据结构
|
||||||
|
useEffect(() => {
|
||||||
|
const initCheckbox: CheckboxStatus = {
|
||||||
|
type: undefined,
|
||||||
|
key: undefined,
|
||||||
|
status: false,
|
||||||
|
};
|
||||||
|
const callbacks: (() => void)[] = [];
|
||||||
|
renderInitCheckboxData(treeData, initCheckbox, initCheckbox, [], callbacks);
|
||||||
|
callbacks.forEach((cb) => cb());
|
||||||
|
setCheckboxData(initCheckbox);
|
||||||
|
}, [treeData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTreeData(props.tree);
|
||||||
|
}, [props.tree]);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
getLeafCheckboxData,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return checkboxData ? (
|
||||||
|
<TreeTableItem treeData={treeData} checkboxData={checkboxData?.children} keyTrace={[]} onKeyChange={onKeyChange} />
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TreeTableDrawer = forwardRef((props: TreeTableProps, ref) => {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
|
const treeTableRef = useRef(null);
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
setConfirmLoading(true);
|
||||||
|
props
|
||||||
|
.submitCallback(treeTableRef.current?.getLeafCheckboxData())
|
||||||
|
.then(() => {
|
||||||
|
setVisible(false);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setConfirmLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
open: () => setVisible(true),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
className="indicator-drawer"
|
||||||
|
title="指标筛选"
|
||||||
|
width="868px"
|
||||||
|
forceRender={true}
|
||||||
|
onClose={() => setVisible(false)}
|
||||||
|
visible={visible}
|
||||||
|
maskClosable={false}
|
||||||
|
destroyOnClose
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<Button size="small" onClick={() => setVisible(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" size="small" loading={confirmLoading} onClick={onSubmit}>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
<Divider type="vertical" />
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TreeTable ref={treeTableRef} {...props} />
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
import React, { useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||||
|
import api, { MetricType } from '@src/api';
|
||||||
|
import { arrayMoveImmutable } from 'array-move';
|
||||||
|
import { expandedRowColumns } from '@src/components/ChartOperateBar/MetricSelect';
|
||||||
|
import { TreeTableDataSourceType, TreeTableDrawer, TreeTableData, CheckboxStatus } from './MetricSelect';
|
||||||
|
import { MetricInfo } from '@src/constants/chartConfig';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
import { AppContainer, Utils } from 'knowdesign';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface MetricsFilterProps {
|
||||||
|
metricType: MetricType[];
|
||||||
|
onSelectChange: (list: MetricInfo[]) => void;
|
||||||
|
onRankChange: (rankList: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理图表排序
|
||||||
|
const resolveMetricsRank = (metricList: MetricInfo[]) => {
|
||||||
|
const isRanked = metricList.some(({ rank }) => rank !== null);
|
||||||
|
const listInfo: { [key: string]: { metric: string; rank: number; set: boolean }[] } = {};
|
||||||
|
let shouldUpdate = false;
|
||||||
|
let sortedList: MetricInfo[] = [];
|
||||||
|
|
||||||
|
if (isRanked) {
|
||||||
|
const rankedMetrics = metricList.filter(({ rank }) => rank !== null).sort((a, b) => a.rank - b.rank);
|
||||||
|
const unRankedMetrics = metricList.filter(({ rank }) => rank === null);
|
||||||
|
// 如果有新增/删除指标的情况,需要触发更新
|
||||||
|
if (unRankedMetrics.length || rankedMetrics.some(({ rank }, i) => rank !== i)) {
|
||||||
|
shouldUpdate = true;
|
||||||
|
}
|
||||||
|
sortedList = [...rankedMetrics, ...unRankedMetrics.sort((a, b) => Number(a.name > b.name) - 0.5)];
|
||||||
|
} else {
|
||||||
|
shouldUpdate = true;
|
||||||
|
// 按字母先后顺序初始化指标排序
|
||||||
|
sortedList = metricList.sort((a, b) => a.type - b.type).sort((a, b) => (a.type !== b.type ? 1 : Number(a.name > b.name) - 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
sortedList.forEach((metric, rank) => {
|
||||||
|
!listInfo[metric.type] && (listInfo[metric.type] = []);
|
||||||
|
listInfo[metric.type].push({ metric: metric.name, rank, set: metricList.find(({ name }) => metric.name === name)?.set || false });
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
list: sortedList.map(({ name }) => name),
|
||||||
|
listInfo,
|
||||||
|
shouldUpdate,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const MetricsFilter = forwardRef((props: MetricsFilterProps, ref) => {
|
||||||
|
const [global] = AppContainer.useGlobalValue();
|
||||||
|
const { metricType, onSelectChange, onRankChange } = props;
|
||||||
|
const { clusterId } = useParams<{
|
||||||
|
clusterId: string;
|
||||||
|
}>();
|
||||||
|
const [metricsList, setMetricsList] = useState<MetricInfo[]>([]); // 指标列表
|
||||||
|
const [metricRankList, setMetricRankList] = useState<string[]>([]);
|
||||||
|
const [tree, setTree] = useState<TreeTableData>();
|
||||||
|
const metricSelectRef = useRef(null);
|
||||||
|
|
||||||
|
// 更新指标
|
||||||
|
const setMetricList = (list: { [key: string]: { metric: string; rank: number; set: boolean }[] }) => {
|
||||||
|
const reqArr: Promise<any>[] = [];
|
||||||
|
Object.entries(list).forEach(([type, metricDetailDTOList]) => {
|
||||||
|
reqArr.push(
|
||||||
|
Utils.request(api.getDashboardMetricList(clusterId, type as unknown as MetricType), {
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
metricDetailDTOList,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return Promise.all(reqArr);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: 图表展示顺序变更
|
||||||
|
const rankChange = (oldIndex: number, newIndex: number) => {
|
||||||
|
const newList = arrayMoveImmutable(metricRankList, oldIndex, newIndex);
|
||||||
|
setMetricRankList(newList);
|
||||||
|
const updates: { [key: string]: { metric: string; rank: number; set: boolean }[] } = {};
|
||||||
|
newList.forEach((metric, rank) => {
|
||||||
|
const targetMetric = metricsList.find(({ name }) => metric === name);
|
||||||
|
if (targetMetric) {
|
||||||
|
const info = { metric, rank, set: targetMetric?.set || false };
|
||||||
|
updates[targetMetric.type] ? updates[targetMetric.type].push(info) : (updates[targetMetric.type] = [info]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setMetricList(updates);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新 rank
|
||||||
|
const updateRank = (metricList: MetricInfo[]) => {
|
||||||
|
const { list, listInfo, shouldUpdate } = resolveMetricsRank(metricList);
|
||||||
|
setMetricRankList(list);
|
||||||
|
if (shouldUpdate) {
|
||||||
|
setMetricList(listInfo);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取指标列表
|
||||||
|
const getMetricList = () => {
|
||||||
|
Promise.all(metricType.map((type) => Utils.request(api.getDashboardMetricList(clusterId, type)))).then((list) => {
|
||||||
|
let allSupportMetrics: MetricInfo[] = [];
|
||||||
|
|
||||||
|
const treeData: TreeTableData = {
|
||||||
|
showHeader: true,
|
||||||
|
rowKey: 'category',
|
||||||
|
columns: [{ title: '分类', dataIndex: 'category' }],
|
||||||
|
dataSource: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
(list as unknown as (MetricInfo[] | null)[]).forEach((res, i) => {
|
||||||
|
if (!res) return;
|
||||||
|
const supportMetrics = res.filter((metric) => metric.support);
|
||||||
|
allSupportMetrics = allSupportMetrics.concat(supportMetrics);
|
||||||
|
|
||||||
|
// 处理结构
|
||||||
|
const data: TreeTableDataSourceType = {
|
||||||
|
category: metricType[i] === MetricType.Connect ? 'Connect Cluster' : 'Connector',
|
||||||
|
};
|
||||||
|
const categoryData: {
|
||||||
|
[category: string]: {
|
||||||
|
name: string;
|
||||||
|
unit: string;
|
||||||
|
desc: string;
|
||||||
|
}[];
|
||||||
|
} = {};
|
||||||
|
supportMetrics.forEach(({ name, desc, set }) => {
|
||||||
|
const metricDefine = global.getMetricDefine(metricType[i], name);
|
||||||
|
const returnData = {
|
||||||
|
type: metricType[i],
|
||||||
|
set,
|
||||||
|
name,
|
||||||
|
desc,
|
||||||
|
unit: metricDefine?.unit,
|
||||||
|
};
|
||||||
|
if (metricDefine.category) {
|
||||||
|
if (!categoryData[metricDefine.category]) {
|
||||||
|
categoryData[metricDefine.category] = [returnData];
|
||||||
|
} else {
|
||||||
|
categoryData[metricDefine.category].push(returnData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!categoryData['Other']) {
|
||||||
|
categoryData['Other'] = [returnData];
|
||||||
|
} else {
|
||||||
|
categoryData['Other'].push(returnData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(categoryData).length > 1) {
|
||||||
|
const returnData: TreeTableData = {
|
||||||
|
showHeader: false,
|
||||||
|
rowKey: 'category',
|
||||||
|
columns: [{ title: '分类', dataIndex: 'category' }],
|
||||||
|
dataSource: [],
|
||||||
|
};
|
||||||
|
Object.entries(categoryData).forEach(([category, data]) => {
|
||||||
|
returnData.dataSource.push({
|
||||||
|
category,
|
||||||
|
children: {
|
||||||
|
showHeader: true,
|
||||||
|
rowKey: 'name',
|
||||||
|
columns: expandedRowColumns,
|
||||||
|
dataSource: data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
data.children = returnData;
|
||||||
|
} else {
|
||||||
|
data.children = {
|
||||||
|
showHeader: true,
|
||||||
|
rowKey: 'name',
|
||||||
|
columns: expandedRowColumns,
|
||||||
|
dataSource: Object.values(categoryData)[0] || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
treeData.dataSource.push(data);
|
||||||
|
});
|
||||||
|
setTree(treeData);
|
||||||
|
|
||||||
|
updateRank([...allSupportMetrics]);
|
||||||
|
setMetricsList(allSupportMetrics);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: 指标选中项更新回调
|
||||||
|
const metricSelectCallback = ([newMetrics]: [CheckboxStatus[], any]) => {
|
||||||
|
const updateMetrics: { [key: string]: { metric: string; set: boolean; rank: number }[] } = {};
|
||||||
|
|
||||||
|
newMetrics.forEach((metric) => {
|
||||||
|
const oldMetric = metricsList.find(({ type, name }) => type === metric.type && name === metric.key);
|
||||||
|
|
||||||
|
if (oldMetric) {
|
||||||
|
if (oldMetric.set !== metric.status) {
|
||||||
|
if (updateMetrics[metric.type]) {
|
||||||
|
updateMetrics[metric.type].push({ metric: oldMetric.name, set: !oldMetric.set, rank: oldMetric.rank });
|
||||||
|
} else {
|
||||||
|
updateMetrics[metric.type] = [{ metric: oldMetric.name, set: !oldMetric.set, rank: oldMetric.rank }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestPromise = Object.keys(updateMetrics).length ? setMetricList(updateMetrics) : Promise.resolve();
|
||||||
|
requestPromise.then(
|
||||||
|
() => getMetricList(),
|
||||||
|
() => getMetricList()
|
||||||
|
);
|
||||||
|
return requestPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onSelectChange(metricsList);
|
||||||
|
}, [metricsList]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onRankChange(metricRankList);
|
||||||
|
}, [metricRankList]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getMetricList();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
rankChange,
|
||||||
|
open: () => metricSelectRef.current?.open(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return tree && <TreeTableDrawer ref={metricSelectRef} tree={tree} checkField="set" submitCallback={metricSelectCallback} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default MetricsFilter;
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { DataNode } from 'knowdesign/lib/basic/tree';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { Tree, Input, Utils, Button } from 'knowdesign';
|
||||||
|
import api from '@src/api';
|
||||||
|
import type { ConnectCluster } from '../Connect/AddConnector';
|
||||||
|
|
||||||
|
export interface Connector {
|
||||||
|
connectClusterId: number;
|
||||||
|
connectClusterName: string;
|
||||||
|
connectorName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectContentProps {
|
||||||
|
title: string;
|
||||||
|
scopeList: {
|
||||||
|
connectClusters: ConnectCluster[];
|
||||||
|
connectors: Connector[];
|
||||||
|
};
|
||||||
|
isTop?: boolean;
|
||||||
|
visibleChange?: (v: boolean) => void;
|
||||||
|
onChange?: (list: any, inputValue: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CheckedNodes {
|
||||||
|
connectClusters: number[];
|
||||||
|
connectors: { connectClusterId: number; connectorName: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CheckedKeysProps {
|
||||||
|
checked: React.Key[];
|
||||||
|
halfChecked: React.Key[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const getParentKey = (key: React.Key, tree: DataNode[]): React.Key => {
|
||||||
|
let parentKey: React.Key;
|
||||||
|
for (let i = 0; i < tree.length; i++) {
|
||||||
|
const node = tree[i];
|
||||||
|
if (node.children) {
|
||||||
|
if (node.children.some((item) => item.key === key)) {
|
||||||
|
parentKey = node.key;
|
||||||
|
} else if (getParentKey(key, node.children)) {
|
||||||
|
parentKey = getParentKey(key, node.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parentKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SelectContent = (props: SelectContentProps) => {
|
||||||
|
const { isTop, visibleChange, onChange } = props;
|
||||||
|
const { clusterId } = useParams<{
|
||||||
|
clusterId: string;
|
||||||
|
}>();
|
||||||
|
const [treeData, setTreeData] = useState<DataNode[]>([]);
|
||||||
|
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
|
||||||
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
const [autoExpandParent, setAutoExpandParent] = useState(true);
|
||||||
|
const [checkedKeys, setCheckedKeys] = useState<CheckedKeysProps>({
|
||||||
|
checked: [],
|
||||||
|
halfChecked: [],
|
||||||
|
});
|
||||||
|
const [checkedNodes, setCheckedNodes] = useState<CheckedNodes>({
|
||||||
|
connectClusters: [],
|
||||||
|
connectors: [],
|
||||||
|
});
|
||||||
|
const defaultChecked = useRef<CheckedKeysProps>({
|
||||||
|
checked: [],
|
||||||
|
halfChecked: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const onExpand = (newExpandedKeys: string[]) => {
|
||||||
|
setExpandedKeys(newExpandedKeys);
|
||||||
|
setAutoExpandParent(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCheck = (keys: CheckedKeysProps, { checkedNodes }: { checkedNodes: DataNode[] }) => {
|
||||||
|
const returnData: CheckedNodes = {
|
||||||
|
connectClusters: [],
|
||||||
|
connectors: [],
|
||||||
|
};
|
||||||
|
checkedNodes.map((node) => {
|
||||||
|
if (node.children) {
|
||||||
|
returnData.connectClusters.push(node.key as number);
|
||||||
|
} else {
|
||||||
|
const [id, ...rest] = (node.key as string).split(':');
|
||||||
|
returnData.connectors.push({
|
||||||
|
connectClusterId: Number(id),
|
||||||
|
connectorName: rest.join(':'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setCheckedNodes(returnData);
|
||||||
|
setCheckedKeys(
|
||||||
|
keys as {
|
||||||
|
checked: any[];
|
||||||
|
halfChecked: any[];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
onChange(checkedNodes, `${checkedNodes.connectClusters.length + checkedNodes.connectors.length}项`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
visibleChange(false);
|
||||||
|
setCheckedKeys(defaultChecked.current);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
let newExpandedKeys: React.Key[] = [];
|
||||||
|
treeData.forEach((item) => {
|
||||||
|
if (String(item.title).indexOf(value) > -1) {
|
||||||
|
newExpandedKeys.push(getParentKey(item.key, treeData));
|
||||||
|
}
|
||||||
|
item.children?.forEach((item) => {
|
||||||
|
if (String(item.title).indexOf(value) > -1) {
|
||||||
|
newExpandedKeys.push(getParentKey(item.key, treeData));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
newExpandedKeys = newExpandedKeys.filter((item, i, self) => item && self.indexOf(item) === i);
|
||||||
|
setExpandedKeys(newExpandedKeys as React.Key[]);
|
||||||
|
setSearchValue(value);
|
||||||
|
setAutoExpandParent(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterTreeData = useMemo(() => {
|
||||||
|
const loop = (data: DataNode[]): DataNode[] =>
|
||||||
|
data.map((item) => {
|
||||||
|
const strTitle = item.title as string;
|
||||||
|
const index = strTitle.indexOf(searchValue);
|
||||||
|
const beforeStr = strTitle.substring(0, index);
|
||||||
|
const afterStr = strTitle.slice(index + searchValue.length);
|
||||||
|
const title =
|
||||||
|
index > -1 ? (
|
||||||
|
<span>
|
||||||
|
{beforeStr}
|
||||||
|
<span className="site-tree-search-value">{searchValue}</span>
|
||||||
|
{afterStr}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>{strTitle}</span>
|
||||||
|
);
|
||||||
|
if (item.children) {
|
||||||
|
return { title, key: item.key, children: loop(item.children) };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
key: item.key,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return loop(treeData);
|
||||||
|
}, [treeData, searchValue]);
|
||||||
|
|
||||||
|
// 获取节点范围列表
|
||||||
|
const getScopeList = async () => {
|
||||||
|
const clustersMap: { [key: string]: DataNode } = {};
|
||||||
|
props.scopeList.connectClusters.forEach((connectCluster) => {
|
||||||
|
clustersMap[connectCluster.id] = {
|
||||||
|
title: connectCluster.name,
|
||||||
|
key: connectCluster.id,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
props.scopeList.connectors.forEach((connector) => {
|
||||||
|
const targetConnectCluster = clustersMap[connector.connectClusterId];
|
||||||
|
if (targetConnectCluster) {
|
||||||
|
targetConnectCluster.children.push({
|
||||||
|
title: connector.connectorName,
|
||||||
|
key: `${connector.connectClusterId}:${connector.connectorName}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setTreeData(Object.values(clustersMap));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isTop) {
|
||||||
|
setCheckedKeys({
|
||||||
|
checked: [],
|
||||||
|
halfChecked: [],
|
||||||
|
});
|
||||||
|
setCheckedNodes({
|
||||||
|
connectClusters: [],
|
||||||
|
connectors: [],
|
||||||
|
});
|
||||||
|
defaultChecked.current = {
|
||||||
|
checked: [],
|
||||||
|
halfChecked: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [isTop]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getScopeList();
|
||||||
|
}, [props.scopeList]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h6 className="time_title">{props.title}</h6>
|
||||||
|
<div className="custom-scope dashboard-custom-scope">
|
||||||
|
<div style={{ padding: '0 16px 12px 16px' }}>
|
||||||
|
<Input size="small" placeholder="请输入内容筛选" onChange={onInputChange} />
|
||||||
|
</div>
|
||||||
|
<Tree
|
||||||
|
className="connect-dashboard-option-tree"
|
||||||
|
checkable
|
||||||
|
checkStrictly
|
||||||
|
treeData={filterTreeData}
|
||||||
|
checkedKeys={checkedKeys}
|
||||||
|
expandedKeys={expandedKeys}
|
||||||
|
autoExpandParent={autoExpandParent}
|
||||||
|
onExpand={onExpand}
|
||||||
|
onCheck={onCheck}
|
||||||
|
/>
|
||||||
|
<div className="btn-con">
|
||||||
|
<Button type="primary" size="small" className="btn-sure" onClick={onSubmit} disabled={checkedKeys.checked.length === 0}>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
<Button size="small" onClick={onCancel}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SelectContent;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
.dcloud-tree.connect-dashboard-option-tree {
|
||||||
|
background: transparent;
|
||||||
|
height: 198px;
|
||||||
|
padding: 0 10px;
|
||||||
|
overflow: auto;
|
||||||
|
.site-tree-search-value {
|
||||||
|
color: #f50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-node-scope-module .flx_con .flx_r .custom-scope.dashboard-custom-scope {
|
||||||
|
height: 292px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Utils, AppContainer } from 'knowdesign';
|
||||||
|
import api, { MetricType } from '@src/api';
|
||||||
|
import DBreadcrumb from 'knowdesign/es/extend/d-breadcrumb';
|
||||||
|
import ConnectCard from '@src/components/CardBar/ConnectCard';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { FormattedMetricData, formatChartData, MetricInfo } from '@src/constants/chartConfig';
|
||||||
|
import ChartOperateBar, { KsHeaderOptions } from '@src/components/ChartOperateBar';
|
||||||
|
import ChartDetail from '@src/components/DraggableCharts/Detail';
|
||||||
|
import ChartList from '@src/components/DraggableCharts/ChartList';
|
||||||
|
import MetricsFilter from './MetricsFilter';
|
||||||
|
import SelectContent, { Connector } from './SelectContent';
|
||||||
|
import './index.less';
|
||||||
|
import { ConnectCluster } from '../Connect/AddConnector';
|
||||||
|
import HasConnector from '../Connect/HasConnector';
|
||||||
|
|
||||||
|
type ChartFilterOptions = Omit<KsHeaderOptions, 'gridNum'>;
|
||||||
|
|
||||||
|
const { EventBus } = Utils;
|
||||||
|
const busInstance = new EventBus();
|
||||||
|
|
||||||
|
const DraggableCharts = (): JSX.Element => {
|
||||||
|
const [global] = AppContainer.useGlobalValue();
|
||||||
|
const { clusterId } = useParams<{
|
||||||
|
clusterId: string;
|
||||||
|
}>();
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
// const [scopeList, setScopeList] = useState<IcustomScope[]>([]); // 节点范围列表
|
||||||
|
const [curHeaderOptions, setCurHeaderOptions] = useState<ChartFilterOptions>();
|
||||||
|
const [metricList, setMetricList] = useState<{ [key: string]: (string | number)[] }>({});
|
||||||
|
const [metricChartData, setMetricChartData] = useState<FormattedMetricData[]>([]); // 指标图表数据列表
|
||||||
|
const [gridNum, setGridNum] = useState<number>(12); // 图表列布局
|
||||||
|
const [scopeList, setScopeList] = useState<{
|
||||||
|
connectClusters: ConnectCluster[];
|
||||||
|
connectors: Connector[];
|
||||||
|
}>({
|
||||||
|
connectClusters: [],
|
||||||
|
connectors: [],
|
||||||
|
});
|
||||||
|
const curFetchingTimestamp = useRef(0);
|
||||||
|
const metricRankList = useRef<string[]>([]);
|
||||||
|
const metricFilterRef = useRef(null);
|
||||||
|
const chartDetailRef = useRef(null);
|
||||||
|
|
||||||
|
// 根据筛选项获取图表信息
|
||||||
|
const getMetricChartData = () => {
|
||||||
|
!curHeaderOptions.isAutoReload && setLoading(true);
|
||||||
|
const curTimestamp = Date.now();
|
||||||
|
curFetchingTimestamp.current = curTimestamp;
|
||||||
|
|
||||||
|
const [startTime, endTime] = curHeaderOptions.rangeTime;
|
||||||
|
const { connectClusters, connectors } = curHeaderOptions.scopeData.data;
|
||||||
|
const isTop = curHeaderOptions?.scopeData?.isTop;
|
||||||
|
const reqBasicBody = {
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
topNu: isTop ? curHeaderOptions.scopeData.data : null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getConnectClusterMetrics =
|
||||||
|
metricList[MetricType.Connect] && (isTop || connectClusters?.length)
|
||||||
|
? Utils.post(
|
||||||
|
api.getConnectClusterMetrics(clusterId),
|
||||||
|
Object.assign(
|
||||||
|
{
|
||||||
|
...reqBasicBody,
|
||||||
|
metricsNames: metricList[MetricType.Connect],
|
||||||
|
},
|
||||||
|
isTop
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
connectClusterIdList: connectClusters,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: Promise.resolve([]);
|
||||||
|
const getConnectorMetrics =
|
||||||
|
metricList[MetricType.Connectors] && (isTop || connectors?.length)
|
||||||
|
? Utils.post(
|
||||||
|
api.getConnectorMetrics(clusterId),
|
||||||
|
Object.assign(
|
||||||
|
{
|
||||||
|
...reqBasicBody,
|
||||||
|
metricsNames: metricList[MetricType.Connectors],
|
||||||
|
},
|
||||||
|
isTop ? {} : { connectorNameList: connectors }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: Promise.resolve([]);
|
||||||
|
|
||||||
|
Promise.all([getConnectClusterMetrics, getConnectorMetrics]).then(
|
||||||
|
(res: any) => {
|
||||||
|
// 如果当前请求不是最新请求,则不做任何操作
|
||||||
|
if (curFetchingTimestamp.current !== curTimestamp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为保证指标排序结果正确,当指标全部返回后才展示图表
|
||||||
|
if (res.length === 1 || (res.length === 2 && res[0] && res[1])) {
|
||||||
|
const connectClusterData = formatChartData(
|
||||||
|
res[0],
|
||||||
|
global.getMetricDefine || {},
|
||||||
|
MetricType.Connect,
|
||||||
|
curHeaderOptions.rangeTime
|
||||||
|
) as FormattedMetricData[];
|
||||||
|
const connectorData = formatChartData(
|
||||||
|
res[1],
|
||||||
|
global.getMetricDefine || {},
|
||||||
|
MetricType.Connectors,
|
||||||
|
curHeaderOptions.rangeTime
|
||||||
|
) as FormattedMetricData[];
|
||||||
|
// 指标排序
|
||||||
|
const formattedMetricData = [...connectClusterData, ...connectorData];
|
||||||
|
formattedMetricData.sort((a, b) => metricRankList.current.indexOf(a.metricName) - metricRankList.current.indexOf(b.metricName));
|
||||||
|
|
||||||
|
setMetricChartData(formattedMetricData);
|
||||||
|
} else {
|
||||||
|
setMetricChartData([]);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
},
|
||||||
|
() => curFetchingTimestamp.current === curTimestamp && setLoading(false)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScopeList = () => {
|
||||||
|
const getConnectClusters = Utils.request(api.getConnectClusters(clusterId));
|
||||||
|
const getConnectors = Utils.request(api.getConnectors(clusterId));
|
||||||
|
Promise.all([getConnectClusters, getConnectors]).then(([connectClusters, connectors]: [ConnectCluster[], Connector[]]) => {
|
||||||
|
setScopeList({
|
||||||
|
connectClusters,
|
||||||
|
connectors,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 筛选项变化或者点击刷新按钮
|
||||||
|
const ksHeaderChange = (ksOptions: KsHeaderOptions) => {
|
||||||
|
const { isAutoReload, gridNum: newGridNum, isRelativeRangeTime, rangeTime, scopeData } = ksOptions;
|
||||||
|
let newRangeTime = rangeTime;
|
||||||
|
// 重新渲染图表
|
||||||
|
if (newGridNum !== gridNum) {
|
||||||
|
setGridNum(newGridNum || 12);
|
||||||
|
busInstance.emit('chartResize');
|
||||||
|
} else {
|
||||||
|
// 如果为相对时间,则当前时间减去 1 分钟,避免最近一分钟的数据还没采集到时前端多补一个点
|
||||||
|
if (isRelativeRangeTime) {
|
||||||
|
newRangeTime = rangeTime.map((timestamp) => timestamp - 60 * 1000) as [number, number];
|
||||||
|
}
|
||||||
|
setCurHeaderOptions({
|
||||||
|
isRelativeRangeTime: isRelativeRangeTime,
|
||||||
|
isAutoReload: isAutoReload,
|
||||||
|
rangeTime: newRangeTime,
|
||||||
|
scopeData: scopeData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 图表拖拽
|
||||||
|
const dragCallback = (oldIndex: number, newIndex: number) => {
|
||||||
|
const originFrom = metricRankList.current.indexOf(metricChartData[oldIndex].metricName);
|
||||||
|
const originTarget = metricRankList.current.indexOf(metricChartData[newIndex].metricName);
|
||||||
|
metricFilterRef.current?.rankChange(originFrom, originTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 展开图表详情
|
||||||
|
const onExpand = (metricName: string, metricType: MetricType) => {
|
||||||
|
const linesName =
|
||||||
|
metricType === MetricType.Connect
|
||||||
|
? scopeList.connectClusters.map((cluster) => cluster.id)
|
||||||
|
: scopeList.connectors.map((connector) => ({
|
||||||
|
connectClusterId: connector.connectClusterId,
|
||||||
|
connectorName: connector.connectorName,
|
||||||
|
}));
|
||||||
|
chartDetailRef.current.onOpen(metricType, metricName, linesName);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取图表指标
|
||||||
|
useEffect(() => {
|
||||||
|
if (Object.values(metricList).some((list) => list.length) && curHeaderOptions) {
|
||||||
|
getMetricChartData();
|
||||||
|
}
|
||||||
|
}, [curHeaderOptions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Object.values(metricList).some((list) => list.length) && curHeaderOptions) {
|
||||||
|
setLoading(true);
|
||||||
|
getMetricChartData();
|
||||||
|
}
|
||||||
|
}, [metricList]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getScopeList();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="dashboard-drag-chart" className="topic-dashboard">
|
||||||
|
<ChartOperateBar
|
||||||
|
onChange={ksHeaderChange}
|
||||||
|
hideNodeScope={false}
|
||||||
|
openMetricFilter={() => metricFilterRef.current?.open()}
|
||||||
|
nodeSelect={{
|
||||||
|
name: 'Connect',
|
||||||
|
customContent: <SelectContent scopeList={scopeList} title="请选择 Connect 范围" />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<MetricsFilter
|
||||||
|
ref={metricFilterRef}
|
||||||
|
metricType={[MetricType.Connect, MetricType.Connectors]}
|
||||||
|
onSelectChange={(list) => {
|
||||||
|
const res: { [key: string]: (string | number)[] } = {};
|
||||||
|
list.forEach(({ type, name, set }) => {
|
||||||
|
set && (res[type] ? res[type].push(name) : (res[type] = [name]));
|
||||||
|
});
|
||||||
|
setMetricList(res);
|
||||||
|
}}
|
||||||
|
onRankChange={(rankList) => {
|
||||||
|
metricRankList.current = rankList;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ChartList
|
||||||
|
busInstance={busInstance}
|
||||||
|
loading={loading}
|
||||||
|
gridNum={gridNum}
|
||||||
|
data={metricChartData}
|
||||||
|
autoReload={curHeaderOptions?.isAutoReload}
|
||||||
|
dragCallback={dragCallback}
|
||||||
|
onExpand={onExpand}
|
||||||
|
/>
|
||||||
|
{/* 图表详情 */}
|
||||||
|
<ChartDetail ref={chartDetailRef} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConnectDashboard = (): 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: 'Connect', aHref: `` },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<HasConnector>
|
||||||
|
<>
|
||||||
|
<ConnectCard />
|
||||||
|
<DraggableCharts />
|
||||||
|
</>
|
||||||
|
</HasConnector>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConnectDashboard;
|
||||||
@@ -79,7 +79,7 @@ const BrokerList: React.FC = (props: any) => {
|
|||||||
<div style={{ margin: '12px 0' }}>
|
<div style={{ margin: '12px 0' }}>
|
||||||
<ConsumerGroupHealthCheck />
|
<ConsumerGroupHealthCheck />
|
||||||
</div>
|
</div>
|
||||||
<div className="clustom-table-content">
|
<div className="custom-table-content">
|
||||||
<div className={tableHeaderPrefix}>
|
<div className={tableHeaderPrefix}>
|
||||||
<div className={`${tableHeaderPrefix}-left`}>
|
<div className={`${tableHeaderPrefix}-left`}>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -181,12 +181,12 @@ const AutoPage = (props: any) => {
|
|||||||
<ConsumerGroupHealthCheck></ConsumerGroupHealthCheck>
|
<ConsumerGroupHealthCheck></ConsumerGroupHealthCheck>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={`operating-state ${scene !== 'topicDetail' && 'clustom-table-content'}`}>
|
<div className={`operating-state ${scene !== 'topicDetail' && 'custom-table-content'}`}>
|
||||||
{/* <CardBar cardColumns={data}></CardBar> */}
|
{/* <CardBar cardColumns={data}></CardBar> */}
|
||||||
{scene !== 'topicDetail' && (
|
{scene !== 'topicDetail' && (
|
||||||
<div className={tableHeaderPrefix}>
|
<div className={tableHeaderPrefix}>
|
||||||
<div className={`${tableHeaderPrefix}-left`}>
|
<div className={`${tableHeaderPrefix}-left`}>
|
||||||
<Tooltip placement="topLeft" arrowPointAtCenter title='数据刷新间隔为1min,可能会有延迟'>
|
<Tooltip placement="topLeft" arrowPointAtCenter title="数据刷新间隔为1min,可能会有延迟">
|
||||||
<div className={`${tableHeaderPrefix}-left-refresh`} onClick={() => searchFn()}>
|
<div className={`${tableHeaderPrefix}-left-refresh`} onClick={() => searchFn()}>
|
||||||
<IconFont className={`${tableHeaderPrefix}-left-refresh-icon`} type="icon-shuaxin1" />
|
<IconFont className={`${tableHeaderPrefix}-left-refresh-icon`} type="icon-shuaxin1" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
.dcloud-table-container::after {
|
.dcloud-table-container::after {
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
.dcloud-pagination{
|
.dcloud-pagination {
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,13 +118,14 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
&-text,&-text-right{
|
&-text,
|
||||||
|
&-text-right {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
}
|
}
|
||||||
&-text-right{
|
&-text-right {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #556EE6;
|
color: #556ee6;
|
||||||
}
|
}
|
||||||
&-img {
|
&-img {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -155,15 +156,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dcloud-checkbox-table-serch{
|
.dcloud-checkbox-table-serch {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
.clustom-table-content {
|
|
||||||
.anticon {
|
.jobs-self {
|
||||||
padding: 3px;
|
.dcloud-table-cell {
|
||||||
border-radius: 50%;
|
.anticon {
|
||||||
&:hover {
|
padding: 3px;
|
||||||
background: rgba(33, 37, 41, 0.04);
|
border-radius: 50%;
|
||||||
|
&:hover {
|
||||||
|
background: rgba(33, 37, 41, 0.04);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ const JobsList: React.FC = (props: any) => {
|
|||||||
<JobsCheck />
|
<JobsCheck />
|
||||||
</div>
|
</div>
|
||||||
{/* <Form form={form} layout="inline" onFinish={onFinish}> */}
|
{/* <Form form={form} layout="inline" onFinish={onFinish}> */}
|
||||||
<div className="clustom-table-content">
|
<div className="custom-table-content jobs-self">
|
||||||
<div className={tableHeaderPrefix}>
|
<div className={tableHeaderPrefix}>
|
||||||
<div className={`${tableHeaderPrefix}-left`}>
|
<div className={`${tableHeaderPrefix}-left`}>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -349,7 +349,7 @@ const LoadBalance: React.FC = (props: any) => {
|
|||||||
<LoadRebalanceCardBar trigger={trigger} genData={resetList} filterList={filterList} />
|
<LoadRebalanceCardBar trigger={trigger} genData={resetList} filterList={filterList} />
|
||||||
</div>
|
</div>
|
||||||
<div className="load-rebalance-container">
|
<div className="load-rebalance-container">
|
||||||
<div className="balance-main clustom-table-content">
|
<div className="balance-main custom-table-content">
|
||||||
<div className={tableHeaderPrefix}>
|
<div className={tableHeaderPrefix}>
|
||||||
<div className={`${tableHeaderPrefix}-left`}>
|
<div className={`${tableHeaderPrefix}-left`}>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const carouselList = [
|
|||||||
<img className="carousel-eg-ctr-two-img img-one" src={egTwoContent} />
|
<img className="carousel-eg-ctr-two-img img-one" src={egTwoContent} />
|
||||||
<div className="carousel-eg-ctr-two-desc desc-one">
|
<div className="carousel-eg-ctr-two-desc desc-one">
|
||||||
<span>Github: </span>
|
<span>Github: </span>
|
||||||
<span>5.4K</span>
|
<span>5.6K</span>
|
||||||
<span>+ Star的项目 Know Streaming</span>
|
<span>+ Star的项目 Know Streaming</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="carousel-eg-ctr-two-desc desc-two">
|
<div className="carousel-eg-ctr-two-desc desc-two">
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Button, Divider, Drawer, Form, Input, InputNumber, Radio, Select, Spin, Space, Utils } from 'knowdesign';
|
import { Button, Divider, Drawer, Form, Input, InputNumber, Radio, Select, Spin, Space, Utils, Tabs, Collapse, Empty } from 'knowdesign';
|
||||||
import message from '@src/components/Message';
|
import message from '@src/components/Message';
|
||||||
import * as React from 'react';
|
import React, { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import api from '@src/api';
|
import api from '@src/api';
|
||||||
import { regClusterName, regUsername } from '@src/constants/reg';
|
import { regClusterName, regIpAndPort, regUsername } from '@src/constants/reg';
|
||||||
import { bootstrapServersErrCodes, jmxErrCodes, zkErrCodes } from './config';
|
import { bootstrapServersErrCodes, jmxErrCodes, zkErrCodes } from './config';
|
||||||
import CodeMirrorFormItem from '@src/components/CodeMirrorFormItem';
|
import CodeMirrorFormItem from '@src/components/CodeMirrorFormItem';
|
||||||
|
import { IconFont } from '@knowdesign/icons';
|
||||||
|
import notification from '@src/components/Notification';
|
||||||
const LOW_KAFKA_VERSION = '2.8.0';
|
const LOW_KAFKA_VERSION = '2.8.0';
|
||||||
const CLIENT_PROPERTIES_PLACEHOLDER = `用于创建Kafka客户端进行信息获取的相关配置,
|
const CLIENT_PROPERTIES_PLACEHOLDER = `用于创建Kafka客户端进行信息获取的相关配置,
|
||||||
例如开启SCRAM-SHA-256安全管控模式的集群需输入如下配置,
|
例如开启SCRAM-SHA-256安全管控模式的集群需输入如下配置,
|
||||||
@@ -20,13 +21,12 @@ word=\\"xxxxxx\\";"
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const AccessClusters = (props: any): JSX.Element => {
|
const { Panel } = Collapse;
|
||||||
const { afterSubmitSuccess, clusterInfo, visible } = props;
|
|
||||||
|
|
||||||
|
const ClusterTabContent = forwardRef((props: any, ref): JSX.Element => {
|
||||||
|
const { form, clusterInfo, visible } = props;
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [form] = Form.useForm();
|
|
||||||
const [loading, setLoading] = React.useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
const [confirmLoading, setConfirmLoading] = React.useState(false);
|
|
||||||
const [curClusterInfo, setCurClusterInfo] = React.useState<any>({});
|
const [curClusterInfo, setCurClusterInfo] = React.useState<any>({});
|
||||||
const [extra, setExtra] = React.useState({
|
const [extra, setExtra] = React.useState({
|
||||||
versionExtra: '',
|
versionExtra: '',
|
||||||
@@ -66,6 +66,7 @@ const AccessClusters = (props: any): JSX.Element => {
|
|||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
setCurClusterInfo({});
|
||||||
setExtra({
|
setExtra({
|
||||||
versionExtra: '',
|
versionExtra: '',
|
||||||
zooKeeperExtra: '',
|
zooKeeperExtra: '',
|
||||||
@@ -73,60 +74,6 @@ const AccessClusters = (props: any): JSX.Element => {
|
|||||||
jmxExtra: '',
|
jmxExtra: '',
|
||||||
});
|
});
|
||||||
lastFormItemValue.current = { bootstrapServers: '', zookeeper: '', clientProperties: {} };
|
lastFormItemValue.current = { bootstrapServers: '', zookeeper: '', clientProperties: {} };
|
||||||
props.setVisible && props.setVisible(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = () => {
|
|
||||||
form.validateFields().then((res) => {
|
|
||||||
setConfirmLoading(true);
|
|
||||||
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.openSSL || false,
|
|
||||||
token: res.token,
|
|
||||||
username: res.username,
|
|
||||||
},
|
|
||||||
kafkaVersion: res.kafkaVersion,
|
|
||||||
name: res.name,
|
|
||||||
zookeeper: res.zookeeper || '',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isNaN(curClusterInfo?.id)) {
|
|
||||||
Utils.put(api.phyCluster, {
|
|
||||||
...params,
|
|
||||||
id: curClusterInfo?.id,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
message.success('编辑成功');
|
|
||||||
afterSubmitSuccess && afterSubmitSuccess();
|
|
||||||
onCancel();
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setConfirmLoading(false);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Utils.post(api.phyCluster, params)
|
|
||||||
.then(() => {
|
|
||||||
message.success('集群接入成功。注意:新接入集群数据稳定需要1-2分钟');
|
|
||||||
afterSubmitSuccess && afterSubmitSuccess();
|
|
||||||
onCancel();
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setConfirmLoading(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const connectTest = () => {
|
const connectTest = () => {
|
||||||
@@ -213,39 +160,37 @@ const AccessClusters = (props: any): JSX.Element => {
|
|||||||
|
|
||||||
// 获取集群详情数据
|
// 获取集群详情数据
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (visible) {
|
if (clusterInfo?.id && visible) {
|
||||||
if (clusterInfo?.id) {
|
setLoading(true);
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const resolveJmxProperties = (obj: any) => {
|
const resolveJmxProperties = (obj: any) => {
|
||||||
const res = { ...obj };
|
const res = { ...obj };
|
||||||
try {
|
try {
|
||||||
const originValue = obj?.jmxProperties;
|
const originValue = obj?.jmxProperties;
|
||||||
if (originValue) {
|
if (originValue) {
|
||||||
const jmxProperties = JSON.parse(originValue);
|
const jmxProperties = JSON.parse(originValue);
|
||||||
typeof jmxProperties === 'object' && jmxProperties !== null && Object.assign(res, jmxProperties);
|
typeof jmxProperties === 'object' && jmxProperties !== null && Object.assign(res, jmxProperties);
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('jmxProperties not JSON: ', err);
|
|
||||||
}
|
}
|
||||||
return res;
|
} catch (err) {
|
||||||
};
|
console.error('jmxProperties not JSON: ', err);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
Utils.request(api.getPhyClusterBasic(clusterInfo.id))
|
Utils.request(api.getPhyClusterBasic(clusterInfo.id))
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
setCurClusterInfo(resolveJmxProperties(res));
|
setCurClusterInfo(resolveJmxProperties(res));
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setCurClusterInfo(resolveJmxProperties(clusterInfo));
|
setCurClusterInfo(resolveJmxProperties(clusterInfo));
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setCurClusterInfo({});
|
setCurClusterInfo({});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [visible, clusterInfo]);
|
}, [clusterInfo, visible]);
|
||||||
|
|
||||||
const validators = {
|
const validators = {
|
||||||
name: async (_: any, value: string) => {
|
name: async (_: any, value: string) => {
|
||||||
@@ -358,13 +303,510 @@ const AccessClusters = (props: any): JSX.Element => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
onCancel,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<Form form={form} layout="vertical" onValuesChange={onHandleValuesChange}>
|
||||||
|
<Form.Item
|
||||||
|
name="name"
|
||||||
|
label="集群名称"
|
||||||
|
validateTrigger="onBlur"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
validator: validators.name,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="bootstrapServers"
|
||||||
|
label="Bootstrap Servers"
|
||||||
|
extra={<span className={!extra.bootstrapExtra.includes('连接成功') ? 'error-extra-info' : ''}>{extra.bootstrapExtra}</span>}
|
||||||
|
validateTrigger={'onBlur'}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
validator: validators.bootstrapServers,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<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={<span className={!extra.zooKeeperExtra.includes('连接成功') ? 'error-extra-info' : ''}>{extra.zooKeeperExtra}</span>}
|
||||||
|
validateTrigger={'onBlur'}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
validator: validators.zookeeper,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<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="metrics-form-item" label="Metrics">
|
||||||
|
<div className="horizontal-form-container">
|
||||||
|
<div className="inline-items">
|
||||||
|
<Form.Item name="jmxPort" label="JMX Port :" extra={extra.jmxExtra}>
|
||||||
|
<InputNumber min={0} max={99999} style={{ width: 129 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="maxConn" label="Max Conn :">
|
||||||
|
<InputNumber addonAfter="个" min={0} max={99999} style={{ width: 124 }} />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<Form.Item name="openSSL" label="Security :">
|
||||||
|
<Radio.Group>
|
||||||
|
<Radio value={false}>None</Radio>
|
||||||
|
<Radio value={true}>Password Authentication</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item dependencies={['openSSL']} noStyle>
|
||||||
|
{({ getFieldValue }) => {
|
||||||
|
return getFieldValue('openSSL') ? (
|
||||||
|
<div className="user-info-form-items">
|
||||||
|
<Form.Item className="user-info-label" label="User Info :" required />
|
||||||
|
<div className="inline-items">
|
||||||
|
<Form.Item
|
||||||
|
name="username"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
validator: validators.securityUserName,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入用户名" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
className="token-form-item"
|
||||||
|
name="token"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
validator: validators.securityToken,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入密码" />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="kafkaVersion"
|
||||||
|
label="Version"
|
||||||
|
dependencies={['zookeeper']}
|
||||||
|
extra={<span className="error-extra-info">{extra.versionExtra}</span>}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
validator: validators.kafkaVersion,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<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={[
|
||||||
|
{
|
||||||
|
validator: validators.clientProperties,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<CodeMirrorFormItem
|
||||||
|
resize
|
||||||
|
defaultInput={form.getFieldValue('clientProperties')}
|
||||||
|
placeholder={CLIENT_PROPERTIES_PLACEHOLDER}
|
||||||
|
onBeforeChange={(clientProperties: string) => {
|
||||||
|
form.setFieldsValue({ clientProperties });
|
||||||
|
form.validateFields(['clientProperties']);
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
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(() => {
|
||||||
|
lastFormItemValue.current.clientProperties = clientProperties;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
message.error('连接失败');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="description"
|
||||||
|
label="集群描述"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
validator: validators.description,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.TextArea rows={4} />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Spin>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ConnectorForm = (props: {
|
||||||
|
initFieldsValue: any;
|
||||||
|
kafkaVersion: string[];
|
||||||
|
setSelectedTabKey: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
getConnectClustersList: any;
|
||||||
|
clusterInfo: any;
|
||||||
|
}) => {
|
||||||
|
const { initFieldsValue, kafkaVersion, setSelectedTabKey, getConnectClustersList, clusterInfo } = props;
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const validators = {
|
||||||
|
name: async (_: any, value: string) => {
|
||||||
|
if (!value) {
|
||||||
|
return Promise.reject('集群名称不能为空');
|
||||||
|
}
|
||||||
|
if (value === initFieldsValue?.name) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
if (!new RegExp(regClusterName).test(value)) {
|
||||||
|
return Promise.reject('集群名称支持中英文、数字、特殊字符 ! " # $ % & \' ( ) * + , - . / : ; < = > ? @ [ ] ^ _ ` { | } ~');
|
||||||
|
}
|
||||||
|
return Utils.request(api.getConnectClusterBasicExit(clusterInfo.id, value))
|
||||||
|
.then((res: any) => {
|
||||||
|
const data = res || {};
|
||||||
|
return data?.exist ? Promise.reject('集群名称重复') : Promise.resolve();
|
||||||
|
})
|
||||||
|
.catch(() => Promise.reject('连接超时! 请重试或检查服务'));
|
||||||
|
},
|
||||||
|
address: async (_: any, value: string) => {
|
||||||
|
if (!value) {
|
||||||
|
return Promise.reject('请输入集群地址');
|
||||||
|
}
|
||||||
|
if (!new RegExp(regIpAndPort).test(value)) {
|
||||||
|
return Promise.reject('格式错误,正确示例:http://1.1.1.1, http://1.1.1.1:65535, https://1.1.1.1, https://1.1.1.1:65535');
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFinish = (values: any) => {
|
||||||
|
const params = {
|
||||||
|
...values,
|
||||||
|
id: initFieldsValue?.id,
|
||||||
|
};
|
||||||
|
Utils.put(api.batchConnectClusters, [params])
|
||||||
|
.then((res) => {
|
||||||
|
// setSelectedTabKey(undefined);
|
||||||
|
getConnectClustersList();
|
||||||
|
notification.success({
|
||||||
|
message: '修改Connect集群成功',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
notification.success({
|
||||||
|
message: '修改Connect集群失败',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
setSelectedTabKey(undefined);
|
||||||
|
try {
|
||||||
|
const jmxPortInfo = JSON.parse(initFieldsValue.jmxProperties) || {};
|
||||||
|
form.setFieldsValue({ ...initFieldsValue, jmxPort: jmxPortInfo.jmxPort });
|
||||||
|
} catch {
|
||||||
|
form.setFieldsValue({ ...initFieldsValue });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
try {
|
||||||
|
const jmxPortInfo = JSON.parse(initFieldsValue.jmxProperties) || {};
|
||||||
|
form.setFieldsValue({ ...initFieldsValue, jmxPort: jmxPortInfo.jmxPort });
|
||||||
|
} catch {
|
||||||
|
form.setFieldsValue({ ...initFieldsValue });
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Drawer
|
<Form form={form} layout="vertical" onFinish={onFinish}>
|
||||||
className="drawer-content drawer-access-cluster"
|
<Form.Item name="name" label="集群名称" validateTrigger="onBlur" rules={[{ required: true, validator: validators.name }]}>
|
||||||
onClose={onCancel}
|
<Input placeholder="请输入集群名称" maxLength={64} />
|
||||||
maskClosable={false}
|
</Form.Item>
|
||||||
extra={
|
<Form.Item name="groupName" label="ConsumerGroup Name">
|
||||||
|
<Input disabled={true} placeholder="请输入 ConsumerGroup Name" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="clusterUrl" label="集群地址">
|
||||||
|
<Input disabled placeholder="请输入集群地址" />
|
||||||
|
</Form.Item>
|
||||||
|
{/* <Form.Item
|
||||||
|
name="kafkaVersion"
|
||||||
|
label="版本号"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请选择版本号',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select placeholder="请选择版本,如无匹配可选择相邻版本">
|
||||||
|
{(kafkaVersion || []).map((item: string) => (
|
||||||
|
<Select.Option key={item} value={item}>
|
||||||
|
{item}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="JMX Port" name="priority" rules={[{ required: true, message: 'Principle 不能为空' }]} initialValue="throughput">
|
||||||
|
<Radio.Group>
|
||||||
|
<Radio value="allBroker">应用于所有Broker</Radio>
|
||||||
|
<Radio value="givenBroker">应用于特定Broker</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item dependencies={['priority']} style={{ marginBottom: 0 }}>
|
||||||
|
{({ getFieldValue }) =>
|
||||||
|
getFieldValue('priority') === 'allBroker' ? (
|
||||||
|
<Form.Item name="jmxPort">
|
||||||
|
<InputNumber min={0} max={99999} style={{ width: 202 }} />
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
<Form.Item name="jmxPort">
|
||||||
|
<Input style={{ width: 202 }} />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Form.Item> */}
|
||||||
|
<div className="inline-form-items" style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<Form.Item
|
||||||
|
name="version"
|
||||||
|
label="版本号"
|
||||||
|
style={{ width: 202 }}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请选择版本号',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select placeholder="请选择版本,如无匹配可选择相邻版本">
|
||||||
|
{(kafkaVersion || []).map((item: string) => (
|
||||||
|
<Select.Option key={item} value={item}>
|
||||||
|
{item}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="jmxPort" label="JMX Port" style={{ width: 202 }}>
|
||||||
|
<InputNumber min={0} max={99999} style={{ width: 202 }} />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<Form.Item style={{ marginBottom: 0 }}>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" htmlType="submit" size="small" style={{ width: 56 }}>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
<Button size="small" style={{ width: 56 }} onClick={onCancel}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConnectTabContent = forwardRef((props: any, ref) => {
|
||||||
|
const { kafkaVersion, clusterInfo, visible } = props;
|
||||||
|
const [connectors, setConnectors] = useState<any[]>([]);
|
||||||
|
const [selectedTabKey, setSelectedTabKey] = useState<string>(undefined);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const genExtra = (connector: any) => (
|
||||||
|
<IconFont
|
||||||
|
type="icon-shanchu1"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
Utils.delete(api.deleteConnectClusters, {
|
||||||
|
params: {
|
||||||
|
connectClusterId: connector.id,
|
||||||
|
},
|
||||||
|
}).then((res) => {
|
||||||
|
// setSelectedTabKey(undefined);
|
||||||
|
getConnectClustersList();
|
||||||
|
notification.success({
|
||||||
|
message: '删除Connect集群成功',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const getConnectClustersList = () => {
|
||||||
|
setLoading(true);
|
||||||
|
Utils.request(api.getConnectClusters(clusterInfo.id))
|
||||||
|
.then((res: any) => {
|
||||||
|
setConnectors(res || []);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
visible && getConnectClustersList();
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
{connectors?.length ? (
|
||||||
|
<Collapse
|
||||||
|
accordion
|
||||||
|
bordered={false}
|
||||||
|
activeKey={selectedTabKey}
|
||||||
|
className="cluster-connect-custom-collapse"
|
||||||
|
expandIcon={({ isActive }) => <IconFont type="icon-jiantou_1" rotate={isActive ? 90 : 0} />}
|
||||||
|
onChange={(key: string) => {
|
||||||
|
setSelectedTabKey(key);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{connectors.map((connector, i) => {
|
||||||
|
return (
|
||||||
|
<Panel header={connector.name} key={i} className="cluster-connect-custom-panel" extra={genExtra(connector)}>
|
||||||
|
<ConnectorForm
|
||||||
|
initFieldsValue={connector}
|
||||||
|
kafkaVersion={kafkaVersion}
|
||||||
|
setSelectedTabKey={setSelectedTabKey}
|
||||||
|
getConnectClustersList={getConnectClustersList}
|
||||||
|
clusterInfo={clusterInfo}
|
||||||
|
/>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Collapse>
|
||||||
|
) : (
|
||||||
|
<Empty description="暂无Connect集群" image={Empty.PRESENTED_IMAGE_CUSTOM} style={{ padding: '100px 0' }} />
|
||||||
|
)}
|
||||||
|
</Spin>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
interface AccessClusterDrawerProps {
|
||||||
|
visible: boolean;
|
||||||
|
setVisible: (visible: boolean) => void;
|
||||||
|
clusterInfo: any;
|
||||||
|
afterSubmitSuccess: () => void;
|
||||||
|
kafkaVersion: string[];
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AccessClusterDrawer = (props: AccessClusterDrawerProps) => {
|
||||||
|
const { afterSubmitSuccess, clusterInfo, visible, setVisible, kafkaVersion } = props;
|
||||||
|
const intl = useIntl();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
|
const clusterRef = useRef(null);
|
||||||
|
const [positionType, setPositionType] = useState<string>('cluster');
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
setPositionType('cluster');
|
||||||
|
form.resetFields();
|
||||||
|
clusterRef.current.onCancel();
|
||||||
|
setVisible && setVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const callback = (key: any) => {
|
||||||
|
setPositionType(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
form.validateFields().then((res) => {
|
||||||
|
setConfirmLoading(true);
|
||||||
|
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.openSSL || false,
|
||||||
|
token: res.token,
|
||||||
|
username: res.username,
|
||||||
|
},
|
||||||
|
kafkaVersion: res.kafkaVersion,
|
||||||
|
name: res.name,
|
||||||
|
zookeeper: res.zookeeper || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isNaN(clusterInfo?.id)) {
|
||||||
|
Utils.put(api.phyCluster, {
|
||||||
|
...params,
|
||||||
|
id: clusterInfo?.id,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
message.success('编辑成功');
|
||||||
|
afterSubmitSuccess && afterSubmitSuccess();
|
||||||
|
onCancel();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setConfirmLoading(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Utils.post(api.phyCluster, params)
|
||||||
|
.then(() => {
|
||||||
|
message.success('集群接入成功。注意:新接入集群数据稳定需要1-2分钟');
|
||||||
|
afterSubmitSuccess && afterSubmitSuccess();
|
||||||
|
onCancel();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setConfirmLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
className="drawer-content drawer-access-cluster"
|
||||||
|
onClose={onCancel}
|
||||||
|
maskClosable={false}
|
||||||
|
extra={
|
||||||
|
positionType === 'cluster' ? (
|
||||||
<div className="operate-wrap">
|
<div className="operate-wrap">
|
||||||
<Space>
|
<Space>
|
||||||
<Button size="small" onClick={onCancel}>
|
<Button size="small" onClick={onCancel}>
|
||||||
@@ -376,189 +818,25 @@ const AccessClusters = (props: any): JSX.Element => {
|
|||||||
<Divider type="vertical" />
|
<Divider type="vertical" />
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
}
|
) : null
|
||||||
title={intl.formatMessage({ id: props.title || clusterInfo?.id ? 'edit.cluster' : 'access.cluster' })}
|
}
|
||||||
visible={props.visible}
|
title={intl.formatMessage({ id: props.title || clusterInfo?.id ? 'edit.cluster' : 'access.cluster' })}
|
||||||
placement="right"
|
visible={visible}
|
||||||
width={480}
|
placement="right"
|
||||||
>
|
width={480}
|
||||||
<Spin spinning={loading}>
|
>
|
||||||
<Form form={form} layout="vertical" onValuesChange={onHandleValuesChange}>
|
<Tabs onChange={callback} activeKey={positionType} defaultActiveKey="cluster">
|
||||||
<Form.Item
|
<Tabs.TabPane tab="Cluster" key="cluster">
|
||||||
name="name"
|
<ClusterTabContent ref={clusterRef} form={form} clusterInfo={clusterInfo} kafkaVersion={kafkaVersion} visible={visible} />
|
||||||
label="集群名称"
|
</Tabs.TabPane>
|
||||||
validateTrigger="onBlur"
|
{clusterInfo?.id && (
|
||||||
rules={[
|
<Tabs.TabPane tab="Connect" key="connect">
|
||||||
{
|
<ConnectTabContent kafkaVersion={kafkaVersion} clusterInfo={clusterInfo} visible={visible} />
|
||||||
required: true,
|
</Tabs.TabPane>
|
||||||
validator: validators.name,
|
)}
|
||||||
},
|
</Tabs>
|
||||||
]}
|
</Drawer>
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name="bootstrapServers"
|
|
||||||
label="Bootstrap Servers"
|
|
||||||
extra={<span className={!extra.bootstrapExtra.includes('连接成功') ? 'error-extra-info' : ''}>{extra.bootstrapExtra}</span>}
|
|
||||||
validateTrigger={'onBlur'}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
validator: validators.bootstrapServers,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<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={<span className={!extra.zooKeeperExtra.includes('连接成功') ? 'error-extra-info' : ''}>{extra.zooKeeperExtra}</span>}
|
|
||||||
validateTrigger={'onBlur'}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
validator: validators.zookeeper,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<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="metrics-form-item" label="Metrics">
|
|
||||||
<div className="horizontal-form-container">
|
|
||||||
<div className="inline-items">
|
|
||||||
<Form.Item name="jmxPort" label="JMX Port :" extra={extra.jmxExtra}>
|
|
||||||
<InputNumber min={0} max={99999} style={{ width: 129 }} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="maxConn" label="Max Conn :">
|
|
||||||
<InputNumber addonAfter="个" min={0} max={99999} style={{ width: 124 }} />
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
<Form.Item name="openSSL" label="Security :">
|
|
||||||
<Radio.Group>
|
|
||||||
<Radio value={false}>None</Radio>
|
|
||||||
<Radio value={true}>Password Authentication</Radio>
|
|
||||||
</Radio.Group>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item dependencies={['openSSL']} noStyle>
|
|
||||||
{({ getFieldValue }) => {
|
|
||||||
return getFieldValue('openSSL') ? (
|
|
||||||
<div className="user-info-form-items">
|
|
||||||
<Form.Item className="user-info-label" label="User Info :" required />
|
|
||||||
<div className="inline-items">
|
|
||||||
<Form.Item
|
|
||||||
name="username"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
validator: validators.securityUserName,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input placeholder="请输入用户名" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
className="token-form-item"
|
|
||||||
name="token"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
validator: validators.securityToken,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input placeholder="请输入密码" />
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name="kafkaVersion"
|
|
||||||
label="Version"
|
|
||||||
dependencies={['zookeeper']}
|
|
||||||
extra={<span className="error-extra-info">{extra.versionExtra}</span>}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
validator: validators.kafkaVersion,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<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={[
|
|
||||||
{
|
|
||||||
validator: validators.clientProperties,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<CodeMirrorFormItem
|
|
||||||
resize
|
|
||||||
defaultInput={form.getFieldValue('clientProperties')}
|
|
||||||
placeholder={CLIENT_PROPERTIES_PLACEHOLDER}
|
|
||||||
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={[
|
|
||||||
{
|
|
||||||
validator: validators.description,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input.TextArea rows={4} />
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Spin>
|
|
||||||
</Drawer>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AccessClusters;
|
export default AccessClusterDrawer;
|
||||||
|
|||||||
@@ -466,6 +466,7 @@
|
|||||||
max-width: 180px;
|
max-width: 180px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
font-family: @font-family-bold;
|
font-family: @font-family-bold;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: #495057;
|
color: #495057;
|
||||||
@@ -633,6 +634,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.drawer-access-cluster {
|
||||||
|
.dcloud-drawer-title {
|
||||||
|
height: 27px;
|
||||||
|
line-height: 27px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.drawer-content {
|
.drawer-content {
|
||||||
.dcloud-form-item-extra {
|
.dcloud-form-item-extra {
|
||||||
@@ -674,6 +681,41 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.cluster-connect-custom-collapse {
|
||||||
|
background-color: transparent;
|
||||||
|
.cluster-connect-custom-panel,
|
||||||
|
.cluster-connect-custom-panel:last-child {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 0px;
|
||||||
|
border-radius: 8px;
|
||||||
|
.dcloud-collapse-header {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #495057;
|
||||||
|
.dcloud-collapse-extra {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover .dcloud-collapse-extra {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
&:not(.dcloud-collapse-item-active) {
|
||||||
|
.dcloud-collapse-header:hover {
|
||||||
|
background: #f1f3ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dcloud-collapse-content-box {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dcloud-collapse-header .dcloud-collapse-arrow {
|
||||||
|
margin-right: 8px !important;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-page {
|
.empty-page {
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ const SecurityACLs = (): JSX.Element => {
|
|||||||
<div className="card-bar">
|
<div className="card-bar">
|
||||||
<ACLsCardBar />
|
<ACLsCardBar />
|
||||||
</div>
|
</div>
|
||||||
<div className="security-acls-page-list clustom-table-content">
|
<div className="security-acls-page-list custom-table-content">
|
||||||
<div className={tableHeaderPrefix}>
|
<div className={tableHeaderPrefix}>
|
||||||
<div className={`${tableHeaderPrefix}-left`}>
|
<div className={`${tableHeaderPrefix}-left`}>
|
||||||
<div className={`${tableHeaderPrefix}-left-refresh`} onClick={() => getACLs()}>
|
<div className={`${tableHeaderPrefix}-left-refresh`} onClick={() => getACLs()}>
|
||||||
|
|||||||
@@ -1,20 +1,13 @@
|
|||||||
import { Col, Row, SingleChart, Utils, Modal, Spin, Empty, AppContainer, Tooltip } from 'knowdesign';
|
import { SingleChart, Utils, Spin, AppContainer, Tooltip } from 'knowdesign';
|
||||||
import { IconFont } from '@knowdesign/icons';
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { arrayMoveImmutable } from 'array-move';
|
import { arrayMoveImmutable } from 'array-move';
|
||||||
import api from '@src/api';
|
import api from '@src/api';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import {
|
import { OriginMetricData, FormattedMetricData, formatChartData, supplementaryPoints } from '@src/constants/chartConfig';
|
||||||
OriginMetricData,
|
|
||||||
FormattedMetricData,
|
|
||||||
formatChartData,
|
|
||||||
supplementaryPoints,
|
|
||||||
resolveMetricsRank,
|
|
||||||
MetricInfo,
|
|
||||||
} from '@src/constants/chartConfig';
|
|
||||||
import { MetricType } from '@src/api';
|
import { MetricType } from '@src/api';
|
||||||
import { getDataUnit } from '@src/constants/chartConfig';
|
import { getDataUnit } from '@src/constants/chartConfig';
|
||||||
import ChartOperateBar, { KsHeaderOptions } from '@src/components/ChartOperateBar';
|
import ChartOperateBar, { KsHeaderOptions } from '@src/components/ChartOperateBar';
|
||||||
|
import MetricsFilter from '@src/components/ChartOperateBar/MetricSelect';
|
||||||
import RenderEmpty from '@src/components/RenderEmpty';
|
import RenderEmpty from '@src/components/RenderEmpty';
|
||||||
import DragGroup from '@src/components/DragGroup';
|
import DragGroup from '@src/components/DragGroup';
|
||||||
import { getChartConfig } from './config';
|
import { getChartConfig } from './config';
|
||||||
@@ -56,7 +49,6 @@ const DEFUALT_METRIC_NEED_METRICS = [DEFAULT_METRIC, 'TotalLogSize', 'TotalProdu
|
|||||||
const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
|
const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
|
||||||
const [global] = AppContainer.useGlobalValue();
|
const [global] = AppContainer.useGlobalValue();
|
||||||
const { clusterId } = useParams<{ clusterId: string }>();
|
const { clusterId } = useParams<{ clusterId: string }>();
|
||||||
const [metricList, setMetricList] = useState<MetricInfo[]>([]); // 指标列表
|
|
||||||
const [selectedMetricNames, setSelectedMetricNames] = useState<(string | number)[]>([]); // 默认选中的指标的列表
|
const [selectedMetricNames, setSelectedMetricNames] = useState<(string | number)[]>([]); // 默认选中的指标的列表
|
||||||
const [metricDataList, setMetricDataList] = useState<any>([]);
|
const [metricDataList, setMetricDataList] = useState<any>([]);
|
||||||
const [messagesInMetricData, setMessagesInMetricData] = useState<MessagesInMetric>({
|
const [messagesInMetricData, setMessagesInMetricData] = useState<MessagesInMetric>({
|
||||||
@@ -72,6 +64,7 @@ const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
|
|||||||
messagesIn: 0,
|
messagesIn: 0,
|
||||||
other: 0,
|
other: 0,
|
||||||
});
|
});
|
||||||
|
const metricFilterRef = useRef(null);
|
||||||
|
|
||||||
// 筛选项变化或者点击刷新按钮
|
// 筛选项变化或者点击刷新按钮
|
||||||
const ksHeaderChange = (ksOptions: KsHeaderOptions) => {
|
const ksHeaderChange = (ksOptions: KsHeaderOptions) => {
|
||||||
@@ -86,65 +79,9 @@ const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新 rank
|
|
||||||
const updateRank = (metricList: MetricInfo[]) => {
|
|
||||||
const { list, listInfo, shouldUpdate } = resolveMetricsRank(metricList);
|
|
||||||
metricRankList.current = list;
|
|
||||||
if (shouldUpdate) {
|
|
||||||
updateMetricList(listInfo);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取指标列表
|
|
||||||
const getMetricList = () => {
|
|
||||||
Utils.request(api.getDashboardMetricList(clusterId, MetricType.Cluster)).then((res: MetricInfo[] | null) => {
|
|
||||||
if (!res) return;
|
|
||||||
const supportMetrics = res.filter((metric) => metric.support);
|
|
||||||
const selectedMetrics = supportMetrics.filter((metric) => metric.set).map((metric) => metric.name);
|
|
||||||
!selectedMetrics.includes(DEFAULT_METRIC) && selectedMetrics.push(DEFAULT_METRIC);
|
|
||||||
updateRank([...supportMetrics]);
|
|
||||||
setMetricList(supportMetrics);
|
|
||||||
setSelectedMetricNames(selectedMetrics);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 更新指标
|
|
||||||
const updateMetricList = (metricDetailDTOList: { metric: string; rank: number; set: boolean }[]) => {
|
|
||||||
return Utils.request(api.getDashboardMetricList(clusterId, MetricType.Cluster), {
|
|
||||||
method: 'POST',
|
|
||||||
data: {
|
|
||||||
metricDetailDTOList,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 指标选中项更新回调
|
|
||||||
const indicatorChangeCallback = (newMetricNames: (string | number)[]) => {
|
|
||||||
const updateMetrics: { metric: string; set: boolean; rank: number }[] = [];
|
|
||||||
// 需要选中的指标
|
|
||||||
newMetricNames.forEach(
|
|
||||||
(name) =>
|
|
||||||
!selectedMetricNames.includes(name) &&
|
|
||||||
updateMetrics.push({ metric: name as string, set: true, rank: metricList.find(({ name: metric }) => metric === name)?.rank })
|
|
||||||
);
|
|
||||||
// 取消选中的指标
|
|
||||||
selectedMetricNames.forEach(
|
|
||||||
(name) =>
|
|
||||||
!newMetricNames.includes(name) &&
|
|
||||||
updateMetrics.push({ metric: name as string, set: false, rank: metricList.find(({ name: metric }) => metric === name)?.rank })
|
|
||||||
);
|
|
||||||
const requestPromise = Object.keys(updateMetrics).length ? updateMetricList(updateMetrics) : Promise.resolve();
|
|
||||||
requestPromise.then(
|
|
||||||
() => getMetricList(),
|
|
||||||
() => getMetricList()
|
|
||||||
);
|
|
||||||
|
|
||||||
return requestPromise;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取 metric 列表的图表数据
|
// 获取 metric 列表的图表数据
|
||||||
const getMetricData = () => {
|
const getMetricData = () => {
|
||||||
if (!selectedMetricNames.length) return;
|
if (!selectedMetricNames?.length) return;
|
||||||
!curHeaderOptions.isAutoReload && setChartLoading(true);
|
!curHeaderOptions.isAutoReload && setChartLoading(true);
|
||||||
const [startTime, endTime] = curHeaderOptions.rangeTime;
|
const [startTime, endTime] = curHeaderOptions.rangeTime;
|
||||||
|
|
||||||
@@ -286,7 +223,7 @@ const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
|
|||||||
const originTarget = metricRankList.current.indexOf(metricDataList[newIndex].metricName);
|
const originTarget = metricRankList.current.indexOf(metricDataList[newIndex].metricName);
|
||||||
const newList = arrayMoveImmutable(metricRankList.current, originFrom, originTarget);
|
const newList = arrayMoveImmutable(metricRankList.current, originFrom, originTarget);
|
||||||
metricRankList.current = newList;
|
metricRankList.current = newList;
|
||||||
updateMetricList(newList.map((metric, rank) => ({ metric, rank, set: metricList.find(({ name }) => metric === name)?.set || false })));
|
metricFilterRef.current?.rankChange(originFrom, originTarget);
|
||||||
setMetricDataList(arrayMoveImmutable(metricDataList, oldIndex, newIndex));
|
setMetricDataList(arrayMoveImmutable(metricDataList, oldIndex, newIndex));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -302,29 +239,23 @@ const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
|
|||||||
}, [curHeaderOptions]);
|
}, [curHeaderOptions]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getMetricList();
|
|
||||||
setTimeout(() => observeDashboardWidthChange());
|
setTimeout(() => observeDashboardWidthChange());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chart-panel cluster-detail-container">
|
<div className="chart-panel cluster-detail-container">
|
||||||
<ChartOperateBar
|
<ChartOperateBar
|
||||||
|
openMetricFilter={() => metricFilterRef.current?.open()}
|
||||||
onChange={ksHeaderChange}
|
onChange={ksHeaderChange}
|
||||||
hideNodeScope={true}
|
hideNodeScope={true}
|
||||||
hideGridSelect={true}
|
hideGridSelect={true}
|
||||||
metricSelect={{
|
/>
|
||||||
hide: false,
|
<MetricsFilter
|
||||||
metricType: MetricType.Cluster,
|
ref={metricFilterRef}
|
||||||
tableData: metricList,
|
metricType={MetricType.Cluster}
|
||||||
selectedRows: selectedMetricNames,
|
onSelectChange={(list, rankList) => {
|
||||||
checkboxProps: (record: MetricInfo) => {
|
metricRankList.current = rankList;
|
||||||
return record.name === DEFAULT_METRIC
|
setSelectedMetricNames(list);
|
||||||
? {
|
|
||||||
disabled: true,
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
},
|
|
||||||
submitCallback: indicatorChangeCallback,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ export const dimensionMap = {
|
|||||||
label: 'Zookeeper',
|
label: 'Zookeeper',
|
||||||
href: '/zookeeper',
|
href: '/zookeeper',
|
||||||
},
|
},
|
||||||
|
5: {
|
||||||
|
label: 'Connect',
|
||||||
|
href: '/connect',
|
||||||
|
},
|
||||||
|
6: {
|
||||||
|
label: 'Connector',
|
||||||
|
href: '/connect/connectors',
|
||||||
|
},
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const toLowerCase = (name = '') => {
|
const toLowerCase = (name = '') => {
|
||||||
@@ -78,6 +86,15 @@ const CONFIG_ITEM_DETAIL_DESC = {
|
|||||||
SentRate: (valueGroup: any) => {
|
SentRate: (valueGroup: any) => {
|
||||||
return `Zookeeper 首发包数小于 ${valueGroup?.ratio * 100}% 总容量`;
|
return `Zookeeper 首发包数小于 ${valueGroup?.ratio * 100}% 总容量`;
|
||||||
},
|
},
|
||||||
|
TaskStartupFailurePercentage: (valueGroup: any) => {
|
||||||
|
return `任务启动失败概率 小于 ${valueGroup?.value * 100}%`;
|
||||||
|
},
|
||||||
|
ConnectorFailedTaskCount: (valueGroup: any) => {
|
||||||
|
return `失败状态的任务数量 小于 ${valueGroup?.value}`;
|
||||||
|
},
|
||||||
|
ConnectorUnassignedTaskCount: (valueGroup: any) => {
|
||||||
|
return `未被分配的任务数量 小于 ${valueGroup?.value}`;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getConfigItemDetailDesc = (item: keyof typeof CONFIG_ITEM_DETAIL_DESC, valueGroup: any) => {
|
export const getConfigItemDetailDesc = (item: keyof typeof CONFIG_ITEM_DETAIL_DESC, valueGroup: any) => {
|
||||||
@@ -145,9 +162,9 @@ export const getDetailColumn = (clusterId: number) => [
|
|||||||
// eslint-disable-next-line react/display-name
|
// eslint-disable-next-line react/display-name
|
||||||
render: (text: number, record: any) => {
|
render: (text: number, record: any) => {
|
||||||
return dimensionMap[text] ? (
|
return dimensionMap[text] ? (
|
||||||
<Link to={`/${systemKey}/${clusterId}${dimensionMap[text]?.href}`}>{toLowerCase(record?.dimensionName)}</Link>
|
<Link to={`/${systemKey}/${clusterId}${dimensionMap[text]?.href}`}>{record?.dimensionDisplayName}</Link>
|
||||||
) : (
|
) : (
|
||||||
toLowerCase(record?.dimensionName)
|
record?.dimensionDisplayName
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -219,9 +236,9 @@ export const getHealthySettingColumn = (form: any, data: any, clusterId: string)
|
|||||||
// eslint-disable-next-line react/display-name
|
// eslint-disable-next-line react/display-name
|
||||||
render: (text: number, record: any) => {
|
render: (text: number, record: any) => {
|
||||||
return dimensionMap[text] ? (
|
return dimensionMap[text] ? (
|
||||||
<Link to={`/${systemKey}/${clusterId}${dimensionMap[text]?.href}`}>{toLowerCase(record?.dimensionName)}</Link>
|
<Link to={`/${systemKey}/${clusterId}${dimensionMap[text]?.href}`}>{record?.dimensionDisplayName}</Link>
|
||||||
) : (
|
) : (
|
||||||
toLowerCase(record?.dimensionName)
|
record?.dimensionDisplayName
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -365,6 +382,33 @@ export const getHealthySettingColumn = (form: any, data: any, clusterId: string)
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
case 'TaskStartupFailurePercentage': {
|
||||||
|
return (
|
||||||
|
<div className="table-form-item">
|
||||||
|
<span className="left-text">{'>'}</span>
|
||||||
|
{getFormItem({ configItem, percent: true })}
|
||||||
|
<span className="right-text">则不通过</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case 'ConnectorFailedTaskCount': {
|
||||||
|
return (
|
||||||
|
<div className="table-form-item">
|
||||||
|
<span className="left-text">{'>'}</span>
|
||||||
|
{getFormItem({ configItem, attrs: { min: 0, max: 99998 } })}
|
||||||
|
<span className="right-text">则不通过</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case 'ConnectorUnassignedTaskCount': {
|
||||||
|
return (
|
||||||
|
<div className="table-form-item">
|
||||||
|
<span className="left-text">{'>'}</span>
|
||||||
|
{getFormItem({ configItem, attrs: { min: 0, max: 99998 } })}
|
||||||
|
<span className="right-text">则不通过</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,13 +149,13 @@ const AutoPage = (props: any) => {
|
|||||||
title: 'Partitions',
|
title: 'Partitions',
|
||||||
dataIndex: 'partitionNum',
|
dataIndex: 'partitionNum',
|
||||||
key: 'partitionNum',
|
key: 'partitionNum',
|
||||||
width: 95,
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Replications',
|
title: 'Replications',
|
||||||
dataIndex: 'replicaNum',
|
dataIndex: 'replicaNum',
|
||||||
key: 'replicaNum',
|
key: 'replicaNum',
|
||||||
width: 95,
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '健康状态',
|
title: '健康状态',
|
||||||
@@ -163,7 +163,7 @@ const AutoPage = (props: any) => {
|
|||||||
key: 'HealthState',
|
key: 'HealthState',
|
||||||
sorter: true,
|
sorter: true,
|
||||||
// 设计图上量出来的是144,但做的时候发现写144 header部分的sort箭头不出来,所以临时调大些
|
// 设计图上量出来的是144,但做的时候发现写144 header部分的sort箭头不出来,所以临时调大些
|
||||||
width: 170,
|
width: 100,
|
||||||
render: (value: any, record: any) => {
|
render: (value: any, record: any) => {
|
||||||
return calcCurValue(record, 'HealthState');
|
return calcCurValue(record, 'HealthState');
|
||||||
},
|
},
|
||||||
@@ -289,7 +289,7 @@ const AutoPage = (props: any) => {
|
|||||||
<div style={{ margin: '12px 0' }}>
|
<div style={{ margin: '12px 0' }}>
|
||||||
<TopicHealthCheck></TopicHealthCheck>
|
<TopicHealthCheck></TopicHealthCheck>
|
||||||
</div>
|
</div>
|
||||||
<div className="clustom-table-content">
|
<div className="custom-table-content">
|
||||||
<div className={`${tableHeaderPrefix}`}>
|
<div className={`${tableHeaderPrefix}`}>
|
||||||
<div className={`${tableHeaderPrefix}-left`}>
|
<div className={`${tableHeaderPrefix}-left`}>
|
||||||
{/* 批量扩缩副本 */}
|
{/* 批量扩缩副本 */}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ const ZookeeperList: React.FC = () => {
|
|||||||
<div style={{ margin: '12px 0' }}>
|
<div style={{ margin: '12px 0' }}>
|
||||||
<ZookeeperCard />
|
<ZookeeperCard />
|
||||||
</div>
|
</div>
|
||||||
<div className="clustom-table-content">
|
<div className="custom-table-content">
|
||||||
<div className={tableHeaderPrefix}>
|
<div className={tableHeaderPrefix}>
|
||||||
<div className={`${tableHeaderPrefix}-left`}>
|
<div className={`${tableHeaderPrefix}-left`}>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ import LoadRebalance from './LoadRebalance';
|
|||||||
import Zookeeper from './Zookeeper';
|
import Zookeeper from './Zookeeper';
|
||||||
import ZookeeperDashboard from './ZookeeperDashboard';
|
import ZookeeperDashboard from './ZookeeperDashboard';
|
||||||
|
|
||||||
|
import ConnectDashboard from './ConnectDashboard';
|
||||||
|
import Connectors from './Connect';
|
||||||
|
import Workers from './Connect/Workers';
|
||||||
|
|
||||||
const pageRoutes = [
|
const pageRoutes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -130,6 +134,24 @@ const pageRoutes = [
|
|||||||
component: Zookeeper,
|
component: Zookeeper,
|
||||||
noSider: false,
|
noSider: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'connect',
|
||||||
|
exact: true,
|
||||||
|
component: ConnectDashboard,
|
||||||
|
noSider: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'connect/connectors',
|
||||||
|
exact: true,
|
||||||
|
component: Connectors,
|
||||||
|
noSider: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'connect/workers',
|
||||||
|
exact: true,
|
||||||
|
component: Workers,
|
||||||
|
noSider: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'security/acls',
|
path: 'security/acls',
|
||||||
exact: true,
|
exact: true,
|
||||||
|
|||||||
@@ -279,7 +279,7 @@
|
|||||||
|
|
||||||
|
|
||||||
// Protable 样式替换
|
// Protable 样式替换
|
||||||
.clustom-table-content {
|
.custom-table-content {
|
||||||
padding: 16px 24px;
|
padding: 16px 24px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border-top-left-radius: 12px;
|
border-top-left-radius: 12px;
|
||||||
@@ -349,6 +349,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.@{select-prefix-cls}.@{select-prefix-cls}-single.@{select-prefix-cls}-disabled {
|
||||||
|
.@{select-prefix-cls}-selector {
|
||||||
|
background: #eff2f7 !important;
|
||||||
|
border: 1px solid #ced4da !important;
|
||||||
|
.@{select-prefix-cls}-selection-item {
|
||||||
|
color: #ADB5BC;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.@{select-prefix-cls}-dropdown {
|
.@{select-prefix-cls}-dropdown {
|
||||||
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);
|
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);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|||||||
Reference in New Issue
Block a user