未验证 提交 6515dd28 编写于 作者: E EricZeng 提交者: GitHub

Merge pull request #589 from didi/master

合并主分支
## v3.0.0-beta.2
**文档**
- 新增登录系统对接文档
- 优化前端工程打包构建部分文档说明
- FAQ补充KnowStreaming连接特定JMX IP的说明
**Bug修复**
- 修复logi_security_oplog表字段过短,导致删除Topic等操作无法记录的问题
- 修复ES查询时,抛java.lang.NumberFormatException: For input string: "{"value":0,"relation":"eq"}" 问题
- 修复LogStartOffset和LogEndOffset指标单位错误问题
- 修复进行副本变更时,旧副本数为NULL的问题
- 修复集群Group列表,在第二页搜索时,搜索时返回的分页信息错误问题
- 修复重置Offset时,返回的错误信息提示不一致的问题
- 修复集群查看,系统查看,LoadRebalance等页面权限点缺失问题
- 修复查询不存在的Topic时,错误信息提示不明显的问题
- 修复Windows用户打包前端工程报错的问题
- package-lock.json锁定前端依赖版本号,修复因依赖自动升级导致打包失败等问题
- 系统管理子应用,补充后端返回的Code码拦截,解决后端接口返回报错不展示的问题
- 修复用户登出后,依旧可以访问系统的问题
- 修复巡检任务配置时,数值显示错误的问题
- 修复Broker/Topic Overview 图表和图表详情问题
- 修复Job扩缩副本任务明细数据错误的问题
- 修复重置Offset时,分区ID,Offset数值无限制问题
- 修复扩缩/迁移副本时,无法选中Kafka系统Topic的问题
- 修复Topic的Config页面,编辑表单时不能正确回显当前值的问题
- 修复Broker Card返回数据后依旧展示加载态的问题
**体验优化**
- 缩短新增集群后,集群信息加载的耗时
- 集群Broker列表,增加Controller角色信息
- 优化默认密码为admin/admin
- 副本变更任务结束后,增加进行优先副本选举的操作
- Task模块任务分为Metrics、Common、Metadata三类任务,每类任务配备独立线程池,减少对Job模块的线程池,以及不同类任务之间的相互影响
- 删除代码中存在的多余无用文件
- 自动新增ES索引模版及近7天索引,减少用户搭建时需要做的事项
- 优化前端工程打包流程
- 优化登录页文案,页面左侧栏内容,单集群详情样式,Topic列表趋势图等
- 首次进入Broker/Topic图表详情时,进行预缓存数据从而优化体验
- 优化Topic详情Partition Tab的展示
- 多集群列表页增加编辑功能
- 优化副本变更时,迁移时间支持分钟级别粒度
- logi-security版本升级至2.10.13
- logi-elasticsearch-client版本升级至1.0.24
**能力提升**
- 支持Ldap登录认证
---
## v3.0.0-beta.1
**文档**
......@@ -35,6 +90,7 @@
- 增加周期任务,用于主动创建缺少的ES模版及索引的能力,减少额外的脚本操作
- 增加JMX连接的Broker地址可选择的能力
---
## v3.0.0-beta.0
......
![Logo](https://user-images.githubusercontent.com/71620349/185368586-aed82d30-1534-453d-86ff-ecfa9d0f35bd.png)
## 登录系统对接
[KnowStreaming](https://github.com/didi/KnowStreaming)(以下简称KS) 除了实现基于本地MySQL的用户登录认证方式外,还已经实现了基于Ldap的登录认证。
但是,登录认证系统并非仅此两种。因此,为了具有更好的拓展性,KS具有自定义登陆认证逻辑,快速对接已有系统的特性。
在KS中,我们将登陆认证相关的一些文件放在[km-extends](https://github.com/didi/KnowStreaming/tree/master/km-extends)模块下的[km-account](https://github.com/didi/KnowStreaming/tree/master/km-extends/km-account)模块里。
本文将介绍KS如何快速对接自有的用户登录认证系统。
### 对接步骤
- 创建一个登陆认证类,实现[LogiCommon](https://github.com/didi/LogiCommon)的LoginExtend接口;
-[application.yml](https://github.com/didi/KnowStreaming/blob/master/km-rest/src/main/resources/application.yml)中的spring.logi-security.login-extend-bean-name字段改为登陆认证类的bean名称;
```Java
//LoginExtend 接口
public interface LoginExtend {
/**
* 验证登录信息,同时记住登录状态
*/
UserBriefVO verifyLogin(AccountLoginDTO var1, HttpServletRequest var2, HttpServletResponse var3) throws LogiSecurityException;
/**
* 登出接口,清楚登录状态
*/
Result<Boolean> logout(HttpServletRequest var1, HttpServletResponse var2);
/**
* 检查是否已经登录
*/
boolean interceptorCheck(HttpServletRequest var1, HttpServletResponse var2, String var3, List<String> var4) throws IOException;
}
```
### 对接例子
我们以Ldap对接为例,说明KS如何对接登录认证系统。
+ 编写[LdapLoginServiceImpl](https://github.com/didi/KnowStreaming/blob/master/km-extends/km-account/src/main/java/com/xiaojukeji/know/streaming/km/account/login/ldap/LdapLoginServiceImpl.java)类,实现LoginExtend接口。
+ 设置[application.yml](https://github.com/didi/KnowStreaming/blob/master/km-rest/src/main/resources/application.yml)中的spring.logi-security.login-extend-bean-name=ksLdapLoginService。
完成上述两步即可实现KS对接Ldap认证登陆。
```Java
@Service("ksLdapLoginService")
public class LdapLoginServiceImpl implements LoginExtend {
@Override
public UserBriefVO verifyLogin(AccountLoginDTO loginDTO,
HttpServletRequest request,
HttpServletResponse response) throws LogiSecurityException {
String decodePasswd = AESUtils.decrypt(loginDTO.getPw());
// 去LDAP验证账密
LdapPrincipal ldapAttrsInfo = ldapAuthentication.authenticate(loginDTO.getUserName(), decodePasswd);
if (ldapAttrsInfo == null) {
// 用户不存在,正常来说上如果有问题,上一步会直接抛出异常
throw new LogiSecurityException(ResultCode.USER_NOT_EXISTS);
}
// 进行业务相关操作
// 记录登录状态,Ldap因为无法记录登录状态,因此有KnowStreaming进行记录
initLoginContext(request, response, loginDTO.getUserName(), user.getId());
return CopyBeanUtil.copy(user, UserBriefVO.class);
}
@Override
public Result<Boolean> logout(HttpServletRequest request, HttpServletResponse response) {
//清理cookie和session
return Result.buildSucc(Boolean.TRUE);
}
@Override
public boolean interceptorCheck(HttpServletRequest request, HttpServletResponse response, String requestMappingValue, List<String> whiteMappingValues) throws IOException {
// 检查是否已经登录
String userName = HttpRequestUtil.getOperator(request);
if (StringUtils.isEmpty(userName)) {
// 未登录,则进行登出
logout(request, response);
return Boolean.FALSE;
}
return Boolean.TRUE;
}
}
```
### 实现原理
因为登陆和登出整体实现逻辑是一致的,所以我们以登陆逻辑为例进行介绍。
+ 登陆原理
登陆走的是[LogiCommon](https://github.com/didi/LogiCommon)自带的LoginController。
```java
@RestController
public class LoginController {
//登陆接口
@PostMapping({"/login"})
public Result<UserBriefVO> login(HttpServletRequest request, HttpServletResponse response, @RequestBody AccountLoginDTO loginDTO) {
try {
//登陆认证
UserBriefVO userBriefVO = this.loginService.verifyLogin(loginDTO, request, response);
return Result.success(userBriefVO);
} catch (LogiSecurityException var5) {
return Result.fail(var5);
}
}
}
```
而登陆操作是调用LoginServiceImpl类来实现,但是具体由哪个登陆认证类来执行登陆操作却由loginExtendBeanTool来指定。
```java
//LoginServiceImpl类
@Service
public class LoginServiceImpl implements LoginService {
//实现登陆操作,但是具体哪个登陆类由loginExtendBeanTool来管理
public UserBriefVO verifyLogin(AccountLoginDTO loginDTO, HttpServletRequest request, HttpServletResponse response) throws LogiSecurityException {
return this.loginExtendBeanTool.getLoginExtendImpl().verifyLogin(loginDTO, request, response);
}
}
```
而loginExtendBeanTool类会优先去查找用户指定的登陆认证类,如果失败则调用默认的登陆认证函数。
```java
//LoginExtendBeanTool类
@Component("logiSecurityLoginExtendBeanTool")
public class LoginExtendBeanTool {
public LoginExtend getLoginExtendImpl() {
LoginExtend loginExtend;
//先调用用户指定登陆类,如果失败则调用系统默认登陆认证
try {
//调用的类由spring.logi-security.login-extend-bean-name指定
loginExtend = this.getCustomLoginExtendImplBean();
} catch (UnsupportedOperationException var3) {
loginExtend = this.getDefaultLoginExtendImplBean();
}
return loginExtend;
}
}
```
+ 认证原理
认证的实现则比较简单,向Spring中注册我们的拦截器PermissionInterceptor。
拦截器会调用LoginServiceImpl类的拦截方法,LoginServiceImpl后续处理逻辑就和前面登陆是一致的。
```java
public class PermissionInterceptor implements HandlerInterceptor {
/**
* 拦截预处理
* @return boolean false:拦截, 不向下执行, true:放行
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//免登录相关校验,如果验证通过,提前返回
//走拦截函数,进行普通用户验证
return loginService.interceptorCheck(request, response, classRequestMappingValue, whiteMappingValues);
}
}
```
![Logo](https://user-images.githubusercontent.com/71620349/185368586-aed82d30-1534-453d-86ff-ecfa9d0f35bd.png)
![Logo](https://user-images.githubusercontent.com/71620349/185368586-aed82d30-1534-453d-86ff-ecfa9d0f35bd.png)
## JMX-连接失败问题解决
- [JMX-连接失败问题解决](#jmx-连接失败问题解决)
- [1、问题&说明](#1问题说明)
- [2、解决方法](#2解决方法)
- [3、解决方法 —— 认证的JMX](#3解决方法--认证的jmx)
集群正常接入Logi-KafkaManager之后,即可以看到集群的Broker列表,此时如果查看不了Topic的实时流量,或者是Broker的实时流量信息时,那么大概率就是JMX连接的问题了。
集群正常接入`KnowStreaming`之后,即可以看到集群的Broker列表,此时如果查看不了Topic的实时流量,或者是Broker的实时流量信息时,那么大概率就是`JMX`连接的问题了。
下面我们按照步骤来一步一步的检查。
### 1、问题&说明
### 1、问题说明
**类型一:JMX配置未开启**
......@@ -43,6 +38,26 @@ java.rmi.ConnectException: Connection refused to host: 192.168.0.1; nested excep
java.rmi.ConnectException: Connection refused to host: 127.0.0.1;; nested exception is:
```
**类型三:连接特定IP**
Broker 配置了内外网,而JMX在配置时,可能配置了内网IP或者外网IP,此时 `KnowStreaming` 需要连接到特定网络的IP才可以进行访问。
比如:
Broker在ZK的存储结构如下所示,我们期望连接到 `endpoints` 中标记为 `INTERNAL` 的地址,但是 `KnowStreaming` 却连接了 `EXTERNAL` 的地址,此时可以看 `4、解决方法 —— JMX连接特定网络` 进行解决。
```json
{
"listener_security_protocol_map": {"EXTERNAL":"SASL_PLAINTEXT","INTERNAL":"SASL_PLAINTEXT"},
"endpoints": ["EXTERNAL://192.168.0.1:7092","INTERNAL://192.168.0.2:7093"],
"jmx_port": 8099,
"host": "192.168.0.1",
"timestamp": "1627289710439",
"port": -1,
"version": 4
}
```
### 2、解决方法
这里仅介绍一下比较通用的解决方式,如若有更好的方式,欢迎大家指导告知一下。
......@@ -76,26 +91,36 @@ fi
如果您是直接看的这个部分,建议先看一下上一节:`2、解决方法`以确保`JMX`的配置没有问题了。
在JMX的配置等都没有问题的情况下,如果是因为认证的原因导致连接不了的,此时可以使用下面介绍的方法进行解决。
`JMX`的配置等都没有问题的情况下,如果是因为认证的原因导致连接不了的,可以在集群接入界面配置你的`JMX`认证信息。
<img src='http://img-ys011.didistatic.com/static/dc2img/do1_EUU352qMEX1Jdp7pxizp' width=350>
**当前这块后端刚刚开发完成,可能还不够完善,有问题随时沟通。**
`Logi-KafkaManager 2.2.0+`之后的版本后端已经支持`JMX`认证方式的连接,但是还没有界面,此时我们可以往`cluster`表的`jmx_properties`字段写入`JMX`的认证信息。
### 4、解决方法 —— JMX连接特定网络
这个数据是`json`格式的字符串,例子如下所示:
可以手动往`ks_km_physical_cluster`表的`jmx_properties`字段增加一个`useWhichEndpoint`字段,从而控制 `KnowStreaming` 连接到特定的JMX IP及PORT。
`jmx_properties`格式:
```json
{
"maxConn": 10, # KM对单台Broker的最大JMX连接数
"username": "xxxxx", # 用户名
"password": "xxxx", # 密码
"maxConn": 100, # KM对单台Broker的最大JMX连接数
"username": "xxxxx", # 用户名,可以不填写
"password": "xxxx", # 密码,可以不填写
"openSSL": true, # 开启SSL, true表示开启ssl, false表示关闭
"useWhichEndpoint": "EXTERNAL" #指定要连接的网络名称,填写EXTERNAL就是连接endpoints里面的EXTERNAL地址
}
```
&nbsp;
SQL例子:
SQL例子:
```sql
UPDATE cluster SET jmx_properties='{ "maxConn": 10, "username": "xxxxx", "password": "xxxx", "openSSL": false }' where id={xxx};
```
\ No newline at end of file
UPDATE ks_km_physical_cluster SET jmx_properties='{ "maxConn": 10, "username": "xxxxx", "password": "xxxx", "openSSL": false , "useWhichEndpoint": "xxx"}' where id={xxx};
```
注意:
+ 目前此功能只支持采用 `ZK` 做分布式协调的kafka集群。
\ No newline at end of file
......@@ -6,7 +6,7 @@
### 2.1.1、安装说明
-`v3.0.0-bete` 版本为例进行部署;
-`v3.0.0-beta.1` 版本为例进行部署;
- 以 CentOS-7 为例,系统基础配置要求 4C-8G;
- 部署完成后,可通过浏览器:`IP:PORT` 进行访问,默认端口是 `8080`,系统默认账号密码: `admin` / `admin2022_`
- 本文为单机部署,如需分布式部署,[请联系我们](https://knowstreaming.com/support-center)
......@@ -19,7 +19,7 @@
| ElasticSearch | v7.6+ | 8060 |
| JDK | v8+ | - |
| CentOS | v6+ | - |
| Ubantu | v16+ | - |
| Ubuntu | v16+ | - |
&nbsp;
......@@ -29,7 +29,7 @@
```bash
# 在服务器中下载安装脚本, 该脚本中会在当前目录下,重新安装MySQL。重装后的mysql密码存放在当前目录的mysql.password文件中。
wget https://s3-gzpu.didistatic.com/pub/knowstreaming/deploy_KnowStreaming.sh
wget https://s3-gzpu.didistatic.com/pub/knowstreaming/deploy_KnowStreaming-3.0.0-beta.1.sh
# 执行脚本
sh deploy_KnowStreaming.sh
......@@ -42,10 +42,10 @@ sh deploy_KnowStreaming.sh
```bash
# 将安装包下载到本地且传输到目标服务器
wget https://s3-gzpu.didistatic.com/pub/knowstreaming/KnowStreaming-3.0.0-betaoffline.tar.gz
wget https://s3-gzpu.didistatic.com/pub/knowstreaming/KnowStreaming-3.0.0-beta.1-offline.tar.gz
# 解压安装包
tar -zxf KnowStreaming-3.0.0-betaoffline.tar.gz
tar -zxf KnowStreaming-3.0.0-beta.1-offline.tar.gz
# 执行安装脚本
sh deploy_KnowStreaming-offline.sh
......@@ -62,24 +62,25 @@ sh deploy_KnowStreaming-offline.sh
- Kubernetes >= 1.14 ,Helm >= 2.17.0
- 默认配置为全部安装( ElasticSearch + MySQL + KnowStreaming)
- 默认依赖全部安装,ElasticSearch(3 节点集群模式) + MySQL(单机) + KnowStreaming-manager + KnowStreaming-ui
- 如果使用已有的 ElasticSearch(7.6.x) 和 MySQL(5.7) 只需调整 values.yaml 部分参数即可
- 使用已有的 ElasticSearch(7.6.x) 和 MySQL(5.7) 只需调整 values.yaml 部分参数即可
**安装命令**
```bash
# 下载安装包
wget https://s3-gzpu.didistatic.com/pub/knowstreaming/knowstreaming-3.0.0-hlem.tgz
# 解压安装包
tar -zxf knowstreaming-3.0.0-hlem.tgz
# 执行命令(NAMESPACE需要更改为已存在的)
helm install -n [NAMESPACE] knowstreaming knowstreaming-manager/
# 相关镜像在Docker Hub都可以下载
# 快速安装(NAMESPACE需要更改为已存在的,安装启动需要几分钟初始化请稍等~)
helm install -n [NAMESPACE] [NAME] http://download.knowstreaming.com/charts/knowstreaming-manager-0.1.3.tgz
# 获取KnowStreaming前端ui的service. 默认nodeport方式.
# (http://nodeIP:nodeport,默认用户名密码:admin/admin2022_)
# 添加仓库
helm repo add knowstreaming http://download.knowstreaming.com/charts
# 拉取最新版本
helm pull knowstreaming/knowstreaming-manager
```
&nbsp;
......@@ -219,10 +220,10 @@ sh /data/elasticsearch/control.sh status
```bash
# 下载安装包
wget https://s3-gzpu.didistatic.com/pub/knowstreaming/KnowStreaming-3.0.0-beta.tar.gz
wget https://s3-gzpu.didistatic.com/pub/knowstreaming/KnowStreaming-3.0.0-beta.1.tar.gz
# 解压安装包到指定目录
tar -zxf KnowStreaming-3.0.0-beta.tar.gz -C /data/
tar -zxf KnowStreaming-3.0.0-beta.1.tar.gz -C /data/
# 修改启动脚本并加入systemd管理
cd /data/KnowStreaming/
......@@ -236,7 +237,7 @@ mysql -uroot -pDidi_km_678 know_streaming < ./init/sql/dml-ks-km.sql
mysql -uroot -pDidi_km_678 know_streaming < ./init/sql/dml-logi.sql
# 创建elasticsearch初始化数据
sh ./init/template/template.sh
sh ./bin/init_es_template.sh
# 修改配置文件
vim ./conf/application.yml
......
![Logo](https://user-images.githubusercontent.com/71620349/185368586-aed82d30-1534-453d-86ff-ecfa9d0f35bd.png)
# `Know Streaming` 源码编译打包手册
## 1、环境信息
......@@ -11,7 +9,7 @@
`windows7+``Linux``Mac`
**环境依赖**
- Maven 3.6.3 (后端)
- Node v12.20.0/v14.17.3 (前端)
- Java 8+ (后端)
......@@ -25,27 +23,23 @@
具体见下面描述。
### 2.1、前后端合并打包
1. 下载源码;
2. 进入 `KS-KM` 工程目录,执行 `mvn -Prelease-package -Dmaven.test.skip=true clean install -U` 命令;
3. 打包命令执行完成后,会在 `km-dist/target` 目录下面生成一个 `KnowStreaming-*.tar.gz` 的安装包。
### 2.2、前端单独打包
### 2.2、前端单独打包
1. 下载源码;
2. 进入 `KS-KM/km-console` 工程目录;
3. 执行 `npm run build`命令,会在 `KS-KM/km-console` 目录下生成一个名为 `pub` 的前端静态资源包;
2. 跳转到 [前端打包构建文档](https://github.com/didi/KnowStreaming/blob/master/km-console/README.md) 按步骤进行。打包成功后,会在 `km-rest/src/main/resources` 目录下生成名为 `templates` 的前端静态资源包;
3. 如果上一步过程中报错,请查看 [FAQ](https://github.com/didi/KnowStreaming/blob/master/docs/user_guide/faq.md) 第 8.10 条;
### 2.3、后端单独打包
### 2.3、后端单独打包
1. 下载源码;
2. 修改顶层 `pom.xml` ,去掉其中的 `km-console` 模块,如下所示;
```xml
<modules>
<!-- <module>km-console</module>-->
......@@ -62,10 +56,7 @@
<module>km-rest</module>
<module>km-dist</module>
</modules>
```
3. 执行 `mvn -U clean package -Dmaven.test.skip=true`命令;
4. 执行完成之后会在 `KS-KM/km-rest/target` 目录下面生成一个 `ks-km.jar` 即为KS的后端部署的Jar包,也可以执行 `mvn -Prelease-package -Dmaven.test.skip=true clean install -U` 生成的tar包也仅有后端服务的功能;
```
3. 执行 `mvn -U clean package -Dmaven.test.skip=true`命令;
4. 执行完成之后会在 `KS-KM/km-rest/target` 目录下面生成一个 `ks-km.jar` 即为 KS 的后端部署的 Jar 包,也可以执行 `mvn -Prelease-package -Dmaven.test.skip=true clean install -U` 生成的 tar 包也仅有后端服务的功能;
......@@ -6,9 +6,79 @@
暂无
### 6.2.1、升级至 `v3.0.0-beta.2`版本
**配置变更**
```yaml
# 新增配置
spring:
logi-security: # know-streaming 依赖的 logi-security 模块的数据库的配置,默认与 know-streaming 的数据库配置保持一致即可
login-extend-bean-name: logiSecurityDefaultLoginExtendImpl # 使用的登录系统Service的Bean名称,无需修改
# 线程池大小相关配置,在task模块中,新增了三类线程池,
# 从而减少不同类型任务之间的相互影响,以及减少对logi-job内的线程池的影响
thread-pool:
task: # 任务模块的配置
metrics: # metrics采集任务配置
thread-num: 18 # metrics采集任务线程池核心线程数
queue-size: 180 # metrics采集任务线程池队列大小
metadata: # metadata同步任务配置
thread-num: 27 # metadata同步任务线程池核心线程数
queue-size: 270 # metadata同步任务线程池队列大小
common: # 剩余其他任务配置
thread-num: 15 # 剩余其他任务线程池核心线程数
queue-size: 150 # 剩余其他任务线程池队列大小
# 删除配置,下列配置将不再使用
thread-pool:
task: # 任务模块的配置
heaven: # 采集任务配置
thread-num: 20 # 采集任务线程池核心线程数
queue-size: 1000 # 采集任务线程池队列大小
```
**SQL变更**
```sql
-- 多集群管理权限2022-09-06新增
INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2000', '多集群管理查看', '1593', '1', '2', '多集群管理查看', '0', 'know-streaming');
INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2002', 'Topic-迁移副本', '1593', '1', '2', 'Topic-迁移副本', '0', 'know-streaming');
INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2004', 'Topic-扩缩副本', '1593', '1', '2', 'Topic-扩缩副本', '0', 'know-streaming');
INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2006', 'Cluster-LoadReBalance-周期均衡', '1593', '1', '2', 'Cluster-LoadReBalance-周期均衡', '0', 'know-streaming');
INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2008', 'Cluster-LoadReBalance-立即均衡', '1593', '1', '2', 'Cluster-LoadReBalance-立即均衡', '0', 'know-streaming');
INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('2010', 'Cluster-LoadReBalance-设置集群规格', '1593', '1', '2', 'Cluster-LoadReBalance-设置集群规格', '0', 'know-streaming');
-- 系统管理权限2022-09-06新增
INSERT INTO `logi_security_permission` (`id`, `permission_name`, `parent_id`, `leaf`, `level`, `description`, `is_delete`, `app_name`) VALUES ('3000', '系统管理查看', '1595', '1', '2', '系统管理查看', '0', 'know-streaming');
INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2000', '0', 'know-streaming');
INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2002', '0', 'know-streaming');
INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2004', '0', 'know-streaming');
INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2006', '0', 'know-streaming');
INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2008', '0', 'know-streaming');
INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '2010', '0', 'know-streaming');
INSERT INTO `logi_security_role_permission` (`role_id`, `permission_id`, `is_delete`, `app_name`) VALUES ('1677', '3000', '0', 'know-streaming');
-- 修改字段长度
ALTER TABLE `logi_security_oplog`
CHANGE COLUMN `operator_ip` `operator_ip` VARCHAR(64) NOT NULL COMMENT '操作者ip' ,
CHANGE COLUMN `operator` `operator` VARCHAR(64) NULL DEFAULT NULL COMMENT '操作者账号' ,
CHANGE COLUMN `operate_page` `operate_page` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '操作页面' ,
CHANGE COLUMN `operate_type` `operate_type` VARCHAR(64) NOT NULL COMMENT '操作类型' ,
CHANGE COLUMN `target_type` `target_type` VARCHAR(64) NOT NULL COMMENT '对象分类' ,
CHANGE COLUMN `target` `target` VARCHAR(1024) NOT NULL COMMENT '操作对象' ,
CHANGE COLUMN `operation_methods` `operation_methods` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '操作方式' ;
```
---
### 6.2.1、升级至 `v3.0.0-beta.1`版本
### 6.2.2、升级至 `v3.0.0-beta.1`版本
**SQL变更**
......@@ -29,7 +99,7 @@ ALTER COLUMN `operation_methods` set default '';
---
### 6.2.2、`2.x`版本 升级至 `v3.0.0-beta.0`版本
### 6.2.3、`2.x`版本 升级至 `v3.0.0-beta.0`版本
**升级步骤:**
......
# FAQ
# FAQ
## 8.1、支持哪些 Kafka 版本?
......@@ -110,20 +109,60 @@ SECURITY.TRICK_USERS
但是还有一点需要注意,绕过的用户仅能调用他有权限的接口,比如一个普通用户,那么他就只能调用普通的接口,不能去调用运维人员的接口。
## 8.8、Specified key was too long; max key length is 767 bytes
## 8.8、Specified key was too long; max key length is 767 bytes
**原因:**不同版本的InoDB引擎,参数‘innodb_large_prefix’默认值不同,即在5.6默认值为OFF,5.7默认值为ON。
**原因:** 不同版本的 InoDB 引擎,参数‘innodb_large_prefix’默认值不同,即在 5.6 默认值为 OFF,5.7 默认值为 ON。
对于引擎为InnoDB,innodb_large_prefix=OFF,且行格式为Antelope即支持REDUNDANT或COMPACT时,索引键前缀长度最大为 767 字节。innodb_large_prefix=ON,且行格式为Barracuda即支持DYNAMIC或COMPRESSED时,索引键前缀长度最大为3072字节。
对于引擎为 InnoDB,innodb_large_prefix=OFF,且行格式为 Antelope 即支持 REDUNDANT 或 COMPACT 时,索引键前缀长度最大为 767 字节。innodb_large_prefix=ON,且行格式为 Barracuda 即支持 DYNAMIC 或 COMPRESSED 时,索引键前缀长度最大为 3072 字节。
**解决方案:**
- 减少varchar字符大小低于767/4=191。
- 将字符集改为latin1(一个字符=一个字节)。
- 开启‘innodb_large_prefix’,修改默认行格式‘innodb_file_format’为Barracuda,并设置row_format=dynamic。
- 减少 varchar 字符大小低于 767/4=191。
- 将字符集改为 latin1(一个字符=一个字节)。
- 开启‘innodb_large_prefix’,修改默认行格式‘innodb_file_format’为 Barracuda,并设置 row_format=dynamic。
## 8.9、出现 ESIndexNotFoundEXception 报错
**原因 :**没有创建 ES 索引模版
**解决方案:**执行 init_es_template.sh 脚本,创建 ES 索引模版即可。
## 8.10、km-console 打包构建失败
首先,**请确保您正在使用最新版本**,版本列表见 [Tags](https://github.com/didi/KnowStreaming/tags)。如果不是最新版本,请升级后再尝试有无问题。
常见的原因是由于工程依赖没有正常安装,导致在打包过程中缺少依赖,造成打包失败。您可以检查是否有以下文件夹,且文件夹内是否有内容
```
KnowStreaming/km-console/node_modules
KnowStreaming/km-console/packages/layout-clusters-fe/node_modules
KnowStreaming/km-console/packages/config-manager-fe/node_modules
```
如果发现没有对应的 `node_modules` 目录或着目录内容为空,说明依赖没有安装成功。请按以下步骤操作,
1. 手动删除上述三个文件夹(如果有)
2. 如果之前是通过 `mvn install` 打包 `km-console`,请到项目根目录(KnowStreaming)下重新输入该指令进行打包。观察打包过程有无报错。如有报错,请见步骤 4。
3. 如果是通过本地独立构建前端工程的方式(指直接执行 `npm run build`),请进入 `KnowStreaming/km-console` 目录,执行下述步骤(注意:执行时请确保您在使用 `node v12` 版本)
a. 执行 `npm run i`。如有报错,请见步骤 4。
b. 执行 `npm run build`。如有报错,请见步骤 4。
4. 麻烦联系我们协助解决。推荐提供以下信息,方面我们快速定位问题,示例如下。
```
操作系统: Mac
命令行终端:bash
Node 版本: v12.22.12
复现步骤: 1. -> 2.
错误截图:
```
## 8.9、出现ESIndexNotFoundEXception报错
## 8.11、在 `km-console` 目录下执行 `npm run start` 时看不到应用构建和热加载过程?如何启动单个应用?
**原因 :**没有创建ES索引模版
需要到具体的应用中执行 `npm run start`,例如 `cd packages/layout-clusters-fe` 后,执行 `npm run start`
**解决方案:**执行init_es_template.sh脚本,创建ES索引模版即可。
应用启动后需要到基座应用中查看(需要启动基座应用,即 layout-clusters-fe)。
......@@ -14,6 +14,7 @@ import com.xiaojukeji.know.streaming.km.common.bean.entity.topic.Topic;
import com.xiaojukeji.know.streaming.km.common.bean.vo.cluster.res.ClusterBrokersOverviewVO;
import com.xiaojukeji.know.streaming.km.common.bean.vo.cluster.res.ClusterBrokersStateVO;
import com.xiaojukeji.know.streaming.km.common.bean.vo.kafkacontroller.KafkaControllerVO;
import com.xiaojukeji.know.streaming.km.common.constant.KafkaConstant;
import com.xiaojukeji.know.streaming.km.common.enums.SortTypeEnum;
import com.xiaojukeji.know.streaming.km.common.utils.PaginationMetricsUtil;
import com.xiaojukeji.know.streaming.km.common.utils.PaginationUtil;
......@@ -71,6 +72,9 @@ public class ClusterBrokersManagerImpl implements ClusterBrokersManager {
Topic groupTopic = topicService.getTopic(clusterPhyId, org.apache.kafka.common.internals.Topic.GROUP_METADATA_TOPIC_NAME);
Topic transactionTopic = topicService.getTopic(clusterPhyId, org.apache.kafka.common.internals.Topic.TRANSACTION_STATE_TOPIC_NAME);
//获取controller信息
KafkaController kafkaController = kafkaControllerService.getKafkaControllerFromDB(clusterPhyId);
// 格式转换
return PaginationResult.buildSuc(
this.convert2ClusterBrokersOverviewVOList(
......@@ -78,7 +82,8 @@ public class ClusterBrokersManagerImpl implements ClusterBrokersManager {
brokerList,
metricsResult.getData(),
groupTopic,
transactionTopic
transactionTopic,
kafkaController
),
paginationResult
);
......@@ -159,7 +164,8 @@ public class ClusterBrokersManagerImpl implements ClusterBrokersManager {
List<Broker> brokerList,
List<BrokerMetrics> metricsList,
Topic groupTopic,
Topic transactionTopic) {
Topic transactionTopic,
KafkaController kafkaController) {
Map<Integer, BrokerMetrics> metricsMap = metricsList == null? new HashMap<>(): metricsList.stream().collect(Collectors.toMap(BrokerMetrics::getBrokerId, Function.identity()));
Map<Integer, Broker> brokerMap = brokerList == null? new HashMap<>(): brokerList.stream().collect(Collectors.toMap(Broker::getBrokerId, Function.identity()));
......@@ -169,12 +175,12 @@ public class ClusterBrokersManagerImpl implements ClusterBrokersManager {
Broker broker = brokerMap.get(brokerId);
BrokerMetrics brokerMetrics = metricsMap.get(brokerId);
voList.add(this.convert2ClusterBrokersOverviewVO(brokerId, broker, brokerMetrics, groupTopic, transactionTopic));
voList.add(this.convert2ClusterBrokersOverviewVO(brokerId, broker, brokerMetrics, groupTopic, transactionTopic, kafkaController));
}
return voList;
}
private ClusterBrokersOverviewVO convert2ClusterBrokersOverviewVO(Integer brokerId, Broker broker, BrokerMetrics brokerMetrics, Topic groupTopic, Topic transactionTopic) {
private ClusterBrokersOverviewVO convert2ClusterBrokersOverviewVO(Integer brokerId, Broker broker, BrokerMetrics brokerMetrics, Topic groupTopic, Topic transactionTopic, KafkaController kafkaController) {
ClusterBrokersOverviewVO clusterBrokersOverviewVO = new ClusterBrokersOverviewVO();
clusterBrokersOverviewVO.setBrokerId(brokerId);
if (broker != null) {
......@@ -192,6 +198,9 @@ public class ClusterBrokersManagerImpl implements ClusterBrokersManager {
if (transactionTopic != null && transactionTopic.getBrokerIdSet().contains(brokerId)) {
clusterBrokersOverviewVO.getKafkaRoleList().add(transactionTopic.getTopicName());
}
if (kafkaController != null && kafkaController.getBrokerId().equals(brokerId)) {
clusterBrokersOverviewVO.getKafkaRoleList().add(KafkaConstant.CONTROLLER_ROLE);
}
clusterBrokersOverviewVO.setLatestMetrics(brokerMetrics);
return clusterBrokersOverviewVO;
......
......@@ -20,6 +20,7 @@ import com.xiaojukeji.know.streaming.km.common.bean.vo.group.GroupTopicOverviewV
import com.xiaojukeji.know.streaming.km.common.constant.MsgConstant;
import com.xiaojukeji.know.streaming.km.common.enums.AggTypeEnum;
import com.xiaojukeji.know.streaming.km.common.enums.GroupOffsetResetEnum;
import com.xiaojukeji.know.streaming.km.common.enums.group.GroupStateEnum;
import com.xiaojukeji.know.streaming.km.common.exception.AdminOperateException;
import com.xiaojukeji.know.streaming.km.common.exception.NotExistException;
import com.xiaojukeji.know.streaming.km.common.utils.ConvertUtil;
......@@ -75,7 +76,7 @@ public class GroupManagerImpl implements GroupManager {
}
if (!paginationResult.hasData()) {
return PaginationResult.buildSuc(dto);
return PaginationResult.buildSuc(new ArrayList<>(), paginationResult);
}
// 获取指标
......@@ -171,7 +172,7 @@ public class GroupManagerImpl implements GroupManager {
}
if (!ConsumerGroupState.EMPTY.equals(description.state()) && !ConsumerGroupState.DEAD.equals(description.state())) {
return Result.buildFromRSAndMsg(ResultStatus.KAFKA_OPERATE_FAILED, String.format("group处于%s, 重置失败(仅Empty情况可重置)", description.state().name()));
return Result.buildFromRSAndMsg(ResultStatus.KAFKA_OPERATE_FAILED, String.format("group处于%s, 重置失败(仅Empty情况可重置)", GroupStateEnum.getByRawState(description.state()).getState()));
}
// 获取offset
......
package com.xiaojukeji.know.streaming.km.common.bean.event.cluster;
import lombok.Getter;
/**
* 集群新增事件
* @author zengqiao
* @date 22/02/25
*/
@Getter
public class ClusterPhyAddedEvent extends ClusterPhyBaseEvent {
public ClusterPhyAddedEvent(Object source, Long clusterPhyId) {
super(source, clusterPhyId);
}
}
package com.xiaojukeji.know.streaming.km.common.bean.event.kafka.zk;
import lombok.Getter;
@Getter
public abstract class BaseKafkaZKEvent {
/**
* 触发时间
*/
protected Long eventTime;
/**
* 初始化数据的事件
*/
protected Boolean initEvent;
/**
* 集群ID
*/
protected Long clusterPhyId;
protected BaseKafkaZKEvent(Long eventTime, Long clusterPhyId) {
this.eventTime = eventTime;
this.clusterPhyId = clusterPhyId;
}
}
package com.xiaojukeji.know.streaming.km.common.bean.event.kafka.zk;
import lombok.Getter;
@Getter
public class ControllerChangeEvent extends BaseKafkaZKEvent {
public ControllerChangeEvent(Long eventTime, Long clusterPhyId) {
super(eventTime, clusterPhyId);
}
}
......@@ -41,6 +41,8 @@ public class KafkaConstant {
public static final Long POLL_ONCE_TIMEOUT_UNIT_MS = 2000L;
public static final String CONTROLLER_ROLE = "controller";
public static final Map<String, ConfigDef.ConfigKey> KAFKA_ALL_CONFIG_DEF_MAP = new ConcurrentHashMap<>();
static {
......
......@@ -170,6 +170,7 @@ public class ReassignConverter {
detail.setOriginalBrokerIdList(CommonUtils.string2IntList(subJobPO.getOriginalBrokerIds()));
detail.setReassignBrokerIdList(CommonUtils.string2IntList(subJobPO.getReassignBrokerIds()));
detail.setStatus(subJobPO.getStatus());
detail.setOldReplicaNum(detail.getOriginalBrokerIdList().size());
ReassignSubJobExtendData extendData = ConvertUtil.str2ObjByJson(subJobPO.getExtendData(), ReassignSubJobExtendData.class);
if (extendData != null) {
......@@ -217,6 +218,7 @@ public class ReassignConverter {
topicDetail.setPresentReplicaNum(partitionDetailList.get(0).getPresentReplicaNum());
topicDetail.setNewReplicaNum(partitionDetailList.get(0).getNewReplicaNum());
topicDetail.setOldReplicaNum(partitionDetailList.get(0).getOldReplicaNum());
topicDetail.setOriginalRetentionTimeUnitMs(partitionDetailList.get(0).getOriginalRetentionTimeUnitMs());
topicDetail.setReassignRetentionTimeUnitMs(partitionDetailList.get(0).getReassignRetentionTimeUnitMs());
......
......@@ -241,4 +241,14 @@ public class CommonUtils {
}
return intList;
}
public static boolean isNumeric(String str){
for (int i = 0; i < str.length(); i++){
if (!Character.isDigit(str.charAt(i))){
return false;
}
}
return true;
}
}
......@@ -9,6 +9,5 @@ build/
coverage
versions/
debug.log
package-lock.json
yarn.lock
target
\ No newline at end of file
## 安装项目依赖
## 前提
- 安装 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` 版本运行(例如 `node v12.22.12`)。
另外,`windows` 用户请在 `git bash` 下运行下面的命令。
## 一、进入 km-console 目录
在终端执行以下步骤时,需要先进入 `KnowStreaming/km-console` 目录。
## 二、安装项目依赖(必须)
1. 安装 lerna(可选,安装后可以直接通过 lerna 的全局指令管理项目,如果不了解 lerna 可以不安装)
```
npm install -g lerna
```
- 安装项目依赖
2. 安装项目依赖
```
npm run i
```
## 启动项目
我们默认保留了 `package-lock.json` 文件,以防止可能的依赖包自动升级导致的问题。依赖默认会通过 taobao 镜像 `https://registry.npmmirror.com/` 服务下载。
## 三、启动项目(可选,打包构建请直接看步骤三)
```
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
```
项目构建成功后,会存放到 km-rest/src/main/resources/tamplates 目录下。
## 目录结构
- packages
- layout-clusters-fe: 基座应用 & 多集群管理
- layout-clusters-fe: 基座应用 & 多集群管理(其余应用启动需要首先启动该应用)
- config-manager-fe: 子应用 - 系统管理
- tool: 启动 & 打包脚本
- ...
## 常见问题
## QA
Q: 在 `km-console` 目录下执行 `npm run start` 时看不到应用构建和热加载过程?如何启动单个应用?
Q: 执行 `npm run start` 时看不到应用构建和热加载过程?
A: 需要到具体的应用中执行 `npm run start`,例如 `cd packages/layout-clusters-fe` 后,执行 `npm run start`
如遇到其它问题,请见 [faq](https://github.com/didi/KnowStreaming/blob/master/docs/user_guide/faq.md)
此差异已折叠。
......@@ -17,15 +17,15 @@
"eslint-plugin-react": "7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"husky": "4.3.7",
"lerna": "^4.0.0",
"lerna": "^5.5.0",
"lint-staged": "10.5.3",
"prettier": "2.3.2"
},
"scripts": {
"i": "npm install && lerna bootstrap",
"clean": "rm -rf node_modules package-lock.json packages/*/node_modules packages/*/package-lock.json",
"start": "sh ./tool/start.sh",
"build": "sh ./tool/build.sh",
"start": "lerna run start",
"build": "lerna run build",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md",
"cm": "git add . && cz"
},
......
......@@ -9,5 +9,4 @@ build/
coverage
versions/
debug.log
package-lock.json
yarn.lock
\ No newline at end of file
## 使用说明
### 依赖安装:
### 依赖安装(如在 km-console 目录下执行 npm run i 安装过依赖,这步可以省略)
```
npm install
```
注意,这种方式只会安装当前应用的依赖。如果您不了解,推荐在 km-console 目录下执行 npm run i 安装依赖。
### 启动:
```
npm run start
```
该应用为子应用,启动后需要到基座应用中查看(需要启动基座应用,即 layout-clusters-fe),地址为 http://localhost:8000
### 构建:
```
......
此差异已折叠。
......@@ -18,7 +18,7 @@
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1",
"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": {
"babel-preset-react-app": "^10.0.0",
......@@ -58,6 +58,7 @@
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.1",
"@types/lodash": "^4.14.138",
"@types/react-dom": "^17.0.5",
"@types/react-router": "5.1.18",
"@types/react-router-dom": "^5.3.3",
"@types/single-spa-react": "^2.12.0",
"@typescript-eslint/eslint-plugin": "4.13.0",
......
......@@ -35,7 +35,16 @@ serviceInstance.interceptors.request.use(
// 响应拦截
serviceInstance.interceptors.response.use(
(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) => {
const config = err.config;
......
......@@ -73,12 +73,12 @@ const CheckboxGroupContainer = (props: CheckboxGroupType) => {
</Checkbox>
</div>
<Checkbox.Group disabled={disabled} style={{ width: '100%' }} value={checkedList} onChange={onCheckedChange}>
<Row gutter={[34, 10]}>
<Row gutter={[10, 10]}>
{options.map((option) => {
return (
<Col span={8} key={option.value}>
<Checkbox value={option.value} className="checkbox-content-ellipsis">
{option.label}
{option.label.replace('Cluster-Load', '')}
</Checkbox>
</Col>
);
......
......@@ -20,7 +20,7 @@ import {
IconFont,
} from 'knowdesign';
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 { RoleProps, PermissionNode, AssignUser, RoleOperate, FormItemPermission } from './config';
import api from 'api';
......@@ -50,11 +50,21 @@ const RoleDetailAndUpdate = forwardRef((props, ref): JSX.Element => {
useEffect(() => {
const globalPermissions = global.permissions;
if (globalPermissions && globalPermissions.length) {
const sysPermissions = globalPermissions.map((sys: PermissionNode) => ({
id: sys.id,
name: sys.permissionName,
options: sys.childList.map((node) => ({ label: node.permissionName, value: node.id })),
}));
const sysPermissions = globalPermissions.map((sys: PermissionNode) => {
const result = {
id: sys.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);
}
}, [global]);
......@@ -77,10 +87,12 @@ const RoleDetailAndUpdate = forwardRef((props, ref): JSX.Element => {
const onSubmit = () => {
form.validateFields().then((formData) => {
formData.permissionIdList = formData.permissionIdList.filter((l) => l);
formData.permissionIdList.forEach((arr, i) => {
// 如果分配的系统下的子权限,自动赋予该系统的权限
if (arr !== null && arr.length) {
if (!Array.isArray(arr)) {
arr = [];
}
if (arr?.length) {
arr.push(permissions[i].id);
}
});
......@@ -210,10 +222,20 @@ const RoleDetailAndUpdate = forwardRef((props, ref): JSX.Element => {
<Form.Item
label="分配权限"
name="permissionIdList"
required
rules={[
() => ({
validator(_, value) {
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.reject(new Error('请为角色至少分配一项权限'));
......
......@@ -59,5 +59,6 @@ export enum RoleOperate {
export interface FormItemPermission {
id: number;
name: string;
essentialPermission: { label: string; value: number };
options: { label: string; value: number }[];
}
......@@ -9,6 +9,5 @@ build/
coverage
versions/
debug.log
package-lock.json
yarn.lock
.d1-workspace.json
## 使用说明
### 依赖安装:
### 依赖安装(如在 km-console 目录下执行 npm run i 安装过依赖,这步可以省略)
```
npm install
```
注意,这种方式只会安装当前应用的依赖。如果您不了解,推荐在 km-console 目录下执行 npm run i 安装依赖。
### 启动:
```
npm run start
```
启动后访问地址为 http://localhost:8000
### 构建:
```
......
......@@ -17,7 +17,7 @@
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1",
"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": {
"production": [
......@@ -39,6 +39,7 @@
"@types/react-copy-to-clipboard": "^5.0.2",
"@types/react-dom": "^17.0.11",
"@types/react-highlight-words": "^0.16.0",
"@types/react-router": "5.1.18",
"@types/react-router-dom": "^5.3.3",
"@types/react-transition-group": "^4.2.2",
"@types/react-virtualized": "^9.21.13",
......
......@@ -59,6 +59,7 @@ const logout = () => {
}).then((res) => {
window.location.href = '/login';
});
localStorage.removeItem('userInfo');
};
const LicenseLimitModal = () => {
......@@ -117,7 +118,7 @@ const AppContent = (props: { setlanguage: (language: string) => void }) => {
<DProLayout.Container
headerProps={{
title: (
<div>
<div style={{ cursor: 'pointer' }}>
<img className="header-logo" src={ksLogo} />
</div>
),
......
......@@ -90,7 +90,7 @@ export default () => {
return (
<div>
<span style={{ display: 'inline-block', marginRight: '8px' }}>Similar Config</span>
<Tooltip overlayClassName="rebalance-tooltip" title="所有broker配置是否一致">
<Tooltip overlayClassName="rebalance-tooltip" title="所有Broker配置是否一致">
<QuestionCircleOutlined />
</Tooltip>
</div>
......@@ -111,7 +111,7 @@ export default () => {
];
setCardData(cordRightMap);
});
Promise.all([brokerMetric, brokersState]).then((res) => {
Promise.all([brokerMetric, brokersState]).finally(() => {
setLoading(false);
});
}, [routeParams.clusterId]);
......
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
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 StateChart from './StateChart';
import ClusterNorms from '@src/pages/LoadRebalance/ClusterNorms';
import { QuestionCircleOutlined } from '@ant-design/icons';
import moment from 'moment';
import { ClustersPermissionMap } from '@src/pages/CommonConfig';
const transUnitTimePro = (ms: number, num = 0) => {
if (!ms) return '';
......@@ -23,6 +24,7 @@ const transUnitTimePro = (ms: number, num = 0) => {
};
const LoadRebalanceCardBar = (props: any) => {
const [global] = AppContainer.useGlobalValue();
const { clusterId } = useParams<{
clusterId: string;
}>();
......@@ -53,12 +55,14 @@ const LoadRebalanceCardBar = (props: any) => {
return (
<div style={{ height: '20px' }}>
<span style={{ display: 'inline-block', marginRight: '8px' }}>State</span>
<IconFont
className="cutomIcon-config"
style={{ fontSize: '15px' }}
onClick={() => setNormsVisible(true)}
type="icon-shezhi"
></IconFont>
{global.hasPermission(ClustersPermissionMap.REBALANCE_SETTING) && (
<IconFont
className="cutomIcon-config"
style={{ fontSize: '15px' }}
onClick={() => setNormsVisible(true)}
type="icon-shezhi"
></IconFont>
)}
</div>
);
},
......
......@@ -46,30 +46,42 @@ export const supplementaryPoints = (
extraCallback?: (point: [number, 0]) => any[]
) => {
lines.forEach(({ data }) => {
// 获取未补点前线条的点的个数
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) {
let firstPointTimestamp = data[0][0] as number;
while (firstPointTimestamp - interval > timeRange[0]) {
const prePointTimestamp = firstPointTimestamp - interval;
data.unshift(extraCallback ? extraCallback([prePointTimestamp, 0]) : [prePointTimestamp, 0]);
const prevPointTimestamp = firstPointTimestamp - interval;
data.unshift(extraCallback ? extraCallback([prevPointTimestamp, 0]) : [prevPointTimestamp, 0]);
firstPointTimestamp = prevPointTimestamp;
len++;
i++;
firstPointTimestamp = prePointTimestamp;
}
}
if (i === len - 1) {
let lastPointTimestamp = data[len - 1][0] as number;
let lastPointTimestamp = data[i][0] as number;
while (lastPointTimestamp + interval < timeRange[1]) {
const next = lastPointTimestamp + interval;
data.push(extraCallback ? extraCallback([next, 0]) : [next, 0]);
lastPointTimestamp = next;
const nextPointTimestamp = lastPointTimestamp + interval;
data.push(extraCallback ? extraCallback([nextPointTimestamp, 0]) : [nextPointTimestamp, 0]);
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 len = CHART_COLOR_LIST.length;
// series 配置
return lines.map((line) => {
return lines.map((line, i) => {
return {
...line,
lineStyle: {
width: 1.5,
},
connectNulls: false,
symbol: 'emptyCircle',
symbolSize: 4,
smooth: 0.25,
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],
endValue: sliderPos[1],
zoomOnMouseWheel: false,
minValueSpan: 10 * 60 * 1000,
},
{
start: 0,
......
......@@ -63,56 +63,63 @@
}
}
}
.chart-detail-modal-container {
position: relative;
.expand-icon-box {
position: absolute;
z-index: 1000;
top: 14px;
right: 44px;
width: 24px;
height: 24px;
cursor: pointer;
font-size: 16px;
text-align: center;
border-radius: 50%;
transition: background-color 0.3s ease;
.expand-icon {
color: #adb5bc;
line-height: 24px;
}
&:hover {
background: rgba(33, 37, 41, 0.04);
.expand-icon {
color: #74788d;
}
}
.overview-chart-detail-drawer {
.dcloud-spin-nested-loading > div > .dcloud-spin.dcloud-spin-spinning {
height: 300px;
}
.detail-title {
&.dcloud-drawer .dcloud-drawer-body {
padding: 0 20px;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
.left {
display: flex;
align-items: flex-end;
.title {
align-items: flex-end;
font-weight: normal;
.title {
font-family: @font-family-bold;
font-size: 18px;
color: #495057;
letter-spacing: 0;
.unit {
font-family: @font-family-bold;
font-size: 18px;
color: #495057;
letter-spacing: 0;
.unit {
font-family: @font-family-bold;
font-size: 14px;
letter-spacing: 0.5px;
}
}
.info {
margin-left: 10px;
font-size: 14px;
letter-spacing: 0.5px;
}
}
.slider-info {
margin-left: 10px;
font-size: 12px;
font-family: @font-family;
color: #303a51;
}
}
.detail-table {
margin-top: 16px;
.chart-detail-modal-container {
position: relative;
overflow: hidden;
.expand-icon-box {
position: absolute;
z-index: 1000;
top: 14px;
right: 44px;
width: 24px;
height: 24px;
cursor: pointer;
font-size: 16px;
text-align: center;
border-radius: 50%;
transition: background-color 0.3s ease;
.expand-icon {
color: #adb5bc;
line-height: 24px;
}
&:hover {
background: rgba(33, 37, 41, 0.04);
.expand-icon {
color: #74788d;
}
}
}
.detail-table {
margin-top: 16px;
}
}
}
......@@ -216,8 +216,8 @@ const DashboardDragChart = (props: PropsType): JSX.Element => {
onChange={ksHeaderChange}
nodeScopeModule={{
customScopeList: scopeList,
scopeName: `自定义 ${dashboardType === MetricType.Broker ? 'Broker' : 'Topic'} 范围`,
showSearch: dashboardType === MetricType.Topic,
scopeName: dashboardType === MetricType.Broker ? 'Broker' : 'Topic',
scopeLabel: `自定义 ${dashboardType === MetricType.Broker ? 'Broker' : 'Topic'} 范围`,
}}
indicatorSelectModule={{
hide: false,
......
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;
......@@ -26,8 +26,8 @@ const OptionsDefault = [
const NodeScope = ({ nodeScopeModule, change }: propsType) => {
const {
customScopeList: customList,
scopeName = '自定义节点范围',
showSearch = false,
scopeName = '',
scopeLabel = '自定义范围',
searchPlaceholder = '输入内容进行搜索',
} = nodeScopeModule;
const [topNum, setTopNum] = useState<number>(5);
......@@ -70,7 +70,7 @@ const NodeScope = ({ nodeScopeModule, change }: propsType) => {
change(checkedListTemp, false);
setIsTop(false);
setTopNum(null);
setInputValue(`已选${checkedListTemp?.length}项`);
setInputValue(`${checkedListTemp?.length}项`);
setPopVisible(false);
}
};
......@@ -109,7 +109,7 @@ const NodeScope = ({ nodeScopeModule, change }: propsType) => {
{/* <span>时间:</span> */}
<div className="flx_con">
<div className="flx_l">
<h6 className="time_title">选择top范围</h6>
<h6 className="time_title">选择 top 范围</h6>
<Radio.Group
optionType="button"
buttonStyle="solid"
......@@ -128,7 +128,7 @@ const NodeScope = ({ nodeScopeModule, change }: propsType) => {
</Radio.Group>
</div>
<div className="flx_r">
<h6 className="time_title">{scopeName}</h6>
<h6 className="time_title">{scopeLabel}</h6>
<div className="custom-scope">
<div className="check-row">
<Checkbox className="check-all" indeterminate={indeterminate} onChange={onCheckAllChange} checked={checkAll}>
......@@ -136,9 +136,7 @@ const NodeScope = ({ nodeScopeModule, change }: propsType) => {
</Checkbox>
<Input
className="search-input"
suffix={
<IconFont type="icon-fangdajing" style={{ fontSize: '16px' }} />
}
suffix={<IconFont type="icon-fangdajing" style={{ fontSize: '16px' }} />}
size="small"
placeholder={searchPlaceholder}
onChange={(e) => setScopeSearchValue(e.target.value)}
......@@ -148,7 +146,7 @@ const NodeScope = ({ nodeScopeModule, change }: propsType) => {
<Checkbox.Group style={{ width: '100%' }} onChange={checkChange} value={checkedListTemp}>
<Row gutter={[10, 12]}>
{customList
.filter((item) => !showSearch || item.label.includes(scopeSearchValue))
.filter((item) => item.label.includes(scopeSearchValue))
.map((item) => (
<Col span={12} key={item.value}>
<Checkbox value={item.value}>{item.label}</Checkbox>
......@@ -180,6 +178,7 @@ const NodeScope = ({ nodeScopeModule, change }: propsType) => {
return (
<>
<div id="d-node-scope">
<div className="scope-title">{scopeName}筛选:</div>
<Popover
trigger={['click']}
visible={popVisible}
......
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 { DRangeTime } from 'knowdesign';
import IndicatorDrawer from './IndicatorDrawer';
......@@ -48,7 +48,7 @@ export interface IcustomScope {
export interface InodeScopeModule {
customScopeList: IcustomScope[];
scopeName?: string;
showSearch?: boolean;
scopeLabel?: string;
searchPlaceholder?: string;
change?: () => void;
}
......@@ -138,9 +138,13 @@ const SingleChartHeader = ({
};
const reloadRangeTime = () => {
const timeLen = rangeTime[1] - rangeTime[0] || 0;
const curTimeStamp = moment().valueOf();
setRangeTime([curTimeStamp - timeLen, curTimeStamp]);
if (isRelativeRangeTime) {
const timeLen = rangeTime[1] - rangeTime[0] || 0;
const curTimeStamp = moment().valueOf();
setRangeTime([curTimeStamp - timeLen, curTimeStamp]);
} else {
setRangeTime([...rangeTime]);
}
};
const openIndicatorDrawer = () => {
......@@ -174,12 +178,10 @@ const SingleChartHeader = ({
{!hideGridSelect && (
<Select className="grid-select" style={{ width: 70 }} value={gridNum} options={GRID_SIZE_OPTIONS} onChange={sizeChange} />
)}
<Divider type="vertical" style={{ height: 20, top: 0 }} />
<Tooltip title="点击指标筛选,可选择指标" placement="bottomRight">
<div className="icon-box" onClick={openIndicatorDrawer}>
<IconFont className="icon" type="icon-shezhi1" />
</div>
</Tooltip>
{(!hideNodeScope || !hideGridSelect) && <Divider type="vertical" style={{ height: 20, top: 0 }} />}
<Button type="primary" onClick={openIndicatorDrawer}>
指标筛选
</Button>
</div>
</div>
</div>
......
......@@ -3,8 +3,13 @@
@import '~knowdesign/es/basic/style/mixins/index';
#d-node-scope {
display: flex;
align-items: center;
position: relative;
display: inline-block;
.scope-title {
font-size: 14px;
color: #74788d;
}
.input-span {
cursor: pointer;
}
......@@ -29,10 +34,10 @@
box-shadow: none;
}
&.relativeTime {
width: 160px;
width: 200px;
}
&.absoluteTime {
width: 300px;
width: 200px;
}
input {
......
......@@ -30,8 +30,8 @@ const { TextArea } = Input;
const { Option } = Select;
const jobNameMap: any = {
expandAndReduce: '批量扩缩副本',
transfer: '批量迁移副本',
expandAndReduce: '扩缩副本',
transfer: '迁移副本',
};
interface DefaultConfig {
......@@ -325,8 +325,7 @@ export default (props: DefaultConfig) => {
!jobId &&
Utils.request(Api.getTopicMetaData(+routeParams.clusterId))
.then((res: any) => {
const filterRes = res.filter((item: any) => item.type !== 1);
const topics = (filterRes || []).map((item: any) => {
const topics = (res || []).map((item: any) => {
return {
label: item.topicName,
value: item.topicName,
......
......@@ -19,6 +19,7 @@ import {
Divider,
Transfer,
IconFont,
Tooltip,
} from 'knowdesign';
import './index.less';
import Api, { MetricType } from '@src/api/index';
......@@ -31,8 +32,8 @@ const { TextArea } = Input;
const { Option } = Select;
const jobNameMap: any = {
expandAndReduce: '批量扩缩副本',
transfer: '批量迁移副本',
expandAndReduce: '扩缩副本',
transfer: '迁移副本',
};
interface DefaultConfig {
......@@ -56,6 +57,7 @@ export default (props: DefaultConfig) => {
const [topicNewReplicas, setTopicNewReplicas] = useState([]);
const [needMovePartitions, setNeedMovePartitions] = useState([]);
const [moveDataTimeRanges, setMoveDataTimeRanges] = useState([]);
const [moveDataTimeRangesType, setMoveDataTimeRangesType] = useState([]);
const [form] = Form.useForm();
const [global] = AppContainer.useGlobalValue();
const [loadingTopic, setLoadingTopic] = useState<boolean>(true);
......@@ -142,8 +144,23 @@ export default (props: DefaultConfig) => {
title: '迁移数据时间范围',
dataIndex: 'newRetentionMs',
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 (
<InputNumber
width={80}
min={0}
max={99999}
defaultValue={moveDataTimeRanges[i]}
......@@ -153,8 +170,10 @@ export default (props: DefaultConfig) => {
moveDataTimeRangesCopy[i] = n;
setMoveDataTimeRanges(moveDataTimeRangesCopy);
}}
formatter={(value) => (value ? `${value} h` : '')}
parser={(value) => value.replace('h', '')}
className={'move-dete-time-tanges'}
// formatter={(value) => (value ? `${value} h` : '')}
// parser={(value) => value.replace('h', '')}
addonAfter={selectAfter}
></InputNumber>
);
},
......@@ -319,8 +338,7 @@ export default (props: DefaultConfig) => {
drawerVisible &&
Utils.request(Api.getTopicMetaData(+routeParams.clusterId))
.then((res: any) => {
const filterRes = res.filter((item: any) => item.type !== 1);
const topics = (filterRes || []).map((item: any) => {
const topics = (res || []).map((item: any) => {
return {
label: item.topicName,
value: item.topicName,
......@@ -402,7 +420,7 @@ export default (props: DefaultConfig) => {
originalBrokerIdList: taskPlanData[index].currentBrokerIdList,
reassignBrokerIdList: taskPlanData[index].reassignBrokerIdList,
originalRetentionTimeUnitMs: topicData[index].retentionMs,
reassignRetentionTimeUnitMs: moveDataTimeRanges[index] * 60 * 60 * 1000,
reassignRetentionTimeUnitMs: (moveDataTimeRanges[index] * 60 * 60 * 1000) / (moveDataTimeRangesType[index] || 1),
latestDaysAvgBytesInList: topicData[index].latestDaysAvgBytesInList,
latestDaysMaxBytesInList: topicData[index].latestDaysMaxBytesInList,
partitionPlanList: taskPlanData[index].partitionPlanList,
......@@ -476,6 +494,19 @@ export default (props: DefaultConfig) => {
setTopicSelectValue(v);
}}
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>
</Form.Item>
</Col>
......
......@@ -64,11 +64,6 @@
.task-form {
margin-top: 16px;
}
.dcloud-select-selector {
max-height: 100px;
overflow: scroll;
}
}
.preview-task-plan-drawer {
......@@ -80,4 +75,18 @@
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;
}
}
}
\ No newline at end of file
......@@ -70,7 +70,7 @@ const ClusterDetailSteps = {
},
},
{
target: '.single-cluster-detail .ks-chart-container-header .header-right .icon-box',
target: '.single-cluster-detail .ks-chart-container-header .header-right .dcloud-btn',
title: '指标筛选',
content: '点击这里可以对展示的图表进行筛选',
placement: 'left-start' as const,
......
import moment from 'moment';
export const CHART_COLOR_LIST = [
'#657DFC',
'#A7B1EB',
'#2AC8E4',
'#9DDEEB',
'#3991FF',
'#556ee6',
'#94BEF2',
'#95e7ff',
'#9DDEEB',
'#A7B1EB',
'#C2D0E3',
'#F5B6B3',
'#85C80D',
'#C9E795',
'#A76CEC',
'#CCABF1',
'#FF9C1B',
'#F5C993',
'#FFC300',
'#F9D77B',
'#12CA7A',
'#8BA3C4',
'#FF7066',
'#F5C993',
'#A7E6C7',
'#F19FC9',
'#AEAEAE',
'#D1D1D1',
'#F5B6B3',
'#C9E795',
];
export const UNIT_MAP = {
......
......@@ -12,20 +12,6 @@ export const leftMenus = (clusterId?: string) => ({
name: 'cluster',
path: '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',
......@@ -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
? {
name: 'produce-consume',
......@@ -127,11 +132,6 @@ export const leftMenus = (clusterId?: string) => ({
// path: 'acls',
// icon: 'icon-wodegongzuotai',
// },
{
name: 'jobs',
path: 'jobs',
icon: 'icon-Jobs',
},
].filter((m) => m),
});
......
......@@ -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;
}
}
......@@ -44,6 +44,10 @@ export default {
[`menu.${systemKey}.consumer-group.operating-state`]: 'Operating State',
[`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}.jobs`]: 'Job',
......
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 { useParams } from 'react-router-dom';
import Api from '@src/api';
......@@ -31,6 +31,10 @@ export const ConfigurationEdit = (props: any) => {
});
};
React.useEffect(() => {
form.setFieldsValue(props.record);
}, [props.record]);
return (
<Drawer
title={
......@@ -44,6 +48,7 @@ export const ConfigurationEdit = (props: any) => {
visible={props.visible}
onClose={() => props.setVisible(false)}
maskClosable={false}
destroyOnClose
extra={
<Space>
<Button size="small" onClick={onClose}>
......@@ -70,7 +75,7 @@ export const ConfigurationEdit = (props: any) => {
{props.record?.documentation || '-'}
</Col>
</Row>
<Form form={form} layout="vertical" initialValues={props.record}>
<Form form={form} layout="vertical">
<Form.Item name="defaultValue" label="Kafka默认配置">
<Input disabled />
</Form.Item>
......
......@@ -14,19 +14,49 @@ export const getBrokerListColumns = (arg?: any) => {
// eslint-disable-next-line react/display-name
render: (t: number, r: any) => {
return r?.alive ? (
<a
onClick={() => {
window.location.hash = `brokerId=${t || t === 0 ? t : ''}&host=${r.host || ''}`;
}}
>
{t}
</a>
<>
<a
onClick={() => {
window.location.hash = `brokerId=${t || t === 0 ? t : ''}&host=${r.host || ''}`;
}}
>
{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',
width: 120,
width: 150,
},
// {
// title: 'Rack',
......
......@@ -6,11 +6,16 @@ import { goLogin } from '@src/constants/axiosConfig';
// 权限对应表
export enum ClustersPermissionMap {
CLUSTERS_MANAGE = '多集群管理',
CLUSTERS_MANAGE_VIEW = '多集群管理查看',
// Cluster
CLUSTER_ADD = '接入集群',
CLUSTER_DEL = '删除集群',
CLUSTER_CHANGE_HEALTHY = 'Cluster-修改健康规则',
CLUSTER_CHANGE_INFO = 'Cluster-修改集群信息',
// LoadReBalance
REBALANCE_CYCLE = 'Cluster-LoadReBalance-周期均衡',
REBALANCE_IMMEDIATE = 'Cluster-LoadReBalance-立即均衡',
REBALANCE_SETTING = 'Cluster-LoadReBalance-设置集群规格',
// Broker
BROKER_CHANGE_CONFIG = 'Broker-修改Broker配置',
// Topic
......@@ -19,6 +24,8 @@ export enum ClustersPermissionMap {
TOPIC_DEL = 'Topic-删除Topic',
TOPIC_EXPOND = 'Topic-扩分区',
TOPIC_ADD = 'Topic-新增Topic',
TOPIC_MOVE_REPLICA = 'Topic-迁移副本',
TOPIC_CHANGE_REPLICA = 'Topic-扩缩副本',
// Consumers
CONSUMERS_RESET_OFFSET = 'Consumers-重置Offset',
// Test
......
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 EditTable from '../TestingProduce/component/EditTable';
import Api from '@src/api/index';
......@@ -53,11 +53,28 @@ export default (props: any) => {
const [resetOffsetVisible, setResetOffsetVisible] = useState(false);
const customFormRef: any = React.createRef();
const clusterPhyId = Number(routeParams.clusterId);
const [partitionIdList, setPartitionIdList] = useState([]);
useEffect(() => {
form.setFieldsValue({
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 = () => {
let tableData;
if (customFormRef.current) {
......@@ -160,8 +177,9 @@ export default (props: any) => {
colCustomConfigs={[
{
title: 'PartitionID',
inputType: 'number',
inputType: 'select',
placeholder: '请输入Partition',
options: partitionIdList,
},
{
title: 'Offset',
......
......@@ -30,7 +30,7 @@ const AutoPage = (props: any) => {
const searchFn = () => {
const params: getOperatingStateListParams = {
pageNo: pageIndex,
pageNo: 1,
pageSize,
fuzzySearchDTOList: [],
};
......
......@@ -61,9 +61,11 @@ const columns: any = [
const totalSize = r.totalSize ? Number(Utils.formatAssignSize(t, 'MB')) : 0;
return (
<div className="message-size">
<Tooltip title={(movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0) + '%'}>
<Tooltip
title={(movedSize === 0 && totalSize === 0 ? 100 : movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0) + '%'}
>
<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"
trailColor="#ECECF1"
showInfo={false}
......
......@@ -237,12 +237,12 @@ const RebalancePlan = (props: PropsType) => {
<Descriptions.Item labelStyle={{ width: '100px' }} label="迁移副本数">
{data?.replicas || '-'}
</Descriptions.Item>
<Descriptions.Item label="均衡阈值">
<Descriptions.Item label="均衡区间">
{data?.clusterBalanceIntervalList
? data?.clusterBalanceIntervalList?.map((item: any) => {
return (
<Tag style={{ padding: '4px 8px', backgroundColor: 'rgba(33,37,41,0.08)', marginRight: '4px' }} key={item?.priority}>
{item.type + ':' + item.intervalPercent + '%'}
<Tag style={{ padding: '4px 5px', backgroundColor: 'rgba(33,37,41,0.08)', marginRight: '4px' }} key={item?.priority}>
{item.type?.slice(0, 1).toUpperCase() + item.type?.slice(1) + ':' + ' ±' + item.intervalPercent + '%'}
</Tag>
);
})
......
......@@ -314,9 +314,13 @@ export const getTaskDetailsColumns = (arg?: any) => {
const totalSize = r.totalSize ? Number(Utils.formatAssignSize(t, 'MB')) : 0;
return (
<div className="message-size">
<Tooltip title={(movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0) + '%'}>
<Tooltip
title={
(r.success === r.total && r.total > 0 ? 100 : movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0) + '%'
}
>
<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"
showInfo={false}
/>
......@@ -438,9 +442,13 @@ export const getMoveBalanceColumns = (arg?: any) => {
const totalSize = r.totalSize ? Number(Utils.formatAssignSize(t, 'MB')) : 0;
return (
<div className="message-size">
<Tooltip title={(movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0) + '%'}>
<Tooltip
title={
(r.success === r.total && r.total > 0 ? 100 : movedSize > 0 && totalSize > 0 ? (movedSize / totalSize) * 100 : 0) + '%'
}
>
<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"
showInfo={false}
/>
......
......@@ -209,7 +209,7 @@ const JobsList: React.FC = (props: any) => {
tableProps={{
tableId: 'jobs_list',
showHeader: false,
rowKey: 'jobs_list',
rowKey: 'id',
loading: loading,
columns: getJobsListColumns({ onDelete, setViewProgress }),
dataSource: data,
......
......@@ -168,7 +168,6 @@ const BalanceDrawer: React.FC<PropsType> = ({ onClose, visible, isCycle = false,
const init = () => {
if (formData && Object.keys(formData).length > 0) {
console.log(formData, '有FormData');
const tableData = formData?.clusterBalanceIntervalList?.map((item: any) => {
const finfIndex = BalancedDimensions.findIndex((item1) => item1?.value === item?.type);
return {
......@@ -201,7 +200,6 @@ const BalanceDrawer: React.FC<PropsType> = ({ onClose, visible, isCycle = false,
priority: index + 1,
};
});
console.log(res, '表单回显立即均衡');
setTableData(res);
setDimension(['disk', 'bytesIn', 'bytesOut']);
setNodeTargetKeys([]);
......@@ -220,14 +218,12 @@ const BalanceDrawer: React.FC<PropsType> = ({ onClose, visible, isCycle = false,
throttleUnitB: values?.throttleUnitM * 1024 * 1024,
};
if (!isCycle) {
if (values?.priority === 'throughput') {
params.parallelNum = 0;
params.executionStrategy = 1;
} else if (values?.priority === 'stability') {
params.parallelNum = 1;
params.executionStrategy = 2;
}
if (values?.priority === 'throughput') {
params.parallelNum = 0;
params.executionStrategy = 1;
} else if (values?.priority === 'stability') {
params.parallelNum = 1;
params.executionStrategy = 2;
}
if (formData?.jobId) {
......@@ -382,6 +378,8 @@ const BalanceDrawer: React.FC<PropsType> = ({ onClose, visible, isCycle = false,
const drawerClose = (isArg?: boolean) => {
isArg ? onClose(isArg) : onClose();
setParallelNum(0);
setExecutionStrategy(1);
form.resetFields();
};
......@@ -540,17 +538,38 @@ const BalanceDrawer: React.FC<PropsType> = ({ onClose, visible, isCycle = false,
</Form.Item>
<h6 className="form-title">运行配置</h6>
{!isCycle && (
<Form.Item label="" name="priority" rules={[{ required: true, message: 'Principle 不能为空' }]} initialValue="throughput">
<Radio.Group onChange={priorityChange}>
<Radio value="throughput">吞吐量优先</Radio>
<Radio value="stability">稳定性优先</Radio>
<Radio value="custom">自定义</Radio>
</Radio.Group>
{isCycle && (
<Form.Item
className="schedule-cron"
name="scheduleCron"
label="任务周期"
rules={[
{
required: true,
message: `请输入!`,
},
{
validator: (_, value) => {
const valArr = value.split(' ');
if (valArr[1] === '*' || valArr[2] === '*') {
return Promise.reject(new Error('任务周期必须指定分钟、小时'));
}
return Promise.resolve();
},
},
]}
>
<CronInput />
</Form.Item>
)}
{!isCycle && (
<Form.Item label="" name="priority" rules={[{ required: true, message: 'Principle 不能为空' }]} initialValue="throughput">
<Radio.Group onChange={priorityChange}>
<Radio value="throughput">吞吐量优先</Radio>
<Radio value="stability">稳定性优先</Radio>
<Radio value="custom">自定义</Radio>
</Radio.Group>
</Form.Item>
{
<Form.Item dependencies={['priority']} style={{ marginBottom: 0 }}>
{({ getFieldValue }) =>
getFieldValue('priority') === 'custom' ? (
......@@ -600,9 +619,9 @@ const BalanceDrawer: React.FC<PropsType> = ({ onClose, visible, isCycle = false,
) : null
}
</Form.Item>
)}
}
{isCycle && (
{/* {isCycle && (
<Form.Item
name="parallelNum"
label={
......@@ -622,9 +641,9 @@ const BalanceDrawer: React.FC<PropsType> = ({ onClose, visible, isCycle = false,
>
<InputNumber min={0} max={999} placeholder="请输入任务并行度" style={{ width: '100%' }} />
</Form.Item>
)}
)} */}
{isCycle && (
{/* {isCycle && (
<Form.Item
className="schedule-cron"
name="scheduleCron"
......@@ -647,9 +666,9 @@ const BalanceDrawer: React.FC<PropsType> = ({ onClose, visible, isCycle = false,
>
<CronInput />
</Form.Item>
)}
)} */}
{isCycle && (
{/* {isCycle && (
<Form.Item
name="executionStrategy"
label={
......@@ -672,7 +691,7 @@ const BalanceDrawer: React.FC<PropsType> = ({ onClose, visible, isCycle = false,
<Radio value={2}>优先最小副本</Radio>
</Radio.Group>
</Form.Item>
)}
)} */}
<Form.Item
name="throttleUnitM"
......
......@@ -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',
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',
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',
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) => {
getList({ page: curPagination.current, size: curPagination.pageSize });
getList({ pageNo: curPagination.current, pageSize: curPagination.pageSize });
};
return (
......
......@@ -143,3 +143,19 @@
// 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;
}
}
......@@ -8,6 +8,7 @@ import api from '../../api';
import './index.less';
import LoadRebalanceCardBar from '@src/components/CardBar/LoadRebalanceCardBar';
import { BalanceFilter } from './BalanceFilter';
import { ClustersPermissionMap } from '../CommonConfig';
const Balance_Status_OPTIONS = [
{
......@@ -288,21 +289,17 @@ const LoadBalance: React.FC = (props: any) => {
setVisible(false);
};
const balanceClick = (val: boolean = false) => {
if (val) {
Utils.request(api.getBalanceForm(global?.clusterInfo?.id), {
method: 'GET',
const balanceClick = (val: boolean) => {
Utils.request(api.getBalanceForm(global?.clusterInfo?.id), {
method: 'GET',
})
.then((res: any) => {
const dataDe = res || {};
setCircleFormData(dataDe);
})
.then((res: any) => {
const dataDe = res || {};
setCircleFormData(dataDe);
})
.catch(() => {
setCircleFormData(null);
});
} else {
setCircleFormData(null);
}
.catch(() => {
setCircleFormData(null);
});
setIsCycle(val);
setVisible(true);
};
......@@ -365,19 +362,23 @@ const LoadBalance: React.FC = (props: any) => {
value: searchValue,
onChange: setSearchValue,
placeholder: '请输入 Host',
style: { width: '210px' },
style: { width: '248px' },
maxLength: 128,
}}
/>
<Button type="primary" ghost onClick={() => setPlanVisible(true)}>
均衡历史
</Button>
<Button type="primary" ghost onClick={() => balanceClick(true)}>
周期均衡
</Button>
<Button type="primary" onClick={() => balanceClick(false)}>
立即均衡
</Button>
{global.hasPermission(ClustersPermissionMap.REBALANCE_CYCLE) && (
<Button type="primary" ghost onClick={() => balanceClick(true)}>
周期均衡
</Button>
)}
{global.hasPermission(ClustersPermissionMap.REBALANCE_IMMEDIATE) && (
<Button type="primary" onClick={() => balanceClick(false)}>
立即均衡
</Button>
)}
</div>
</div>
{filterList && filterList.length > 0 && (
......
......@@ -13,7 +13,7 @@ const carouselList = [
<img className="carousel-eg-ctr-two-img img-one" src={egTwoContent} />
<div className="carousel-eg-ctr-two-desc desc-one">
<span>Github: </span>
<span>4K</span>
<span>5K</span>
<span>+ Star的项目 Know Streaming</span>
</div>
<div className="carousel-eg-ctr-two-desc desc-two">
......
import { Button, Divider, Drawer, Form, Input, InputNumber, message, Radio, Select, Spin, Space, Utils } from 'knowdesign';
import * as React from 'react';
import { useIntl } from 'react-intl';
import api from '../../api';
import { regClusterName, regUsername } from '../../constants/reg';
import api from '@src/api';
import { regClusterName, regUsername } from '@src/constants/reg';
import { bootstrapServersErrCodes, jmxErrCodes, zkErrCodes } from './config';
import CodeMirrorFormItem from '@src/components/CodeMirrorFormItem';
......@@ -21,40 +21,28 @@ word=\\"xxxxxx\\";"
`;
const AccessClusters = (props: any): JSX.Element => {
const { afterSubmitSuccess, clusterInfo, visible } = props;
const intl = useIntl();
const [form] = Form.useForm();
const { afterSubmitSuccess, infoLoading, clusterInfo, visible } = props;
const [loading, setLoading] = React.useState(false);
const [security, setSecurity] = React.useState(clusterInfo?.security || 'None');
const [curClusterInfo, setCurClusterInfo] = React.useState<any>({});
const [security, setSecurity] = React.useState(curClusterInfo?.security || 'None');
const [extra, setExtra] = React.useState({
versionExtra: '',
zooKeeperExtra: '',
bootstrapExtra: '',
jmxExtra: '',
});
const [isLowVersion, setIsLowVersion] = React.useState<any>(false);
const [zookeeperErrorStatus, setZookeeperErrorStatus] = React.useState<any>(false);
const [isLowVersion, setIsLowVersion] = React.useState<boolean>(false);
const [zookeeperErrorStatus, setZookeeperErrorStatus] = React.useState<boolean>(false);
const lastFormItemValue = React.useRef({
bootstrap: clusterInfo?.bootstrapServers || '',
zookeeper: clusterInfo?.zookeeper || '',
clientProperties: clusterInfo?.clientProperties || {},
bootstrap: curClusterInfo?.bootstrapServers || '',
zookeeper: curClusterInfo?.zookeeper || '',
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) => {
Object.keys(value).forEach((key) => {
switch (key) {
......@@ -128,10 +116,10 @@ const AccessClusters = (props: any): JSX.Element => {
zookeeper: res.zookeeper || '',
};
setLoading(true);
if (!isNaN(clusterInfo?.id)) {
if (!isNaN(curClusterInfo?.id)) {
Utils.put(api.phyCluster, {
...params,
id: clusterInfo?.id,
id: curClusterInfo?.id,
})
.then(() => {
message.success('编辑成功');
......@@ -219,7 +207,11 @@ const AccessClusters = (props: any): JSX.Element => {
});
// 如果kafkaVersion小于最低版本则提示
const showLowVersion = !(clusterInfo?.zookeeper || !clusterInfo?.kafkaVersion || clusterInfo?.kafkaVersion >= lowKafkaVersion);
const showLowVersion = !(
curClusterInfo?.zookeeper ||
!curClusterInfo?.kafkaVersion ||
curClusterInfo?.kafkaVersion >= lowKafkaVersion
);
setIsLowVersion(showLowVersion);
setExtra({
...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 (
<>
<Drawer
......@@ -256,16 +297,8 @@ const AccessClusters = (props: any): JSX.Element => {
placement="right"
width={480}
>
<Spin spinning={loading || !!infoLoading}>
<Form
form={form}
initialValues={{
security,
...clusterInfo,
}}
layout="vertical"
onValuesChange={onHandleValuesChange}
>
<Spin spinning={loading}>
<Form form={form} layout="vertical" onValuesChange={onHandleValuesChange}>
<Form.Item
name="name"
label="集群名称"
......@@ -277,11 +310,9 @@ const AccessClusters = (props: any): JSX.Element => {
if (!value) {
return Promise.reject('集群名称不能为空');
}
if (value === clusterInfo?.name) {
if (value === curClusterInfo?.name) {
return Promise.resolve();
}
if (value?.length > 128) {
return Promise.reject('集群名称长度限制在1~128字符');
}
......@@ -307,13 +338,7 @@ const AccessClusters = (props: any): JSX.Element => {
<Form.Item
name="bootstrapServers"
label="Bootstrap Servers"
extra={
extra.bootstrapExtra.includes('连接成功') ? (
<span>{extra.bootstrapExtra}</span>
) : (
<span className="error-extra-info">{extra.bootstrapExtra}</span>
)
}
extra={<span className={extra.bootstrapExtra.includes('连接成功') ? 'error-extra-info' : ''}>{extra.bootstrapExtra}</span>}
validateTrigger={'onBlur'}
rules={[
{
......@@ -349,13 +374,7 @@ const AccessClusters = (props: any): JSX.Element => {
<Form.Item
name="zookeeper"
label="Zookeeper"
extra={
extra.zooKeeperExtra.includes('连接成功') ? (
<span>{extra.zooKeeperExtra}</span>
) : (
<span className="error-extra-info">{extra.zooKeeperExtra}</span>
)
}
extra={<span className={extra.zooKeeperExtra.includes('连接成功') ? 'error-extra-info' : ''}>{extra.zooKeeperExtra}</span>}
validateStatus={zookeeperErrorStatus ? 'error' : 'success'}
validateTrigger={'onBlur'}
rules={[
......@@ -458,7 +477,7 @@ const AccessClusters = (props: any): JSX.Element => {
style={{ width: '58%' }}
rules={[
{
required: security === 'Password' || clusterInfo?.security === 'Password',
required: security === 'Password' || curClusterInfo?.security === 'Password',
validator: async (rule: any, value: string) => {
if (!value) {
return Promise.reject('用户名不能为空');
......@@ -483,7 +502,7 @@ const AccessClusters = (props: any): JSX.Element => {
style={{ width: '38%', marginRight: 0 }}
rules={[
{
required: security === 'Password' || clusterInfo?.security === 'Password',
required: security === 'Password' || curClusterInfo?.security === 'Password',
validator: async (rule: any, value: string) => {
if (!value) {
return Promise.reject('密码不能为空');
......
import { DoubleRightOutlined } from '@ant-design/icons';
import { Checkbox } from 'knowdesign';
import { CheckboxValueType } from 'knowdesign/es/basic/checkbox/Group';
import { debounce } from 'lodash';
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
const CheckboxGroup = Checkbox.Group;
interface IVersion {
firstLine: string[];
leftVersions: string[];
}
const CustomCheckGroup = (props: { kafkaVersions: string[]; onChangeCheckGroup: any }) => {
const { kafkaVersions, onChangeCheckGroup } = props;
const [checkedKafkaVersion, setCheckedKafkaVersion] = React.useState<IVersion>({
firstLine: [],
leftVersions: [],
});
const [allVersion, setAllVersion] = React.useState<IVersion>({
firstLine: [],
leftVersions: [],
});
const { kafkaVersions: newVersions, onChangeCheckGroup } = props;
const [versions, setVersions] = React.useState<string[]>([]);
const [versionsState, setVersionsState] = React.useState<{
[key: string]: boolean;
}>({});
const [indeterminate, setIndeterminate] = React.useState(false);
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);
useEffect(() => {
document.addEventListener('click', handleDocumentClick);
return () => {
document.removeEventListener('click', handleDocumentClick);
};
}, []);
const handleDocumentClick = (e: Event) => {
setShowMore(false);
};
const setCheckAllStauts = (list: string[], otherList: string[]) => {
onChangeCheckGroup([...list, ...otherList]);
setIndeterminate(!!list.length && list.length + otherList.length < kafkaVersions.length);
setCheckAll(list.length + otherList.length === kafkaVersions.length);
};
const getTwoPanelVersion = () => {
const updateGroupInfo = () => {
const width = (document.getElementsByClassName('custom-check-group')[0] as any)?.offsetWidth;
const checkgroupWidth = width - 100 - 86;
const num = (checkgroupWidth / 108) | 0;
const firstLine = Array.from(kafkaVersions).splice(0, num);
setMoreGroupWidth(num * 108 + 88 + 66);
const leftVersions = Array.from(kafkaVersions).splice(num);
return { firstLine, leftVersions };
};
const onFirstVersionChange = (list: []) => {
setCheckedKafkaVersion({
...checkedKafkaVersion,
firstLine: list,
setGroupInfo({
width: num * 108 + 88 + 66,
num,
});
};
setCheckAllStauts(list, checkedKafkaVersion.leftVersions);
const getCheckedList = (
versionState: {
[key: string]: boolean;
},
filterFunc: (item: [string, boolean], i: number) => boolean
) => {
return Object.entries(versionState)
.filter(filterFunc)
.map(([key]) => key);
};
const onLeftVersionChange = (list: []) => {
setCheckedKafkaVersion({
...checkedKafkaVersion,
leftVersions: list,
const onVersionsChange = (isFirstLine: boolean, list: CheckboxValueType[]) => {
const newVersionsState = { ...versionsState };
Object.keys(newVersionsState).forEach((key, i) => {
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 versions = getTwoPanelVersion();
setCheckedKafkaVersion(
e.target.checked
? versions
: {
firstLine: [],
leftVersions: [],
}
);
onChangeCheckGroup(e.target.checked ? [...versions.firstLine, ...versions.leftVersions] : []);
const checked = e.target.checked;
const newVersionsState = { ...versionsState };
Object.keys(newVersionsState).forEach((key) => (newVersionsState[key] = checked));
setVersionsState(newVersionsState);
setIndeterminate(false);
setCheckAll(e.target.checked);
setCheckAll(checked);
onChangeCheckGroup(e.target.checked ? versions : []);
};
React.useEffect(() => {
const handleVersionLine = () => {
const versions = getTwoPanelVersion();
setAllVersion(versions);
setCheckedKafkaVersion(versions);
};
handleVersionLine();
useEffect(() => {
const newVersionsState = { ...versionsState };
Object.keys(newVersionsState).forEach((key) => {
if (!newVersions.includes(key)) {
delete newVersionsState[key];
}
});
newVersions.forEach((version) => {
if (!Object.keys(newVersionsState).includes(version)) {
newVersionsState[version] = true;
}
});
const checkedLen = Object.values(newVersionsState).filter((v) => v).length;
setVersions([...newVersions]);
setVersionsState(newVersionsState);
setIndeterminate(checkedLen && checkedLen < newVersions.length);
setCheckAll(checkedLen === newVersions.length);
onChangeCheckGroup(getCheckedList(newVersionsState, ([, state]) => state));
}, [newVersions]);
window.addEventListener('resize', handleVersionLine); //监听窗口大小改变
return () => window.removeEventListener('resize', debounce(handleVersionLine, 500));
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 (
......@@ -107,17 +113,21 @@ const CustomCheckGroup = (props: { kafkaVersions: string[]; onChangeCheckGroup:
全选
</Checkbox>
</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 ? (
<CheckboxGroup
style={{ width: moreGroupWidth }}
style={{ width: groupInfo.width }}
className="more-check-group"
options={allVersion.leftVersions}
value={checkedKafkaVersion.leftVersions}
onChange={onLeftVersionChange}
options={Array.from(versions).splice(groupInfo.num)}
value={getCheckedList(versionsState, ([, state], i) => i >= groupInfo.num && state)}
onChange={(list) => onVersionsChange(false, list)}
/>
) : null}
{allVersion.leftVersions.length ? (
{versions.length > groupInfo.num ? (
<div className="more-btn" onClick={() => setShowMore(!showMore)}>
<a>
{!showMore ? '展开更多' : '收起更多'} <DoubleRightOutlined style={{ transform: `rotate(${showMore ? '270' : '90'}deg)` }} />
......
import { AppContainer, Divider, Form, IconFont, Input, List, message, Modal, Progress, Spin, Tooltip, Utils } from 'knowdesign';
import moment from 'moment';
import React, { useEffect, useMemo, useState, useReducer } from 'react';
import API from '@src/api';
import React, { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import InfiniteScroll from 'react-infinite-scroll-component';
import { Link, useHistory } from 'react-router-dom';
import { timeFormat } from '../../constants/common';
import { IMetricPoint, linesMetric } from './config';
import { timeFormat, oneDayMillims } from '@src/constants/common';
import { IMetricPoint, linesMetric, pointsMetric } from './config';
import { useIntl } from 'react-intl';
import api, { MetricType } from '../../api';
import api, { MetricType } from '@src/api';
import { getHealthClassName, getHealthProcessColor, getHealthText } from '../SingleClusterDetail/config';
import { ClustersPermissionMap } from '../CommonConfig';
import { getUnit, getDataNumberUnit } from '@src/constants/chartConfig';
import SmallChart from '@src/components/SmallChart';
import { SearchParams } from './HomePage';
const ListScroll = (props: { loadMoreData: any; list: any; pagination: any; getPhyClusterState: any }) => {
const history = useHistory();
const [global] = AppContainer.useGlobalValue();
const [form] = Form.useForm();
const [list, setList] = useState<[]>(props.list || []);
const [loading, setLoading] = useState(false);
const [visible, setVisible] = useState(false);
const [clusterInfo, setClusterInfo] = useState({} as any);
const [pagination, setPagination] = useState(
props.pagination || {
pageNo: 1,
pageSize: 10,
total: 0,
}
);
const DEFAULT_PAGE_SIZE = 10;
const DeleteCluster = React.forwardRef((_, ref) => {
const intl = useIntl();
const [form] = Form.useForm();
const [visible, setVisible] = useState<boolean>(false);
const [clusterInfo, setClusterInfo] = useState<any>({});
const callback = useRef(() => {
return;
});
useEffect(() => {
setList(props.list || []);
setPagination(props.pagination || {});
}, [props.list, props.pagination]);
const onFinish = () => {
form.validateFields().then(() => {
Utils.delete(api.phyCluster, {
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(() => {
if (visible) {
......@@ -40,19 +53,164 @@ const ListScroll = (props: { loadMoreData: any; list: any; pagination: any; getP
}
}, [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 () => {
if (loading) {
if (isLoadingMore) {
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;
setList(_data);
setPagination(res.pagination);
setLoading(false);
setIsLoadingMore(false);
};
// 重载列表
useEffect(
() => (searchParams.isReloadAll ? reloadClusterList(pagination.pageNo * pagination.pageSize) : reloadClusterList()),
[searchParams]
);
const RenderItem = (itemData: any) => {
itemData = itemData || {};
const metrics = linesMetric;
......@@ -160,7 +318,7 @@ const ListScroll = (props: { loadMoreData: any; list: any; pagination: any; getP
title={
<span>
尚未开启 {name} 均衡策略,
<Link to={`/cluster/${itemData.id}/cluster/balance`}>前往开启</Link>
<Link to={`/cluster/${itemData.id}/operation/balance`}>前往开启</Link>
</span>
}
>
......@@ -225,11 +383,34 @@ const ListScroll = (props: { loadMoreData: any; list: any; pagination: any; getP
</div>
</div>
</div>
{global.hasPermission && global.hasPermission(ClustersPermissionMap.CLUSTER_DEL) ? (
{global.hasPermission ? (
<div className="multi-cluster-list-item-btn">
<div className="icon" onClick={(event) => onClickDeleteBtn(event, itemData)}>
<IconFont type="icon-shanchu1" />
</div>
{global.hasPermission(ClustersPermissionMap.CLUSTER_CHANGE_INFO) && (
<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>
) : (
<></>
......@@ -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 (
<>
<Spin spinning={clusterLoading}>
{useMemo(
() => (
<InfiniteScroll
dataLength={list.length}
next={loadMoreData}
hasMore={list.length < pagination.total}
loader={<Spin style={{ paddingLeft: '50%', paddingTop: 15 }} spinning={loading} />}
loader={<Spin style={{ paddingLeft: '50%', paddingTop: 15 }} spinning={true} />}
endMessage={
!pagination.total ? (
''
) : (
<Divider className="load-completed-tip" plain>
加载完成 共{pagination.total}
加载完成 共 {pagination.total}
</Divider>
)
}
......@@ -293,81 +450,11 @@ const ListScroll = (props: { loadMoreData: any; list: any; pagination: any; getP
/>
</InfiniteScroll>
),
[list, pagination, loading]
[list, pagination, isLoadingMore]
)}
<Modal
width={570}
destroyOnClose={true}
centered={true}
className="custom-modal"
wrapClassName="del-topic-modal delete-modal"
title={intl.formatMessage({
id: 'delete.cluster.confirm.title',
})}
visible={visible}
onOk={onFinish}
okText={intl.formatMessage({
id: 'btn.delete',
})}
cancelText={intl.formatMessage({
id: 'btn.cancel',
})}
onCancel={() => setVisible(false)}
okButtonProps={{
style: {
width: 56,
},
danger: true,
size: 'small',
}}
cancelButtonProps={{
style: {
width: 56,
},
size: 'small',
}}
>
<div className="tip-info">
<IconFont type="icon-warning-circle"></IconFont>
<span>
{intl.formatMessage({
id: 'delete.cluster.confirm.tip',
})}
</span>
</div>
<Form form={form} className="form" labelCol={{ span: 4 }} wrapperCol={{ span: 16 }} autoComplete="off">
<Form.Item label="集群名称" name="name" rules={[{ required: false, message: '' }]}>
<span>{clusterInfo.name}</span>
</Form.Item>
<Form.Item
label="集群名称"
name="clusterName"
rules={[
{
required: true,
message: intl.formatMessage({
id: 'delete.cluster.confirm.cluster',
}),
validator: (rule: any, value: string) => {
value = value || '';
if (!value.trim() || value.trim() !== clusterInfo.name)
return Promise.reject(
intl.formatMessage({
id: 'delete.cluster.confirm.cluster',
})
);
return Promise.resolve();
},
},
]}
>
<Input />
</Form.Item>
</Form>
</Modal>
</>
<DeleteCluster ref={deleteModalRef} />
</Spin>
);
};
export default ListScroll;
export default ClusterList;
......@@ -364,8 +364,12 @@
.multi-cluster-list-item-btn {
opacity: 1;
.icon {
width: 24px;
background: rgba(33, 37, 41, 0.04);
border-radius: 12px;
color: #74788d;
font-size: 14px;
margin-left: 10px;
}
.icon:hover {
......@@ -375,16 +379,14 @@
}
.multi-cluster-list-item-btn {
display: flex;
opacity: 0;
position: absolute;
right: 20px;
top: 8px;
z-index: 10;
text-align: right;
width: 24px;
height: 24px;
background: rgba(33, 37, 41, 0.04);
border-radius: 14px;
text-align: center;
line-height: 24px;
}
......
......@@ -7,6 +7,7 @@ import moment from 'moment';
import { timeFormat } from '../../constants/common';
import { DownOutlined } from '@ant-design/icons';
import { renderToolTipValue } from './config';
import RenderEmpty from '@src/components/RenderEmpty';
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) => {
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}`;
......@@ -73,7 +63,7 @@ const ChangeLog = () => {
<div className="change-log-panel">
<div className="title">历史变更记录</div>
{!loading && !data.length ? (
renderEmpty()
<RenderEmpty message="暂无配置记录" />
) : (
<div id="changelog-scroll-box">
<Spin spinning={loading} style={{ paddingLeft: '42%', marginTop: 100 }} />
......
/* 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 { useIntl } from 'react-intl';
import { getDetailColumn } from './config';
import API from '../../api';
import { useParams } from 'react-router-dom';
const CheckDetail = forwardRef((props: any, ref): JSX.Element => {
const intl = useIntl();
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [data, setData] = useState([]);
......@@ -28,7 +25,6 @@ const CheckDetail = forwardRef((props: any, ref): JSX.Element => {
};
const onCancel = () => {
form.resetFields();
setVisible(false);
};
......
......@@ -38,15 +38,17 @@
&-main {
.header-chart-container {
&-loading {
display: flex;
justify-content: center;
align-items: center;
}
width: 100%;
height: 244px;
margin-bottom: 12px;
.cluster-container-border();
.dcloud-spin.dcloud-spin-spinning {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 244px;
}
}
.content {
......
......@@ -14,6 +14,7 @@ import { MetricType } from '@src/api';
import { getDataNumberUnit, getUnit } from '@src/constants/chartConfig';
import SingleChartHeader, { KsHeaderOptions } from '@src/components/SingleChartHeader';
import { MAX_TIME_RANGE_WITH_SMALL_POINT_INTERVAL } from '@src/constants/common';
import RenderEmpty from '@src/components/RenderEmpty';
type ChartFilterOptions = Omit<KsHeaderOptions, 'gridNum'>;
interface MetricInfo {
......@@ -64,7 +65,7 @@ const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
const [messagesInMetricData, setMessagesInMetricData] = useState<MessagesInMetric>({
name: 'MessagesIn',
unit: '',
data: [],
data: undefined,
});
const [curHeaderOptions, setCurHeaderOptions] = useState<ChartFilterOptions>();
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));
}
// 补充缺少的图表点
const extraMetrics = result[0][2].map((info) => ({
...info,
value: 0,
}));
const supplementaryInterval =
(curHeaderOptions.rangeTime[1] - curHeaderOptions.rangeTime[0] > MAX_TIME_RANGE_WITH_SMALL_POINT_INTERVAL ? 10 : 1) * 60 * 1000;
supplementaryPoints([line], curHeaderOptions.rangeTime, supplementaryInterval, (point) => {
point.push(extraMetrics as any);
return point;
});
if (result.length) {
// 补充缺少的图表点
const extraMetrics = result[0][2].map((info) => ({
...info,
value: 0,
}));
const supplementaryInterval =
(curHeaderOptions.rangeTime[1] - curHeaderOptions.rangeTime[0] > MAX_TIME_RANGE_WITH_SMALL_POINT_INTERVAL ? 10 : 1) * 60 * 1000;
supplementaryPoints([line], curHeaderOptions.rangeTime, supplementaryInterval, (point) => {
point.push(extraMetrics as any);
return point;
});
}
setMessagesInMetricData(line);
setDefaultChartLoading(false);
......@@ -299,10 +302,9 @@ const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
<div className="cluster-detail-container-main">
{/* MessageIn 图表 */}
<div className={`header-chart-container ${!messagesInMetricData.data.length ? 'header-chart-container-loading' : ''}`}>
<div className="header-chart-container">
<Spin spinning={defaultChartLoading}>
{/* TODO: 暂时通过判断是否有图表数据来修复,有时间可以查找下宽度溢出的原因 */}
{messagesInMetricData.data.length ? (
{messagesInMetricData.data && (
<>
<div className="chart-box-title">
<Tooltip
......@@ -322,26 +324,27 @@ const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
</span>
</Tooltip>
</div>
<SingleChart
chartKey="messagesIn"
chartTypeProp="line"
showHeader={false}
wrapStyle={{
width: 'auto',
height: 210,
}}
connectEventName="clusterChart"
eventBus={busInstance}
propChartData={[messagesInMetricData]}
{...getChartConfig({
// metricName: `${messagesInMetricData.name}{unit|(${messagesInMetricData.unit})}`,
lineColor: CHART_LINE_COLORS[0],
isDefaultMetric: true,
})}
/>
{messagesInMetricData.data.length ? (
<SingleChart
chartKey="messagesIn"
chartTypeProp="line"
showHeader={false}
wrapStyle={{
width: 'auto',
height: 210,
}}
connectEventName="clusterChart"
eventBus={busInstance}
propChartData={[messagesInMetricData]}
{...getChartConfig({
lineColor: CHART_LINE_COLORS[0],
isDefaultMetric: true,
})}
/>
) : (
!defaultChartLoading && <RenderEmpty message="暂无数据" height={200} />
)}
</>
) : (
''
)}
</Spin>
</div>
......@@ -408,7 +411,7 @@ const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
) : chartLoading ? (
<></>
) : (
<Empty description="请先选择指标或刷新" style={{ width: '100%', height: '100%' }} />
<RenderEmpty message="请先选择指标或刷新" />
)}
</Row>
</Spin>
......
......@@ -153,7 +153,7 @@ const LeftSider = () => {
<Divider />
<div className="title">
<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)}>
<IconFont className="edit-icon" type="icon-bianji2" />
</div>
......@@ -239,8 +239,7 @@ const LeftSider = () => {
<AccessClusters
visible={visible}
setVisible={setVisible}
title={'edit.cluster'}
infoLoading={loading}
title="edit.cluster"
afterSubmitSuccess={getPhyClusterInfo}
clusterInfo={clusterInfo}
kafkaVersion={Object.keys(kafkaVersion)}
......
......@@ -267,10 +267,10 @@ export const getHealthySettingColumn = (form: any, data: any, clusterId: string)
<InputNumber
size="small"
min={0}
max={100}
max={1}
style={{ width: 86 }}
formatter={(value) => `${value}%`}
parser={(value: any) => value.replace('%', '')}
formatter={(value) => `${value * 100}%`}
parser={(value: any) => parseFloat(value.replace('%', '')) / 100}
/>
) : (
<InputNumber style={{ width: 86 }} size="small" {...attrs} />
......
......@@ -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;
}
}
}
}
}
......
......@@ -178,38 +178,35 @@ const ConsumeClientTest = () => {
partitionProcessRef.current = processList;
curPartitionList.current = _partitionList;
switch (until) {
case 'timestamp':
setIsStop(currentTime >= untilDate);
isStopStatus.current = currentTime >= untilDate;
break;
case 'number of messages':
setIsStop(+recordCountCur.current >= untilMsgNum);
isStopStatus.current = +recordCountCur.current >= untilMsgNum;
break;
case 'number of messages per partition': // 所有分区都达到了设定值
// 过滤出消费数量不足设定值的partition
const filtersPartition = _partitionList.filter((item: any) => item.recordCount < untilMsgNum);
curPartitionList.current = filtersPartition; // 用作下一次请求的入参
if (!isStop) {
if (!isStopStatus.current) {
switch (until) {
case 'timestamp':
setIsStop(currentTime >= untilDate);
isStopStatus.current = currentTime >= untilDate;
break;
case 'number of messages':
setIsStop(+recordCountCur.current >= untilMsgNum);
isStopStatus.current = +recordCountCur.current >= untilMsgNum;
break;
case 'number of messages per partition': // 所有分区都达到了设定值
// 过滤出消费数量不足设定值的partition
const filtersPartition = _partitionList.filter((item: any) => item.recordCount < untilMsgNum);
curPartitionList.current = filtersPartition; // 用作下一次请求的入参
setIsStop(filtersPartition.length < 1);
isStopStatus.current = filtersPartition.length < 1;
}
break;
case 'max size':
setIsStop(+recordSizeCur.current >= unitMsgSize);
isStopStatus.current = +recordSizeCur.current >= unitMsgSize;
break;
case 'max size per partition':
// 过滤出消费size不足设定值的partition
const filters = partitionConsumedList.filter((item: any) => item.recordSizeUnitB < unitMsgSize);
if (!isStop) {
break;
case 'max size':
setIsStop(+recordSizeCur.current >= unitMsgSize);
isStopStatus.current = +recordSizeCur.current >= unitMsgSize;
break;
case 'max size per partition':
// 过滤出消费size不足设定值的partition
const filters = partitionConsumedList.filter((item: any) => item.recordSizeUnitB < unitMsgSize);
setIsStop(filters.length < 1);
isStopStatus.current = filters.length < 1;
}
curPartitionList.current = filters;
break;
curPartitionList.current = filters;
break;
}
}
};
......
/* eslint-disable react/display-name */
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 { 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 =
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} />
);
......
......@@ -26,10 +26,14 @@ export const ConfigurationEdit = (props: any) => {
props.setVisible(false);
props.genData({ pageNo: 1, pageSize: 10 });
})
.catch((err: any) => { });
.catch((err: any) => {});
});
};
React.useEffect(() => {
form.setFieldsValue(props.record);
}, [props.record]);
return (
<Drawer
title={
......@@ -43,6 +47,7 @@ export const ConfigurationEdit = (props: any) => {
visible={props.visible}
onClose={() => props.setVisible(false)}
maskClosable={false}
destroyOnClose
extra={
<Space>
<Button size="small" onClick={onClose}>
......@@ -76,7 +81,7 @@ export const ConfigurationEdit = (props: any) => {
{props.record?.documentation || '-'}
</Col>
</Row>
<Form form={form} layout={'vertical'} initialValues={props.record}>
<Form form={form} layout={'vertical'}>
<Form.Item name="defaultValue" label="Kafka默认配置">
<Input disabled />
</Form.Item>
......
......@@ -77,7 +77,7 @@ export const getTopicMessagesColmns = () => {
key: 'partitionId',
},
{
title: 'offset',
title: 'Offset',
dataIndex: 'offset',
key: 'offset',
},
......
......@@ -651,3 +651,8 @@
}
}
}
.@{ant-prefix}-empty-img-default{
width: 100% !important;
}
\ No newline at end of file
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册