Merge pull request #575 from GraceWalk/dev

同步前端代码
This commit is contained in:
lucasun
2022-09-13 15:13:02 +08:00
committed by GitHub
75 changed files with 38995 additions and 1354 deletions

View File

@@ -9,6 +9,5 @@ build/
coverage coverage
versions/ versions/
debug.log debug.log
package-lock.json
yarn.lock yarn.lock
target target

View File

@@ -1,43 +1,60 @@
## 安装项目依赖 ## 前提
- 安装 lerna 正常情况下,您应该通过 [本地源码启动手册](https://github.com/didi/KnowStreaming/blob/master/docs/dev_guide/%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%90%AF%E5%8A%A8%E6%89%8B%E5%86%8C.md) 来打包工程。如果您有需要在本地独立启动前端服务,请参考以下手册。
在进行以下的步骤之前,首先确保您已经安装了 `node`。如已安装,可以通过在终端执行 `node -v` 来获取到 node 版本,项目推荐使用 `node v12` 版本运行。
另外,`windows` 用户请在 `git bash` 下运行下面的命令。
## 一、安装项目依赖(必须)
1. 安装 lerna可选安装后可以直接通过 lerna 的全局指令管理项目,如果不了解 lerna 可以不安装)
``` ```
npm install -g lerna npm install -g lerna
``` ```
- 安装项目依赖 2. 安装项目依赖
``` ```
npm run i npm run i
``` ```
## 启动项目 我们默认保留了 `package-lock.json` 文件,以防止可能的依赖包自动升级导致的问题。依赖默认会通过 `https://registry.npmjs.org` 服务下载,如果您无法连通该服务器,请删除当前目录及 `packages/*` 子目录下的 `package-lock.json` 后,在当前目录下使用 `node v12` 版本执行命令 `npm run i`
## 二、启动项目
``` ```
npm run start npm run start
``` ```
### 环境信息 该指令会启动 `packages` 目录下的所有应用,如果需要单独启动应用,其查看下方 QA。
http://localhost:port 多集群管理应用会启动在 http://localhost:8000系统管理应用会占用 http://localhost:8001。
请确认 `8000``8001` 端口没有被其他应用占用。
## 构建项目 后端本地服务启动在 http://localhost:8080请求通过 webpack dev server 代理访问 8080 端口,需要启动后端服务后才能正常请求接口。
如果启动失败,可以参见另外一种本地启动方式 [本地源码启动手册](https://github.com/didi/KnowStreaming/blob/master/docs/dev_guide/%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%90%AF%E5%8A%A8%E6%89%8B%E5%86%8C.md)
## 三、构建项目
``` ```
npm run build npm run build
``` ```
项目构建成功后,会存放到 km-rest/src/main/resources/tamplates 目录下。
## 目录结构 ## 目录结构
- packages - packages
- layout-clusters-fe: 基座应用 & 多集群管理 - layout-clusters-fe: 基座应用 & 多集群管理(其余应用启动需要首先启动该应用)
- config-manager-fe: 子应用 - 系统管理 - config-manager-fe: 子应用 - 系统管理
- tool: 启动 & 打包脚本
- ... - ...
## 常见问题 ## 常见问题
Q: 执行 `npm run start` 时看不到应用构建和热加载过程? Q: `km-console` 目录下执行 `npm run start` 时看不到应用构建和热加载过程?如何启动单个应用?
A: 需要到具体的应用中执行 `npm run start`,例如 `cd packages/layout-clusters-fe` 后,执行 `npm run start` A: 需要到具体的应用中执行 `npm run start`,例如 `cd packages/layout-clusters-fe` 后,执行 `npm run start`

8567
km-console/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -17,15 +17,15 @@
"eslint-plugin-react": "7.22.0", "eslint-plugin-react": "7.22.0",
"eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-react-hooks": "^4.2.0",
"husky": "4.3.7", "husky": "4.3.7",
"lerna": "^4.0.0", "lerna": "^5.5.0",
"lint-staged": "10.5.3", "lint-staged": "10.5.3",
"prettier": "2.3.2" "prettier": "2.3.2"
}, },
"scripts": { "scripts": {
"i": "npm install && lerna bootstrap", "i": "npm install && lerna bootstrap",
"clean": "rm -rf node_modules package-lock.json packages/*/node_modules packages/*/package-lock.json", "clean": "rm -rf node_modules package-lock.json packages/*/node_modules packages/*/package-lock.json",
"start": "sh ./tool/start.sh", "start": "lerna run start",
"build": "sh ./tool/build.sh", "build": "lerna run build",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md",
"cm": "git add . && cz" "cm": "git add . && cz"
}, },

View File

@@ -9,5 +9,4 @@ build/
coverage coverage
versions/ versions/
debug.log debug.log
package-lock.json
yarn.lock yarn.lock

View File

@@ -1,6 +1,6 @@
## 使用说明 ## 使用说明
### 依赖安装: ### 依赖安装(如在 km-console 目录下执行 npm run i 安装过依赖,这步可以省略)
``` ```
npm install npm install
@@ -12,6 +12,8 @@ npm install
npm run start npm run start
``` ```
该应用为子应用,启动后需要到基座应用中查看(需要启动基座应用),地址为 http://localhost:8000
### 构建: ### 构建:
``` ```

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@
"scripts": { "scripts": {
"test": "echo \"Error: run tests from root\" && exit 1", "test": "echo \"Error: run tests from root\" && exit 1",
"start": "cross-env NODE_ENV=development webpack-dev-server", "start": "cross-env NODE_ENV=development webpack-dev-server",
"build": "rm -rf ../../pub/layout & cross-env NODE_ENV=production webpack --max_old_space_size=8000" "build": "cross-env NODE_ENV=production webpack --max_old_space_size=8000"
}, },
"dependencies": { "dependencies": {
"babel-preset-react-app": "^10.0.0", "babel-preset-react-app": "^10.0.0",

View File

@@ -35,7 +35,16 @@ serviceInstance.interceptors.request.use(
// 响应拦截 // 响应拦截
serviceInstance.interceptors.response.use( serviceInstance.interceptors.response.use(
(config: any) => { (config: any) => {
return config.data; const res: { code: number; message: string; data: any } = config.data;
if (res.code !== 0 && res.code !== 200) {
const desc = res.message;
notification.error({
message: desc,
duration: 3,
});
throw res;
}
return res;
}, },
(err: any) => { (err: any) => {
const config = err.config; const config = err.config;

View File

@@ -73,12 +73,12 @@ const CheckboxGroupContainer = (props: CheckboxGroupType) => {
</Checkbox> </Checkbox>
</div> </div>
<Checkbox.Group disabled={disabled} style={{ width: '100%' }} value={checkedList} onChange={onCheckedChange}> <Checkbox.Group disabled={disabled} style={{ width: '100%' }} value={checkedList} onChange={onCheckedChange}>
<Row gutter={[34, 10]}> <Row gutter={[10, 10]}>
{options.map((option) => { {options.map((option) => {
return ( return (
<Col span={8} key={option.value}> <Col span={8} key={option.value}>
<Checkbox value={option.value} className="checkbox-content-ellipsis"> <Checkbox value={option.value} className="checkbox-content-ellipsis">
{option.label} {option.label.replace('Cluster-Load', '')}
</Checkbox> </Checkbox>
</Col> </Col>
); );

View File

@@ -20,7 +20,7 @@ import {
IconFont, IconFont,
} from 'knowdesign'; } from 'knowdesign';
import moment from 'moment'; import moment from 'moment';
import { CloseOutlined, LoadingOutlined, PlusOutlined } from '@ant-design/icons'; import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
import { defaultPagination } from 'constants/common'; import { defaultPagination } from 'constants/common';
import { RoleProps, PermissionNode, AssignUser, RoleOperate, FormItemPermission } from './config'; import { RoleProps, PermissionNode, AssignUser, RoleOperate, FormItemPermission } from './config';
import api from 'api'; import api from 'api';
@@ -50,11 +50,21 @@ const RoleDetailAndUpdate = forwardRef((props, ref): JSX.Element => {
useEffect(() => { useEffect(() => {
const globalPermissions = global.permissions; const globalPermissions = global.permissions;
if (globalPermissions && globalPermissions.length) { if (globalPermissions && globalPermissions.length) {
const sysPermissions = globalPermissions.map((sys: PermissionNode) => ({ const sysPermissions = globalPermissions.map((sys: PermissionNode) => {
id: sys.id, const result = {
name: sys.permissionName, id: sys.id,
options: sys.childList.map((node) => ({ label: node.permissionName, value: node.id })), name: sys.permissionName,
})); essentialPermission: undefined,
options: [],
};
result.options = sys.childList.map((node) => {
if (node.permissionName === '多集群管理查看' || node.permissionName === '系统管理查看') {
result.essentialPermission = { label: node.permissionName, value: node.id };
}
return { label: node.permissionName, value: node.id };
});
return result;
});
setPermissions(sysPermissions); setPermissions(sysPermissions);
} }
}, [global]); }, [global]);
@@ -77,10 +87,12 @@ const RoleDetailAndUpdate = forwardRef((props, ref): JSX.Element => {
const onSubmit = () => { const onSubmit = () => {
form.validateFields().then((formData) => { form.validateFields().then((formData) => {
formData.permissionIdList = formData.permissionIdList.filter((l) => l);
formData.permissionIdList.forEach((arr, i) => { formData.permissionIdList.forEach((arr, i) => {
// 如果分配的系统下的子权限,自动赋予该系统的权限 // 如果分配的系统下的子权限,自动赋予该系统的权限
if (arr !== null && arr.length) { if (!Array.isArray(arr)) {
arr = [];
}
if (arr?.length) {
arr.push(permissions[i].id); arr.push(permissions[i].id);
} }
}); });
@@ -210,10 +222,20 @@ const RoleDetailAndUpdate = forwardRef((props, ref): JSX.Element => {
<Form.Item <Form.Item
label="分配权限" label="分配权限"
name="permissionIdList" name="permissionIdList"
required
rules={[ rules={[
() => ({ () => ({
validator(_, value) { validator(_, value) {
if (Array.isArray(value) && value.some((item) => !!item?.length)) { if (Array.isArray(value) && value.some((item) => !!item?.length)) {
const errs = [];
value.forEach((arr, i) => {
if (arr?.length && !arr.includes(permissions[i].essentialPermission.value)) {
errs.push(`[${permissions[i].essentialPermission.label}]`);
}
});
if (errs.length) {
return Promise.reject(`您必须分配 ${errs.join(' 和 ')} 权限`);
}
return Promise.resolve(); return Promise.resolve();
} }
return Promise.reject(new Error('请为角色至少分配一项权限')); return Promise.reject(new Error('请为角色至少分配一项权限'));

View File

@@ -59,5 +59,6 @@ export enum RoleOperate {
export interface FormItemPermission { export interface FormItemPermission {
id: number; id: number;
name: string; name: string;
essentialPermission: { label: string; value: number };
options: { label: string; value: number }[]; options: { label: string; value: number }[];
} }

View File

@@ -9,6 +9,5 @@ build/
coverage coverage
versions/ versions/
debug.log debug.log
package-lock.json
yarn.lock yarn.lock
.d1-workspace.json .d1-workspace.json

View File

@@ -1,6 +1,6 @@
## 使用说明 ## 使用说明
### 依赖安装: ### 依赖安装(如在 km-console 目录下执行 npm run i 安装过依赖,这步可以省略)
``` ```
npm install npm install
@@ -12,6 +12,8 @@ npm install
npm run start npm run start
``` ```
启动后访问地址为 http://localhost:8000
### 构建: ### 构建:
``` ```

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,7 @@
"scripts": { "scripts": {
"test": "echo \"Error: run tests from root\" && exit 1", "test": "echo \"Error: run tests from root\" && exit 1",
"start": "cross-env NODE_ENV=development webpack-dev-server", "start": "cross-env NODE_ENV=development webpack-dev-server",
"build": "rm -rf ../../pub/layout & cross-env NODE_ENV=production webpack --max_old_space_size=8000" "build": "cross-env NODE_ENV=production webpack --max_old_space_size=8000"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [

View File

@@ -59,6 +59,7 @@ const logout = () => {
}).then((res) => { }).then((res) => {
window.location.href = '/login'; window.location.href = '/login';
}); });
localStorage.removeItem('userInfo');
}; };
const LicenseLimitModal = () => { const LicenseLimitModal = () => {
@@ -117,7 +118,7 @@ const AppContent = (props: { setlanguage: (language: string) => void }) => {
<DProLayout.Container <DProLayout.Container
headerProps={{ headerProps={{
title: ( title: (
<div> <div style={{ cursor: 'pointer' }}>
<img className="header-logo" src={ksLogo} /> <img className="header-logo" src={ksLogo} />
</div> </div>
), ),

View File

@@ -90,7 +90,7 @@ export default () => {
return ( return (
<div> <div>
<span style={{ display: 'inline-block', marginRight: '8px' }}>Similar Config</span> <span style={{ display: 'inline-block', marginRight: '8px' }}>Similar Config</span>
<Tooltip overlayClassName="rebalance-tooltip" title="所有broker配置是否一致"> <Tooltip overlayClassName="rebalance-tooltip" title="所有Broker配置是否一致">
<QuestionCircleOutlined /> <QuestionCircleOutlined />
</Tooltip> </Tooltip>
</div> </div>
@@ -111,7 +111,7 @@ export default () => {
]; ];
setCardData(cordRightMap); setCardData(cordRightMap);
}); });
Promise.all([brokerMetric, brokersState]).then((res) => { Promise.all([brokerMetric, brokersState]).finally(() => {
setLoading(false); setLoading(false);
}); });
}, [routeParams.clusterId]); }, [routeParams.clusterId]);

View File

@@ -1,12 +1,13 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import CardBar from './index'; import CardBar from './index';
import { IconFont, Tag, Utils, Tooltip, Popover } from 'knowdesign'; import { IconFont, Tag, Utils, Tooltip, Popover, AppContainer } from 'knowdesign';
import api from '@src/api'; import api from '@src/api';
import StateChart from './StateChart'; import StateChart from './StateChart';
import ClusterNorms from '@src/pages/LoadRebalance/ClusterNorms'; import ClusterNorms from '@src/pages/LoadRebalance/ClusterNorms';
import { QuestionCircleOutlined } from '@ant-design/icons'; import { QuestionCircleOutlined } from '@ant-design/icons';
import moment from 'moment'; import moment from 'moment';
import { ClustersPermissionMap } from '@src/pages/CommonConfig';
const transUnitTimePro = (ms: number, num = 0) => { const transUnitTimePro = (ms: number, num = 0) => {
if (!ms) return ''; if (!ms) return '';
@@ -23,6 +24,7 @@ const transUnitTimePro = (ms: number, num = 0) => {
}; };
const LoadRebalanceCardBar = (props: any) => { const LoadRebalanceCardBar = (props: any) => {
const [global] = AppContainer.useGlobalValue();
const { clusterId } = useParams<{ const { clusterId } = useParams<{
clusterId: string; clusterId: string;
}>(); }>();
@@ -53,12 +55,14 @@ const LoadRebalanceCardBar = (props: any) => {
return ( return (
<div style={{ height: '20px' }}> <div style={{ height: '20px' }}>
<span style={{ display: 'inline-block', marginRight: '8px' }}>State</span> <span style={{ display: 'inline-block', marginRight: '8px' }}>State</span>
<IconFont {global.hasPermission(ClustersPermissionMap.REBALANCE_SETTING) && (
className="cutomIcon-config" <IconFont
style={{ fontSize: '15px' }} className="cutomIcon-config"
onClick={() => setNormsVisible(true)} style={{ fontSize: '15px' }}
type="icon-shezhi" onClick={() => setNormsVisible(true)}
></IconFont> type="icon-shezhi"
></IconFont>
)}
</div> </div>
); );
}, },

View File

@@ -1,18 +1,20 @@
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { AppContainer, Button, Drawer, IconFont, message, Spin, Table, SingleChart, Utils, Tooltip } from 'knowdesign'; import { AppContainer, Drawer, Spin, Table, SingleChart, Utils, Tooltip } from 'knowdesign';
import moment from 'moment'; import moment from 'moment';
import api, { MetricType } from '@src/api'; import api, { MetricType } from '@src/api';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { MetricDefaultChartDataType, MetricChartDataType, formatChartData, getDetailChartConfig } from './config'; import { MetricDefaultChartDataType, MetricChartDataType, formatChartData, getDetailChartConfig } from './config';
import { UNIT_MAP } from '@src/constants/chartConfig'; import { UNIT_MAP } from '@src/constants/chartConfig';
import { CloseOutlined } from '@ant-design/icons'; import RenderEmpty from '../RenderEmpty';
interface ChartDetailProps { interface ChartDetailProps {
metricType: MetricType; metricType: MetricType;
metricName: string; metricName: string;
queryLines: string[]; queryLines: string[];
onClose: () => void; setSliderRange: (range: string) => void;
// eslint-disable-next-line @typescript-eslint/ban-types
setDisposeChartInstance: Function;
} }
interface MetricTableInfo { interface MetricTableInfo {
@@ -24,6 +26,18 @@ interface MetricTableInfo {
color: string; color: string;
} }
interface ChartInfo {
chartInstance?: echarts.ECharts;
isLoadingAdditionData?: boolean;
isLoadedFullData?: boolean;
fullTimeRange?: readonly [number, number];
curTimeRange?: readonly [number, number];
sliderPos?: readonly [number, number];
transformUnit?: [string, number];
fullMetricData?: MetricChartDataType;
oldDataZoomOption?: any;
}
interface DataZoomEventProps { interface DataZoomEventProps {
type: 'datazoom'; type: 'datazoom';
// 缩放的开始位置的百分比0 - 100 // 缩放的开始位置的百分比0 - 100
@@ -34,8 +48,6 @@ interface DataZoomEventProps {
// 缩放区默认选中范围比例0.011 // 缩放区默认选中范围比例0.011
const DATA_ZOOM_DEFAULT_SCALE = 0.25; const DATA_ZOOM_DEFAULT_SCALE = 0.25;
// 选中范围最少展示的时间长度(默认 10 分钟),单位: ms
const LEAST_SELECTED_TIME_RANGE = 1 * 60 * 1000;
// 单次向服务器请求数据的范围(默认 6 小时,超过后采集频率间隔会变长),单位: ms // 单次向服务器请求数据的范围(默认 6 小时,超过后采集频率间隔会变长),单位: ms
const DEFAULT_REQUEST_TIME_RANGE = 6 * 60 * 60 * 1000; const DEFAULT_REQUEST_TIME_RANGE = 6 * 60 * 60 * 1000;
// 采样间隔,影响前端补点逻辑,单位: ms // 采样间隔,影响前端补点逻辑,单位: ms
@@ -47,70 +59,15 @@ const DEFAULT_ENTER_TIME_RANGE = 2 * 60 * 60 * 1000;
// 预缓存数据阈值,图表展示数据的开始时间处于前端缓存数据的时间范围的前 40% 时,向服务器请求数据 // 预缓存数据阈值,图表展示数据的开始时间处于前端缓存数据的时间范围的前 40% 时,向服务器请求数据
const PRECACHE_THRESHOLD = 0.4; const PRECACHE_THRESHOLD = 0.4;
// 表格列
const colunms = [
{
title: 'Host',
dataIndex: 'name',
width: 200,
render(name: string, record: any) {
return (
<div style={{ display: 'flex', alignItems: 'center' }}>
<div style={{ width: 8, height: 2, marginRight: 4, background: record.color }}></div>
<span>{name}</span>
</div>
);
},
},
{
title: 'Avg',
dataIndex: 'avg',
width: 120,
render(num: number) {
return num.toFixed(2);
},
},
{
title: 'Max',
dataIndex: 'max',
width: 120,
render(num: number, record: any) {
return (
<div>
<span>{num.toFixed(2)}</span>
</div>
);
},
},
{
title: 'Min',
dataIndex: 'min',
width: 120,
render(num: number, record: any) {
return (
<div>
<span>{num.toFixed(2)}</span>
</div>
);
},
},
{
title: 'Latest',
dataIndex: 'latest',
width: 120,
render(latest: number[]) {
return `${latest[1].toFixed(2)}`;
},
},
];
const ChartDetail = (props: ChartDetailProps) => { const ChartDetail = (props: ChartDetailProps) => {
const [global] = AppContainer.useGlobalValue(); const [global] = AppContainer.useGlobalValue();
const { clusterId } = useParams<{ const { clusterId } = useParams<{
clusterId: string; clusterId: string;
}>(); }>();
const { metricType, metricName, queryLines, onClose } = props; const { metricType, metricName, queryLines, setSliderRange, setDisposeChartInstance } = props;
// 初始化拖拽防抖函数
const debouncedZoomDrag = useRef(null);
// 存储图表相关的不需要触发渲染的数据,用于计算图表展示状态并进行操作 // 存储图表相关的不需要触发渲染的数据,用于计算图表展示状态并进行操作
const chartInfo = useRef( const chartInfo = useRef(
(() => { (() => {
@@ -119,16 +76,16 @@ const ChartDetail = (props: ChartDetailProps) => {
const curTimeRange = [curTime - DEFAULT_ENTER_TIME_RANGE, curTime] as const; const curTimeRange = [curTime - DEFAULT_ENTER_TIME_RANGE, curTime] as const;
return { return {
chartInstance: undefined as echarts.ECharts, chartInstance: undefined,
isLoadingAdditionData: false,
isLoadedFullData: false, isLoadedFullData: false,
fullTimeRange: curTimeRange, fullTimeRange: curTimeRange,
fullMetricData: {} as MetricChartDataType, fullMetricData: {} as MetricChartDataType,
curTimeRange, curTimeRange,
oldDataZoomOption: {} as any, oldDataZoomOption: {},
sliderPos: [0, 0] as readonly [number, number], sliderPos: [0, 0],
sliderRange: '', transformUnit: undefined,
transformUnit: undefined as [string, number], } as ChartInfo;
};
})() })()
); );
@@ -137,8 +94,76 @@ const ChartDetail = (props: ChartDetailProps) => {
const [curMetricData, setCurMetricData] = useState<MetricChartDataType>(); const [curMetricData, setCurMetricData] = useState<MetricChartDataType>();
// 图表数据的各项计算指标 // 图表数据的各项计算指标
const [tableInfo, setTableInfo] = useState<MetricTableInfo[]>([]); const [tableInfo, setTableInfo] = useState<MetricTableInfo[]>([]);
// 选中展示的图表 const [linesStatus, setLinesStatus] = useState<{
const [selectedLines, setSelectedLines] = useState<string[]>([]); [lineName: string]: boolean;
}>({});
// 表格列
const colunms = useMemo(
() => [
{
title: metricType === MetricType.Broker ? 'Host' : 'Topic',
dataIndex: 'name',
width: 200,
render(name: string, record: any) {
return (
<div style={{ display: 'flex', alignItems: 'center' }}>
<div style={{ width: 8, height: 2, marginRight: 4, background: record.color }}></div>
<span>{name}</span>
</div>
);
},
},
{
title: 'Avg',
dataIndex: 'avg',
width: 120,
render(num: number) {
return num.toFixed(2);
},
},
{
title: 'Max',
dataIndex: 'max',
width: 120,
render(num: number, record: any) {
return (
<div>
<span>{num.toFixed(2)}</span>
</div>
);
},
},
{
title: 'Min',
dataIndex: 'min',
width: 120,
render(num: number, record: any) {
return (
<div>
<span>{num.toFixed(2)}</span>
</div>
);
},
},
{
title: 'Latest',
dataIndex: 'latest',
width: 120,
render(latest: number[]) {
return `${latest[1].toFixed(2)}`;
},
},
],
[metricType]
);
const updateChartInfo = (changedInfo: ChartInfo) => {
chartInfo.current = {
...chartInfo.current,
...changedInfo,
};
};
// 请求图表数据 // 请求图表数据
const getMetricChartData = ([startTime, endTime]: readonly [number, number]) => { const getMetricChartData = ([startTime, endTime]: readonly [number, number]) => {
@@ -175,11 +200,10 @@ const ChartDetail = (props: ChartDetailProps) => {
// 如果滑块整体拖动,则只更新拖动后滑块的位(保留小数点后三位是防止低位值的干扰) // 如果滑块整体拖动,则只更新拖动后滑块的位(保留小数点后三位是防止低位值的干扰)
if (oldScale.toFixed(3) === newScale.toFixed(3)) { if (oldScale.toFixed(3) === newScale.toFixed(3)) {
chartInfo.current = { updateChartInfo({
...chartInfo.current,
sliderPos: [newStartSliderPos, newEndSliderPos], sliderPos: [newStartSliderPos, newEndSliderPos],
oldDataZoomOption: newDataZoomOption, oldDataZoomOption: newDataZoomOption,
}; });
renderTableInfo(); renderTableInfo();
return false; return false;
@@ -217,23 +241,14 @@ const ChartDetail = (props: ChartDetailProps) => {
} }
} else { } else {
// 3. 滑块拖动后缩放比例变小 // 3. 滑块拖动后缩放比例变小
// 判断拖动后选择的时间范围并提示
if (newEndSliderPos - newStartSliderPos < LEAST_SELECTED_TIME_RANGE) {
// TODO: 补充逻辑
updateChartData([oldStartTimestamp, oldEndTimestamp], [oldStartSliderPos, oldEndSliderPos]);
message.warning(`当前选择范围小于 ${LEAST_SELECTED_TIME_RANGE / 60 / 1000} 分钟,图表可能无数据`);
return true;
}
const isOldLarger = oldScale - DATA_ZOOM_DEFAULT_SCALE > 0.01; const isOldLarger = oldScale - DATA_ZOOM_DEFAULT_SCALE > 0.01;
const isNewLarger = newScale - DATA_ZOOM_DEFAULT_SCALE > 0.01; const isNewLarger = newScale - DATA_ZOOM_DEFAULT_SCALE > 0.01;
if (isOldLarger && isNewLarger) { if (isOldLarger && isNewLarger) {
// 如果拖拽前后比例均高于默认比例,则不对图表展示范围进行操作 // 如果拖拽前后比例均高于默认比例,则不对图表展示范围进行操作
chartInfo.current = { updateChartInfo({
...chartInfo.current,
sliderPos: [newStartSliderPos, newEndSliderPos], sliderPos: [newStartSliderPos, newEndSliderPos],
oldDataZoomOption: newDataZoomOption, oldDataZoomOption: newDataZoomOption,
}; });
renderTableInfo(); renderTableInfo();
return true; return true;
} else { } else {
@@ -259,79 +274,98 @@ const ChartDetail = (props: ChartDetailProps) => {
const updateChartData = (timeRange: [number, number], sliderPos: [number, number]) => { const updateChartData = (timeRange: [number, number], sliderPos: [number, number]) => {
const { const {
fullTimeRange: [fullStartTimestamp, fullEndTimestamp], fullTimeRange: [fullStartTimestamp, fullEndTimestamp],
fullMetricData,
isLoadedFullData, isLoadedFullData,
} = chartInfo.current; } = chartInfo.current;
let leftBoundaryTimestamp = Math.floor(timeRange[0]); const leftBoundaryTimestamp = Math.floor(timeRange[0]);
const isNeedCacheExtraData = leftBoundaryTimestamp < fullStartTimestamp + (fullEndTimestamp - fullStartTimestamp) * PRECACHE_THRESHOLD; const isNeedCacheExtraData = leftBoundaryTimestamp < fullStartTimestamp + (fullEndTimestamp - fullStartTimestamp) * PRECACHE_THRESHOLD;
let isRendered = false; let isRendered = false;
// 如果本地存储的数据足够展示或者已经获取到所有数据,则展示数据 // 如果本地存储的数据足够展示或者已经获取到所有数据,则展示数据
if (leftBoundaryTimestamp > fullStartTimestamp || isLoadedFullData) { if (leftBoundaryTimestamp > fullStartTimestamp || isLoadedFullData) {
chartInfo.current = { updateChartInfo({
...chartInfo.current,
curTimeRange: [leftBoundaryTimestamp > fullStartTimestamp ? leftBoundaryTimestamp : fullStartTimestamp, timeRange[1]], curTimeRange: [leftBoundaryTimestamp > fullStartTimestamp ? leftBoundaryTimestamp : fullStartTimestamp, timeRange[1]],
sliderPos, sliderPos,
}; });
renderNewMetricData(); renderNewMetricData();
isRendered = true; isRendered = true;
} }
if (!isLoadedFullData && isNeedCacheExtraData) { if (!isLoadedFullData && isNeedCacheExtraData) {
// 向服务器请求新的数据缓存 getAdditionChartData(!isRendered, leftBoundaryTimestamp, timeRange[1], sliderPos);
let reqEndTime = fullStartTimestamp; }
const requestArr: any[] = []; };
const requestTimeRanges: [number, number][] = [];
for (let i = 0; i < DEFAULT_REQUEST_COUNT; i++) {
setTimeout(() => {
const nextReqEndTime = reqEndTime - DEFAULT_REQUEST_TIME_RANGE;
requestArr.unshift(getMetricChartData([nextReqEndTime, reqEndTime]));
requestTimeRanges.unshift([nextReqEndTime, reqEndTime]);
reqEndTime = nextReqEndTime;
// 当最后一次请求发送后,处理返回 // 缓存增量的图表数据
if (i === DEFAULT_REQUEST_COUNT - 1) { const getAdditionChartData = (
Promise.all(requestArr).then((resList) => { needRender: boolean,
let isSettle = -1; leftBoundaryTimestamp: number,
// 填充增量的图表数据 rightBoundaryTimestamp: number,
resList.forEach((res: MetricDefaultChartDataType[], i) => { sliderPos?: [number, number]
// 图表没有返回数据的情况 ) => {
if (!res?.length) { const {
if (isSettle === -1) { fullTimeRange: [fullStartTimestamp, fullEndTimestamp],
chartInfo.current = { fullMetricData,
...chartInfo.current, isLoadingAdditionData,
// 标记数据已经全部加载完毕 } = chartInfo.current;
isLoadedFullData: true,
};
isSettle = i;
}
} else {
resolveAdditionChartData(res, requestTimeRanges[i]);
}
});
// 更新左侧边界为当前已获取到数据的最小边界
const curLocalStartTimestamp = Number(fullMetricData.metricLines.map((line) => line.data[0][0]).sort()[0]);
if (leftBoundaryTimestamp < curLocalStartTimestamp) {
leftBoundaryTimestamp = curLocalStartTimestamp;
}
chartInfo.current = { // 当前有缓存数据的任务时,直接退出
...chartInfo.current, if (isLoadingAdditionData) {
fullTimeRange: [reqEndTime - DEFAULT_REQUEST_TIME_RANGE, fullEndTimestamp], return false;
sliderPos, }
}; updateChartInfo({
if (!isRendered) { isLoadingAdditionData: true,
chartInfo.current = { });
...chartInfo.current,
curTimeRange: [leftBoundaryTimestamp, timeRange[1]], let reqEndTime = fullStartTimestamp;
}; const requestArr: any[] = [];
renderNewMetricData(); const requestTimeRanges: [number, number][] = [];
for (let i = 0; i < DEFAULT_REQUEST_COUNT; i++) {
setTimeout(() => {
const nextReqEndTime = reqEndTime - DEFAULT_REQUEST_TIME_RANGE;
requestArr.push(getMetricChartData([nextReqEndTime, reqEndTime]));
requestTimeRanges.push([nextReqEndTime, reqEndTime]);
reqEndTime = nextReqEndTime;
// 当最后一次请求发送后,处理返回
if (i === DEFAULT_REQUEST_COUNT - 1) {
Promise.all(requestArr).then((resList) => {
// 填充增量的图表数据
resList.forEach((res: MetricDefaultChartDataType[], i) => {
// 最后一个请求返回数据为空时,认为已获取到全部图表数据
if (!res?.length) {
// 标记数据已经全部加载完毕
i === resList.length - 1 &&
updateChartInfo({
isLoadedFullData: true,
});
} else {
// TODO: res 可能为 [],需要处理兼容
resolveAdditionChartData(res, requestTimeRanges[i]);
} }
}); });
}
}, i * 10); // 更新左侧边界为当前已获取到数据的最小边界
} const curLocalStartTimestamp = Number(fullMetricData.metricLines.map((line) => line?.data?.[0]?.[0]).sort()[0]);
if (leftBoundaryTimestamp < curLocalStartTimestamp) {
leftBoundaryTimestamp = curLocalStartTimestamp;
}
updateChartInfo({
fullTimeRange: [reqEndTime - DEFAULT_REQUEST_TIME_RANGE, fullEndTimestamp],
...(sliderPos ? { sliderPos } : {}),
isLoadingAdditionData: false,
});
if (needRender) {
updateChartInfo({
curTimeRange: [leftBoundaryTimestamp, rightBoundaryTimestamp],
});
renderNewMetricData();
}
});
}
}, i * 10);
} }
return true;
}; };
// 处理增量图表数据 // 处理增量图表数据
@@ -362,7 +396,7 @@ const ChartDetail = (props: ChartDetailProps) => {
}); });
}; };
// 根据需要展示的时间范围过滤出对应的数据展示 // 根据需要展示的时间范围过滤出对应的数据
const renderNewMetricData = () => { const renderNewMetricData = () => {
const { fullMetricData, curTimeRange } = chartInfo.current; const { fullMetricData, curTimeRange } = chartInfo.current;
const newMetricData = { ...fullMetricData }; const newMetricData = { ...fullMetricData };
@@ -378,12 +412,25 @@ const ChartDetail = (props: ChartDetailProps) => {
}); });
newMetricData.metricLines[i] = line; newMetricData.metricLines[i] = line;
}); });
// 只过滤出当前时间段有数据点的线条,确保 Table 统一展示 // 只过滤出当前时间段有数据点的线条,确保 Table 统一展示
newMetricData.metricLines = newMetricData.metricLines.filter((line) => line.data.length); newMetricData.metricLines = newMetricData.metricLines.filter((line) => line.data.length);
setCurMetricData(newMetricData); setCurMetricData(newMetricData);
setLinesStatus((curStatus) => {
// 过滤维持线条选中状态
const newLinesStatus = { ...curStatus };
const newLineNames = newMetricData.metricLines.map((line) => line.name);
newLineNames.forEach((name) => {
if (newLinesStatus[name] === undefined) {
newLinesStatus[name] = false;
}
});
return newLinesStatus;
});
}; };
// 计算当前选中范围 // 计算展示当前拖拽轴选中的时间范围
const calculateSliderRange = () => { const calculateSliderRange = () => {
const { sliderPos } = chartInfo.current; const { sliderPos } = chartInfo.current;
let minutes = Number(((sliderPos[1] - sliderPos[0]) / 60 / 1000).toFixed(2)); let minutes = Number(((sliderPos[1] - sliderPos[0]) / 60 / 1000).toFixed(2));
@@ -398,13 +445,11 @@ const ChartDetail = (props: ChartDetailProps) => {
hours = Number((hours % 24).toFixed(2)); hours = Number((hours % 24).toFixed(2));
} }
chartInfo.current = { const sliderRange = ` 当前选中范围: ${days > 0 ? `${days}` : ''}${hours > 0 ? `${hours} 小时 ` : ''}${minutes} 分钟`;
...chartInfo.current, setSliderRange(sliderRange);
sliderRange: ` 当前选中范围: ${days > 0 ? `${days}` : ''}${hours > 0 ? `${hours} 小时 ` : ''}${minutes} 分钟`,
};
}; };
// 遍历图表,获取需要的指标数据展示到 Table // 遍历图表,计算得到指标聚合数据展示到表格
const renderTableInfo = () => { const renderTableInfo = () => {
const tableData: MetricTableInfo[] = []; const tableData: MetricTableInfo[] = [];
const { sliderPos, chartInstance } = chartInfo.current; const { sliderPos, chartInstance } = chartInfo.current;
@@ -447,140 +492,131 @@ const ChartDetail = (props: ChartDetailProps) => {
calculateSliderRange(); calculateSliderRange();
setTableInfo(tableData); setTableInfo(tableData);
setSelectedLines(tableData.map((line) => line.name));
}; };
const tableLineChange = (keys: string[]) => { const tableLineChange = (keys: string[]) => {
const updatedLines: { [name: string]: boolean } = {}; const newLinesStatus = { ...linesStatus };
selectedLines.forEach((name) => !keys.includes(name) && (updatedLines[name] = false));
keys.forEach((name) => !selectedLines.includes(name) && (updatedLines[name] = true));
// 更新 Object.entries(newLinesStatus).forEach(([name, status]) => {
Object.keys(updatedLines).forEach((name) => { if (keys.includes(name)) {
chartInfo.current.chartInstance.dispatchAction({ !status && (newLinesStatus[name] = true);
type: 'legendToggleSelect', } else {
// 图例名称 status && (newLinesStatus[name] = false);
name: name, }
});
}); });
setSelectedLines(keys); setLinesStatus(newLinesStatus);
}; };
// 图表数据更新渲染后,更新图表拖拽轴信息并重新计算列表值
useEffect(() => { useEffect(() => {
if (curMetricData) { if (curMetricData) {
setTimeout(() => { setTimeout(() => {
// 新的图表数据渲染后,更新图表拖拽轴信息
chartInfo.current.oldDataZoomOption = (chartInfo.current.chartInstance.getOption() as any).dataZoom[0]; chartInfo.current.oldDataZoomOption = (chartInfo.current.chartInstance.getOption() as any).dataZoom[0];
}); });
renderTableInfo(); renderTableInfo();
} }
}, [curMetricData]); }, [curMetricData]);
// 更新图例选中状态
useEffect(() => {
Object.entries(linesStatus).map(([name, status]) => {
const type = status ? 'legendSelect' : 'legendUnSelect';
chartInfo.current.chartInstance.dispatchAction({
type,
name,
});
});
}, [linesStatus]);
// 进入详情时,首次获取数据 // 进入详情时,首次获取数据
useEffect(() => { useEffect(() => {
if (metricType && metricName) { if (metricType && metricName) {
setLoading(true); setLoading(true);
const { curTimeRange } = chartInfo.current; const { curTimeRange } = chartInfo.current;
getMetricChartData(curTimeRange).then((res: any[] | null) => { getMetricChartData(curTimeRange).then(
// 如果图表返回数据 (res: any[] | null) => {
if (res?.length) { // 如果图表返回数据
// 格式化图表需要的数据 if (res?.length) {
const formattedMetricData = ( // 格式化图表需要的数据
formatChartData( const formattedMetricData = (
res, formatChartData(
global.getMetricDefine || {}, res,
metricType, global.getMetricDefine || {},
curTimeRange, metricType,
DEFAULT_POINT_INTERVAL, curTimeRange,
false DEFAULT_POINT_INTERVAL,
) as MetricChartDataType[] false
)[0]; ) as MetricChartDataType[]
// 填充图表数据 )[0];
let initFullTimeRange = curTimeRange; // 填充图表数据
const pointsOfFirstLine = formattedMetricData.metricLines.find((line) => line.data.length).data; let initFullTimeRange = curTimeRange;
if (pointsOfFirstLine) { const pointsOfFirstLine = formattedMetricData.metricLines.find((line) => line.data.length).data;
initFullTimeRange = [pointsOfFirstLine[0][0] as number, pointsOfFirstLine[pointsOfFirstLine.length - 1][0] as number] as const; if (pointsOfFirstLine) {
} initFullTimeRange = [
pointsOfFirstLine[0][0] as number,
// 获取单位保存起来 pointsOfFirstLine[pointsOfFirstLine.length - 1][0] as number,
let transformUnit = undefined; ] as const;
Object.entries(UNIT_MAP).forEach((unit) => {
if (formattedMetricData.metricUnit.includes(unit[0])) {
transformUnit = unit;
} }
});
chartInfo.current = { // 获取单位保存起来
...chartInfo.current, let transformUnit = undefined;
fullMetricData: formattedMetricData, Object.entries(UNIT_MAP).forEach((unit) => {
fullTimeRange: [...initFullTimeRange], if (formattedMetricData.metricUnit.includes(unit[0])) {
curTimeRange: [...initFullTimeRange], transformUnit = unit;
sliderPos: [ }
initFullTimeRange[1] - (initFullTimeRange[1] - initFullTimeRange[0]) * DATA_ZOOM_DEFAULT_SCALE, });
initFullTimeRange[1],
], updateChartInfo({
transformUnit, fullMetricData: formattedMetricData,
}; fullTimeRange: [...initFullTimeRange],
setCurMetricData(formattedMetricData); curTimeRange: [...initFullTimeRange],
setLoading(false); sliderPos: [
} initFullTimeRange[1] - (initFullTimeRange[1] - initFullTimeRange[0]) * DATA_ZOOM_DEFAULT_SCALE,
}); initFullTimeRange[1],
],
transformUnit,
});
setCurMetricData(formattedMetricData);
const newLinesStatus: { [lineName: string]: boolean } = {};
formattedMetricData.metricLines.forEach((line) => {
newLinesStatus[line.name] = true;
});
setLinesStatus(newLinesStatus);
setLoading(false);
getAdditionChartData(false, initFullTimeRange[0], initFullTimeRange[1]);
}
},
() => setLoading(false)
);
} }
}, []); }, []);
const debounced = debounce(onDataZoomDrag, 300); debouncedZoomDrag.current = debounce(onDataZoomDrag, 300);
return ( return (
<Spin spinning={loading}> <Spin spinning={loading}>
<div className="chart-detail-modal-container"> <div className="chart-detail-modal-container">
{curMetricData && ( {curMetricData ? (
<> <>
<div className="detail-title">
<div className="left">
<div className="title">
<Tooltip
placement="bottomLeft"
title={() => {
let content = '';
const metricDefine = global.getMetricDefine(metricType, curMetricData.metricName);
if (metricDefine) {
content = metricDefine.desc;
}
return content;
}}
>
<span style={{ cursor: 'pointer' }}>
<span>{curMetricData.metricName}</span> <span className="unit">({curMetricData.metricUnit}) </span>
</span>
</Tooltip>
</div>
<div className="info">{chartInfo.current.sliderRange}</div>
</div>
<div className="right">
<Button type="text" size="small" onClick={onClose}>
<CloseOutlined />
</Button>
</div>
</div>
<SingleChart <SingleChart
chartTypeProp="line" chartTypeProp="line"
wrapStyle={{ wrapStyle={{
width: 'auto', width: 'auto',
height: 462, height: 462,
}} }}
// events 事件只注册一次,所以这里使用 ref 来执行防抖函数
onEvents={{ onEvents={{
dataZoom: (record: any) => { dataZoom: (record: any) => debouncedZoomDrag?.current(record),
debounced(record);
},
}} }}
showHeader={false}
propChartData={curMetricData.metricLines} propChartData={curMetricData.metricLines}
optionMergeProps={{ notMerge: true }} optionMergeProps={{ notMerge: true }}
getChartInstance={(chartInstance) => { getChartInstance={(chartInstance) => {
chartInfo.current = { setDisposeChartInstance(() => () => chartInstance.dispose());
...chartInfo.current, updateChartInfo({
chartInstance, chartInstance,
}; });
}} }}
{...getDetailChartConfig(`${curMetricData.metricName}{unit|${curMetricData.metricUnit}}`, chartInfo.current.sliderPos)} {...getDetailChartConfig(`${curMetricData.metricName}{unit|${curMetricData.metricUnit}}`, chartInfo.current.sliderPos)}
/> />
@@ -588,16 +624,10 @@ const ChartDetail = (props: ChartDetailProps) => {
className="detail-table" className="detail-table"
rowKey="name" rowKey="name"
rowSelection={{ rowSelection={{
// hideSelectAll: true,
preserveSelectedRowKeys: true, preserveSelectedRowKeys: true,
selectedRowKeys: selectedLines, selectedRowKeys: Object.entries(linesStatus)
// getCheckboxProps: (record) => { .filter(([, status]) => status)
// return selectedLines.length <= 1 && selectedLines.includes(record.name) .map(([name]) => name),
// ? {
// disabled: true,
// }
// : {};
// },
selections: [Table.SELECTION_INVERT, Table.SELECTION_NONE], selections: [Table.SELECTION_INVERT, Table.SELECTION_NONE],
onChange: (keys: string[]) => tableLineChange(keys), onChange: (keys: string[]) => tableLineChange(keys),
}} }}
@@ -610,6 +640,8 @@ const ChartDetail = (props: ChartDetailProps) => {
pagination={false} pagination={false}
/> />
</> </>
) : (
!loading && <RenderEmpty message="详情加载失败,请重试" height={400} />
)} )}
</div> </div>
</Spin> </Spin>
@@ -618,22 +650,46 @@ const ChartDetail = (props: ChartDetailProps) => {
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
const ChartDrawer = forwardRef((_, ref) => { const ChartDrawer = forwardRef((_, ref) => {
const [global] = AppContainer.useGlobalValue();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [dashboardType, setDashboardType] = useState<MetricType>();
const [metricName, setMetricName] = useState<string>();
const [queryLines, setQueryLines] = useState<string[]>([]); const [queryLines, setQueryLines] = useState<string[]>([]);
const [sliderRange, setSliderRange] = useState<string>('');
const [disposeChartInstance, setDisposeChartInstance] = useState<() => void>(() => 0);
const [metricInfo, setMetricInfo] = useState<{
type: MetricType | undefined;
name: string;
unit: string;
desc: string;
}>({
type: undefined,
name: '',
unit: '',
desc: '',
});
const onOpen = (dashboardType: MetricType, metricName: string, queryLines: string[]) => { const onOpen = (dashboardType: MetricType, metricName: string, queryLines: string[]) => {
setDashboardType(dashboardType); const metricDefine = global.getMetricDefine(dashboardType, metricName);
setMetricName(metricName); setMetricInfo({
type: dashboardType,
name: metricName,
unit: metricDefine?.unit || '',
desc: metricDefine?.desc || '',
});
setQueryLines(queryLines); setQueryLines(queryLines);
setVisible(true); setVisible(true);
}; };
const onClose = () => { const onClose = () => {
setVisible(false); setVisible(false);
setDashboardType(undefined); setSliderRange('');
setMetricName(undefined); disposeChartInstance();
setDisposeChartInstance(() => () => 0);
setMetricInfo({
type: undefined,
name: '',
unit: '',
desc: '',
});
}; };
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
@@ -641,9 +697,36 @@ const ChartDrawer = forwardRef((_, ref) => {
})); }));
return ( return (
<Drawer width={1080} visible={visible} footer={null} closable={false} maskClosable={false} destroyOnClose={true} onClose={onClose}> <Drawer
{dashboardType && metricName && ( className="overview-chart-detail-drawer"
<ChartDetail metricType={dashboardType} metricName={metricName} queryLines={queryLines} onClose={onClose} /> width={1080}
visible={visible}
title={
<div className="detail-header">
<div className="title">
<Tooltip placement="bottomLeft" title={metricInfo.desc}>
<span style={{ cursor: 'pointer' }}>
<span>{metricInfo.name}</span> <span className="unit">({metricInfo.unit}) </span>
</span>
</Tooltip>
</div>
<div className="slider-info">{sliderRange}</div>
</div>
}
footer={null}
closable={true}
maskClosable={false}
destroyOnClose={true}
onClose={onClose}
>
{metricInfo.type && metricInfo.name && (
<ChartDetail
metricType={metricInfo.type}
metricName={metricInfo.name}
queryLines={queryLines}
setSliderRange={setSliderRange}
setDisposeChartInstance={setDisposeChartInstance}
/>
)} )}
</Drawer> </Drawer>
); );

View File

@@ -46,30 +46,42 @@ export const supplementaryPoints = (
extraCallback?: (point: [number, 0]) => any[] extraCallback?: (point: [number, 0]) => any[]
) => { ) => {
lines.forEach(({ data }) => { lines.forEach(({ data }) => {
// 获取未补点前线条的点的个数
let len = data.length; let len = data.length;
for (let i = 0; i < len; i++) { // 记录当前处理到的点的下标值
const timestamp = data[i][0] as number; let i = 0;
// 数组第一个点和最后一个点单独处理
for (; i < len; i++) {
if (i === 0) { if (i === 0) {
let firstPointTimestamp = data[0][0] as number; let firstPointTimestamp = data[0][0] as number;
while (firstPointTimestamp - interval > timeRange[0]) { while (firstPointTimestamp - interval > timeRange[0]) {
const prePointTimestamp = firstPointTimestamp - interval; const prevPointTimestamp = firstPointTimestamp - interval;
data.unshift(extraCallback ? extraCallback([prePointTimestamp, 0]) : [prePointTimestamp, 0]); data.unshift(extraCallback ? extraCallback([prevPointTimestamp, 0]) : [prevPointTimestamp, 0]);
firstPointTimestamp = prevPointTimestamp;
len++; len++;
i++; i++;
firstPointTimestamp = prePointTimestamp;
} }
} }
if (i === len - 1) { if (i === len - 1) {
let lastPointTimestamp = data[len - 1][0] as number; let lastPointTimestamp = data[i][0] as number;
while (lastPointTimestamp + interval < timeRange[1]) { while (lastPointTimestamp + interval < timeRange[1]) {
const next = lastPointTimestamp + interval; const nextPointTimestamp = lastPointTimestamp + interval;
data.push(extraCallback ? extraCallback([next, 0]) : [next, 0]); data.push(extraCallback ? extraCallback([nextPointTimestamp, 0]) : [nextPointTimestamp, 0]);
lastPointTimestamp = next; lastPointTimestamp = nextPointTimestamp;
}
break;
}
{
let timestamp = data[i][0] as number;
while (timestamp + interval < data[i + 1][0]) {
const nextPointTimestamp = timestamp + interval;
data.splice(i + 1, 0, extraCallback ? extraCallback([nextPointTimestamp, 0]) : [nextPointTimestamp, 0]);
timestamp = nextPointTimestamp;
len++;
i++;
} }
} else if (timestamp + interval < data[i + 1][0]) {
data.splice(i + 1, 0, extraCallback ? extraCallback([timestamp + interval, 0]) : [timestamp + interval, 0]);
len++;
} }
} }
}); });
@@ -135,18 +147,37 @@ export const formatChartData = (
}; };
const seriesCallback = (lines: { name: string; data: [number, string | number][] }[]) => { const seriesCallback = (lines: { name: string; data: [number, string | number][] }[]) => {
const len = CHART_COLOR_LIST.length;
// series 配置 // series 配置
return lines.map((line) => { return lines.map((line, i) => {
return { return {
...line, ...line,
lineStyle: { lineStyle: {
width: 1.5, width: 1.5,
}, },
connectNulls: false,
symbol: 'emptyCircle', symbol: 'emptyCircle',
symbolSize: 4, symbolSize: 4,
smooth: 0.25, smooth: 0.25,
areaStyle: { areaStyle: {
opacity: 0.02, color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: CHART_COLOR_LIST[i % len] + '10',
},
{
offset: 1,
color: 'rgba(255,255,255,0)', // 100% 处的颜色
},
],
global: false, // 缺省为 false
},
}, },
}; };
}); });
@@ -189,6 +220,7 @@ export const getDetailChartConfig = (title: string, sliderPos: readonly [number,
startValue: sliderPos[0], startValue: sliderPos[0],
endValue: sliderPos[1], endValue: sliderPos[1],
zoomOnMouseWheel: false, zoomOnMouseWheel: false,
minValueSpan: 10 * 60 * 1000,
}, },
{ {
start: 0, start: 0,

View File

@@ -63,56 +63,63 @@
} }
} }
} }
.overview-chart-detail-drawer {
.chart-detail-modal-container { .dcloud-spin-nested-loading > div > .dcloud-spin.dcloud-spin-spinning {
position: relative; height: 300px;
.expand-icon-box { }
position: absolute; &.dcloud-drawer .dcloud-drawer-body {
z-index: 1000; padding: 0 20px;
top: 14px; }
right: 44px; .detail-header {
width: 24px; display: flex;
height: 24px; align-items: flex-end;
cursor: pointer; font-weight: normal;
font-size: 16px; .title {
text-align: center; font-family: @font-family-bold;
border-radius: 50%; font-size: 18px;
transition: background-color 0.3s ease; color: #495057;
.expand-icon { letter-spacing: 0;
color: #adb5bc; .unit {
line-height: 24px; font-family: @font-family-bold;
} font-size: 14px;
&:hover { letter-spacing: 0.5px;
background: rgba(33, 37, 41, 0.04);
.expand-icon {
color: #74788d;
} }
} }
.slider-info {
margin-left: 10px;
font-size: 12px;
font-family: @font-family;
color: #303a51;
}
} }
.detail-title { .chart-detail-modal-container {
display: flex; position: relative;
justify-content: space-between; overflow: hidden;
align-items: center; .expand-icon-box {
.left { position: absolute;
display: flex; z-index: 1000;
align-items: flex-end; top: 14px;
.title { right: 44px;
font-family: @font-family-bold; width: 24px;
font-size: 18px; height: 24px;
color: #495057; cursor: pointer;
letter-spacing: 0; font-size: 16px;
.unit { text-align: center;
font-family: @font-family-bold; border-radius: 50%;
font-size: 14px; transition: background-color 0.3s ease;
letter-spacing: 0.5px; .expand-icon {
color: #adb5bc;
line-height: 24px;
}
&:hover {
background: rgba(33, 37, 41, 0.04);
.expand-icon {
color: #74788d;
} }
} }
.info { }
margin-left: 10px; .detail-table {
} margin-top: 16px;
} }
} }
.detail-table {
margin-top: 16px;
}
} }

View File

@@ -216,8 +216,8 @@ const DashboardDragChart = (props: PropsType): JSX.Element => {
onChange={ksHeaderChange} onChange={ksHeaderChange}
nodeScopeModule={{ nodeScopeModule={{
customScopeList: scopeList, customScopeList: scopeList,
scopeName: `自定义 ${dashboardType === MetricType.Broker ? 'Broker' : 'Topic'} 范围`, scopeName: dashboardType === MetricType.Broker ? 'Broker' : 'Topic',
showSearch: dashboardType === MetricType.Topic, scopeLabel: `自定义 ${dashboardType === MetricType.Broker ? 'Broker' : 'Topic'} 范围`,
}} }}
indicatorSelectModule={{ indicatorSelectModule={{
hide: false, hide: false,

View File

@@ -0,0 +1,15 @@
import React from 'react';
const RenderEmpty = (props: { height?: string | number; message: string }) => {
const { height = 200, message } = props;
return (
<>
<div className="empty-panel" style={{ height }}>
<div className="img" />
<div className="text">{message}</div>
</div>
</>
);
};
export default RenderEmpty;

View File

@@ -26,8 +26,8 @@ const OptionsDefault = [
const NodeScope = ({ nodeScopeModule, change }: propsType) => { const NodeScope = ({ nodeScopeModule, change }: propsType) => {
const { const {
customScopeList: customList, customScopeList: customList,
scopeName = '自定义节点范围', scopeName = '',
showSearch = false, scopeLabel = '自定义范围',
searchPlaceholder = '输入内容进行搜索', searchPlaceholder = '输入内容进行搜索',
} = nodeScopeModule; } = nodeScopeModule;
const [topNum, setTopNum] = useState<number>(5); const [topNum, setTopNum] = useState<number>(5);
@@ -70,7 +70,7 @@ const NodeScope = ({ nodeScopeModule, change }: propsType) => {
change(checkedListTemp, false); change(checkedListTemp, false);
setIsTop(false); setIsTop(false);
setTopNum(null); setTopNum(null);
setInputValue(`已选${checkedListTemp?.length}`); setInputValue(`${checkedListTemp?.length}`);
setPopVisible(false); setPopVisible(false);
} }
}; };
@@ -109,7 +109,7 @@ const NodeScope = ({ nodeScopeModule, change }: propsType) => {
{/* <span>时间:</span> */} {/* <span>时间:</span> */}
<div className="flx_con"> <div className="flx_con">
<div className="flx_l"> <div className="flx_l">
<h6 className="time_title">top</h6> <h6 className="time_title"> top </h6>
<Radio.Group <Radio.Group
optionType="button" optionType="button"
buttonStyle="solid" buttonStyle="solid"
@@ -128,7 +128,7 @@ const NodeScope = ({ nodeScopeModule, change }: propsType) => {
</Radio.Group> </Radio.Group>
</div> </div>
<div className="flx_r"> <div className="flx_r">
<h6 className="time_title">{scopeName}</h6> <h6 className="time_title">{scopeLabel}</h6>
<div className="custom-scope"> <div className="custom-scope">
<div className="check-row"> <div className="check-row">
<Checkbox className="check-all" indeterminate={indeterminate} onChange={onCheckAllChange} checked={checkAll}> <Checkbox className="check-all" indeterminate={indeterminate} onChange={onCheckAllChange} checked={checkAll}>
@@ -136,9 +136,7 @@ const NodeScope = ({ nodeScopeModule, change }: propsType) => {
</Checkbox> </Checkbox>
<Input <Input
className="search-input" className="search-input"
suffix={ suffix={<IconFont type="icon-fangdajing" style={{ fontSize: '16px' }} />}
<IconFont type="icon-fangdajing" style={{ fontSize: '16px' }} />
}
size="small" size="small"
placeholder={searchPlaceholder} placeholder={searchPlaceholder}
onChange={(e) => setScopeSearchValue(e.target.value)} onChange={(e) => setScopeSearchValue(e.target.value)}
@@ -148,7 +146,7 @@ const NodeScope = ({ nodeScopeModule, change }: propsType) => {
<Checkbox.Group style={{ width: '100%' }} onChange={checkChange} value={checkedListTemp}> <Checkbox.Group style={{ width: '100%' }} onChange={checkChange} value={checkedListTemp}>
<Row gutter={[10, 12]}> <Row gutter={[10, 12]}>
{customList {customList
.filter((item) => !showSearch || item.label.includes(scopeSearchValue)) .filter((item) => item.label.includes(scopeSearchValue))
.map((item) => ( .map((item) => (
<Col span={12} key={item.value}> <Col span={12} key={item.value}>
<Checkbox value={item.value}>{item.label}</Checkbox> <Checkbox value={item.value}>{item.label}</Checkbox>
@@ -180,6 +178,7 @@ const NodeScope = ({ nodeScopeModule, change }: propsType) => {
return ( return (
<> <>
<div id="d-node-scope"> <div id="d-node-scope">
<div className="scope-title">{scopeName}</div>
<Popover <Popover
trigger={['click']} trigger={['click']}
visible={popVisible} visible={popVisible}

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Tooltip, Select, IconFont, Utils, Divider } from 'knowdesign'; import { Tooltip, Select, IconFont, Utils, Divider, Button } from 'knowdesign';
import moment from 'moment'; import moment from 'moment';
import { DRangeTime } from 'knowdesign'; import { DRangeTime } from 'knowdesign';
import IndicatorDrawer from './IndicatorDrawer'; import IndicatorDrawer from './IndicatorDrawer';
@@ -48,7 +48,7 @@ export interface IcustomScope {
export interface InodeScopeModule { export interface InodeScopeModule {
customScopeList: IcustomScope[]; customScopeList: IcustomScope[];
scopeName?: string; scopeName?: string;
showSearch?: boolean; scopeLabel?: string;
searchPlaceholder?: string; searchPlaceholder?: string;
change?: () => void; change?: () => void;
} }
@@ -138,9 +138,13 @@ const SingleChartHeader = ({
}; };
const reloadRangeTime = () => { const reloadRangeTime = () => {
const timeLen = rangeTime[1] - rangeTime[0] || 0; if (isRelativeRangeTime) {
const curTimeStamp = moment().valueOf(); const timeLen = rangeTime[1] - rangeTime[0] || 0;
setRangeTime([curTimeStamp - timeLen, curTimeStamp]); const curTimeStamp = moment().valueOf();
setRangeTime([curTimeStamp - timeLen, curTimeStamp]);
} else {
setRangeTime([...rangeTime]);
}
}; };
const openIndicatorDrawer = () => { const openIndicatorDrawer = () => {
@@ -174,12 +178,10 @@ const SingleChartHeader = ({
{!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} />
)} )}
<Divider type="vertical" style={{ height: 20, top: 0 }} /> {(!hideNodeScope || !hideGridSelect) && <Divider type="vertical" style={{ height: 20, top: 0 }} />}
<Tooltip title="点击指标筛选,可选择指标" placement="bottomRight"> <Button type="primary" onClick={openIndicatorDrawer}>
<div className="icon-box" onClick={openIndicatorDrawer}>
<IconFont className="icon" type="icon-shezhi1" /> </Button>
</div>
</Tooltip>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -3,8 +3,13 @@
@import '~knowdesign/es/basic/style/mixins/index'; @import '~knowdesign/es/basic/style/mixins/index';
#d-node-scope { #d-node-scope {
display: flex;
align-items: center;
position: relative; position: relative;
display: inline-block; .scope-title {
font-size: 14px;
color: #74788d;
}
.input-span { .input-span {
cursor: pointer; cursor: pointer;
} }
@@ -29,10 +34,10 @@
box-shadow: none; box-shadow: none;
} }
&.relativeTime { &.relativeTime {
width: 160px; width: 200px;
} }
&.absoluteTime { &.absoluteTime {
width: 300px; width: 200px;
} }
input { input {

View File

@@ -30,8 +30,8 @@ const { TextArea } = Input;
const { Option } = Select; const { Option } = Select;
const jobNameMap: any = { const jobNameMap: any = {
expandAndReduce: '批量扩缩副本', expandAndReduce: '扩缩副本',
transfer: '批量迁移副本', transfer: '迁移副本',
}; };
interface DefaultConfig { interface DefaultConfig {
@@ -325,8 +325,7 @@ export default (props: DefaultConfig) => {
!jobId && !jobId &&
Utils.request(Api.getTopicMetaData(+routeParams.clusterId)) Utils.request(Api.getTopicMetaData(+routeParams.clusterId))
.then((res: any) => { .then((res: any) => {
const filterRes = res.filter((item: any) => item.type !== 1); const topics = (res || []).map((item: any) => {
const topics = (filterRes || []).map((item: any) => {
return { return {
label: item.topicName, label: item.topicName,
value: item.topicName, value: item.topicName,

View File

@@ -19,6 +19,7 @@ import {
Divider, Divider,
Transfer, Transfer,
IconFont, IconFont,
Tooltip,
} from 'knowdesign'; } from 'knowdesign';
import './index.less'; import './index.less';
import Api, { MetricType } from '@src/api/index'; import Api, { MetricType } from '@src/api/index';
@@ -31,8 +32,8 @@ const { TextArea } = Input;
const { Option } = Select; const { Option } = Select;
const jobNameMap: any = { const jobNameMap: any = {
expandAndReduce: '批量扩缩副本', expandAndReduce: '扩缩副本',
transfer: '批量迁移副本', transfer: '迁移副本',
}; };
interface DefaultConfig { interface DefaultConfig {
@@ -56,6 +57,7 @@ export default (props: DefaultConfig) => {
const [topicNewReplicas, setTopicNewReplicas] = useState([]); const [topicNewReplicas, setTopicNewReplicas] = useState([]);
const [needMovePartitions, setNeedMovePartitions] = useState([]); const [needMovePartitions, setNeedMovePartitions] = useState([]);
const [moveDataTimeRanges, setMoveDataTimeRanges] = useState([]); const [moveDataTimeRanges, setMoveDataTimeRanges] = useState([]);
const [moveDataTimeRangesType, setMoveDataTimeRangesType] = useState([]);
const [form] = Form.useForm(); const [form] = Form.useForm();
const [global] = AppContainer.useGlobalValue(); const [global] = AppContainer.useGlobalValue();
const [loadingTopic, setLoadingTopic] = useState<boolean>(true); const [loadingTopic, setLoadingTopic] = useState<boolean>(true);
@@ -142,8 +144,23 @@ export default (props: DefaultConfig) => {
title: '迁移数据时间范围', title: '迁移数据时间范围',
dataIndex: 'newRetentionMs', dataIndex: 'newRetentionMs',
render: (v: any, r: any, i: number) => { render: (v: any, r: any, i: number) => {
const selectAfter = (
<Select
onChange={(n: any) => {
const moveDataTimeRangesCopyType = JSON.parse(JSON.stringify(moveDataTimeRangesType));
moveDataTimeRangesCopyType[i] = n === 'h' ? 1 : 60;
setMoveDataTimeRangesType(moveDataTimeRangesCopyType);
}}
defaultValue="h"
style={{ width: 82 }}
>
<Option value="m">Minute</Option>
<Option value="h">Hour</Option>
</Select>
);
return ( return (
<InputNumber <InputNumber
width={80}
min={0} min={0}
max={99999} max={99999}
defaultValue={moveDataTimeRanges[i]} defaultValue={moveDataTimeRanges[i]}
@@ -153,8 +170,10 @@ export default (props: DefaultConfig) => {
moveDataTimeRangesCopy[i] = n; moveDataTimeRangesCopy[i] = n;
setMoveDataTimeRanges(moveDataTimeRangesCopy); setMoveDataTimeRanges(moveDataTimeRangesCopy);
}} }}
formatter={(value) => (value ? `${value} h` : '')} className={'move-dete-time-tanges'}
parser={(value) => value.replace('h', '')} // formatter={(value) => (value ? `${value} h` : '')}
// parser={(value) => value.replace('h', '')}
addonAfter={selectAfter}
></InputNumber> ></InputNumber>
); );
}, },
@@ -319,8 +338,7 @@ export default (props: DefaultConfig) => {
drawerVisible && drawerVisible &&
Utils.request(Api.getTopicMetaData(+routeParams.clusterId)) Utils.request(Api.getTopicMetaData(+routeParams.clusterId))
.then((res: any) => { .then((res: any) => {
const filterRes = res.filter((item: any) => item.type !== 1); const topics = (res || []).map((item: any) => {
const topics = (filterRes || []).map((item: any) => {
return { return {
label: item.topicName, label: item.topicName,
value: item.topicName, value: item.topicName,
@@ -402,7 +420,7 @@ export default (props: DefaultConfig) => {
originalBrokerIdList: taskPlanData[index].currentBrokerIdList, originalBrokerIdList: taskPlanData[index].currentBrokerIdList,
reassignBrokerIdList: taskPlanData[index].reassignBrokerIdList, reassignBrokerIdList: taskPlanData[index].reassignBrokerIdList,
originalRetentionTimeUnitMs: topicData[index].retentionMs, originalRetentionTimeUnitMs: topicData[index].retentionMs,
reassignRetentionTimeUnitMs: moveDataTimeRanges[index] * 60 * 60 * 1000, reassignRetentionTimeUnitMs: (moveDataTimeRanges[index] * 60 * 60 * 1000) / (moveDataTimeRangesType[index] || 1),
latestDaysAvgBytesInList: topicData[index].latestDaysAvgBytesInList, latestDaysAvgBytesInList: topicData[index].latestDaysAvgBytesInList,
latestDaysMaxBytesInList: topicData[index].latestDaysMaxBytesInList, latestDaysMaxBytesInList: topicData[index].latestDaysMaxBytesInList,
partitionPlanList: taskPlanData[index].partitionPlanList, partitionPlanList: taskPlanData[index].partitionPlanList,
@@ -476,6 +494,19 @@ export default (props: DefaultConfig) => {
setTopicSelectValue(v); setTopicSelectValue(v);
}} }}
options={topicMetaData} options={topicMetaData}
// 点击Tooltip会触发Select的下拉
// maxTagPlaceholder={(v) => {
// const tooltipValue = v
// .map((item) => {
// return item.value;
// })
// .join('、');
// return (
// <Tooltip visible={true} placement="topLeft" key={tooltipValue} title={tooltipValue}>
// <span>{'+' + v.length + '...'}</span>
// </Tooltip>
// );
// }}
></Select> ></Select>
</Form.Item> </Form.Item>
</Col> </Col>

View File

@@ -64,11 +64,6 @@
.task-form { .task-form {
margin-top: 16px; margin-top: 16px;
} }
.dcloud-select-selector {
max-height: 100px;
overflow: scroll;
}
} }
.preview-task-plan-drawer { .preview-task-plan-drawer {
@@ -80,4 +75,18 @@
background: #F8F9FA; background: #F8F9FA;
} }
} }
}
.move-dete-time-tanges{
.dcloud-input-number-input-wrap{
width: 80px;
}
.dcloud-input-number-wrapper{
.dcloud-select-selector{
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
background-color: inherit !important;
background: #F8F9FA;
}
}
} }

View File

@@ -1,29 +1,19 @@
import moment from 'moment'; import moment from 'moment';
export const CHART_COLOR_LIST = [ export const CHART_COLOR_LIST = [
'#657DFC', '#556ee6',
'#A7B1EB',
'#2AC8E4',
'#9DDEEB',
'#3991FF',
'#94BEF2', '#94BEF2',
'#95e7ff',
'#9DDEEB',
'#A7B1EB',
'#C2D0E3', '#C2D0E3',
'#F5B6B3',
'#85C80D',
'#C9E795',
'#A76CEC',
'#CCABF1', '#CCABF1',
'#FF9C1B',
'#F5C993',
'#FFC300',
'#F9D77B', '#F9D77B',
'#12CA7A', '#F5C993',
'#8BA3C4',
'#FF7066',
'#A7E6C7', '#A7E6C7',
'#F19FC9', '#F19FC9',
'#AEAEAE', '#F5B6B3',
'#D1D1D1', '#C9E795',
]; ];
export const UNIT_MAP = { export const UNIT_MAP = {

View File

@@ -12,20 +12,6 @@ export const leftMenus = (clusterId?: string) => ({
name: 'cluster', name: 'cluster',
path: 'cluster', path: 'cluster',
icon: 'icon-Cluster', icon: 'icon-Cluster',
children: [
{
name: 'overview',
path: '',
icon: '#icon-luoji',
},
process.env.BUSINESS_VERSION
? {
name: 'balance',
path: 'balance',
icon: '#icon-luoji',
}
: undefined,
].filter((m) => m),
}, },
{ {
name: 'broker', name: 'broker',
@@ -83,6 +69,25 @@ export const leftMenus = (clusterId?: string) => ({
// }, // },
// ], // ],
}, },
{
name: 'operation',
path: 'operation',
icon: 'icon-Jobs',
children: [
process.env.BUSINESS_VERSION
? {
name: 'balance',
path: 'balance',
icon: '#icon-luoji',
}
: undefined,
{
name: 'jobs',
path: 'jobs',
icon: 'icon-Jobs',
},
].filter((m) => m),
},
process.env.BUSINESS_VERSION process.env.BUSINESS_VERSION
? { ? {
name: 'produce-consume', name: 'produce-consume',
@@ -127,11 +132,6 @@ export const leftMenus = (clusterId?: string) => ({
// path: 'acls', // path: 'acls',
// icon: 'icon-wodegongzuotai', // icon: 'icon-wodegongzuotai',
// }, // },
{
name: 'jobs',
path: 'jobs',
icon: 'icon-Jobs',
},
].filter((m) => m), ].filter((m) => m),
}); });

View File

@@ -258,3 +258,25 @@ li {
} }
} }
} }
.empty-panel {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-bottom: 18px;
.img {
width: 51px;
height: 34px;
margin-bottom: 7px;
background-size: cover;
background-image: url('./assets/empty.png');
}
.text {
font-size: 10px;
color: #919aac;
line-height: 20px;
}
}

View File

@@ -44,6 +44,10 @@ export default {
[`menu.${systemKey}.consumer-group.operating-state`]: 'Operating State', [`menu.${systemKey}.consumer-group.operating-state`]: 'Operating State',
[`menu.${systemKey}.consumer-group.group-list`]: 'GroupList', [`menu.${systemKey}.consumer-group.group-list`]: 'GroupList',
[`menu.${systemKey}.operation`]: 'Operation',
[`menu.${systemKey}.operation.balance`]: 'Load Rebalance',
[`menu.${systemKey}.operation.jobs`]: 'Job',
[`menu.${systemKey}.acls`]: 'ACLs', [`menu.${systemKey}.acls`]: 'ACLs',
[`menu.${systemKey}.jobs`]: 'Job', [`menu.${systemKey}.jobs`]: 'Job',

View File

@@ -1,4 +1,4 @@
import React from 'react'; import React, { useEffect } from 'react';
import { Drawer, Form, Input, Space, Button, Checkbox, Utils, Row, Col, IconFont, Divider, message } from 'knowdesign'; import { Drawer, Form, Input, Space, Button, Checkbox, Utils, Row, Col, IconFont, Divider, message } from 'knowdesign';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import Api from '@src/api'; import Api from '@src/api';
@@ -31,6 +31,10 @@ export const ConfigurationEdit = (props: any) => {
}); });
}; };
React.useEffect(() => {
form.setFieldsValue(props.record);
}, [props.record]);
return ( return (
<Drawer <Drawer
title={ title={
@@ -44,6 +48,7 @@ export const ConfigurationEdit = (props: any) => {
visible={props.visible} visible={props.visible}
onClose={() => props.setVisible(false)} onClose={() => props.setVisible(false)}
maskClosable={false} maskClosable={false}
destroyOnClose
extra={ extra={
<Space> <Space>
<Button size="small" onClick={onClose}> <Button size="small" onClick={onClose}>
@@ -70,7 +75,7 @@ export const ConfigurationEdit = (props: any) => {
{props.record?.documentation || '-'} {props.record?.documentation || '-'}
</Col> </Col>
</Row> </Row>
<Form form={form} layout="vertical" initialValues={props.record}> <Form form={form} layout="vertical">
<Form.Item name="defaultValue" label="Kafka默认配置"> <Form.Item name="defaultValue" label="Kafka默认配置">
<Input disabled /> <Input disabled />
</Form.Item> </Form.Item>

View File

@@ -14,19 +14,49 @@ export const getBrokerListColumns = (arg?: any) => {
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
render: (t: number, r: any) => { render: (t: number, r: any) => {
return r?.alive ? ( return r?.alive ? (
<a <>
onClick={() => { <a
window.location.hash = `brokerId=${t || t === 0 ? t : ''}&host=${r.host || ''}`; onClick={() => {
}} window.location.hash = `brokerId=${t || t === 0 ? t : ''}&host=${r.host || ''}`;
> }}
{t} >
</a> {t}
</a>
{r?.kafkaRoleList?.includes('controller') && (
<Tag
style={{
color: '#556EE6',
padding: '2px 5px',
background: '#eff1fd',
marginLeft: '4px',
transform: 'scale(0.83,0.83)',
}}
>
Controller
</Tag>
)}
</>
) : ( ) : (
<span>{t}</span> <>
<span>{t}</span>
{r?.kafkaRoleList?.includes('controller') && (
<Tag
style={{
color: '#556EE6',
padding: '2px 5px',
background: '#eff1fd',
marginLeft: '4px',
transform: 'scale(0.83,0.83)',
}}
>
Controller
</Tag>
)}
</>
); );
}, },
fixed: 'left', fixed: 'left',
width: 120, width: 150,
}, },
// { // {
// title: 'Rack', // title: 'Rack',

View File

@@ -6,11 +6,16 @@ import { goLogin } from '@src/constants/axiosConfig';
// 权限对应表 // 权限对应表
export enum ClustersPermissionMap { export enum ClustersPermissionMap {
CLUSTERS_MANAGE = '多集群管理', CLUSTERS_MANAGE = '多集群管理',
CLUSTERS_MANAGE_VIEW = '多集群管理查看',
// Cluster // Cluster
CLUSTER_ADD = '接入集群', CLUSTER_ADD = '接入集群',
CLUSTER_DEL = '删除集群', CLUSTER_DEL = '删除集群',
CLUSTER_CHANGE_HEALTHY = 'Cluster-修改健康规则', CLUSTER_CHANGE_HEALTHY = 'Cluster-修改健康规则',
CLUSTER_CHANGE_INFO = 'Cluster-修改集群信息', CLUSTER_CHANGE_INFO = 'Cluster-修改集群信息',
// LoadReBalance
REBALANCE_CYCLE = 'Cluster-LoadReBalance-周期均衡',
REBALANCE_IMMEDIATE = 'Cluster-LoadReBalance-立即均衡',
REBALANCE_SETTING = 'Cluster-LoadReBalance-设置集群规格',
// Broker // Broker
BROKER_CHANGE_CONFIG = 'Broker-修改Broker配置', BROKER_CHANGE_CONFIG = 'Broker-修改Broker配置',
// Topic // Topic
@@ -19,6 +24,8 @@ export enum ClustersPermissionMap {
TOPIC_DEL = 'Topic-删除Topic', TOPIC_DEL = 'Topic-删除Topic',
TOPIC_EXPOND = 'Topic-扩分区', TOPIC_EXPOND = 'Topic-扩分区',
TOPIC_ADD = 'Topic-新增Topic', TOPIC_ADD = 'Topic-新增Topic',
TOPIC_MOVE_REPLICA = 'Topic-迁移副本',
TOPIC_CHANGE_REPLICA = 'Topic-扩缩副本',
// Consumers // Consumers
CONSUMERS_RESET_OFFSET = 'Consumers-重置Offset', CONSUMERS_RESET_OFFSET = 'Consumers-重置Offset',
// Test // Test

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Button, DatePicker, Drawer, Form, notification, Radio, Utils, Space, Divider } from 'knowdesign'; import { Button, DatePicker, Drawer, Form, notification, Radio, Utils, Space, Divider, message } from 'knowdesign';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import EditTable from '../TestingProduce/component/EditTable'; import EditTable from '../TestingProduce/component/EditTable';
import Api from '@src/api/index'; import Api from '@src/api/index';
@@ -53,11 +53,28 @@ export default (props: any) => {
const [resetOffsetVisible, setResetOffsetVisible] = useState(false); const [resetOffsetVisible, setResetOffsetVisible] = useState(false);
const customFormRef: any = React.createRef(); const customFormRef: any = React.createRef();
const clusterPhyId = Number(routeParams.clusterId); const clusterPhyId = Number(routeParams.clusterId);
const [partitionIdList, setPartitionIdList] = useState([]);
useEffect(() => { useEffect(() => {
form.setFieldsValue({ form.setFieldsValue({
resetType: defaultResetType, resetType: defaultResetType,
}); });
}, []); }, []);
useEffect(() => {
Utils.request(Api.getTopicsMetaData(record?.topicName, +routeParams.clusterId))
.then((res: any) => {
const partitionLists = (res?.partitionIdList || []).map((item: any) => {
return {
label: item,
value: item,
};
});
setPartitionIdList(partitionLists);
})
.catch((err) => {
message.error(err);
});
}, []);
const confirm = () => { const confirm = () => {
let tableData; let tableData;
if (customFormRef.current) { if (customFormRef.current) {
@@ -160,8 +177,9 @@ export default (props: any) => {
colCustomConfigs={[ colCustomConfigs={[
{ {
title: 'PartitionID', title: 'PartitionID',
inputType: 'number', inputType: 'select',
placeholder: '请输入Partition', placeholder: '请输入Partition',
options: partitionIdList,
}, },
{ {
title: 'Offset', title: 'Offset',

View File

@@ -30,7 +30,7 @@ const AutoPage = (props: any) => {
const searchFn = () => { const searchFn = () => {
const params: getOperatingStateListParams = { const params: getOperatingStateListParams = {
pageNo: pageIndex, pageNo: 1,
pageSize, pageSize,
fuzzySearchDTOList: [], fuzzySearchDTOList: [],
}; };

View File

@@ -61,9 +61,11 @@ const columns: any = [
const totalSize = r.totalSize ? Number(Utils.formatAssignSize(t, 'MB')) : 0; const totalSize = r.totalSize ? Number(Utils.formatAssignSize(t, 'MB')) : 0;
return ( return (
<div className="message-size"> <div className="message-size">
<Tooltip title={(movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0) + '%'}> <Tooltip
title={(movedSize === 0 && totalSize === 0 ? 100 : movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0) + '%'}
>
<Progress <Progress
percent={movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0} percent={movedSize === 0 && totalSize === 0 ? 100 : movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0}
strokeColor="#556EE6" strokeColor="#556EE6"
trailColor="#ECECF1" trailColor="#ECECF1"
showInfo={false} showInfo={false}

View File

@@ -237,12 +237,12 @@ const RebalancePlan = (props: PropsType) => {
<Descriptions.Item labelStyle={{ width: '100px' }} label="迁移副本数"> <Descriptions.Item labelStyle={{ width: '100px' }} label="迁移副本数">
{data?.replicas || '-'} {data?.replicas || '-'}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="均衡阈值"> <Descriptions.Item label="均衡区间">
{data?.clusterBalanceIntervalList {data?.clusterBalanceIntervalList
? data?.clusterBalanceIntervalList?.map((item: any) => { ? data?.clusterBalanceIntervalList?.map((item: any) => {
return ( return (
<Tag style={{ padding: '4px 8px', backgroundColor: 'rgba(33,37,41,0.08)', marginRight: '4px' }} key={item?.priority}> <Tag style={{ padding: '4px 5px', backgroundColor: 'rgba(33,37,41,0.08)', marginRight: '4px' }} key={item?.priority}>
{item.type + ':' + item.intervalPercent + '%'} {item.type?.slice(0, 1).toUpperCase() + item.type?.slice(1) + ':' + ' ±' + item.intervalPercent + '%'}
</Tag> </Tag>
); );
}) })

View File

@@ -314,9 +314,13 @@ export const getTaskDetailsColumns = (arg?: any) => {
const totalSize = r.totalSize ? Number(Utils.formatAssignSize(t, 'MB')) : 0; const totalSize = r.totalSize ? Number(Utils.formatAssignSize(t, 'MB')) : 0;
return ( return (
<div className="message-size"> <div className="message-size">
<Tooltip title={(movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0) + '%'}> <Tooltip
title={
(r.success === r.total && r.total > 0 ? 100 : movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0) + '%'
}
>
<Progress <Progress
percent={movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0} percent={r.success === r.total && r.total > 0 ? 100 : movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0}
strokeColor="#556EE6" strokeColor="#556EE6"
showInfo={false} showInfo={false}
/> />
@@ -438,9 +442,13 @@ export const getMoveBalanceColumns = (arg?: any) => {
const totalSize = r.totalSize ? Number(Utils.formatAssignSize(t, 'MB')) : 0; const totalSize = r.totalSize ? Number(Utils.formatAssignSize(t, 'MB')) : 0;
return ( return (
<div className="message-size"> <div className="message-size">
<Tooltip title={(movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0) + '%'}> <Tooltip
title={
(r.success === r.total && r.total > 0 ? 100 : movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0) + '%'
}
>
<Progress <Progress
percent={movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0} percent={r.success === r.total && r.total > 0 ? 100 : movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0}
strokeColor="#556EE6" strokeColor="#556EE6"
showInfo={false} showInfo={false}
/> />

View File

@@ -209,7 +209,7 @@ const JobsList: React.FC = (props: any) => {
tableProps={{ tableProps={{
tableId: 'jobs_list', tableId: 'jobs_list',
showHeader: false, showHeader: false,
rowKey: 'jobs_list', rowKey: 'id',
loading: loading, loading: loading,
columns: getJobsListColumns({ onDelete, setViewProgress }), columns: getJobsListColumns({ onDelete, setViewProgress }),
dataSource: data, dataSource: data,

View File

@@ -168,7 +168,6 @@ const BalanceDrawer: React.FC<PropsType> = ({ onClose, visible, isCycle = false,
const init = () => { const init = () => {
if (formData && Object.keys(formData).length > 0) { if (formData && Object.keys(formData).length > 0) {
console.log(formData, '有FormData');
const tableData = formData?.clusterBalanceIntervalList?.map((item: any) => { const tableData = formData?.clusterBalanceIntervalList?.map((item: any) => {
const finfIndex = BalancedDimensions.findIndex((item1) => item1?.value === item?.type); const finfIndex = BalancedDimensions.findIndex((item1) => item1?.value === item?.type);
return { return {
@@ -201,7 +200,6 @@ const BalanceDrawer: React.FC<PropsType> = ({ onClose, visible, isCycle = false,
priority: index + 1, priority: index + 1,
}; };
}); });
console.log(res, '表单回显立即均衡');
setTableData(res); setTableData(res);
setDimension(['disk', 'bytesIn', 'bytesOut']); setDimension(['disk', 'bytesIn', 'bytesOut']);
setNodeTargetKeys([]); setNodeTargetKeys([]);
@@ -220,14 +218,12 @@ const BalanceDrawer: React.FC<PropsType> = ({ onClose, visible, isCycle = false,
throttleUnitB: values?.throttleUnitM * 1024 * 1024, throttleUnitB: values?.throttleUnitM * 1024 * 1024,
}; };
if (!isCycle) { if (values?.priority === 'throughput') {
if (values?.priority === 'throughput') { params.parallelNum = 0;
params.parallelNum = 0; params.executionStrategy = 1;
params.executionStrategy = 1; } else if (values?.priority === 'stability') {
} else if (values?.priority === 'stability') { params.parallelNum = 1;
params.parallelNum = 1; params.executionStrategy = 2;
params.executionStrategy = 2;
}
} }
if (formData?.jobId) { if (formData?.jobId) {
@@ -382,6 +378,8 @@ const BalanceDrawer: React.FC<PropsType> = ({ onClose, visible, isCycle = false,
const drawerClose = (isArg?: boolean) => { const drawerClose = (isArg?: boolean) => {
isArg ? onClose(isArg) : onClose(); isArg ? onClose(isArg) : onClose();
setParallelNum(0);
setExecutionStrategy(1);
form.resetFields(); form.resetFields();
}; };
@@ -540,17 +538,38 @@ const BalanceDrawer: React.FC<PropsType> = ({ onClose, visible, isCycle = false,
</Form.Item> </Form.Item>
<h6 className="form-title"></h6> <h6 className="form-title"></h6>
{!isCycle && ( {isCycle && (
<Form.Item label="" name="priority" rules={[{ required: true, message: 'Principle 不能为空' }]} initialValue="throughput"> <Form.Item
<Radio.Group onChange={priorityChange}> className="schedule-cron"
<Radio value="throughput"></Radio> name="scheduleCron"
<Radio value="stability"></Radio> label="任务周期"
<Radio value="custom"></Radio> rules={[
</Radio.Group> {
required: true,
message: `请输入!`,
},
{
validator: (_, value) => {
const valArr = value.split(' ');
if (valArr[1] === '*' || valArr[2] === '*') {
return Promise.reject(new Error('任务周期必须指定分钟、小时'));
}
return Promise.resolve();
},
},
]}
>
<CronInput />
</Form.Item> </Form.Item>
)} )}
<Form.Item label="" name="priority" rules={[{ required: true, message: 'Principle 不能为空' }]} initialValue="throughput">
{!isCycle && ( <Radio.Group onChange={priorityChange}>
<Radio value="throughput"></Radio>
<Radio value="stability"></Radio>
<Radio value="custom"></Radio>
</Radio.Group>
</Form.Item>
{
<Form.Item dependencies={['priority']} style={{ marginBottom: 0 }}> <Form.Item dependencies={['priority']} style={{ marginBottom: 0 }}>
{({ getFieldValue }) => {({ getFieldValue }) =>
getFieldValue('priority') === 'custom' ? ( getFieldValue('priority') === 'custom' ? (
@@ -600,9 +619,9 @@ const BalanceDrawer: React.FC<PropsType> = ({ onClose, visible, isCycle = false,
) : null ) : null
} }
</Form.Item> </Form.Item>
)} }
{isCycle && ( {/* {isCycle && (
<Form.Item <Form.Item
name="parallelNum" name="parallelNum"
label={ label={
@@ -622,9 +641,9 @@ const BalanceDrawer: React.FC<PropsType> = ({ onClose, visible, isCycle = false,
> >
<InputNumber min={0} max={999} placeholder="请输入任务并行度" style={{ width: '100%' }} /> <InputNumber min={0} max={999} placeholder="请输入任务并行度" style={{ width: '100%' }} />
</Form.Item> </Form.Item>
)} )} */}
{isCycle && ( {/* {isCycle && (
<Form.Item <Form.Item
className="schedule-cron" className="schedule-cron"
name="scheduleCron" name="scheduleCron"
@@ -647,9 +666,9 @@ const BalanceDrawer: React.FC<PropsType> = ({ onClose, visible, isCycle = false,
> >
<CronInput /> <CronInput />
</Form.Item> </Form.Item>
)} )} */}
{isCycle && ( {/* {isCycle && (
<Form.Item <Form.Item
name="executionStrategy" name="executionStrategy"
label={ label={
@@ -672,7 +691,7 @@ const BalanceDrawer: React.FC<PropsType> = ({ onClose, visible, isCycle = false,
<Radio value={2}>优先最小副本</Radio> <Radio value={2}>优先最小副本</Radio>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
)} )} */}
<Form.Item <Form.Item
name="throttleUnitM" name="throttleUnitM"

View File

@@ -45,24 +45,57 @@ const HistoryDrawer: React.FC<PropsType> = ({ onClose, visible }) => {
// } // }
// }, // },
{ {
title: 'Disk均衡率', title: (
<span>
Disk<span style={{ fontSize: '12px', color: '#74788D' }}>{'(已均衡丨未均衡)'}</span>
</span>
),
dataIndex: 'disk', dataIndex: 'disk',
render: (text: any, row: any) => { render: (text: any, row: any) => {
return `${row?.sub?.disk?.successNu} (已均衡) / ${row?.sub?.disk?.failedNu} (未均衡)`; // return `${row?.sub?.disk?.successNu} 丨 ${row?.sub?.disk?.failedNu}`;
return (
<div className="balance-history-column">
<span>{row?.sub?.disk?.successNu}</span>
<span></span>
<span>{row?.sub?.disk?.failedNu}</span>
</div>
);
}, },
}, },
{ {
title: 'BytesIn均衡率', title: (
<span>
BytesIn<span style={{ fontSize: '12px', color: '#74788D' }}>{'(已均衡丨未均衡)'}</span>
</span>
),
dataIndex: 'bytesIn', dataIndex: 'bytesIn',
render: (text: any, row: any) => { render: (text: any, row: any) => {
return `${row?.sub?.bytesIn?.successNu} (已均衡) / ${row?.sub?.bytesIn?.failedNu} (未均衡)`; // return `${row?.sub?.bytesIn?.successNu} 丨 ${row?.sub?.bytesIn?.failedNu}`;
return (
<div className="balance-history-column">
<span>{row?.sub?.bytesIn?.successNu}</span>
<span></span>
<span>{row?.sub?.bytesIn?.failedNu}</span>
</div>
);
}, },
}, },
{ {
title: 'BytesOut均衡率', title: (
<span>
BytesOut<span style={{ fontSize: '12px', color: '#74788D' }}>{'(已均衡丨未均衡)'}</span>
</span>
),
dataIndex: 'bytesOut', dataIndex: 'bytesOut',
render: (text: any, row: any) => { render: (text: any, row: any) => {
return `${row?.sub?.bytesOut?.successNu} (已均衡) / ${row?.sub?.bytesOut?.failedNu} (未均衡)`; // return `${row?.sub?.bytesOut?.successNu} 丨 ${row?.sub?.bytesOut?.failedNu}`;
return (
<div className="balance-history-column">
<span>{row?.sub?.bytesOut?.successNu}</span>
<span></span>
<span>{row?.sub?.bytesOut?.failedNu}</span>
</div>
);
}, },
}, },
{ {
@@ -124,7 +157,7 @@ const HistoryDrawer: React.FC<PropsType> = ({ onClose, visible }) => {
}; };
const onTableChange = (curPagination: any) => { const onTableChange = (curPagination: any) => {
getList({ page: curPagination.current, size: curPagination.pageSize }); getList({ pageNo: curPagination.current, pageSize: curPagination.pageSize });
}; };
return ( return (

View File

@@ -143,3 +143,19 @@
// margin: 0 !important; // margin: 0 !important;
// } // }
} }
.balance-history-column{
display: flex;
&>span:nth-child(1){
width: 20px;
}
&>span:nth-child(2){
color: #74788d;
font-size: 12px;
opacity: 0.3;
}
&>span:last-child{
width: 20px;
margin-left: 8px;
}
}

View File

@@ -8,6 +8,7 @@ import api from '../../api';
import './index.less'; import './index.less';
import LoadRebalanceCardBar from '@src/components/CardBar/LoadRebalanceCardBar'; import LoadRebalanceCardBar from '@src/components/CardBar/LoadRebalanceCardBar';
import { BalanceFilter } from './BalanceFilter'; import { BalanceFilter } from './BalanceFilter';
import { ClustersPermissionMap } from '../CommonConfig';
const Balance_Status_OPTIONS = [ const Balance_Status_OPTIONS = [
{ {
@@ -288,21 +289,17 @@ const LoadBalance: React.FC = (props: any) => {
setVisible(false); setVisible(false);
}; };
const balanceClick = (val: boolean = false) => { const balanceClick = (val: boolean) => {
if (val) { Utils.request(api.getBalanceForm(global?.clusterInfo?.id), {
Utils.request(api.getBalanceForm(global?.clusterInfo?.id), { method: 'GET',
method: 'GET', })
.then((res: any) => {
const dataDe = res || {};
setCircleFormData(dataDe);
}) })
.then((res: any) => { .catch(() => {
const dataDe = res || {}; setCircleFormData(null);
setCircleFormData(dataDe); });
})
.catch(() => {
setCircleFormData(null);
});
} else {
setCircleFormData(null);
}
setIsCycle(val); setIsCycle(val);
setVisible(true); setVisible(true);
}; };
@@ -365,19 +362,23 @@ const LoadBalance: React.FC = (props: any) => {
value: searchValue, value: searchValue,
onChange: setSearchValue, onChange: setSearchValue,
placeholder: '请输入 Host', placeholder: '请输入 Host',
style: { width: '210px' }, style: { width: '248px' },
maxLength: 128, maxLength: 128,
}} }}
/> />
<Button type="primary" ghost onClick={() => setPlanVisible(true)}> <Button type="primary" ghost onClick={() => setPlanVisible(true)}>
</Button> </Button>
<Button type="primary" ghost onClick={() => balanceClick(true)}> {global.hasPermission(ClustersPermissionMap.REBALANCE_CYCLE) && (
<Button type="primary" ghost onClick={() => balanceClick(true)}>
</Button>
<Button type="primary" onClick={() => balanceClick(false)}> </Button>
)}
</Button> {global.hasPermission(ClustersPermissionMap.REBALANCE_IMMEDIATE) && (
<Button type="primary" onClick={() => balanceClick(false)}>
</Button>
)}
</div> </div>
</div> </div>
{filterList && filterList.length > 0 && ( {filterList && filterList.length > 0 && (

View File

@@ -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>4K</span> <span>5K</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">

View File

@@ -1,8 +1,8 @@
import { Button, Divider, Drawer, Form, Input, InputNumber, message, Radio, Select, Spin, Space, Utils } from 'knowdesign'; import { Button, Divider, Drawer, Form, Input, InputNumber, message, Radio, Select, Spin, Space, Utils } from 'knowdesign';
import * as React from 'react'; import * as React from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import api from '../../api'; import api from '@src/api';
import { regClusterName, regUsername } from '../../constants/reg'; import { regClusterName, 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';
@@ -21,40 +21,28 @@ word=\\"xxxxxx\\";"
`; `;
const AccessClusters = (props: any): JSX.Element => { const AccessClusters = (props: any): JSX.Element => {
const { afterSubmitSuccess, clusterInfo, visible } = props;
const intl = useIntl(); const intl = useIntl();
const [form] = Form.useForm(); const [form] = Form.useForm();
const { afterSubmitSuccess, infoLoading, clusterInfo, visible } = props;
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const [security, setSecurity] = React.useState(clusterInfo?.security || 'None'); const [curClusterInfo, setCurClusterInfo] = React.useState<any>({});
const [security, setSecurity] = React.useState(curClusterInfo?.security || 'None');
const [extra, setExtra] = React.useState({ const [extra, setExtra] = React.useState({
versionExtra: '', versionExtra: '',
zooKeeperExtra: '', zooKeeperExtra: '',
bootstrapExtra: '', bootstrapExtra: '',
jmxExtra: '', jmxExtra: '',
}); });
const [isLowVersion, setIsLowVersion] = React.useState<any>(false); const [isLowVersion, setIsLowVersion] = React.useState<boolean>(false);
const [zookeeperErrorStatus, setZookeeperErrorStatus] = React.useState<any>(false); const [zookeeperErrorStatus, setZookeeperErrorStatus] = React.useState<boolean>(false);
const lastFormItemValue = React.useRef({ const lastFormItemValue = React.useRef({
bootstrap: clusterInfo?.bootstrapServers || '', bootstrap: curClusterInfo?.bootstrapServers || '',
zookeeper: clusterInfo?.zookeeper || '', zookeeper: curClusterInfo?.zookeeper || '',
clientProperties: clusterInfo?.clientProperties || {}, clientProperties: curClusterInfo?.clientProperties || {},
}); });
React.useEffect(() => {
const showLowVersion = !(clusterInfo?.zookeeper || !clusterInfo?.kafkaVersion || clusterInfo?.kafkaVersion >= lowKafkaVersion);
lastFormItemValue.current.bootstrap = clusterInfo?.bootstrapServers || '';
lastFormItemValue.current.zookeeper = clusterInfo?.zookeeper || '';
lastFormItemValue.current.clientProperties = clusterInfo?.clientProperties || {};
setIsLowVersion(showLowVersion);
setExtra({
...extra,
versionExtra: showLowVersion ? intl.formatMessage({ id: 'access.cluster.low.version.tip' }) : '',
});
form.setFieldsValue({ ...clusterInfo });
}, [clusterInfo]);
const onHandleValuesChange = (value: any, allValues: any) => { const onHandleValuesChange = (value: any, allValues: any) => {
Object.keys(value).forEach((key) => { Object.keys(value).forEach((key) => {
switch (key) { switch (key) {
@@ -128,10 +116,10 @@ const AccessClusters = (props: any): JSX.Element => {
zookeeper: res.zookeeper || '', zookeeper: res.zookeeper || '',
}; };
setLoading(true); setLoading(true);
if (!isNaN(clusterInfo?.id)) { if (!isNaN(curClusterInfo?.id)) {
Utils.put(api.phyCluster, { Utils.put(api.phyCluster, {
...params, ...params,
id: clusterInfo?.id, id: curClusterInfo?.id,
}) })
.then(() => { .then(() => {
message.success('编辑成功'); message.success('编辑成功');
@@ -219,7 +207,11 @@ const AccessClusters = (props: any): JSX.Element => {
}); });
// 如果kafkaVersion小于最低版本则提示 // 如果kafkaVersion小于最低版本则提示
const showLowVersion = !(clusterInfo?.zookeeper || !clusterInfo?.kafkaVersion || clusterInfo?.kafkaVersion >= lowKafkaVersion); const showLowVersion = !(
curClusterInfo?.zookeeper ||
!curClusterInfo?.kafkaVersion ||
curClusterInfo?.kafkaVersion >= lowKafkaVersion
);
setIsLowVersion(showLowVersion); setIsLowVersion(showLowVersion);
setExtra({ setExtra({
...extraMsg, ...extraMsg,
@@ -232,6 +224,55 @@ const AccessClusters = (props: any): JSX.Element => {
}); });
}; };
React.useEffect(() => {
const showLowVersion = !(curClusterInfo?.zookeeper || !curClusterInfo?.kafkaVersion || curClusterInfo?.kafkaVersion >= lowKafkaVersion);
lastFormItemValue.current = {
bootstrap: curClusterInfo?.bootstrapServers || '',
zookeeper: curClusterInfo?.zookeeper || '',
clientProperties: curClusterInfo?.clientProperties || {},
};
setIsLowVersion(showLowVersion);
setExtra({
...extra,
versionExtra: showLowVersion ? intl.formatMessage({ id: 'access.cluster.low.version.tip' }) : '',
});
form.setFieldsValue({ ...curClusterInfo });
}, [curClusterInfo]);
React.useEffect(() => {
if (visible) {
if (clusterInfo?.id) {
setLoading(true);
Utils.request(api.getPhyClusterBasic(clusterInfo.id))
.then((res: any) => {
let jmxProperties = null;
try {
jmxProperties = JSON.parse(res?.jmxProperties);
} catch (err) {
console.error(err);
}
// 转化值对应成表单值
if (jmxProperties?.openSSL) {
jmxProperties.security = 'Password';
}
if (jmxProperties) {
res = Object.assign({}, res || {}, jmxProperties);
}
setCurClusterInfo(res);
setLoading(false);
})
.catch((err) => {
setCurClusterInfo(clusterInfo);
setLoading(false);
});
} else {
setCurClusterInfo(clusterInfo);
}
}
}, [visible, clusterInfo]);
return ( return (
<> <>
<Drawer <Drawer
@@ -256,16 +297,8 @@ const AccessClusters = (props: any): JSX.Element => {
placement="right" placement="right"
width={480} width={480}
> >
<Spin spinning={loading || !!infoLoading}> <Spin spinning={loading}>
<Form <Form form={form} layout="vertical" onValuesChange={onHandleValuesChange}>
form={form}
initialValues={{
security,
...clusterInfo,
}}
layout="vertical"
onValuesChange={onHandleValuesChange}
>
<Form.Item <Form.Item
name="name" name="name"
label="集群名称" label="集群名称"
@@ -277,11 +310,9 @@ const AccessClusters = (props: any): JSX.Element => {
if (!value) { if (!value) {
return Promise.reject('集群名称不能为空'); return Promise.reject('集群名称不能为空');
} }
if (value === curClusterInfo?.name) {
if (value === clusterInfo?.name) {
return Promise.resolve(); return Promise.resolve();
} }
if (value?.length > 128) { if (value?.length > 128) {
return Promise.reject('集群名称长度限制在1128字符'); return Promise.reject('集群名称长度限制在1128字符');
} }
@@ -307,13 +338,7 @@ const AccessClusters = (props: any): JSX.Element => {
<Form.Item <Form.Item
name="bootstrapServers" name="bootstrapServers"
label="Bootstrap Servers" label="Bootstrap Servers"
extra={ extra={<span className={extra.bootstrapExtra.includes('连接成功') ? 'error-extra-info' : ''}>{extra.bootstrapExtra}</span>}
extra.bootstrapExtra.includes('连接成功') ? (
<span>{extra.bootstrapExtra}</span>
) : (
<span className="error-extra-info">{extra.bootstrapExtra}</span>
)
}
validateTrigger={'onBlur'} validateTrigger={'onBlur'}
rules={[ rules={[
{ {
@@ -349,13 +374,7 @@ const AccessClusters = (props: any): JSX.Element => {
<Form.Item <Form.Item
name="zookeeper" name="zookeeper"
label="Zookeeper" label="Zookeeper"
extra={ extra={<span className={extra.zooKeeperExtra.includes('连接成功') ? 'error-extra-info' : ''}>{extra.zooKeeperExtra}</span>}
extra.zooKeeperExtra.includes('连接成功') ? (
<span>{extra.zooKeeperExtra}</span>
) : (
<span className="error-extra-info">{extra.zooKeeperExtra}</span>
)
}
validateStatus={zookeeperErrorStatus ? 'error' : 'success'} validateStatus={zookeeperErrorStatus ? 'error' : 'success'}
validateTrigger={'onBlur'} validateTrigger={'onBlur'}
rules={[ rules={[
@@ -458,7 +477,7 @@ const AccessClusters = (props: any): JSX.Element => {
style={{ width: '58%' }} style={{ width: '58%' }}
rules={[ rules={[
{ {
required: security === 'Password' || clusterInfo?.security === 'Password', required: security === 'Password' || curClusterInfo?.security === 'Password',
validator: async (rule: any, value: string) => { validator: async (rule: any, value: string) => {
if (!value) { if (!value) {
return Promise.reject('用户名不能为空'); return Promise.reject('用户名不能为空');
@@ -483,7 +502,7 @@ const AccessClusters = (props: any): JSX.Element => {
style={{ width: '38%', marginRight: 0 }} style={{ width: '38%', marginRight: 0 }}
rules={[ rules={[
{ {
required: security === 'Password' || clusterInfo?.security === 'Password', required: security === 'Password' || curClusterInfo?.security === 'Password',
validator: async (rule: any, value: string) => { validator: async (rule: any, value: string) => {
if (!value) { if (!value) {
return Promise.reject('密码不能为空'); return Promise.reject('密码不能为空');

View File

@@ -1,102 +1,108 @@
import { DoubleRightOutlined } from '@ant-design/icons'; import { DoubleRightOutlined } from '@ant-design/icons';
import { Checkbox } from 'knowdesign'; import { Checkbox } from 'knowdesign';
import { CheckboxValueType } from 'knowdesign/es/basic/checkbox/Group';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import React, { useEffect } from 'react'; import React, { useEffect, useState } from 'react';
const CheckboxGroup = Checkbox.Group; const CheckboxGroup = Checkbox.Group;
interface IVersion {
firstLine: string[];
leftVersions: string[];
}
const CustomCheckGroup = (props: { kafkaVersions: string[]; onChangeCheckGroup: any }) => { const CustomCheckGroup = (props: { kafkaVersions: string[]; onChangeCheckGroup: any }) => {
const { kafkaVersions, onChangeCheckGroup } = props; const { kafkaVersions: newVersions, onChangeCheckGroup } = props;
const [checkedKafkaVersion, setCheckedKafkaVersion] = React.useState<IVersion>({ const [versions, setVersions] = React.useState<string[]>([]);
firstLine: [], const [versionsState, setVersionsState] = React.useState<{
leftVersions: [], [key: string]: boolean;
}); }>({});
const [allVersion, setAllVersion] = React.useState<IVersion>({
firstLine: [],
leftVersions: [],
});
const [indeterminate, setIndeterminate] = React.useState(false); const [indeterminate, setIndeterminate] = React.useState(false);
const [checkAll, setCheckAll] = React.useState(true); const [checkAll, setCheckAll] = React.useState(true);
const [moreGroupWidth, setMoreGroupWidth] = React.useState(400); const [groupInfo, setGroupInfo] = useState({
width: 400,
num: 0,
});
const [showMore, setShowMore] = React.useState(false); const [showMore, setShowMore] = React.useState(false);
useEffect(() => {
document.addEventListener('click', handleDocumentClick);
return () => {
document.removeEventListener('click', handleDocumentClick);
};
}, []);
const handleDocumentClick = (e: Event) => { const handleDocumentClick = (e: Event) => {
setShowMore(false); setShowMore(false);
}; };
const setCheckAllStauts = (list: string[], otherList: string[]) => { const updateGroupInfo = () => {
onChangeCheckGroup([...list, ...otherList]);
setIndeterminate(!!list.length && list.length + otherList.length < kafkaVersions.length);
setCheckAll(list.length + otherList.length === kafkaVersions.length);
};
const getTwoPanelVersion = () => {
const width = (document.getElementsByClassName('custom-check-group')[0] as any)?.offsetWidth; const width = (document.getElementsByClassName('custom-check-group')[0] as any)?.offsetWidth;
const checkgroupWidth = width - 100 - 86; const checkgroupWidth = width - 100 - 86;
const num = (checkgroupWidth / 108) | 0; const num = (checkgroupWidth / 108) | 0;
const firstLine = Array.from(kafkaVersions).splice(0, num); setGroupInfo({
setMoreGroupWidth(num * 108 + 88 + 66); width: num * 108 + 88 + 66,
const leftVersions = Array.from(kafkaVersions).splice(num); num,
return { firstLine, leftVersions }; });
}; };
const onFirstVersionChange = (list: []) => { const getCheckedList = (
setCheckedKafkaVersion({ versionState: {
...checkedKafkaVersion, [key: string]: boolean;
firstLine: list, },
}); filterFunc: (item: [string, boolean], i: number) => boolean
) => {
setCheckAllStauts(list, checkedKafkaVersion.leftVersions); return Object.entries(versionState)
.filter(filterFunc)
.map(([key]) => key);
}; };
const onLeftVersionChange = (list: []) => { const onVersionsChange = (isFirstLine: boolean, list: CheckboxValueType[]) => {
setCheckedKafkaVersion({ const newVersionsState = { ...versionsState };
...checkedKafkaVersion, Object.keys(newVersionsState).forEach((key, i) => {
leftVersions: list, if (isFirstLine && i < groupInfo.num) {
newVersionsState[key] = list.includes(key);
} else if (!isFirstLine && i >= groupInfo.num) {
newVersionsState[key] = list.includes(key);
}
}); });
setCheckAllStauts(list, checkedKafkaVersion.firstLine); const checkedLen = Object.values(newVersionsState).filter((v) => v).length;
setVersionsState(newVersionsState);
setIndeterminate(checkedLen && checkedLen < newVersions.length);
setCheckAll(checkedLen === newVersions.length);
onChangeCheckGroup(getCheckedList(newVersionsState, ([, state]) => state));
}; };
const onCheckAllChange = (e: any) => { const onCheckAllChange = (e: any) => {
const versions = getTwoPanelVersion(); const checked = e.target.checked;
const newVersionsState = { ...versionsState };
setCheckedKafkaVersion( Object.keys(newVersionsState).forEach((key) => (newVersionsState[key] = checked));
e.target.checked
? versions
: {
firstLine: [],
leftVersions: [],
}
);
onChangeCheckGroup(e.target.checked ? [...versions.firstLine, ...versions.leftVersions] : []);
setVersionsState(newVersionsState);
setIndeterminate(false); setIndeterminate(false);
setCheckAll(e.target.checked); setCheckAll(checked);
onChangeCheckGroup(e.target.checked ? versions : []);
}; };
React.useEffect(() => { useEffect(() => {
const handleVersionLine = () => { const newVersionsState = { ...versionsState };
const versions = getTwoPanelVersion(); Object.keys(newVersionsState).forEach((key) => {
setAllVersion(versions); if (!newVersions.includes(key)) {
setCheckedKafkaVersion(versions); delete newVersionsState[key];
}; }
handleVersionLine(); });
newVersions.forEach((version) => {
if (!Object.keys(newVersionsState).includes(version)) {
newVersionsState[version] = true;
}
});
const checkedLen = Object.values(newVersionsState).filter((v) => v).length;
window.addEventListener('resize', handleVersionLine); //监听窗口大小改变 setVersions([...newVersions]);
return () => window.removeEventListener('resize', debounce(handleVersionLine, 500)); setVersionsState(newVersionsState);
setIndeterminate(checkedLen && checkedLen < newVersions.length);
setCheckAll(checkedLen === newVersions.length);
onChangeCheckGroup(getCheckedList(newVersionsState, ([, state]) => state));
}, [newVersions]);
useEffect(() => {
updateGroupInfo();
const listen = debounce(updateGroupInfo, 500);
window.addEventListener('resize', listen); //监听窗口大小改变
document.addEventListener('click', handleDocumentClick);
return () => {
window.removeEventListener('resize', listen);
document.removeEventListener('click', handleDocumentClick);
};
}, []); }, []);
return ( return (
@@ -107,17 +113,21 @@ const CustomCheckGroup = (props: { kafkaVersions: string[]; onChangeCheckGroup:
</Checkbox> </Checkbox>
</div> </div>
<CheckboxGroup options={allVersion.firstLine} value={checkedKafkaVersion.firstLine} onChange={onFirstVersionChange} /> <CheckboxGroup
options={Array.from(versions).splice(0, groupInfo.num)}
value={getCheckedList(versionsState, ([, state], i) => i < groupInfo.num && state)}
onChange={(list) => onVersionsChange(true, list)}
/>
{showMore ? ( {showMore ? (
<CheckboxGroup <CheckboxGroup
style={{ width: moreGroupWidth }} style={{ width: groupInfo.width }}
className="more-check-group" className="more-check-group"
options={allVersion.leftVersions} options={Array.from(versions).splice(groupInfo.num)}
value={checkedKafkaVersion.leftVersions} value={getCheckedList(versionsState, ([, state], i) => i >= groupInfo.num && state)}
onChange={onLeftVersionChange} onChange={(list) => onVersionsChange(false, list)}
/> />
) : null} ) : null}
{allVersion.leftVersions.length ? ( {versions.length > groupInfo.num ? (
<div className="more-btn" onClick={() => setShowMore(!showMore)}> <div className="more-btn" onClick={() => setShowMore(!showMore)}>
<a> <a>
{!showMore ? '展开更多' : '收起更多'} <DoubleRightOutlined style={{ transform: `rotate(${showMore ? '270' : '90'}deg)` }} /> {!showMore ? '展开更多' : '收起更多'} <DoubleRightOutlined style={{ transform: `rotate(${showMore ? '270' : '90'}deg)` }} />

View File

@@ -1,11 +1,10 @@
import React, { useEffect, useMemo, useRef, useState, useReducer } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { Slider, Input, Select, Checkbox, Button, Utils, Spin, IconFont, AppContainer } from 'knowdesign'; import { Slider, Input, Select, Checkbox, Button, Utils, Spin, IconFont, AppContainer } from 'knowdesign';
import API from '../../api'; import API from '@src/api';
import TourGuide, { MultiPageSteps } from '@src/components/TourGuide'; import TourGuide, { MultiPageSteps } from '@src/components/TourGuide';
import './index.less'; import './index.less';
import { healthSorceList, linesMetric, pointsMetric, sortFieldList, sortTypes, statusFilters } from './config'; import { healthSorceList, sortFieldList, sortTypes, statusFilters } from './config';
import { oneDayMillims } from '../../constants/common'; import ClusterList from './List';
import ListScroll from './List';
import AccessClusters from './AccessCluster'; import AccessClusters from './AccessCluster';
import CustomCheckGroup from './CustomCheckGroup'; import CustomCheckGroup from './CustomCheckGroup';
import { ClustersPermissionMap } from '../CommonConfig'; import { ClustersPermissionMap } from '../CommonConfig';
@@ -13,98 +12,85 @@ import { ClustersPermissionMap } from '../CommonConfig';
const CheckboxGroup = Checkbox.Group; const CheckboxGroup = Checkbox.Group;
const { Option } = Select; const { Option } = Select;
interface ClustersState {
liveCount: number;
downCount: number;
total: number;
}
export interface SearchParams {
healthScoreRange?: [number, number];
checkedKafkaVersions?: string[];
sortInfo?: {
sortField: string;
sortType: string;
};
keywords?: string;
clusterStatus?: number[];
isReloadAll?: boolean;
}
// 未接入集群默认页
const DefaultPage = (props: { setVisible: (visible: boolean) => void }) => {
return (
<div className="empty-page">
<div className="title">Kafka </div>
<div className="img">
<div className="img-card-1" />
<div className="img-card-2" />
<div className="img-card-3" />
</div>
<div>
<Button className="header-filter-top-button" type="primary" onClick={() => props.setVisible(true)}>
<span>
<IconFont type="icon-jiahao" />
<span className="text"></span>
</span>
</Button>
</div>
</div>
);
};
// 加载状态
const LoadingState = () => {
return (
<div style={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Spin spinning={true} />
</div>
);
};
const MultiClusterPage = () => { const MultiClusterPage = () => {
const [run, setRun] = useState<boolean>(false);
const [global] = AppContainer.useGlobalValue(); const [global] = AppContainer.useGlobalValue();
const [statusList, setStatusList] = React.useState([1, 0]); const [pageLoading, setPageLoading] = useState(true);
const [accessClusterVisible, setAccessClusterVisible] = React.useState(false);
const [curClusterInfo, setCurClusterInfo] = useState<any>({});
const [kafkaVersions, setKafkaVersions] = React.useState<string[]>([]); const [kafkaVersions, setKafkaVersions] = React.useState<string[]>([]);
const [existKafkaVersion, setExistKafkaVersion] = React.useState<string[]>([]); const [existKafkaVersion, setExistKafkaVersion] = React.useState<string[]>([]);
const [visible, setVisible] = React.useState(false); const [stateInfo, setStateInfo] = React.useState<ClustersState>({
const [list, setList] = useState<[]>([]);
const [healthScoreRange, setHealthScoreRange] = React.useState([0, 100]);
const [checkedKafkaVersions, setCheckedKafkaVersions] = React.useState<string[]>([]);
const [sortInfo, setSortInfo] = React.useState({
sortField: 'HealthScore',
sortType: 'asc',
});
const [clusterLoading, setClusterLoading] = useState(true);
const [pageLoading, setPageLoading] = useState(true);
const [isReload, setIsReload] = useState(false);
const [versionLoading, setVersionLoading] = useState(true);
const [searchKeywords, setSearchKeywords] = useState('');
const [stateInfo, setStateInfo] = React.useState({
downCount: 0, downCount: 0,
liveCount: 0, liveCount: 0,
total: 0, total: 0,
}); });
const [pagination, setPagination] = useState({ // TODO: 首次进入因 searchParams 状态变化导致获取两次列表数据的问题
pageNo: 1, const [searchParams, setSearchParams] = React.useState<SearchParams>({
pageSize: 10, keywords: '',
total: 0, checkedKafkaVersions: [],
healthScoreRange: [0, 100],
sortInfo: {
sortField: 'HealthScore',
sortType: 'asc',
},
clusterStatus: [0, 1],
// 是否拉取当前所有数据
isReloadAll: false,
}); });
const searchKeyword = useRef(''); const searchKeyword = useRef('');
const isReload = useRef(false);
const getPhyClustersDashbord = (pageNo: number, pageSize: number) => { // 获取集群状态
const endTime = new Date().getTime();
const startTime = endTime - oneDayMillims;
const params = {
metricLines: {
endTime,
metricsNames: linesMetric,
startTime,
},
latestMetricNames: pointsMetric,
pageNo: pageNo || 1,
pageSize: pageSize || 10,
preciseFilterDTOList: [
{
fieldName: 'kafkaVersion',
fieldValueList: checkedKafkaVersions as (string | number)[],
},
],
rangeFilterDTOList: [
{
fieldMaxValue: healthScoreRange[1],
fieldMinValue: healthScoreRange[0],
fieldName: 'HealthScore',
},
],
searchKeywords,
...sortInfo,
};
if (statusList.length === 1) {
params.preciseFilterDTOList.push({
fieldName: 'Alive',
fieldValueList: statusList,
});
}
return Utils.post(API.phyClustersDashbord, params);
};
const getSupportKafkaVersion = () => {
Utils.request(API.supportKafkaVersion).then((res) => {
setKafkaVersions(Object.keys(res || {}));
});
};
const getExistKafkaVersion = () => {
setVersionLoading(true);
Utils.request(API.getClustersVersion)
.then((versions: string[]) => {
if (!Array.isArray(versions)) {
versions = [];
}
setExistKafkaVersion(versions.sort().reverse() || []);
setVersionLoading(false);
setCheckedKafkaVersions(versions || []);
})
.catch((err) => {
setVersionLoading(false);
});
};
const getPhyClusterState = () => { const getPhyClusterState = () => {
Utils.request(API.phyClusterState) Utils.request(API.phyClusterState)
.then((res: any) => { .then((res: any) => {
@@ -115,213 +101,224 @@ const MultiClusterPage = () => {
}); });
}; };
// 获取 kafka 全部版本
const getSupportKafkaVersion = () => {
Utils.request(API.supportKafkaVersion).then((res) => {
setKafkaVersions(Object.keys(res || {}));
});
};
const updateSearchParams = (params: SearchParams) => {
setSearchParams((curParams) => ({ ...curParams, isReloadAll: false, ...params }));
};
const searchParamsChangeFunc = {
// 健康分改变
onSilderChange: (value: [number, number]) =>
updateSearchParams({
healthScoreRange: value,
}),
// 排序信息改变
onSortInfoChange: (type: string, value: string) =>
updateSearchParams({
sortInfo: {
...searchParams.sortInfo,
[type]: value,
},
}),
// Live / Down 筛选
onClusterStatusChange: (list: number[]) =>
updateSearchParams({
clusterStatus: list,
}),
// 集群名称搜索项改变
onInputChange: () =>
updateSearchParams({
keywords: searchKeyword.current,
}),
// 集群版本筛选
onChangeCheckGroup: (list: string[]) => {
updateSearchParams({
checkedKafkaVersions: list,
isReloadAll: isReload.current,
});
isReload.current = false;
},
};
// 获取当前接入集群的 kafka 版本
const getExistKafkaVersion = (isReloadAll = false) => {
isReload.current = isReloadAll;
Utils.request(API.getClustersVersion).then((versions: string[]) => {
if (!Array.isArray(versions)) {
versions = [];
}
setExistKafkaVersion(versions.sort().reverse() || []);
});
};
// 接入/编辑集群
const showAccessCluster = (clusterInfo: any = {}) => {
setCurClusterInfo(clusterInfo);
setAccessClusterVisible(true);
};
// 接入/编辑集群回调
const afterAccessCluster = () => {
getPhyClusterState();
getExistKafkaVersion(true);
};
useEffect(() => { useEffect(() => {
getPhyClusterState(); getPhyClusterState();
getSupportKafkaVersion(); getSupportKafkaVersion();
getExistKafkaVersion(); getExistKafkaVersion();
}, []); }, []);
useEffect(() => {
if (!pageLoading && stateInfo.total) {
setRun(true);
}
}, [pageLoading, stateInfo]);
useEffect(() => {
if (versionLoading) return;
setClusterLoading(true);
getPhyClustersDashbord(pagination.pageNo, pagination.pageSize)
.then((res: any) => {
setPagination(res.pagination);
setList(res?.bizData || []);
return res;
})
.finally(() => {
setClusterLoading(false);
});
}, [sortInfo, checkedKafkaVersions, healthScoreRange, statusList, searchKeywords, isReload]);
const onSilderChange = (value: number[]) => {
setHealthScoreRange(value);
};
const onSelectChange = (type: string, value: string) => {
setSortInfo({
...sortInfo,
[type]: value,
});
};
const onStatusChange = (list: []) => {
setStatusList(list);
};
const onInputChange = (e: any) => {
const { value } = e.target;
setSearchKeywords(value.trim());
};
const onChangeCheckGroup = (list: []) => {
setCheckedKafkaVersions(list);
};
const afterSubmitSuccessAccessClusters = () => {
getPhyClusterState();
setIsReload(!isReload);
};
const renderEmpty = () => {
return (
<div className="empty-page">
<div className="title">Kafka </div>
<div className="img">
<div className="img-card-1" />
<div className="img-card-2" />
<div className="img-card-3" />
</div>
<div>
<Button className="header-filter-top-button" type="primary" onClick={() => setVisible(true)}>
<span>
<IconFont type="icon-jiahao" />
<span className="text"></span>
</span>
</Button>
</div>
</div>
);
};
const renderLoading = () => {
return (
<div style={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Spin spinning={true} />
</div>
);
};
const renderContent = () => {
return (
<div className="multi-cluster-page" id="scrollableDiv">
<div className="multi-cluster-page-fixed">
<div className="content-container">
<div className="multi-cluster-header">
<div className="cluster-header-card">
<div className="cluster-header-card-bg-left"></div>
<div className="cluster-header-card-bg-right"></div>
<h5 className="header-card-title">
Clusters<span className="chinese-text"> </span>
</h5>
<div className="header-card-total">{stateInfo.total}</div>
<div className="header-card-info">
<div className="card-info-item card-info-item-live">
<div>
live
<span className="info-item-value">
<em>{stateInfo.liveCount}</em>
</span>
</div>
</div>
<div className="card-info-item card-info-item-down">
<div>
down
<span className="info-item-value">
<em>{stateInfo.downCount}</em>
</span>
</div>
</div>
</div>
</div>
<div className="cluster-header-filter">
<div className="header-filter-top">
<div className="header-filter-top-input">
<Input
onPressEnter={onInputChange}
onChange={(e) => (searchKeyword.current = e.target.value)}
allowClear
bordered={false}
placeholder="请输入ClusterName进行搜索"
suffix={<IconFont className="icon" type="icon-fangdajing" onClick={() => setSearchKeywords(searchKeyword.current)} />}
/>
</div>
{global.hasPermission && global.hasPermission(ClustersPermissionMap.CLUSTER_ADD) ? (
<>
<div className="header-filter-top-divider"></div>
<Button className="header-filter-top-button" type="primary" onClick={() => setVisible(true)}>
<IconFont type="icon-jiahao" />
<span className="text"></span>
</Button>
</>
) : (
<></>
)}
</div>
<div className="header-filter-bottom">
<div className="header-filter-bottom-item header-filter-bottom-item-checkbox">
<h3 className="header-filter-bottom-item-title"></h3>
<div className="header-filter-bottom-item-content flex">
{existKafkaVersion.length ? (
<CustomCheckGroup kafkaVersions={existKafkaVersion} onChangeCheckGroup={onChangeCheckGroup} />
) : null}
</div>
</div>
<div className="header-filter-bottom-item header-filter-bottom-item-slider">
<h3 className="header-filter-bottom-item-title title-right"></h3>
<div className="header-filter-bottom-item-content">
<Slider range step={20} defaultValue={[0, 100]} marks={healthSorceList} onAfterChange={onSilderChange} />
</div>
</div>
</div>
</div>
</div>
<div className="multi-cluster-filter">
<div className="multi-cluster-filter-select">
<Select
onChange={(value) => onSelectChange('sortField', value)}
defaultValue="HealthScore"
style={{ width: 170, marginRight: 12 }}
>
{sortFieldList.map((item) => (
<Option key={item.value} value={item.value}>
{item.label}
</Option>
))}
</Select>
<Select onChange={(value) => onSelectChange('sortType', value)} defaultValue="asc" style={{ width: 170 }}>
{sortTypes.map((item) => (
<Option key={item.value} value={item.value}>
{item.label}
</Option>
))}
</Select>
</div>
<div className="multi-cluster-filter-checkbox">
<CheckboxGroup options={statusFilters} value={statusList} onChange={onStatusChange} />
</div>
</div>
<div className="test-modal-23"></div>
</div>
</div>
<div className="multi-cluster-page-dashboard">
<Spin spinning={clusterLoading}>{renderList}</Spin>
</div>
</div>
);
};
const renderList = useMemo(() => {
return <ListScroll list={list} pagination={pagination} loadMoreData={getPhyClustersDashbord} getPhyClusterState={getPhyClusterState} />;
}, [list, pagination]);
return ( return (
<> <>
<TourGuide guide={MultiPageSteps} run={run} /> {pageLoading ? (
{pageLoading ? renderLoading() : stateInfo.total ? renderContent() : renderEmpty()} <LoadingState />
) : !stateInfo?.total ? (
<DefaultPage setVisible={setAccessClusterVisible} />
) : (
<>
<div className="multi-cluster-page" id="scrollableDiv">
<div className="multi-cluster-page-fixed">
<div className="content-container">
<div className="multi-cluster-header">
<div className="cluster-header-card">
<div className="cluster-header-card-bg-left"></div>
<div className="cluster-header-card-bg-right"></div>
<h5 className="header-card-title">
Clusters<span className="chinese-text"> </span>
</h5>
<div className="header-card-total">{stateInfo.total}</div>
<div className="header-card-info">
<div className="card-info-item card-info-item-live">
<div>
live
<span className="info-item-value">
<em>{stateInfo.liveCount}</em>
</span>
</div>
</div>
<div className="card-info-item card-info-item-down">
<div>
down
<span className="info-item-value">
<em>{stateInfo.downCount}</em>
</span>
</div>
</div>
</div>
</div>
<div className="cluster-header-filter">
<div className="header-filter-top">
<div className="header-filter-top-input">
<Input
onPressEnter={searchParamsChangeFunc.onInputChange}
onChange={(e) => (searchKeyword.current = e.target.value)}
allowClear
bordered={false}
placeholder="请输入ClusterName进行搜索"
suffix={<IconFont className="icon" type="icon-fangdajing" onClick={searchParamsChangeFunc.onInputChange} />}
/>
</div>
{global.hasPermission && global.hasPermission(ClustersPermissionMap.CLUSTER_ADD) ? (
<>
<div className="header-filter-top-divider"></div>
<Button className="header-filter-top-button" type="primary" onClick={() => showAccessCluster()}>
<IconFont type="icon-jiahao" />
<span className="text"></span>
</Button>
</>
) : (
<></>
)}
</div>
<div className="header-filter-bottom">
<div className="header-filter-bottom-item header-filter-bottom-item-checkbox">
<h3 className="header-filter-bottom-item-title"></h3>
<div className="header-filter-bottom-item-content flex">
{existKafkaVersion.length ? (
<CustomCheckGroup
kafkaVersions={existKafkaVersion}
onChangeCheckGroup={searchParamsChangeFunc.onChangeCheckGroup}
/>
) : null}
</div>
</div>
<div className="header-filter-bottom-item header-filter-bottom-item-slider">
<h3 className="header-filter-bottom-item-title title-right"></h3>
<div className="header-filter-bottom-item-content">
<Slider
range
step={20}
defaultValue={[0, 100]}
marks={healthSorceList}
onAfterChange={searchParamsChangeFunc.onSilderChange}
/>
</div>
</div>
</div>
</div>
</div>
<div className="multi-cluster-filter">
<div className="multi-cluster-filter-select">
<Select
onChange={(value) => searchParamsChangeFunc.onSortInfoChange('sortField', value)}
defaultValue="HealthScore"
style={{ width: 170, marginRight: 12 }}
>
{sortFieldList.map((item) => (
<Option key={item.value} value={item.value}>
{item.label}
</Option>
))}
</Select>
<Select
onChange={(value) => searchParamsChangeFunc.onSortInfoChange('sortType', value)}
defaultValue="asc"
style={{ width: 170 }}
>
{sortTypes.map((item) => (
<Option key={item.value} value={item.value}>
{item.label}
</Option>
))}
</Select>
</div>
<div className="multi-cluster-filter-checkbox">
<CheckboxGroup
options={statusFilters}
value={searchParams.clusterStatus}
onChange={searchParamsChangeFunc.onClusterStatusChange}
/>
</div>
</div>
</div>
</div>
<div className="multi-cluster-page-dashboard">
<ClusterList
searchParams={searchParams}
showAccessCluster={showAccessCluster}
getPhyClusterState={getPhyClusterState}
getExistKafkaVersion={getExistKafkaVersion}
/>
</div>
</div>
{/* 引导页 */}
<TourGuide guide={MultiPageSteps} run={true} />
</>
)}
<AccessClusters <AccessClusters
visible={visible} clusterInfo={curClusterInfo}
setVisible={setVisible}
kafkaVersion={kafkaVersions} kafkaVersion={kafkaVersions}
afterSubmitSuccess={afterSubmitSuccessAccessClusters} visible={accessClusterVisible}
setVisible={setAccessClusterVisible}
afterSubmitSuccess={afterAccessCluster}
/> />
</> </>
); );

View File

@@ -1,38 +1,51 @@
import { AppContainer, Divider, Form, IconFont, Input, List, message, Modal, Progress, Spin, Tooltip, Utils } from 'knowdesign'; import { AppContainer, Divider, Form, IconFont, Input, List, message, Modal, Progress, Spin, Tooltip, Utils } from 'knowdesign';
import moment from 'moment'; import moment from 'moment';
import React, { useEffect, useMemo, useState, useReducer } from 'react'; import API from '@src/api';
import React, { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import InfiniteScroll from 'react-infinite-scroll-component'; import InfiniteScroll from 'react-infinite-scroll-component';
import { Link, useHistory } from 'react-router-dom'; import { Link, useHistory } from 'react-router-dom';
import { timeFormat } from '../../constants/common'; import { timeFormat, oneDayMillims } from '@src/constants/common';
import { IMetricPoint, linesMetric } from './config'; import { IMetricPoint, linesMetric, pointsMetric } from './config';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import api, { MetricType } from '../../api'; import api, { MetricType } from '@src/api';
import { getHealthClassName, getHealthProcessColor, getHealthText } from '../SingleClusterDetail/config'; import { getHealthClassName, getHealthProcessColor, getHealthText } from '../SingleClusterDetail/config';
import { ClustersPermissionMap } from '../CommonConfig'; import { ClustersPermissionMap } from '../CommonConfig';
import { getUnit, getDataNumberUnit } from '@src/constants/chartConfig'; import { getUnit, getDataNumberUnit } from '@src/constants/chartConfig';
import SmallChart from '@src/components/SmallChart'; import SmallChart from '@src/components/SmallChart';
import { SearchParams } from './HomePage';
const ListScroll = (props: { loadMoreData: any; list: any; pagination: any; getPhyClusterState: any }) => { const DEFAULT_PAGE_SIZE = 10;
const history = useHistory();
const [global] = AppContainer.useGlobalValue(); const DeleteCluster = React.forwardRef((_, ref) => {
const [form] = Form.useForm();
const [list, setList] = useState<[]>(props.list || []);
const [loading, setLoading] = useState(false);
const [visible, setVisible] = useState(false);
const [clusterInfo, setClusterInfo] = useState({} as any);
const [pagination, setPagination] = useState(
props.pagination || {
pageNo: 1,
pageSize: 10,
total: 0,
}
);
const intl = useIntl(); const intl = useIntl();
const [form] = Form.useForm();
const [visible, setVisible] = useState<boolean>(false);
const [clusterInfo, setClusterInfo] = useState<any>({});
const callback = useRef(() => {
return;
});
useEffect(() => { const onFinish = () => {
setList(props.list || []); form.validateFields().then(() => {
setPagination(props.pagination || {}); Utils.delete(api.phyCluster, {
}, [props.list, props.pagination]); params: {
clusterPhyId: clusterInfo.id,
},
}).then(() => {
message.success('删除成功');
callback.current();
setVisible(false);
});
});
};
useImperativeHandle(ref, () => ({
onOpen: (clusterInfo: any, cbk: () => void) => {
setClusterInfo(clusterInfo);
callback.current = cbk;
setVisible(true);
},
}));
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
@@ -40,19 +53,164 @@ const ListScroll = (props: { loadMoreData: any; list: any; pagination: any; getP
} }
}, [visible]); }, [visible]);
return (
<Modal
width={570}
destroyOnClose={true}
centered={true}
className="custom-modal"
wrapClassName="del-topic-modal delete-modal"
title={intl.formatMessage({
id: 'delete.cluster.confirm.title',
})}
visible={visible}
onOk={onFinish}
okText={intl.formatMessage({
id: 'btn.delete',
})}
cancelText={intl.formatMessage({
id: 'btn.cancel',
})}
onCancel={() => setVisible(false)}
okButtonProps={{
style: {
width: 56,
},
danger: true,
size: 'small',
}}
cancelButtonProps={{
style: {
width: 56,
},
size: 'small',
}}
>
<div className="tip-info">
<IconFont type="icon-warning-circle"></IconFont>
<span>
{intl.formatMessage({
id: 'delete.cluster.confirm.tip',
})}
</span>
</div>
<Form form={form} className="form" labelCol={{ span: 4 }} wrapperCol={{ span: 16 }} autoComplete="off">
<Form.Item label="集群名称" name="name">
<span>{clusterInfo.name}</span>
</Form.Item>
<Form.Item
label="集群名称"
name="clusterName"
rules={[
{
required: true,
message: intl.formatMessage({
id: 'delete.cluster.confirm.cluster',
}),
validator: (rule: any, value: string) => {
value = value || '';
if (!value.trim() || value.trim() !== clusterInfo.name)
return Promise.reject(
intl.formatMessage({
id: 'delete.cluster.confirm.cluster',
})
);
return Promise.resolve();
},
},
]}
>
<Input />
</Form.Item>
</Form>
</Modal>
);
});
const ClusterList = (props: { searchParams: SearchParams; showAccessCluster: any; getPhyClusterState: any; getExistKafkaVersion: any }) => {
const { searchParams, showAccessCluster, getPhyClusterState, getExistKafkaVersion } = props;
const history = useHistory();
const [global] = AppContainer.useGlobalValue();
const [isReload, setIsReload] = useState<boolean>(false);
const [list, setList] = useState<[]>([]);
const [clusterLoading, setClusterLoading] = useState<boolean>(true);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [pagination, setPagination] = useState({
pageNo: 1,
pageSize: DEFAULT_PAGE_SIZE,
total: 0,
});
const deleteModalRef = useRef(null);
const getClusterList = (pageNo: number, pageSize: number) => {
const endTime = new Date().getTime();
const startTime = endTime - oneDayMillims;
const params = {
metricLines: {
endTime,
metricsNames: linesMetric,
startTime,
},
latestMetricNames: pointsMetric,
pageNo: pageNo,
pageSize: pageSize,
preciseFilterDTOList: [
{
fieldName: 'kafkaVersion',
fieldValueList: searchParams.checkedKafkaVersions as (string | number)[],
},
],
rangeFilterDTOList: [
{
fieldMaxValue: searchParams.healthScoreRange[1],
fieldMinValue: searchParams.healthScoreRange[0],
fieldName: 'HealthScore',
},
],
searchKeywords: searchParams.keywords,
...searchParams.sortInfo,
};
if (searchParams.clusterStatus.length === 1) {
params.preciseFilterDTOList.push({
fieldName: 'Alive',
fieldValueList: searchParams.clusterStatus,
});
}
return Utils.post(API.phyClustersDashbord, params);
};
// 重置集群列表
const reloadClusterList = (pageSize = DEFAULT_PAGE_SIZE) => {
setClusterLoading(true);
getClusterList(1, pageSize)
.then((res: any) => {
setList(res?.bizData || []);
setPagination(res.pagination);
})
.finally(() => setClusterLoading(false));
};
// 加载更多列表
const loadMoreData = async () => { const loadMoreData = async () => {
if (loading) { if (isLoadingMore) {
return; return;
} }
setLoading(true); setIsLoadingMore(true);
const res = await props.loadMoreData(pagination.pageNo + 1, pagination.pageSize); const res: any = await getClusterList(pagination.pageNo + 1, pagination.pageSize);
const _data = list.concat(res.bizData || []) as any; const _data = list.concat(res.bizData || []) as any;
setList(_data); setList(_data);
setPagination(res.pagination); setPagination(res.pagination);
setLoading(false); setIsLoadingMore(false);
}; };
// 重载列表
useEffect(
() => (searchParams.isReloadAll ? reloadClusterList(pagination.pageNo * pagination.pageSize) : reloadClusterList()),
[searchParams]
);
const RenderItem = (itemData: any) => { const RenderItem = (itemData: any) => {
itemData = itemData || {}; itemData = itemData || {};
const metrics = linesMetric; const metrics = linesMetric;
@@ -160,7 +318,7 @@ const ListScroll = (props: { loadMoreData: any; list: any; pagination: any; getP
title={ title={
<span> <span>
{name} {name}
<Link to={`/cluster/${itemData.id}/cluster/balance`}></Link> <Link to={`/cluster/${itemData.id}/operation/balance`}></Link>
</span> </span>
} }
> >
@@ -225,11 +383,34 @@ const ListScroll = (props: { loadMoreData: any; list: any; pagination: any; getP
</div> </div>
</div> </div>
</div> </div>
{global.hasPermission && global.hasPermission(ClustersPermissionMap.CLUSTER_DEL) ? ( {global.hasPermission ? (
<div className="multi-cluster-list-item-btn"> <div className="multi-cluster-list-item-btn">
<div className="icon" onClick={(event) => onClickDeleteBtn(event, itemData)}> {global.hasPermission(ClustersPermissionMap.CLUSTER_CHANGE_INFO) && (
<IconFont type="icon-shanchu1" /> <div
</div> className="icon"
onClick={(e) => {
e.stopPropagation();
showAccessCluster(itemData);
}}
>
<IconFont type="icon-duojiqunbianji" />
</div>
)}
{global.hasPermission(ClustersPermissionMap.CLUSTER_DEL) && (
<div
className="icon"
onClick={(e) => {
e.stopPropagation();
deleteModalRef.current.onOpen(itemData, () => {
getPhyClusterState();
getExistKafkaVersion(true);
reloadClusterList(pagination.pageNo * pagination.pageSize);
});
}}
>
<IconFont type="icon-duojiqunshanchu" />
</div>
)}
</div> </div>
) : ( ) : (
<></> <></>
@@ -239,45 +420,21 @@ const ListScroll = (props: { loadMoreData: any; list: any; pagination: any; getP
); );
}; };
const onFinish = () => {
form.validateFields().then((formData) => {
Utils.delete(api.phyCluster, {
params: {
clusterPhyId: clusterInfo.id,
},
}).then((res) => {
message.success('删除成功');
setVisible(false);
props?.getPhyClusterState();
const fliterList: any = list.filter((item: any) => {
return item?.id !== clusterInfo.id;
});
setList(fliterList || []);
});
});
};
const onClickDeleteBtn = (event: any, clusterInfo: any) => {
event.stopPropagation();
setClusterInfo(clusterInfo);
setVisible(true);
};
return ( return (
<> <Spin spinning={clusterLoading}>
{useMemo( {useMemo(
() => ( () => (
<InfiniteScroll <InfiniteScroll
dataLength={list.length} dataLength={list.length}
next={loadMoreData} next={loadMoreData}
hasMore={list.length < pagination.total} hasMore={list.length < pagination.total}
loader={<Spin style={{ paddingLeft: '50%', paddingTop: 15 }} spinning={loading} />} loader={<Spin style={{ paddingLeft: '50%', paddingTop: 15 }} spinning={true} />}
endMessage={ endMessage={
!pagination.total ? ( !pagination.total ? (
'' ''
) : ( ) : (
<Divider className="load-completed-tip" plain> <Divider className="load-completed-tip" plain>
{pagination.total} {pagination.total}
</Divider> </Divider>
) )
} }
@@ -293,81 +450,11 @@ const ListScroll = (props: { loadMoreData: any; list: any; pagination: any; getP
/> />
</InfiniteScroll> </InfiniteScroll>
), ),
[list, pagination, loading] [list, pagination, isLoadingMore]
)} )}
<Modal <DeleteCluster ref={deleteModalRef} />
width={570} </Spin>
destroyOnClose={true}
centered={true}
className="custom-modal"
wrapClassName="del-topic-modal delete-modal"
title={intl.formatMessage({
id: 'delete.cluster.confirm.title',
})}
visible={visible}
onOk={onFinish}
okText={intl.formatMessage({
id: 'btn.delete',
})}
cancelText={intl.formatMessage({
id: 'btn.cancel',
})}
onCancel={() => setVisible(false)}
okButtonProps={{
style: {
width: 56,
},
danger: true,
size: 'small',
}}
cancelButtonProps={{
style: {
width: 56,
},
size: 'small',
}}
>
<div className="tip-info">
<IconFont type="icon-warning-circle"></IconFont>
<span>
{intl.formatMessage({
id: 'delete.cluster.confirm.tip',
})}
</span>
</div>
<Form form={form} className="form" labelCol={{ span: 4 }} wrapperCol={{ span: 16 }} autoComplete="off">
<Form.Item label="集群名称" name="name" rules={[{ required: false, message: '' }]}>
<span>{clusterInfo.name}</span>
</Form.Item>
<Form.Item
label="集群名称"
name="clusterName"
rules={[
{
required: true,
message: intl.formatMessage({
id: 'delete.cluster.confirm.cluster',
}),
validator: (rule: any, value: string) => {
value = value || '';
if (!value.trim() || value.trim() !== clusterInfo.name)
return Promise.reject(
intl.formatMessage({
id: 'delete.cluster.confirm.cluster',
})
);
return Promise.resolve();
},
},
]}
>
<Input />
</Form.Item>
</Form>
</Modal>
</>
); );
}; };
export default ClusterList;
export default ListScroll;

View File

@@ -364,8 +364,12 @@
.multi-cluster-list-item-btn { .multi-cluster-list-item-btn {
opacity: 1; opacity: 1;
.icon { .icon {
width: 24px;
background: rgba(33, 37, 41, 0.04);
border-radius: 12px;
color: #74788d; color: #74788d;
font-size: 14px; font-size: 14px;
margin-left: 10px;
} }
.icon:hover { .icon:hover {
@@ -375,16 +379,14 @@
} }
.multi-cluster-list-item-btn { .multi-cluster-list-item-btn {
display: flex;
opacity: 0; opacity: 0;
position: absolute; position: absolute;
right: 20px; right: 20px;
top: 8px; top: 8px;
z-index: 10; z-index: 10;
text-align: right; text-align: right;
width: 24px;
height: 24px; height: 24px;
background: rgba(33, 37, 41, 0.04);
border-radius: 14px;
text-align: center; text-align: center;
line-height: 24px; line-height: 24px;
} }

View File

@@ -7,6 +7,7 @@ import moment from 'moment';
import { timeFormat } from '../../constants/common'; import { timeFormat } from '../../constants/common';
import { DownOutlined } from '@ant-design/icons'; import { DownOutlined } from '@ant-design/icons';
import { renderToolTipValue } from './config'; import { renderToolTipValue } from './config';
import RenderEmpty from '@src/components/RenderEmpty';
const { Panel } = Collapse; const { Panel } = Collapse;
@@ -51,17 +52,6 @@ const ChangeLog = () => {
); );
}, []); }, []);
const renderEmpty = () => {
return (
<>
<div className="empty-panel">
<div className="img" />
<div className="text"></div>
</div>
</>
);
};
const getHref = (item: any) => { const getHref = (item: any) => {
if (item.resTypeName.toLowerCase().includes('topic')) return `/cluster/${clusterId}/topic/list#topicName=${item.resName}`; if (item.resTypeName.toLowerCase().includes('topic')) return `/cluster/${clusterId}/topic/list#topicName=${item.resName}`;
if (item.resTypeName.toLowerCase().includes('broker')) return `/cluster/${clusterId}/broker/list#brokerId=${item.resName}`; if (item.resTypeName.toLowerCase().includes('broker')) return `/cluster/${clusterId}/broker/list#brokerId=${item.resName}`;
@@ -73,7 +63,7 @@ const ChangeLog = () => {
<div className="change-log-panel"> <div className="change-log-panel">
<div className="title"></div> <div className="title"></div>
{!loading && !data.length ? ( {!loading && !data.length ? (
renderEmpty() <RenderEmpty message="暂无配置记录" />
) : ( ) : (
<div id="changelog-scroll-box"> <div id="changelog-scroll-box">
<Spin spinning={loading} style={{ paddingLeft: '42%', marginTop: 100 }} /> <Spin spinning={loading} style={{ paddingLeft: '42%', marginTop: 100 }} />

View File

@@ -1,14 +1,11 @@
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import { Drawer, Form, Spin, Table, Utils } from 'knowdesign'; import { Drawer, Spin, Table, Utils } from 'knowdesign';
import React, { useEffect, useState, forwardRef, useImperativeHandle } from 'react'; import React, { useEffect, useState, forwardRef, useImperativeHandle } from 'react';
import { useIntl } from 'react-intl';
import { getDetailColumn } from './config'; import { getDetailColumn } from './config';
import API from '../../api'; import API from '../../api';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
const CheckDetail = forwardRef((props: any, ref): JSX.Element => { const CheckDetail = forwardRef((props: any, ref): JSX.Element => {
const intl = useIntl();
const [form] = Form.useForm();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [data, setData] = useState([]); const [data, setData] = useState([]);
@@ -28,7 +25,6 @@ const CheckDetail = forwardRef((props: any, ref): JSX.Element => {
}; };
const onCancel = () => { const onCancel = () => {
form.resetFields();
setVisible(false); setVisible(false);
}; };

View File

@@ -38,15 +38,17 @@
&-main { &-main {
.header-chart-container { .header-chart-container {
&-loading {
display: flex;
justify-content: center;
align-items: center;
}
width: 100%; width: 100%;
height: 244px; height: 244px;
margin-bottom: 12px; margin-bottom: 12px;
.cluster-container-border(); .cluster-container-border();
.dcloud-spin.dcloud-spin-spinning {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 244px;
}
} }
.content { .content {

View File

@@ -14,6 +14,7 @@ import { MetricType } from '@src/api';
import { getDataNumberUnit, getUnit } from '@src/constants/chartConfig'; import { getDataNumberUnit, getUnit } from '@src/constants/chartConfig';
import SingleChartHeader, { KsHeaderOptions } from '@src/components/SingleChartHeader'; import SingleChartHeader, { KsHeaderOptions } from '@src/components/SingleChartHeader';
import { MAX_TIME_RANGE_WITH_SMALL_POINT_INTERVAL } from '@src/constants/common'; import { MAX_TIME_RANGE_WITH_SMALL_POINT_INTERVAL } from '@src/constants/common';
import RenderEmpty from '@src/components/RenderEmpty';
type ChartFilterOptions = Omit<KsHeaderOptions, 'gridNum'>; type ChartFilterOptions = Omit<KsHeaderOptions, 'gridNum'>;
interface MetricInfo { interface MetricInfo {
@@ -64,7 +65,7 @@ const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
const [messagesInMetricData, setMessagesInMetricData] = useState<MessagesInMetric>({ const [messagesInMetricData, setMessagesInMetricData] = useState<MessagesInMetric>({
name: 'MessagesIn', name: 'MessagesIn',
unit: '', unit: '',
data: [], data: undefined,
}); });
const [curHeaderOptions, setCurHeaderOptions] = useState<ChartFilterOptions>(); const [curHeaderOptions, setCurHeaderOptions] = useState<ChartFilterOptions>();
const [defaultChartLoading, setDefaultChartLoading] = useState<boolean>(true); const [defaultChartLoading, setDefaultChartLoading] = useState<boolean>(true);
@@ -234,17 +235,19 @@ const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
result.forEach((point) => ((point[1] as number) /= unitSize)); result.forEach((point) => ((point[1] as number) /= unitSize));
} }
// 补充缺少的图表点 if (result.length) {
const extraMetrics = result[0][2].map((info) => ({ // 补充缺少的图表点
...info, const extraMetrics = result[0][2].map((info) => ({
value: 0, ...info,
})); value: 0,
const supplementaryInterval = }));
(curHeaderOptions.rangeTime[1] - curHeaderOptions.rangeTime[0] > MAX_TIME_RANGE_WITH_SMALL_POINT_INTERVAL ? 10 : 1) * 60 * 1000; const supplementaryInterval =
supplementaryPoints([line], curHeaderOptions.rangeTime, supplementaryInterval, (point) => { (curHeaderOptions.rangeTime[1] - curHeaderOptions.rangeTime[0] > MAX_TIME_RANGE_WITH_SMALL_POINT_INTERVAL ? 10 : 1) * 60 * 1000;
point.push(extraMetrics as any); supplementaryPoints([line], curHeaderOptions.rangeTime, supplementaryInterval, (point) => {
return point; point.push(extraMetrics as any);
}); return point;
});
}
setMessagesInMetricData(line); setMessagesInMetricData(line);
setDefaultChartLoading(false); setDefaultChartLoading(false);
@@ -299,10 +302,9 @@ const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
<div className="cluster-detail-container-main"> <div className="cluster-detail-container-main">
{/* MessageIn 图表 */} {/* MessageIn 图表 */}
<div className={`header-chart-container ${!messagesInMetricData.data.length ? 'header-chart-container-loading' : ''}`}> <div className="header-chart-container">
<Spin spinning={defaultChartLoading}> <Spin spinning={defaultChartLoading}>
{/* TODO: 暂时通过判断是否有图表数据来修复,有时间可以查找下宽度溢出的原因 */} {messagesInMetricData.data && (
{messagesInMetricData.data.length ? (
<> <>
<div className="chart-box-title"> <div className="chart-box-title">
<Tooltip <Tooltip
@@ -322,26 +324,27 @@ const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
</span> </span>
</Tooltip> </Tooltip>
</div> </div>
<SingleChart {messagesInMetricData.data.length ? (
chartKey="messagesIn" <SingleChart
chartTypeProp="line" chartKey="messagesIn"
showHeader={false} chartTypeProp="line"
wrapStyle={{ showHeader={false}
width: 'auto', wrapStyle={{
height: 210, width: 'auto',
}} height: 210,
connectEventName="clusterChart" }}
eventBus={busInstance} connectEventName="clusterChart"
propChartData={[messagesInMetricData]} eventBus={busInstance}
{...getChartConfig({ propChartData={[messagesInMetricData]}
// metricName: `${messagesInMetricData.name}{unit|${messagesInMetricData.unit}}`, {...getChartConfig({
lineColor: CHART_LINE_COLORS[0], lineColor: CHART_LINE_COLORS[0],
isDefaultMetric: true, isDefaultMetric: true,
})} })}
/> />
) : (
!defaultChartLoading && <RenderEmpty message="暂无数据" height={200} />
)}
</> </>
) : (
''
)} )}
</Spin> </Spin>
</div> </div>
@@ -408,7 +411,7 @@ const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
) : chartLoading ? ( ) : chartLoading ? (
<></> <></>
) : ( ) : (
<Empty description="请先选择指标或刷新" style={{ width: '100%', height: '100%' }} /> <RenderEmpty message="请先选择指标或刷新" />
)} )}
</Row> </Row>
</Spin> </Spin>

View File

@@ -153,7 +153,7 @@ const LeftSider = () => {
<Divider /> <Divider />
<div className="title"> <div className="title">
<div className="name">{renderToolTipValue(clusterInfo?.name, 35)}</div> <div className="name">{renderToolTipValue(clusterInfo?.name, 35)}</div>
{global.hasPermission && global.hasPermission(ClustersPermissionMap.CLUSTER_CHANGE_INFO) ? ( {!loading && global.hasPermission && global.hasPermission(ClustersPermissionMap.CLUSTER_CHANGE_INFO) ? (
<div className="edit-icon-box" onClick={() => setVisible(true)}> <div className="edit-icon-box" onClick={() => setVisible(true)}>
<IconFont className="edit-icon" type="icon-bianji2" /> <IconFont className="edit-icon" type="icon-bianji2" />
</div> </div>
@@ -239,8 +239,7 @@ const LeftSider = () => {
<AccessClusters <AccessClusters
visible={visible} visible={visible}
setVisible={setVisible} setVisible={setVisible}
title={'edit.cluster'} title="edit.cluster"
infoLoading={loading}
afterSubmitSuccess={getPhyClusterInfo} afterSubmitSuccess={getPhyClusterInfo}
clusterInfo={clusterInfo} clusterInfo={clusterInfo}
kafkaVersion={Object.keys(kafkaVersion)} kafkaVersion={Object.keys(kafkaVersion)}

View File

@@ -267,10 +267,10 @@ export const getHealthySettingColumn = (form: any, data: any, clusterId: string)
<InputNumber <InputNumber
size="small" size="small"
min={0} min={0}
max={100} max={1}
style={{ width: 86 }} style={{ width: 86 }}
formatter={(value) => `${value}%`} formatter={(value) => `${value * 100}%`}
parser={(value: any) => value.replace('%', '')} parser={(value: any) => parseFloat(value.replace('%', '')) / 100}
/> />
) : ( ) : (
<InputNumber style={{ width: 86 }} size="small" {...attrs} /> <InputNumber style={{ width: 86 }} size="small" {...attrs} />

View File

@@ -377,26 +377,6 @@
} }
} }
} }
.empty-panel {
margin-top: 96px;
text-align: center;
.img {
width: 51px;
height: 34px;
margin-left: 80px;
margin-bottom: 7px;
background-size: cover;
background-image: url('../../assets/empty.png');
}
.text {
font-size: 10px;
color: #919aac;
line-height: 20px;
}
}
} }
} }
} }

View File

@@ -178,38 +178,35 @@ const ConsumeClientTest = () => {
partitionProcessRef.current = processList; partitionProcessRef.current = processList;
curPartitionList.current = _partitionList; curPartitionList.current = _partitionList;
if (!isStopStatus.current) {
switch (until) { switch (until) {
case 'timestamp': case 'timestamp':
setIsStop(currentTime >= untilDate); setIsStop(currentTime >= untilDate);
isStopStatus.current = currentTime >= untilDate; isStopStatus.current = currentTime >= untilDate;
break; break;
case 'number of messages': case 'number of messages':
setIsStop(+recordCountCur.current >= untilMsgNum); setIsStop(+recordCountCur.current >= untilMsgNum);
isStopStatus.current = +recordCountCur.current >= untilMsgNum; isStopStatus.current = +recordCountCur.current >= untilMsgNum;
break; break;
case 'number of messages per partition': // 所有分区都达到了设定值 case 'number of messages per partition': // 所有分区都达到了设定值
// 过滤出消费数量不足设定值的partition // 过滤出消费数量不足设定值的partition
const filtersPartition = _partitionList.filter((item: any) => item.recordCount < untilMsgNum); const filtersPartition = _partitionList.filter((item: any) => item.recordCount < untilMsgNum);
curPartitionList.current = filtersPartition; // 用作下一次请求的入参 curPartitionList.current = filtersPartition; // 用作下一次请求的入参
if (!isStop) {
setIsStop(filtersPartition.length < 1); setIsStop(filtersPartition.length < 1);
isStopStatus.current = filtersPartition.length < 1; isStopStatus.current = filtersPartition.length < 1;
} break;
break; case 'max size':
case 'max size': setIsStop(+recordSizeCur.current >= unitMsgSize);
setIsStop(+recordSizeCur.current >= unitMsgSize); isStopStatus.current = +recordSizeCur.current >= unitMsgSize;
isStopStatus.current = +recordSizeCur.current >= unitMsgSize; break;
break; case 'max size per partition':
case 'max size per partition': // 过滤出消费size不足设定值的partition
// 过滤出消费size不足设定值的partition const filters = partitionConsumedList.filter((item: any) => item.recordSizeUnitB < unitMsgSize);
const filters = partitionConsumedList.filter((item: any) => item.recordSizeUnitB < unitMsgSize);
if (!isStop) {
setIsStop(filters.length < 1); setIsStop(filters.length < 1);
isStopStatus.current = filters.length < 1; isStopStatus.current = filters.length < 1;
} curPartitionList.current = filters;
curPartitionList.current = filters; break;
break; }
} }
}; };

View File

@@ -1,13 +1,15 @@
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Table, Input, InputNumber, Popconfirm, Form, Typography, Button, message, IconFont } from 'knowdesign'; import { Table, Input, InputNumber, Popconfirm, Form, Typography, Button, message, IconFont, Select } from 'knowdesign';
import './style/edit-table.less'; import './style/edit-table.less';
import { CheckOutlined, CloseOutlined, PlusSquareOutlined } from '@ant-design/icons'; import { CheckOutlined, CloseOutlined, PlusSquareOutlined } from '@ant-design/icons';
const EditableCell = ({ editing, dataIndex, title, inputType, placeholder, record, index, children, ...restProps }: any) => { const EditableCell = ({ editing, dataIndex, title, inputType, placeholder, record, index, children, options, ...restProps }: any) => {
const inputNode = const inputNode =
inputType === 'number' ? ( inputType === 'number' ? (
<InputNumber style={{ width: '130px' }} autoComplete="off" placeholder={placeholder} /> <InputNumber min={0} precision={0} style={{ width: '130px' }} autoComplete="off" placeholder={placeholder} />
) : inputType === 'select' ? (
<Select style={{ width: '140px' }} options={options || []} placeholder={placeholder} />
) : ( ) : (
<Input autoComplete="off" placeholder={placeholder} /> <Input autoComplete="off" placeholder={placeholder} />
); );

View File

@@ -6,6 +6,7 @@ import api, { MetricType } from '@src/api';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import TagsWithHide from '@src/components/TagsWithHide'; import TagsWithHide from '@src/components/TagsWithHide';
import SwitchTab from '@src/components/SwitchTab'; import SwitchTab from '@src/components/SwitchTab';
import RenderEmpty from '@src/components/RenderEmpty';
interface PropsType { interface PropsType {
hashData: any; hashData: any;
@@ -86,18 +87,6 @@ function getTranformedBytes(bytes: number) {
return [outBytes.toFixed(2), unit[i]]; return [outBytes.toFixed(2), unit[i]];
} }
const RenderEmpty = (props: { message: string }) => {
const { message } = props;
return (
<>
<div className="empty-panel">
<div className="img" />
<div className="text">{message}</div>
</div>
</>
);
};
const PartitionPopoverContent = (props: { const PartitionPopoverContent = (props: {
clusterId: string; clusterId: string;
hashData: any; hashData: any;
@@ -125,18 +114,21 @@ const PartitionPopoverContent = (props: {
{ label: 'LeaderBroker', value: leaderBrokerId }, { label: 'LeaderBroker', value: leaderBrokerId },
{ {
label: 'BeginningOffset', label: 'BeginningOffset',
value: `${metricsData.LogStartOffset === undefined ? '-' : metricsData.LogStartOffset} ${global.getMetricDefine(type, 'LogStartOffset')?.unit || '' value: `${metricsData.LogStartOffset === undefined ? '-' : metricsData.LogStartOffset} ${
}`, global.getMetricDefine(type, 'LogStartOffset')?.unit || ''
}`,
}, },
{ {
label: 'EndOffset', label: 'EndOffset',
value: `${metricsData.LogEndOffset === undefined ? '-' : metricsData.LogEndOffset} ${global.getMetricDefine(type, 'LogEndOffset')?.unit || '' value: `${metricsData.LogEndOffset === undefined ? '-' : metricsData.LogEndOffset} ${
}`, global.getMetricDefine(type, 'LogEndOffset')?.unit || ''
}`,
}, },
{ {
label: 'MsgNum', label: 'MsgNum',
value: `${metricsData.Messages === undefined ? '-' : metricsData.Messages} ${global.getMetricDefine(type, 'Messages')?.unit || '' value: `${metricsData.Messages === undefined ? '-' : metricsData.Messages} ${
}`, global.getMetricDefine(type, 'Messages')?.unit || ''
}`,
}, },
{ {
label: 'LogSize', label: 'LogSize',
@@ -281,13 +273,14 @@ const PartitionCard = (props: { clusterId: string; hashData: any }) => {
<div className="broker-container-box-detail"> <div className="broker-container-box-detail">
{partitionState.alive ? ( {partitionState.alive ? (
partitionState?.replicaList?.length ? ( partitionState?.replicaList?.length ? (
<div className="partition-list"> <div className={`partition-list ${hoverPartitionId !== -1 ? 'partition-list-hover-state' : ''}`}>
{partitionState?.replicaList?.map((partition) => { {partitionState?.replicaList?.map((partition) => {
return ( return (
<div <div
key={partition.partitionId} key={partition.partitionId}
className={`partition-list-item partition-list-item-${partition.isLeaderReplace ? 'leader' : partition.inSync ? 'isr' : 'osr' className={`partition-list-item partition-list-item-${
} ${partition.partitionId === hoverPartitionId ? 'partition-active' : ''}`} partition.isLeaderReplace ? 'leader' : partition.inSync ? 'isr' : 'osr'
} ${partition.partitionId === hoverPartitionId ? 'partition-active' : ''}`}
onMouseEnter={() => setHoverPartitionId(partition.partitionId)} onMouseEnter={() => setHoverPartitionId(partition.partitionId)}
onMouseLeave={() => setHoverPartitionId(-1)} onMouseLeave={() => setHoverPartitionId(-1)}
onClick={() => setClickPartition(`${partitionState.brokerId}&${partition.partitionId}`)} onClick={() => setClickPartition(`${partitionState.brokerId}&${partition.partitionId}`)}
@@ -316,10 +309,10 @@ const PartitionCard = (props: { clusterId: string; hashData: any }) => {
})} })}
</div> </div>
) : ( ) : (
<RenderEmpty message="暂无数据" /> <RenderEmpty message="暂无数据" height="unset" />
) )
) : ( ) : (
<RenderEmpty message="暂无数据" /> <RenderEmpty message="暂无数据" height="unset" />
)} )}
</div> </div>
</div> </div>

View File

@@ -26,10 +26,14 @@ export const ConfigurationEdit = (props: any) => {
props.setVisible(false); props.setVisible(false);
props.genData({ pageNo: 1, pageSize: 10 }); props.genData({ pageNo: 1, pageSize: 10 });
}) })
.catch((err: any) => { }); .catch((err: any) => {});
}); });
}; };
React.useEffect(() => {
form.setFieldsValue(props.record);
}, [props.record]);
return ( return (
<Drawer <Drawer
title={ title={
@@ -43,6 +47,7 @@ export const ConfigurationEdit = (props: any) => {
visible={props.visible} visible={props.visible}
onClose={() => props.setVisible(false)} onClose={() => props.setVisible(false)}
maskClosable={false} maskClosable={false}
destroyOnClose
extra={ extra={
<Space> <Space>
<Button size="small" onClick={onClose}> <Button size="small" onClick={onClose}>
@@ -76,7 +81,7 @@ export const ConfigurationEdit = (props: any) => {
{props.record?.documentation || '-'} {props.record?.documentation || '-'}
</Col> </Col>
</Row> </Row>
<Form form={form} layout={'vertical'} initialValues={props.record}> <Form form={form} layout={'vertical'}>
<Form.Item name="defaultValue" label="Kafka默认配置"> <Form.Item name="defaultValue" label="Kafka默认配置">
<Input disabled /> <Input disabled />
</Form.Item> </Form.Item>

View File

@@ -77,7 +77,7 @@ export const getTopicMessagesColmns = () => {
key: 'partitionId', key: 'partitionId',
}, },
{ {
title: 'offset', title: 'Offset',
dataIndex: 'offset', dataIndex: 'offset',
key: 'offset', key: 'offset',
}, },

View File

@@ -215,8 +215,8 @@
position: relative; position: relative;
width: 324px; width: 324px;
min-height: calc(100% - 66px); min-height: calc(100% - 66px);
margin: 0 0 12px 6px; margin: 0 6px 6px 6px;
padding: 22px 20px 0 20px; padding: 12px 12px 0 12px;
border-radius: 12px; border-radius: 12px;
background: #ffffff; background: #ffffff;
overflow: hidden; overflow: hidden;
@@ -226,16 +226,16 @@
flex-flow: row wrap; flex-flow: row wrap;
width: 100%; width: 100%;
&-item { &-item {
width: 32px; width: 34px;
height: 16px; height: 16px;
margin-bottom: 22px; margin-bottom: 12px;
border-radius: 4px; border-radius: 4px;
text-align: center; text-align: center;
line-height: 16px; line-height: 16px;
transition: all ease 0.2s; transition: all ease-in-out 0.3s;
cursor: pointer; cursor: pointer;
&:not(&:nth-of-type(5n)) { &:not(&:nth-of-type(8n)) {
margin-right: 31px; margin-right: 4px;
} }
&-leader { &-leader {
background: rgba(85, 110, 230, 0.1); background: rgba(85, 110, 230, 0.1);
@@ -262,27 +262,21 @@
} }
} }
} }
} &-hover-state {
.partition-list-item {
.empty-panel { &-leader:not(.partition-active) {
display: flex; background-color: #f6f7fd;
flex-direction: column; color: #dbe1f8;
align-items: center; }
margin-bottom: 18px; &-isr:not(.partition-active) {
text-align: center; background-color: #fcfcfc;
color: #c4c6c9;
.img { }
width: 51px; &-osr:not(.partition-active) {
height: 34px; background-color: #fefaf4;
margin-bottom: 7px; color: #f8d6af;
background-size: cover; }
background-image: url('../../assets/empty.png'); }
}
.text {
font-size: 10px;
color: #919aac;
line-height: 20px;
} }
} }
} }

View File

@@ -1,19 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { import { Alert, Button, Checkbox, Divider, Drawer, Form, Input, InputNumber, Modal, notification, Select, Utils } from 'knowdesign';
Alert,
Button,
Checkbox,
Divider,
Drawer,
Form,
Input,
InputNumber,
Modal,
notification,
Select,
Utils,
} from 'knowdesign';
import { PlusOutlined, DownOutlined, UpOutlined } from '@ant-design/icons'; import { PlusOutlined, DownOutlined, UpOutlined } from '@ant-design/icons';
import Api from '@src/api/index'; import Api from '@src/api/index';
@@ -120,9 +107,9 @@ export default (props: any) => {
res = res =
item.name === 'cleanup.policy' item.name === 'cleanup.policy'
? item.defaultValue ? item.defaultValue
.replace(/\[|\]|\s+/g, '') .replace(/\[|\]|\s+/g, '')
.split(',') .split(',')
.filter((_) => _) .filter((_) => _)
: item.defaultValue; : item.defaultValue;
} catch (e) { } catch (e) {
res = []; res = [];
@@ -317,7 +304,7 @@ export default (props: any) => {
} }
/> />
<div className="create-topic-flex-layout"> <div className="create-topic-flex-layout">
<Form.Item name={['properties', 'max.message.bytes']} label="max message size"> <Form.Item name={['properties', 'max.message.bytes']} label="Max message size">
<InputNumber <InputNumber
min={0} min={0}
style={{ width: '100%' }} style={{ width: '100%' }}
@@ -329,7 +316,11 @@ export default (props: any) => {
{defaultConfigs {defaultConfigs
.filter((dc) => !customDefaultFields.includes(dc.name)) .filter((dc) => !customDefaultFields.includes(dc.name))
.map((configItem, i) => ( .map((configItem, i) => (
<Form.Item key={i} name={['properties', configItem.name]} label={configItem.name}> <Form.Item
key={i}
name={['properties', configItem.name]}
label={configItem.name.slice(0, 1).toUpperCase() + configItem.name.slice(1)}
>
<Input /> <Input />
</Form.Item> </Form.Item>
))} ))}

View File

@@ -19,6 +19,7 @@
align-items: center; align-items: center;
> span { > span {
margin-left: 4px; margin-left: 4px;
color: #74788d;
} }
} }
} }
@@ -36,7 +37,7 @@
width: 120px; width: 120px;
margin-right: 8px; margin-right: 8px;
} }
.batch-btn{ .batch-btn {
margin-right: 8px; margin-right: 8px;
} }
.add-btn { .add-btn {
@@ -51,44 +52,44 @@
} }
} }
.metric-data-wrap { .metric-data-wrap {
display: flex; // display: flex;
align-items: center; // align-items: center;
width: 100%; width: 100%;
.cur-val { .cur-val {
width: 34px; display: block;
margin-right: 11px; text-align: right;
} }
.dcloud-spin-nested-loading{ .dcloud-spin-nested-loading {
flex: 1; flex: 1;
} }
} }
.del-topic-modal, .del-topic-modal,
.cluster-topic-add { .cluster-topic-add {
.tip-info { .tip-info {
height: 27px; display: flex;
line-height: 27px;
color: #592d00; color: #592d00;
padding: 0 14px; padding: 6px 14px;
font-size: 13px; font-size: 13px;
background: #fffae0; background: #fffae0;
border-radius: 4px; border-radius: 4px;
.anticon { .anticon {
color: #ffc300; color: #ffc300;
margin-right: 4px; margin-right: 4px;
margin-top: 3px;
} }
.test-right-away { .test-right-away {
color: #556ee6; color: #556ee6;
cursor: pointer; cursor: pointer;
} }
.dcloud-alert-content{ .dcloud-alert-content {
flex: none; flex: none;
} }
} }
} }
.cluster-topic-add { .cluster-topic-add {
.data-save-time-label{ .data-save-time-label {
&>.dcloud-form-item-control{ & > .dcloud-form-item-control {
&>.dcloud-form-item-explain{ & > .dcloud-form-item-explain {
display: none; display: none;
} }
} }
@@ -124,11 +125,11 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
color: #556EE6; color: #556ee6;
.txt { .txt {
width: 26px; width: 26px;
margin-right: 4px; margin-right: 4px;
color: #556EE6; color: #556ee6;
font-family: @font-family; font-family: @font-family;
} }
.anticon { .anticon {
@@ -226,12 +227,11 @@
} }
} }
.create-topic-flex-layout {
.create-topic-flex-layout{
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
.dcloud-form-item{ .dcloud-form-item {
width: 370px; width: 370px;
} }
} }

View File

@@ -91,21 +91,9 @@ const AutoPage = (props: any) => {
if (metricName === 'HealthScore') { if (metricName === 'HealthScore') {
return Math.round(orgVal); return Math.round(orgVal);
} else if (metricName === 'LogSize') { } else if (metricName === 'LogSize') {
return Number(Utils.formatAssignSize(orgVal, 'MB')).toString().length > 3 ? ( return Number(Utils.formatAssignSize(orgVal, 'MB'));
<Tooltip title={Utils.formatAssignSize(orgVal, 'MB')}>
{Number(Utils.formatAssignSize(orgVal, 'MB')).toString().slice(0, 3) + '...'}
</Tooltip>
) : (
Number(Utils.formatAssignSize(orgVal, 'MB'))
);
} else { } else {
return Number(Utils.formatAssignSize(orgVal, 'KB')).toString().length > 3 ? ( return Number(Utils.formatAssignSize(orgVal, 'KB'));
<Tooltip title={Utils.formatAssignSize(orgVal, 'KB')}>
{Number(Utils.formatAssignSize(orgVal, 'KB')).toString().slice(0, 3) + '...'}
</Tooltip>
) : (
Number(Utils.formatAssignSize(orgVal, 'KB'))
);
// return Utils.formatAssignSize(orgVal, 'KB'); // return Utils.formatAssignSize(orgVal, 'KB');
} }
} }
@@ -116,15 +104,15 @@ const AutoPage = (props: any) => {
const points = record.metricLines.find((item: any) => item.metricName === metricName)?.metricPoints || []; const points = record.metricLines.find((item: any) => item.metricName === metricName)?.metricPoints || [];
return ( return (
<div className="metric-data-wrap"> <div className="metric-data-wrap">
<span className="cur-val">{calcCurValue(record, metricName)}</span>
<SmallChart <SmallChart
width={'100%'} width={'100%'}
height={40} height={30}
chartData={{ chartData={{
name: record.metricName, name: record.metricName,
data: points.map((item: any) => ({ time: item.timeStamp, value: item.value })), data: points.map((item: any) => ({ time: item.timeStamp, value: item.value })),
}} }}
/> />
<span className="cur-val">{calcCurValue(record, metricName)}</span>
</div> </div>
); );
}; };
@@ -268,12 +256,16 @@ const AutoPage = (props: any) => {
const menu = ( const menu = (
<Menu> <Menu>
<Menu.Item> {global.hasPermission(ClustersPermissionMap.TOPIC_CHANGE_REPLICA) && (
<a onClick={() => setChangeVisible(true)}></a> <Menu.Item>
</Menu.Item> <a onClick={() => setChangeVisible(true)}></a>
<Menu.Item> </Menu.Item>
<a onClick={() => setMoveVisible(true)}></a> )}
</Menu.Item> {global.hasPermission(ClustersPermissionMap.TOPIC_MOVE_REPLICA) && (
<Menu.Item>
<a onClick={() => setMoveVisible(true)}></a>
</Menu.Item>
)}
</Menu> </Menu>
); );
@@ -345,11 +337,14 @@ const AutoPage = (props: any) => {
setSearchKeywordsInput(e.target.value); setSearchKeywordsInput(e.target.value);
}} }}
/> />
<Dropdown overlay={menu} trigger={['click']}> {(global.hasPermission(ClustersPermissionMap.TOPIC_CHANGE_REPLICA) ||
<Button className="batch-btn" icon={<DownOutlined />} type="primary" ghost> global.hasPermission(ClustersPermissionMap.TOPIC_MOVE_REPLICA)) && (
<Dropdown overlay={menu} trigger={['click']}>
</Button> <Button className="batch-btn" icon={<DownOutlined />} type="primary" ghost>
</Dropdown>
</Button>
</Dropdown>
)}
{global.hasPermission && global.hasPermission(ClustersPermissionMap.TOPIC_ADD) ? ( {global.hasPermission && global.hasPermission(ClustersPermissionMap.TOPIC_ADD) ? (
<Create onConfirm={getTopicsList}></Create> <Create onConfirm={getTopicsList}></Create>
) : ( ) : (

View File

@@ -1,4 +1,4 @@
import HomePage from './MutliClusterPage/HomePage'; import ClusterManage from './MutliClusterPage/HomePage';
import { NoMatch } from '.'; import { NoMatch } from '.';
import CommonRoute from './CommonRoute'; import CommonRoute from './CommonRoute';
@@ -26,7 +26,7 @@ const pageRoutes = [
{ {
path: '/', path: '/',
exact: true, exact: true,
component: HomePage, component: ClusterManage,
commonRoute: CommonConfig, commonRoute: CommonConfig,
noSider: true, noSider: true,
}, },
@@ -37,15 +37,6 @@ const pageRoutes = [
commonRoute: CommonRoute, commonRoute: CommonRoute,
noSider: false, noSider: false,
children: [ children: [
// 负载均衡
process.env.BUSINESS_VERSION
? {
path: 'cluster/balance',
exact: true,
component: LoadRebalance,
noSider: false,
}
: undefined,
{ {
path: 'cluster', path: 'cluster',
exact: true, exact: true,
@@ -109,6 +100,21 @@ const pageRoutes = [
component: Consumers, component: Consumers,
noSider: false, noSider: false,
}, },
// 负载均衡
process.env.BUSINESS_VERSION
? {
path: 'operation/balance',
exact: true,
component: LoadRebalance,
noSider: false,
}
: undefined,
{
path: 'operation/jobs',
exact: true,
component: Jobs,
noSider: false,
},
{ {
path: 'security/acls', path: 'security/acls',
exact: true, exact: true,
@@ -121,12 +127,6 @@ const pageRoutes = [
component: SecurityUsers, component: SecurityUsers,
noSider: false, noSider: false,
}, },
{
path: 'jobs',
exact: true,
component: Jobs,
noSider: false,
},
{ {
path: '*', path: '*',
component: () => NoMatch, component: () => NoMatch,

View File

@@ -651,3 +651,8 @@
} }
} }
} }
.@{ant-prefix}-empty-img-default{
width: 100% !important;
}

View File

@@ -15,11 +15,13 @@ module.exports = merge(getWebpackCommonConfig(), {
layout: ['./src/index.tsx'], layout: ['./src/index.tsx'],
}, },
plugins: [ plugins: [
new CountPlugin({ isProd
pathname: 'knowdesign', ? new CountPlugin({
startCount: true, pathname: 'knowdesign',
isExportExcel: false, startCount: true,
}), isExportExcel: false,
})
: undefined,
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': { 'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV), NODE_ENV: JSON.stringify(process.env.NODE_ENV),
@@ -53,7 +55,7 @@ module.exports = merge(getWebpackCommonConfig(), {
: [] : []
) )
), ),
], ].filter((p) => p),
output: { output: {
path: outPath, path: outPath,
publicPath: isProd ? process.env.PUBLIC_PATH + '/layout/' : '/', publicPath: isProd ? process.env.PUBLIC_PATH + '/layout/' : '/',
@@ -79,11 +81,11 @@ module.exports = merge(getWebpackCommonConfig(), {
proxy: { proxy: {
'/ks-km/api/v3': { '/ks-km/api/v3': {
changeOrigin: true, changeOrigin: true,
target: 'https://api-kylin-xg02.intra.xiaojukeji.com/ks-km/', target: 'http://localhost:8080/',
}, },
'/logi-security/api/v1': { '/logi-security/api/v1': {
changeOrigin: true, changeOrigin: true,
target: 'https://api-kylin-xg02.intra.xiaojukeji.com/ks-km/', target: 'http://localhost:8080/',
}, },
}, },
}, },

View File

@@ -30,19 +30,19 @@
<goal>install-node-and-npm</goal> <goal>install-node-and-npm</goal>
</goals> </goals>
<configuration> <configuration>
<nodeVersion>v12.20.0</nodeVersion> <nodeVersion>v12.22.12</nodeVersion>
<npmVersion>6.14.8</npmVersion> <npmVersion>6.14.16</npmVersion>
<nodeDownloadRoot>https://npm.taobao.org/mirrors/node/</nodeDownloadRoot> <nodeDownloadRoot>https://npm.taobao.org/mirrors/node/</nodeDownloadRoot>
<npmDownloadRoot>https://registry.npm.taobao.org/npm/-/</npmDownloadRoot> <npmDownloadRoot>https://registry.npm.taobao.org/npm/-/</npmDownloadRoot>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>
<id>npm install</id> <id>npm run i</id>
<goals> <goals>
<goal>npm</goal> <goal>npm</goal>
</goals> </goals>
<configuration> <configuration>
<arguments>install</arguments> <arguments>run i</arguments>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>

View File

@@ -1,16 +0,0 @@
#!/bin/sh
set -ex
#检测node版本
echo "node version: " `node -v`
echo "npm version: " `npm -v`
pwd=`pwd`
echo "start install"
# npm run clean
npm run i
echo "install success"
echo "start build"
rm -rf pub/
lerna run build
echo "build success"

View File

@@ -1,17 +0,0 @@
#!/bin/sh
set -ex
# rm -rf node_modules package-lock.json packages/*/node_modules packages/*/package-lock.json yarn.lock packages/*/yarn.lock
#检测node版本
echo "node version: " `node -v`
echo "npm version: " `npm -v`
pwd=`pwd`
echo "start develop"
npm run i
echo "本地开发请打开 http://localhost:8000"
lerna run start
echo "start success"