feat: 支持 Zookeeper 模块

This commit is contained in:
GraceWalk
2022-10-28 17:36:15 +08:00
committed by EricZeng
parent 5f6df3681c
commit 941dd4fd65
22 changed files with 1064 additions and 91 deletions

View File

@@ -1345,9 +1345,9 @@
}
},
"@knowdesign/icons": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/@knowdesign/icons/-/icons-1.0.0.tgz",
"integrity": "sha512-7c+h2TSbh2ihTkXIivuO+DddNC5wG7hVv9SS4ccmkvTKls2ZTLitPu+U0wpufDxPhkPMaKEQfsECsVJ+7jLMiw==",
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/@knowdesign/icons/-/icons-1.0.2.tgz",
"integrity": "sha512-eQuUQZbPRvC1xU4ouzgrk8j6UE39Cui+eEkYkLbfGLpVbGPFKJ7yEmUyKhIjG9zhf1qS7/h08yzq0hAHajBi8g==",
"requires": {
"@ant-design/colors": "^6.0.0",
"@ant-design/icons": "^4.7.0",

View File

@@ -21,7 +21,7 @@
"build": "cross-env NODE_ENV=production webpack --max_old_space_size=8000"
},
"dependencies": {
"@knowdesign/icons": "^1.0.0",
"@knowdesign/icons": "^1.0.2",
"babel-preset-react-app": "^10.0.0",
"classnames": "^2.2.6",
"dotenv": "^16.0.1",

View File

@@ -1388,9 +1388,9 @@
}
},
"@knowdesign/icons": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/@knowdesign/icons/-/icons-1.0.1.tgz",
"integrity": "sha512-EI3s25BJt+Slv7/t6B3K3zv7I6TKkk2Wf1y68zuxK80MMkWf8lqqUtyAZbFDoPUfXAjw6vHktMBH44gbMHMRFA==",
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/@knowdesign/icons/-/icons-1.0.2.tgz",
"integrity": "sha512-eQuUQZbPRvC1xU4ouzgrk8j6UE39Cui+eEkYkLbfGLpVbGPFKJ7yEmUyKhIjG9zhf1qS7/h08yzq0hAHajBi8g==",
"requires": {
"@ant-design/colors": "^6.0.0",
"@ant-design/icons": "^4.7.0",

View File

@@ -35,7 +35,7 @@
"dependencies": {
"@ant-design/compatible": "^1.0.8",
"@ant-design/icons": "^4.6.2",
"@knowdesign/icons": "^1.0.1",
"@knowdesign/icons": "^1.0.2",
"@types/react": "^17.0.39",
"@types/react-copy-to-clipboard": "^5.0.2",
"@types/react-dom": "^17.0.11",

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -26,7 +26,6 @@ const OptionsDefault = [
const NodeScope = ({ nodeScopeModule, change }: propsType) => {
const {
hasCustomScope,
customScopeList: customList,
scopeName = '',
scopeLabel = '自定义范围',
@@ -129,79 +128,75 @@ const NodeScope = ({ nodeScopeModule, change }: propsType) => {
</Space>
</Radio.Group>
</div>
{hasCustomScope && (
<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="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 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 ${hasCustomScope ? '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>
</>
<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>
);
};

View File

@@ -47,7 +47,6 @@ export interface IcustomScope {
}
export interface InodeScopeModule {
hasCustomScope: boolean;
customScopeList: IcustomScope[];
scopeName?: string;
scopeLabel?: string;
@@ -87,7 +86,6 @@ const GRID_SIZE_OPTIONS = [
const MetricOperateBar = ({
metricSelect,
nodeScopeModule = {
hasCustomScope: false,
customScopeList: [],
},
hideNodeScope = false,

View File

@@ -4,7 +4,7 @@ import { getBasicChartConfig, CHART_COLOR_LIST } from '@src/constants/chartConfi
const METRIC_DASHBOARD_REQ_MAP = {
[MetricType.Broker]: (clusterId: string) => api.getDashboardMetricChartData(clusterId, MetricType.Broker),
[MetricType.Topic]: (clusterId: string) => api.getDashboardMetricChartData(clusterId, MetricType.Topic),
[MetricType.Zookeeper]: (clusterId: string) => '',
[MetricType.Zookeeper]: (clusterId: string) => api.getZookeeperMetrics(clusterId),
};
export const getMetricDashboardReq = (clusterId: string, type: MetricType.Broker | MetricType.Topic | MetricType.Zookeeper) =>

View File

@@ -108,10 +108,10 @@ const DraggableCharts = (props: PropsType): JSX.Element => {
startTime,
endTime,
metricsNames: selectedMetricNames,
topNu: curHeaderOptions?.scopeData?.isTop ? curHeaderOptions.scopeData.data : null,
},
dashboardType === MetricType.Broker || dashboardType === MetricType.Topic
? {
topNu: curHeaderOptions?.scopeData?.isTop ? curHeaderOptions.scopeData.data : null,
[dashboardType === MetricType.Broker ? 'brokerIds' : 'topics']: curHeaderOptions?.scopeData?.isTop
? null
: curHeaderOptions.scopeData.data,
@@ -233,8 +233,8 @@ const DraggableCharts = (props: PropsType): JSX.Element => {
<div id="dashboard-drag-chart" className="topic-dashboard">
<ChartOperateBar
onChange={ksHeaderChange}
hideNodeScope={dashboardType === MetricType.Zookeeper}
nodeScopeModule={{
hasCustomScope: !(dashboardType === MetricType.Zookeeper),
customScopeList: scopeList,
scopeName: dashboardType === MetricType.Broker ? 'Broker' : dashboardType === MetricType.Topic ? 'Topic' : 'Zookeeper',
scopeLabel: `自定义 ${

View File

@@ -1,9 +1,10 @@
import React from 'react';
import { ClustersPermissionMap } from '@src/pages/CommonConfig';
import { ClusterRunState } from '@src/pages/MutliClusterPage/List';
const pkgJson = require('../../package');
export const systemKey = pkgJson.ident;
export const leftMenus = (clusterId?: string) => ({
export const leftMenus = (clusterId?: string, clusterRunState?: number) => ({
name: `${systemKey}`,
icon: 'icon-jiqun',
path: `cluster/${clusterId}`,
@@ -11,12 +12,12 @@ export const leftMenus = (clusterId?: string) => ({
{
name: 'cluster',
path: 'cluster',
icon: 'icon-Cluster',
icon: 'icon-Cluster1',
},
{
name: 'broker',
path: 'broker',
icon: 'icon-Brokers',
icon: 'icon-Brokers1',
children: [
{
name: 'dashbord',
@@ -38,7 +39,7 @@ export const leftMenus = (clusterId?: string) => ({
{
name: 'topic',
path: 'topic',
icon: 'icon-Topics',
icon: 'icon-Topics1',
children: [
{
name: 'dashbord',
@@ -52,10 +53,36 @@ export const leftMenus = (clusterId?: string) => ({
},
],
},
clusterRunState && clusterRunState !== ClusterRunState.Raft
? {
name: (intl: any) => {
return (
<div className="menu-item-with-beta-tag">
<span>{intl.formatMessage({ id: 'menu.cluster.zookeeper' })}</span>
<div className="beta-tag"></div>
</div>
);
},
path: 'zookeeper',
icon: 'icon-Zookeeper',
children: [
{
name: (intl: any) => <span>{intl.formatMessage({ id: 'menu.cluster.zookeeper.dashboard' })}</span>,
path: '',
icon: '#icon-luoji',
},
{
name: (intl: any) => <span>{intl.formatMessage({ id: 'menu.cluster.zookeeper.servers' })}</span>,
path: 'servers',
icon: 'icon-Jobs',
},
],
}
: undefined,
{
name: 'consumer-group',
path: 'consumers',
icon: 'icon-ConsumerGroups',
icon: 'icon-Consumer',
// children: [
// {
// name: 'operating-state',
@@ -72,7 +99,7 @@ export const leftMenus = (clusterId?: string) => ({
{
name: 'operation',
path: 'operation',
icon: 'icon-Jobs',
icon: 'icon-Operation',
children: [
process.env.BUSINESS_VERSION
? {
@@ -92,7 +119,7 @@ export const leftMenus = (clusterId?: string) => ({
? {
name: 'produce-consume',
path: 'testing',
icon: 'icon-a-ProduceConsume',
icon: 'icon-Message',
permissionPoint: [ClustersPermissionMap.TEST_CONSUMER, ClustersPermissionMap.TEST_PRODUCER],
children: [
{
@@ -113,7 +140,7 @@ export const leftMenus = (clusterId?: string) => ({
{
name: 'security',
path: 'security',
icon: 'icon-ACLs',
icon: 'icon-Security',
children: [
{
name: 'acls',

View File

@@ -259,6 +259,21 @@ li {
}
}
.menu-item-with-beta-tag {
display: flex;
.beta-tag {
width: 26px;
margin-left: 4px;
background: no-repeat center/26px 15px url('./assets/beta-tag.png');
}
}
.dcloud-menu-item-selected .menu-item-with-beta-tag .beta-tag {
width: 0;
}
.empty-panel {
display: flex;
flex-direction: column;

View File

@@ -51,6 +51,10 @@ export default {
[`menu.${systemKey}.jobs`]: 'Job',
[`menu.${systemKey}.zookeeper`]: 'Zookeeper',
[`menu.${systemKey}.zookeeper.dashboard`]: 'Overview',
[`menu.${systemKey}.zookeeper.servers`]: 'Servers',
'access.cluster': '接入集群',
'access.cluster.low.version.tip': '监测到当前Version较低建议维护Zookeeper信息以便得到更好的产品体验',
'edit.cluster': '编辑集群',

View File

@@ -0,0 +1,60 @@
import React, { useState } from 'react';
import { Controlled as CodeMirror } from 'react-codemirror2';
import SwitchTab from '@src/components/SwitchTab';
const isJSON = (str: string) => {
if (typeof str == 'string') {
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
} else {
return false;
}
};
const ZKData = ({ nodeData }: { nodeData: string }) => {
const [showMode, setShowMode] = useState('default');
return (
<>
<div className="zk-detail-layout-right-content-format">
<SwitchTab defaultKey={showMode} onChange={(key) => setShowMode(key)}>
<SwitchTab.TabItem key="default">
<div style={{ padding: '0 10px' }}></div>
</SwitchTab.TabItem>
<SwitchTab.TabItem key="JSON">
<div style={{ padding: '0 10px' }}>JSON格式</div>
</SwitchTab.TabItem>
</SwitchTab>
</div>
{showMode === 'default' && (
<div className={'zk-detail-layout-right-content-data'}>
{isJSON(nodeData) ? JSON.stringify(JSON.parse(nodeData), null, 2) : nodeData}
</div>
)}
{showMode === 'JSON' && (
<div className={'zk-detail-layout-right-content-code'}>
<CodeMirror
className={'zk-detail-layout-right-content-code-data'}
value={isJSON(nodeData) ? JSON.stringify(JSON.parse(nodeData), null, 2) : nodeData}
options={{
mode: 'application/json',
lineNumbers: true,
lineWrapper: true,
autoCloseBrackets: true,
smartIndent: true,
tabSize: 2,
}}
onBeforeChange={() => {
return;
}}
/>
</div>
)}
</>
);
};
export default ZKData;

View File

@@ -0,0 +1,188 @@
import React, { useState, useEffect } from 'react';
import { Drawer, Utils, AppContainer, Spin, Empty } from 'knowdesign';
import ZKDetailMenu from './Sider';
import Api from '@src/api';
import ZKInfo from './Info';
import ZKData from './Data';
const { request } = Utils;
import './index.less';
import { DataNode } from './config';
const ZookeeperDetail = ({ visible, setVisible }: { visible: boolean; setVisible: (visible: boolean) => void }) => {
const [global] = AppContainer.useGlobalValue();
// const { visible, setVisible } = props;
const [detailLoading, setDetailLoading] = useState(true);
const [isDetail, setIsDetail] = useState(true);
const [detailInfoLoading, setDetailInfoLoading] = useState(false);
const [pathList, setPathList] = useState([]);
const [node, setNode] = useState<any>({});
const [idenKey, setIdenKey] = useState([]);
const [siderWidth, setSiderWidth] = useState(200);
const [startPageX, setStartPageX] = useState(0);
const [dragging, setDragging] = useState(false);
const [detailTreeData, setDetailTreeData] = useState([]);
const onClose = () => {
setVisible(false);
setPathList([]);
setNode({});
setIdenKey([]);
};
const siderMouseDown = (e: { pageX: number }) => {
setStartPageX(e.pageX);
setDragging(true);
};
const siderMouseMove = (e: { pageX: number }) => {
const currentSiderWidth = siderWidth + e.pageX - startPageX;
if (currentSiderWidth < 200) {
setSiderWidth(200);
} else if (currentSiderWidth > 320) {
setSiderWidth(320);
} else {
setSiderWidth(currentSiderWidth);
}
setStartPageX(e.pageX);
};
const siderMouseUp = () => {
setDragging(false);
};
const rootClick = () => {
setPathList([]);
setIdenKey([]);
};
useEffect(() => {
// 第一次加载不触发详情信息的loading
!detailLoading && setDetailInfoLoading(true);
visible &&
request(Api.getZookeeperNodeData(+global?.clusterInfo?.id), {
params: { path: '/' + pathList.map((item: DataNode) => item.title).join('/') },
})
.then((res) => {
setNode(res || {});
})
.catch(() => {
setNode({});
})
.finally(() => {
setDetailInfoLoading(false);
});
}, [pathList]);
useEffect(() => {
setDetailLoading(true);
visible &&
request(Api.getZookeeperNodeChildren(+global?.clusterInfo?.id), { params: { path: '/', keyword: '' } })
// zkDetailInfo()
.then((res: string[]) => {
const newData =
res && res.length > 0
? res.map((item: string, index: number) => {
return {
title: item,
key: `${index}`,
};
})
: [];
if (newData.length > 0) {
setIsDetail(false);
setDetailTreeData(newData);
}
})
.finally(() => {
setDetailLoading(false);
});
}, [visible]);
return (
<Drawer
push={false}
title={'Zookeeper 详情'}
width={1080}
placement="right"
onClose={onClose}
visible={visible}
className="zookeeper-detail-drawer"
destroyOnClose
maskClosable={false}
>
<Spin spinning={detailLoading}>
{isDetail ? (
<div className="zk-detail-empty">
<Empty image={Empty.PRESENTED_IMAGE_CUSTOM} description="暂无数据" />
</div>
) : (
<div className="zk-detail-layout">
<div className="zk-detail-layout-left" style={{ width: siderWidth + 'px' }}>
<div className="zk-detail-layout-left-title"></div>
<div className="zk-detail-layout-left-content">
{visible && (
<ZKDetailMenu
setDetailInfoLoading={setDetailInfoLoading}
detailTreeData={detailTreeData}
setPathList={setPathList}
setIdenKey={setIdenKey}
idenKey={idenKey}
/>
)}
</div>
</div>
<div className="zk-detail-layout-resizer" style={{ left: siderWidth + 'px' }} onMouseDown={siderMouseDown}>
{dragging && <div className="resize-mask" onMouseMove={siderMouseMove} onMouseUp={siderMouseUp} />}
</div>
<div className="zk-detail-layout-right">
<div className="zk-detail-layout-right-title"></div>
<div className="zk-detail-layout-right-content">
{visible && (
<Spin spinning={detailInfoLoading}>
<div className="zk-detail-layout-right-content-countheight">
<div className="zk-detail-layout-right-content-path">
<span onClick={rootClick}>{node.namespace}</span>
{pathList.length > 0 &&
pathList.map((item, index) => {
if (item.key === idenKey[0]) {
return (
<span key={index}>
{' '}
/
<a key={index} onClick={() => setIdenKey([item.key])}>
{item.title}
</a>
</span>
);
}
return (
<span key={index}>
{' '}
/
<span key={index} onClick={() => setIdenKey([item.key])}>
{item.title}
</span>
</span>
);
})}
</div>
{/* <div>{'/' + pathList.map((item: any) => item.title).join(' / ') || '/'}</div> */}
<div className="zk-detail-layout-right-content-info">
<ZKInfo siderWidth={siderWidth} nodeInfo={node?.stat || {}} />
</div>
<ZKData nodeData={node?.data || ''} />
</div>
</Spin>
)}
</div>
</div>
</div>
)}
</Spin>
</Drawer>
);
};
export default ZookeeperDetail;

View File

@@ -0,0 +1,61 @@
import React from 'react';
import { Descriptions } from 'knowdesign';
import moment from 'moment';
const ZKInfo = ({ nodeInfo, siderWidth }: any) => {
const smallStyle = {
width: 82,
};
return (
<Descriptions
style={{ fontSize: '13px' }}
column={siderWidth >= 270 ? 2 : 3}
labelStyle={{
display: 'flex',
textAlign: 'right',
justifyContent: 'end',
color: '#74788D',
fontSize: '13px',
width: siderWidth >= 270 ? 144 : 100,
}}
contentStyle={{ fontSize: '13px' }}
>
<Descriptions.Item labelStyle={siderWidth < 270 && smallStyle} label="aversion">
{nodeInfo.aversion || nodeInfo.aversion === 0 ? nodeInfo.aversion : '-'}
</Descriptions.Item>
<Descriptions.Item label="ctime">{nodeInfo.ctime || nodeInfo.ctime === 0 ? nodeInfo.ctime : '-'}</Descriptions.Item>
<Descriptions.Item label="ctime-pretty">
{nodeInfo.ctime ? moment(nodeInfo.ctime).format('YYYY-MM-DD HH:mm:ss') : '-'}
</Descriptions.Item>
<Descriptions.Item labelStyle={siderWidth < 270 && smallStyle} label="cversion">
{nodeInfo.cversion || nodeInfo.cversion === 0 ? nodeInfo.cversion : '-'}
</Descriptions.Item>
<Descriptions.Item label="czxid">{nodeInfo.czxid || nodeInfo.czxid === 0 ? nodeInfo.czxid : '-'}</Descriptions.Item>
<Descriptions.Item label="dataLength">
{nodeInfo.dataLength || nodeInfo.dataLength === 0 ? nodeInfo.dataLength : '-'}
</Descriptions.Item>
<Descriptions.Item labelStyle={siderWidth < 270 && smallStyle} label="mtime">
{nodeInfo.mtime || nodeInfo.mtime === 0 ? nodeInfo.mtime : '-'}
</Descriptions.Item>
<Descriptions.Item label="mtime-pretty">
{nodeInfo.mtime ? moment(nodeInfo.mtime).format('YYYY-MM-DD HH:mm:ss') : '-'}
</Descriptions.Item>
<Descriptions.Item label="numChildren">
{nodeInfo.numChildren || nodeInfo.numChildren === 0 ? nodeInfo.numChildren : '-'}
</Descriptions.Item>
<Descriptions.Item labelStyle={siderWidth < 270 && smallStyle} label="mzxid">
{nodeInfo.mzxid || nodeInfo.mzxid === 0 ? nodeInfo.mzxid : '-'}
</Descriptions.Item>
<Descriptions.Item label="ephemeralOwner">
{nodeInfo.ephemeralOwner || nodeInfo.ephemeralOwner === 0 ? nodeInfo.ephemeralOwner : '-'}
</Descriptions.Item>
<Descriptions.Item style={{ paddingBottom: '12px' }} label="pzxid">
{nodeInfo.pzxid || nodeInfo.pzxid === 0 ? nodeInfo.pzxid : '-'}
</Descriptions.Item>
<Descriptions.Item labelStyle={siderWidth < 270 && smallStyle} style={{ paddingBottom: '12px' }} label="version">
{nodeInfo.version || nodeInfo.version === 0 ? nodeInfo.version : '-'}
</Descriptions.Item>
</Descriptions>
);
};
export default ZKInfo;

View File

@@ -0,0 +1,179 @@
import React, { useEffect, useState, useRef } from 'react';
import { Tree, SearchInput, AppContainer, Utils } from 'knowdesign';
import { DataNode, DetailMenuType, getPathByKey, updateTreeData } from './config';
import Api from '@src/api';
const { request } = Utils;
const ZKDetailMenu = (props: DetailMenuType) => {
const { detailTreeData, setDetailInfoLoading, setPathList, setIdenKey, idenKey } = props;
const [global] = AppContainer.useGlobalValue();
const [treeData, setTreeData] = useState<DataNode[]>(detailTreeData);
const [autoExpandParent, setAutoExpandParent] = useState<boolean>(true);
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const [loadedKeys, setLoadedKeys] = useState<string[]>([]);
const [childrenClose, setChildrenClose] = useState<boolean>(false);
// const [searchValue, setSearchValue] = useState<string>('');
// const treeRef = useRef();
// 处理参数
const getParams = (key: string, searchValue?: string) => {
const path =
'/' +
getPathByKey(key, treeData)
.map((item: any) => item.title)
.join('/');
return {
path,
keyword: searchValue ? searchValue : '',
};
};
const onSelect = (selectedKeys: string[]) => {
// 控制右侧详情内容的Loading
setDetailInfoLoading(true);
setIdenKey(selectedKeys);
};
const onLoadData = ({ key, children = null }: any) => {
return new Promise<void>((resolve, reject) => {
// 节点关闭,在展开要重新发送请求,以保证节点的准确性
if (children && !childrenClose) {
resolve();
return;
}
request(Api.getZookeeperNodeChildren(+global?.clusterInfo?.id), { params: getParams(key) })
.then((res: string[]) => {
const newData =
res && res.length > 0
? res.map((item: string, index: number) => {
return {
title: item,
key: `${key}-${index}`,
};
})
: [
{
title: '暂无子节点',
key: `${key}-${1}`,
disabled: true,
selectable: false,
isLeaf: true,
},
];
setAutoExpandParent(true);
setTreeData((origin: DataNode[]) => updateTreeData(origin, key, newData));
})
.finally(() => {
return resolve();
});
});
};
const searchChange = (e: string) => {
if (idenKey[0] && idenKey[0].length > 0) {
request(Api.getZookeeperNodeChildren(+global?.clusterInfo?.id), { params: getParams(idenKey[0], e) }).then((res: string[]) => {
const newData =
res && res.length > 0
? res.map((item: string, index: number) => {
return {
title: item,
key: `${idenKey[0]}-${index}`,
};
})
: [
// 如果查询不到节点或者所查询的父节点下没有子节点人为插入一个节点
{
title: e ? '未搜索到相关节点' : '暂无子节点',
key: `${idenKey[0]}-${0}`,
disabled: true,
selectable: false,
isLeaf: true,
},
];
setTreeData((origin: DataNode[]) => {
return updateTreeData(origin, idenKey[0], newData);
});
// 筛选打开的节点中非选中节点的其他节点(排除选中节点以及选中节点的叶子节点)
const filterExpandedKeys = expandedKeys.filter((item) => item.slice(0, idenKey[0].length) !== idenKey[0]);
// 将当前选中的节点再次合并
const newExpandedKeys = [...filterExpandedKeys, ...idenKey];
setExpandedKeys(newExpandedKeys);
setLoadedKeys(newExpandedKeys);
setAutoExpandParent(true);
});
}
};
// 展开收起
const onExpand = (keys: any, arg: any) => {
const { node, expanded } = arg;
let filterExpandedKeys = keys;
let newLoadKeys = loadedKeys;
setChildrenClose(false);
if (!expanded) {
idenKey[0] !== node.key && (idenKey[0] as string)?.slice(0, node.key.length) === node.key && setIdenKey([]);
filterExpandedKeys = keys.filter((item: string) => {
return item !== node.key && item.slice(0, node.key.length) !== node.key;
});
newLoadKeys = loadedKeys.filter((i: string) => i.slice(0, node.key.length) !== node.key);
setChildrenClose(true);
}
setAutoExpandParent(false);
setExpandedKeys(filterExpandedKeys);
setLoadedKeys(newLoadKeys);
};
const onLoad = (loadedKeys: string[]) => {
setLoadedKeys(loadedKeys);
};
useEffect(() => {
// treeRef?.current?.scrollTo({ key: idenKey[0] });
const pathKey = getPathByKey(idenKey[0], treeData);
setPathList(pathKey);
}, [idenKey]);
useEffect(() => {
if (detailTreeData.length > 0) {
setTreeData(detailTreeData);
}
}, [detailTreeData]);
return (
<>
<SearchInput
onSearch={searchChange}
attrs={{
placeholder: '在当前节点进行搜索',
// onChange: searchChange,
size: 'small',
style: {
marginBottom: '15px',
},
// value: searchValue,
// onChange: (value: string) => setSearchValue(value),
}}
/>
<div className="zk-detail-layout-left-content-text">
<Tree
// ref={treeRef}
// height={300}
onLoad={onLoad}
loadData={onLoadData}
loadedKeys={loadedKeys}
expandedKeys={expandedKeys}
selectedKeys={idenKey}
onExpand={onExpand}
autoExpandParent={autoExpandParent}
onSelect={onSelect}
treeData={treeData}
/>
</div>
</>
);
};
export default ZKDetailMenu;

View File

@@ -0,0 +1,133 @@
import React from 'react';
import { Tag } from 'knowdesign';
export interface DetailMenuType {
detailTreeData: any;
setDetailInfoLoading: (loading: boolean) => void;
setPathList: (pathList: DataNode[]) => void;
setIdenKey: (idenKey: string[]) => void;
idenKey: string[];
}
export interface DataNode {
title: string;
key: string;
isLeaf?: boolean;
children?: DataNode[];
}
// 角色
const roleType: any = {
leader: 'Leader',
follower: 'Follower',
ovsever: 'Obsever',
};
export const updateTreeData = (list: DataNode[], key: React.Key, children: DataNode[]): DataNode[] => {
return list.map((node) => {
if (node.key === key) {
return {
...node,
children,
};
}
if (node.children) {
return {
...node,
children: updateTreeData(node.children, key, children),
};
}
return node;
});
};
export const getZookeeperColumns = (arg?: any) => {
const columns = [
{
title: 'Host',
dataIndex: 'host',
key: 'host',
width: 200,
render: (t: string, r: any) => {
return (
<span>
{t}
{r?.status ? <Tag className="tag-success">Live</Tag> : <Tag className="tag-error">Down</Tag>}
</span>
);
},
},
{
title: 'Port',
dataIndex: 'port',
key: 'port',
width: 200,
},
{
title: 'Version',
dataIndex: 'version',
key: 'version',
width: 200,
},
{
title: 'Role',
dataIndex: 'role',
key: 'role',
width: 200,
render(t: string, r: any) {
return (
<Tag
style={{
background: t === 'leader' ? 'rgba(85,110,230,0.10)' : t === 'follower' ? 'rgba(0,192,162,0.10)' : '#fff3e4',
color: t === 'leader' ? '#556EE6' : t === 'follower' ? '#00C0A2' : '#F58342',
padding: '3px 6px',
}}
>
{roleType[t]}
</Tag>
);
},
},
];
return columns;
};
export const defaultPagination = {
current: 1,
pageSize: 10,
position: 'bottomRight',
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
};
export const getPathByKey = (curKey: string, data: DataNode[]) => {
/** 存放搜索到的树节点到顶部节点的路径节点 */
let result: any[] = [];
const traverse = (curKey: string, path: any[], data: DataNode[]) => {
// 树为空时,不执行函数
if (data.length === 0) {
return;
}
// 遍历存放树的数组
for (const item of data) {
// 遍历的数组元素存入path参数数组中
path.push(item);
// 如果目的节点的id值等于当前遍历元素的节点id值
if (item.key === curKey) {
// 把获取到的节点路径数组path赋值到result数组
result = JSON.parse(JSON.stringify(path));
return;
}
// 当前元素的children是数组
const children = Array.isArray(item.children) ? item.children : [];
// 递归遍历子数组内容
traverse(curKey, path, children);
// 利用回溯思想当没有在当前叶树找到目的节点依次删除存入到的path数组路径
path.pop();
}
};
traverse(curKey, [], data);
// 返回找到的树节点路径
return result;
};

View File

@@ -0,0 +1,131 @@
.zookeeper-detail-drawer {
.zk-detail-empty {
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
border: 2px solid #eff2f7;
height: calc(100vh - 90px);
}
.zk-detail-layout {
display: flex;
border-radius: 8px;
border: 2px solid #eff2f7;
height: 100%;
position: relative;
&-left,
&-right {
&-title {
background-color: rgba(116, 120, 141, 0.04);
font-size: 14px;
font-weight: 500;
font-family: @font-family-bold;
padding: 12px 20px;
border-bottom: 2px solid #eff2f7;
}
&-content {
height: calc(100vh - 128px);
}
}
&-left {
min-width: 200px;
max-width: 320px;
&-content {
padding: 10px;
&-text {
height: calc(100vh - 190px);
overflow: auto;
}
}
.dcloud-tree-title {
white-space: nowrap !important;
}
.dcloud-tree-treenode {
font-size: 13px;
padding: 6px 0;
}
.dcloud-tree-node-content-wrapper {
&:hover:not(.dcloud-tree-node-content-wrapper-normal) {
background: #f1f3ff !important;
color: #556ee6;
}
}
.dcloud-tree .dcloud-tree-node-content-wrapper.dcloud-tree-node-selected {
background-color: transparent;
color: #556ee6;
}
}
&-resizer {
.resize-mask {
background: rgba(0, 0, 0, 0);
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 1;
cursor: col-resize;
}
border: 1px solid #eff2f7;
position: absolute;
left: 200px;
top: 0;
bottom: 0;
cursor: col-resize;
&:hover {
border: 1px solid #556ee6;
}
}
&-right {
flex: 1;
&-content {
padding: 12px 20px 16px;
overflow: auto;
&-info {
background: rgba(116, 120, 141, 0.04);
border-radius: 8px;
padding: 12px 0 0;
}
&-countheight {
display: flex;
flex-direction: column;
height: calc(100vh - 156px);
}
&-path {
margin-bottom: 16px;
& > *:hover {
color: #556ee6;
cursor: pointer;
}
}
&-format {
margin: 12px 0 6px;
display: flex;
justify-content: right;
}
&-data {
flex: 1;
overflow: auto;
background: rgba(116, 120, 141, 0.04);
border-radius: 8px;
padding: 12px 20px 0;
}
&-code {
flex: 1;
overflow: auto;
border-radius: 8px;
&-data {
height: 100% !important;
}
.cm-s-default {
height: 100% !important;
// border-radius: 8px !important;
overflow: hidden;
background: rgba(116, 120, 141, 0.04);
}
}
}
}
}
}

View File

@@ -0,0 +1,128 @@
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 { getZookeeperColumns, defaultPagination } from './config';
import { tableHeaderPrefix } from '@src/constants/common';
import ZookeeperDetail from './Detail';
import ZookeeperCard from '@src/components/CardBar/ZookeeperCard';
import DBreadcrumb from 'knowdesign/es/extend/d-breadcrumb';
import './index.less';
const { request } = Utils;
const ZookeeperList: 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 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.getZookeeperList(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,
};
}) || [];
setData(newData);
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 key="brokerList" className="brokerList">
<div className="breadcrumb" style={{ marginBottom: '10px' }}>
<DBreadcrumb
breadcrumbs={[
{ label: '多集群管理', aHref: '/' },
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
{ label: 'Zookeeper', aHref: `/cluster/${global?.clusterInfo?.id}/zookepper` },
{ label: 'Servers', aHref: `` },
]}
/>
</div>
<div style={{ margin: '12px 0' }}>
<ZookeeperCard />
</div>
<div className="clustom-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,
}}
/>
<Button type="primary" onClick={() => setDetailVisible(true)}>
Zookeeper详情
</Button>
</div>
</div>
<ProTable
key="zookeeper-table"
showQueryForm={false}
tableProps={{
showHeader: false,
rowKey: 'zookeeper_list',
loading: loading,
columns: getZookeeperColumns(),
dataSource: data,
paginationProps: { ...pagination },
attrs: {
onChange: onTableChange,
scroll: { y: 'calc(100vh - 400px)' },
bordered: false,
},
}}
/>
</div>
{<ZookeeperDetail visible={detailVisible} setVisible={setDetailVisible} />}
</div>
);
};
export default ZookeeperList;

View File

@@ -0,0 +1,27 @@
import React from 'react';
import { MetricType } from '@src/api';
import DraggableCharts from '@src/components/DraggableCharts';
import DBreadcrumb from 'knowdesign/es/extend/d-breadcrumb';
import { AppContainer } from 'knowdesign';
import ZookeeperCard from '@src/components/CardBar/ZookeeperCard';
const ZookeeperDashboard = (): 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: 'Zookeeper', aHref: `` },
]}
/>
</div>
<ZookeeperCard />
<DraggableCharts type={MetricType.Zookeeper} />
</>
);
};
export default ZookeeperDashboard;

View File

@@ -7,6 +7,7 @@ import { leftMenus, systemKey } from '@src/constants/menu';
import { ClustersPermissionMap } from './CommonConfig';
import { getLicenseInfo } from '@src/constants/common';
import { licenseEventBus } from '@src/constants/axiosConfig';
import { ClusterRunState } from './MutliClusterPage/List';
export const NoMatch = <Redirect to="/404" />;
@@ -35,6 +36,13 @@ const LayoutContainer = () => {
const [showSider, setShowSider] = useState<boolean>(!(notCurrentSystemKey || hasNoSiderPage));
const [handledLeftMenus, setHandledLeftMenus] = useState(leftMenus());
const forbidenPaths = (path: string) => {
// Raft 模式运行的集群没有 ZK 页面
if (path.includes('zookeeper') && global.clusterInfo?.runState === ClusterRunState.Raft) {
history.replace('/404');
}
};
const isShowMenu = useCallback(
(nodes: ClustersPermissionMap | ClustersPermissionMap[]) => {
let isAllow = false;
@@ -67,6 +75,7 @@ const LayoutContainer = () => {
if (permissionNode) {
// 判断用户是否有当前页面的权限
if (global.hasPermission(permissionNode)) {
forbidenPaths(path);
return Promise.resolve(true);
} else {
// 用户没有当前页面权限,跳转到多集群首页
@@ -77,6 +86,7 @@ const LayoutContainer = () => {
return Promise.reject(false);
}
}
forbidenPaths(path);
return Promise.resolve(true);
},
[global.clusterInfo, global.hasPermission, global.getMetricDefine]
@@ -86,12 +96,14 @@ const LayoutContainer = () => {
const notCurrentSystemKey = window.location.pathname.split('/')?.[1] !== systemKey;
const hasNoSiderPage = noSiderPages.findIndex((item) => item.path === getCurrentPathname(pathname)) > -1;
setShowSider(notCurrentSystemKey || hasNoSiderPage ? false : true);
if (pathname.startsWith('/cluster') || pathname === '/') {
const items = pathname.split('/');
const clusterId = items[2];
setHandledLeftMenus(leftMenus(clusterId));
clusterId !== global.clusterInfo?.id &&
setHandledLeftMenus(leftMenus(clusterId, pathname === '/' ? undefined : global.clusterInfo?.runState));
}
}, [pathname]);
}, [pathname, global.clusterInfo]);
return (
<div id="sub-system" style={{ display: 'flex' }}>

View File

@@ -22,6 +22,9 @@ import SecurityACLs from './SecurityACLs';
import SecurityUsers from './SecurityUsers';
import LoadRebalance from './LoadRebalance';
import Zookeeper from './Zookeeper';
import ZookeeperDashboard from './ZookeeperDashboard';
const pageRoutes = [
{
path: '/',
@@ -115,6 +118,18 @@ const pageRoutes = [
component: Jobs,
noSider: false,
},
{
path: 'zookeeper',
exact: true,
component: ZookeeperDashboard,
noSider: false,
},
{
path: 'zookeeper/servers',
exact: true,
component: Zookeeper,
noSider: false,
},
{
path: 'security/acls',
exact: true,