53 Commits

Author SHA1 Message Date
许晓东
fe759aaf74 集群字段长度增加到1024,修复漏洞(CVE-2023-20860):spring升级到5.3.26 2023-04-11 12:25:24 +08:00
许晓东
1e6a7bb269 polish 文档. 2023-02-10 20:41:15 +08:00
许晓东
fb440ae153 polish 文档. 2023-02-10 20:28:23 +08:00
许晓东
07c6714fd9 替换淘宝镜像,限制客户端ID太长显示宽度. 2023-02-09 22:23:34 +08:00
许晓东
5e9304efc2 增加限流说明,fixed acl 开启授权提示. 2023-02-07 22:24:37 +08:00
许晓东
fc7f05cf4e 限流,支持用户and客户端ID同时存在. 2023-02-06 22:11:56 +08:00
许晓东
5a87e9cad8 限流,支持用户. 2023-02-05 23:08:14 +08:00
许晓东
608f7cdc47 限流,支持客户端ID修改、删除. 2023-02-05 22:01:37 +08:00
许晓东
ee6defe5d2 限流,支持客户端ID查询. 2023-02-04 21:28:51 +08:00
许晓东
56621e0b8c 新增客户端限流菜单页. 2023-01-30 21:40:11 +08:00
许晓东
832b20a83e 客户端限流查询接口. 2023-01-09 22:11:50 +08:00
许晓东
4dbadee0d4 客户端限流 service. 2023-01-03 22:01:43 +08:00
许晓东
7d76632f08 客户端限流console 2023-01-03 21:28:11 +08:00
许晓东
d5102f626c 客户端限流console. 2023-01-02 21:28:47 +08:00
许晓东
daf77290da 客户端限流console. 2023-01-02 21:16:22 +08:00
许晓东
4df20f9ca5 contact jpg 2022-12-09 09:21:10 +08:00
许晓东
b465ba78b8 update contact 2022-11-01 19:05:40 +08:00
许晓东
9a69bad93a wechat contact. 2022-10-16 13:32:41 +08:00
许晓东
3785e9aaca wechat contact. 2022-10-16 13:31:35 +08:00
许晓东
d502da1b39 update weixin contact 2022-09-20 20:07:06 +08:00
许晓东
d6282cb902 认证授权分离页面未开启ACL时,显示效果. 2022-08-28 22:48:21 +08:00
许晓东
ca4dc2ebc9 Polish READM. 2022-08-28 22:04:31 +08:00
许晓东
50775994b5 Polish READM. 2022-08-28 22:02:21 +08:00
许晓东
3bd14a35d6 ACL的认证和授权管理功能分离. 2022-08-28 21:39:08 +08:00
许晓东
4c3fe5230c update contact jpg. 2022-08-04 09:12:39 +08:00
许晓东
57d549635b 恢复interceptory路径. 2022-07-27 18:19:50 +08:00
许晓东
923b89b6bd 更新下载地址. 2022-07-24 17:33:22 +08:00
许晓东
e9f34e1d19 支持批量删除topic. 2022-07-24 11:52:48 +08:00
许晓东
ccdcebb24d 更新icon. 2022-07-24 11:09:25 +08:00
许晓东
7ddd75e34f 更新icon. 2022-07-24 11:09:21 +08:00
许晓东
aebea435fa 更新概览. 2022-07-14 21:59:25 +08:00
许晓东
ea788313c6 更新概览. 2022-07-14 21:59:18 +08:00
许晓东
727edfcca8 支持缓存生产者连接,缓存连接默认关闭 2022-07-09 18:54:20 +08:00
Xiaodong Xu
cc1989a74b Merge pull request #17 from comdotwww/main
解决 Windows 操作系统下 CMD 路径转义的问题
2022-07-07 22:46:39 +08:00
comdotwww
0196a90b69 Update start.bat 2022-07-07 22:05:46 +08:00
许晓东
9c3e3988e0 consumer连接属性处理、联系更新 2022-07-07 20:09:27 +08:00
许晓东
458e13c9e0 缓存连接 2022-07-05 10:19:51 +08:00
许晓东
979859b232 支持在线删除消息 2022-07-04 17:16:00 +08:00
许晓东
b163e5f776 升级kafka版本从2.8.0 -> 3.2.0,增加DockerCompose部署说明 2022-06-30 20:11:29 +08:00
Xiaodong Xu
d062e18940 Merge pull request #16 from wdkang123/main
new(md): Docker DockerCompose部署方式
2022-06-30 19:42:14 +08:00
武子康
87c1e7ba4a new(md): Docker DockerCompose部署方式 2022-06-30 19:12:42 +08:00
许晓东
5194c952f2 polish README. 2022-06-29 19:17:21 +08:00
许晓东
c1cc44d32f 修复集群无活跃节点时NPE,更新README. 2022-06-29 17:22:29 +08:00
许晓东
82fafe980d 修复集群无活跃节点时NPE,更新README. 2022-06-29 17:20:57 +08:00
许晓东
34752deca2 update wechat contact. 2022-06-17 10:10:00 +08:00
yinuo
9e42e2c72a 更新联系方式 2022-05-06 11:02:28 +08:00
dongyinuo
e531f5d786 Delete weixin_contact.jpeg 2022-05-06 11:00:56 +08:00
dongyinuo
10e75ac55d Update README.md
更新联系方式
2022-05-06 10:58:55 +08:00
yinuo
4a8d09dc89 更新联系方式 2022-05-06 10:56:22 +08:00
dongyinuo
116bc100a7 Add files via upload
替换微信群图片
2022-05-06 10:48:00 +08:00
Xiaodong Xu
b1feaad9f7 Merge pull request #14 from dongyinuo/feature/dongyinuo/add/contact
Feature/dongyinuo/add/contact
2022-04-29 17:27:19 +08:00
yinuo
4d372f8374 添加联系方式 2022-04-29 17:22:44 +08:00
yinuo
4b2c544c0d 添加联系方式 2022-04-29 17:21:32 +08:00
66 changed files with 4001 additions and 641 deletions

View File

@@ -1,6 +1,6 @@
# kafka可视化管理平台
一款轻量级的kafka可视化管理平台安装配置快捷、简单易用。
为了开发的省事,没有国际化支持,只支持中文展示。
为了开发的省事,没有国际化支持,页面只支持中文展示。
用过rocketmq-console吧前端展示风格跟那个有点类似。
## 页面预览
@@ -8,7 +8,9 @@
## 集群迁移支持说明
当前主分支及日后版本不再提供消息同步、集群迁移的解决方案,如有需要,查看:[集群迁移说明](./document/datasync/集群迁移.md)
## ACL说明
acl配置说明如果kafka集群启用了ACL但是控制台没看到Acl菜单可以查看[Acl配置启用说明](./document/acl/Acl.md)
最新代码运行即可看到acl菜单将权限管理和认证的用户管理SASL_SCRAM)进行了分离。分离之后支持只开启SASL_SCRAM认证的时候未开启鉴权用户变更操作。或者使用其它认证机制下的权限管理操作可视化的权限管理但是可视化的认证用户管理目前只支持Scram。
v1.0.6版本之前如果kafka集群启用了ACL但是控制台没看到Acl菜单可以查看[Acl配置启用说明](./document/acl/Acl.md)
## 功能支持
* 多集群支持
* 集群信息
@@ -16,13 +18,18 @@ acl配置说明如果kafka集群启用了ACL但是控制台没看到Acl菜
* 消费组管理
* 消息管理
* ACL
* 客户端限流
* 运维
功能明细看这个脑图:
![功能特性](./document/img/功能特性.png)
## 安装包下载
点击下载(v1.0.4版本)[kafka-console-ui.zip](https://github.com/xxd763795151/kafka-console-ui/releases/download/v1.0.4/kafka-console-ui.zip)
点击下载(v1.0.6版本)[kafka-console-ui.zip](https://github.com/xxd763795151/kafka-console-ui/releases/download/v1.0.6/kafka-console-ui.zip)
如果安装包下载的比较慢,可以查看下面的源码打包说明,把代码下载下来,本地快速打包.
github下载慢也可以试试从gitee下载点击下载[gitee来源kafka-console-ui.zip](https://gitee.com/xiaodong_xu/kafka-console-ui/releases/download/v1.0.6/kafka-console-ui.zip)
## 快速使用
### Windows
@@ -61,7 +68,7 @@ sh bin/shutdown.sh
在新增集群的时候除了集群地址还可以输入集群的其它属性配置比如请求超时ACL配置等。如果开启了ACL切换到该集群的时候导航栏上便会出现ACL菜单支持进行相关操作目前是基于SASL_SCRAM认证授权管理支持的最完善其它的我也没验证过虽然是我开发的但是我也没具体全部验证这一块功能授权部分应该是通用的
## kafka版本
* 当前使用的kafka 2.8.0
* 当前使用的kafka 3.2.0
## 监控
仅提供运维管理功能监控、告警需要配合其它组件如有需要建议请查看https://blog.csdn.net/x763795151/article/details/119705372
@@ -70,3 +77,23 @@ sh bin/shutdown.sh
## 本地开发
如果需要本地开发,开发环境配置查看:[本地开发](./document/develop/开发配置.md)
## 登录认证和权限
目前主分支不支持登录认证,感谢@dongyinuo 同学开发了一版支持登录认证,及相关的按钮权限(主要有两个角色:管理员和普通开发人员)。
在分支feature/dongyinuo/20220501/devops 上。
如果有需要使用管理台登录认证的,可以切换到这个分支上进行打包,打包方式看 源码打包 说明。
默认登录账户admin/kafka-console-ui521
## DockerCompose部署
感谢@wdkang123 同学分享的部署方式,如果有需要请查看[DockerCompose部署方式](./document/deploy/docker部署.md)
## 联系方式
+ 微信群
<img src="./document/contact/weixin_contact.jpg" width="40%"/>
[//]: # (<img src="https://github.com/xxd763795151/kafka-console-ui/blob/main/document/contact/weixin_contact.jpg" width="40%"/>)
+ 若联系方式失效, 请联系加一下微信, 说明意图
- xxd763795151
- wxid_7jy2ezljvebt12

View File

@@ -5,4 +5,4 @@ set JAVA_OPTS=-Xmx512m -Xms512m -Xmn256m -Xss256k
set CONFIG_FILE=../config/application.yml
set TARGET=../lib/kafka-console-ui.jar
set DATA_DIR=..
%JAVA_CMD% -jar %TARGET% --spring.config.location=%CONFIG_FILE% --data.dir=%DATA_DIR%
"%JAVA_CMD%" -jar %TARGET% --spring.config.location=%CONFIG_FILE% --data.dir=%DATA_DIR%

View File

@@ -26,7 +26,7 @@ kafka:
其中说明了kafka.config.enable-acl配置项需要为true。
注意:**现在不再支持这种方式了**
## 版本说明
## v1.0.6之前的版本说明
因为现在支持多集群配置,关于多集群配置,可以看主页说明的 配置集群 介绍。
所以这里把这些额外的配置项都去掉了。

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

View File

@@ -0,0 +1,189 @@
# Docker/DockerCompose部署
# 1.快速上手
## 1.1 镜像拉取
```shell
docker pull wdkang/kafka-console-ui
```
## 1.2 查看镜像
```shell
docker images
```
## 1.3 启动服务
由于Docker内不会对数据进行持久化 所以这里推荐将数据目录映射到实体机中
详见 **2.数据持久**
```shell
docker run -d -p 7766:7766 wdkang/kafka-console-ui
```
## 1.4 查看状态
```shell
docker ps -a
```
## 1.5 查看日志
```shell
docker logs -f ${containerId}
```
## 1.6 访问服务
```shell
http://localhost:7766
```
# 2. 数据持久
推荐对数据进行持久化
## 2.1 新建目录
```shell
mkdir -p /home/kafka-console-ui/data /home/kafka-console-ui/log
cd /home/kafka-console-ui
```
## 2.2 启动服务
```shell
docker run -d -p 7766:7766 -v $PWD/data:/app/data -v $PWD/log:/app/log wdkang/kafka-console-ui
```
# 3.自主打包
## 3.1 构建镜像
**前置需求**
(可根据自身情况修改Dockerfile)
下载[kafka-console-ui.zip](https://github.com/xxd763795151/kafka-console-ui/releases)包
解压后 将Dockerfile放入文件夹的根目录
**Dockerfile**
```dockerfile
# jdk
FROM openjdk:8-jdk-alpine
# label
LABEL by="https://github.com/xxd763795151/kafka-console-ui"
# root
RUN mkdir -p /app && cd /app
WORKDIR /app
# config log data
RUN mkdir -p /app/config && mkdir -p /app/log && mkdir -p /app/data && mkdir -p /app/lib
# add file
ADD ./lib/kafka-console-ui.jar /app/lib
ADD ./config /app/config
# port
EXPOSE 7766
# start server
CMD java -jar -Xmx512m -Xms512m -Xmn256m -Xss256k /app/lib/kafka-console-ui.jar --spring.config.location="/app/config/" --logging.home="/app/log" --data.dir="/app/data"
```
**进行打包**
在文件夹根目录下
(注意末尾有个点)
```shell
docker build -t ${your_docker_hub_addr} .
```
## 3.2 上传镜像
```shell
docker push ${your_docker_hub_addr}
```
# 4.容器编排
```dockerfile
# docker-compose 编排
version: '3'
services:
# 服务名
kafka-console-ui:
# 容器名
container_name: "kafka-console-ui"
# 端口
ports:
- "7766:7766"
# 持久化
volumes:
- ./data:/app/data
- ./log:/app/log
# 防止读写文件有问题
privileged: true
user: root
# 镜像地址
image: "wdkang/kafka-console-ui"
```
## 4.1 拉取镜像
```shell
docker-compose pull kafka-console-ui
```
## 4.2 构建启动
```shell
docker-compose up --detach --build kafka-console-ui
```
## 4.3 查看状态
```shell
docker-compose ps -a
```
## 4.3 停止服务
```shell
docker-compose down
```

View File

@@ -12,8 +12,11 @@
* scala 2.13
* maven >=3.6+
* webstorm
* Node
除了webstorm是开发前端的ide可以根据自己需要代替jdk scala是必须有的。
开发的时候我本地用的node版本是v14.16.0下载目录https://nodejs.org/download/release/v14.16.0/ . 过高或过低版本是否适用,我也没测试过。
scala 2.13下载地址在这个页面最下面https://www.scala-lang.org/download/scala2.html
## 克隆代码
@@ -21,7 +24,8 @@ scala 2.13下载地址在这个页面最下面https://www.scala-lang.org/d
## 后端配置
1. 用idea打开项目
2. 打开idea的Project Structure(Settings) -> Modules -> 设置src/main/scala为Sources因为约定src/main/java是源码目录所以这里要再加一个源码目录
3. 打开idea的Project Structure(Settings) -> Libraries 添加scala sdk然后选择本地下载的scala 2.13的目录确定添加进来如果使用的idea可以直接勾选也可以不用先下载到本地
3. 打开idea的Settings -> plugins 搜索scala plugin并安装然后应该是要重启idea生效这一步必须在第4步之前
4. 打开idea的Project Structure(Settings) -> Libraries 添加scala sdk然后选择本地下载的scala 2.13的目录确定添加进来如果使用的idea可以直接勾选也可以不用先下载到本地
## 前端
前端代码在工程的ui目录下找个前端开发的ide如web storm打开进行开发即可。

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 439 KiB

View File

@@ -29,4 +29,6 @@ package.bat
cd kafka-console-ui
# linux或mac执行
sh package.sh
```
```
打包完成会在target目录下生成一个kafka-console-ui.zip的安装包

20
pom.xml
View File

@@ -10,7 +10,7 @@
</parent>
<groupId>com.xuxd</groupId>
<artifactId>kafka-console-ui</artifactId>
<version>1.0.4</version>
<version>1.0.7</version>
<name>kafka-console-ui</name>
<description>Kafka console manage ui</description>
<properties>
@@ -21,10 +21,11 @@
<ui.path>${project.basedir}/ui</ui.path>
<frontend-maven-plugin.version>1.11.0</frontend-maven-plugin.version>
<compiler.version>1.8</compiler.version>
<kafka.version>2.8.0</kafka.version>
<kafka.version>3.2.0</kafka.version>
<maven.assembly.plugin.version>3.0.0</maven.assembly.plugin.version>
<mybatis-plus-boot-starter.version>3.4.2</mybatis-plus-boot-starter.version>
<scala.version>2.13.6</scala.version>
<spring-framework.version>5.3.26</spring-framework.version>
</properties>
<dependencies>
<dependency>
@@ -76,6 +77,18 @@
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.13</artifactId>
<version>${kafka.version}</version>
<exclusions>
<exclusion>
<groupId>com.typesafe.scala-logging</groupId>
<artifactId>scala-logging_2.13</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.typesafe.scala-logging</groupId>
<artifactId>scala-logging_2.13</artifactId>
<version>3.9.2</version>
</dependency>
<dependency>
@@ -207,7 +220,8 @@
<goal>npm</goal>
</goals>
<configuration>
<arguments>install --registry=https://registry.npmjs.org/</arguments>
<!-- <arguments>install &#45;&#45;registry=https://registry.npmjs.org/</arguments>-->
<arguments>install --registry=https://registry.npm.taobao.org</arguments>
</configuration>
</execution>
<execution>

View File

@@ -1,18 +1,15 @@
package com.xuxd.kafka.console.beans;
import java.util.Objects;
import org.apache.commons.lang3.StringUtils;
import org.apache.kafka.common.acl.AccessControlEntry;
import org.apache.kafka.common.acl.AccessControlEntryFilter;
import org.apache.kafka.common.acl.AclBinding;
import org.apache.kafka.common.acl.AclBindingFilter;
import org.apache.kafka.common.acl.AclOperation;
import org.apache.kafka.common.acl.AclPermissionType;
import org.apache.kafka.common.acl.*;
import org.apache.kafka.common.resource.PatternType;
import org.apache.kafka.common.resource.ResourcePattern;
import org.apache.kafka.common.resource.ResourcePatternFilter;
import org.apache.kafka.common.resource.ResourceType;
import org.apache.kafka.common.security.auth.KafkaPrincipal;
import org.apache.kafka.common.utils.SecurityUtils;
import java.util.Objects;
/**
* kafka-console-ui.
@@ -41,7 +38,9 @@ public class AclEntry {
entry.setResourceType(binding.pattern().resourceType().name());
entry.setName(binding.pattern().name());
entry.setPatternType(binding.pattern().patternType().name());
entry.setPrincipal(KafkaPrincipal.fromString(binding.entry().principal()).getName());
// entry.setPrincipal(KafkaPrincipal.fromString(binding.entry().principal()).getName());
// 3.x版本使用该方法
entry.setPrincipal(SecurityUtils.parseKafkaPrincipal(binding.entry().principal()).getName());
entry.setHost(binding.entry().host());
entry.setOperation(binding.entry().operation().name());
entry.setPermissionType(binding.entry().permissionType().name());

View File

@@ -0,0 +1,27 @@
package com.xuxd.kafka.console.beans.dto;
import lombok.Data;
import java.util.List;
/**
* @author: xuxd
* @date: 2023/1/10 20:12
**/
@Data
public class AlterClientQuotaDTO {
private String type;
private List<String> types;
private List<String> names;
private String consumerRate;
private String producerRate;
private String requestPercentage;
private List<String> deleteConfigs;
}

View File

@@ -0,0 +1,17 @@
package com.xuxd.kafka.console.beans.dto;
import lombok.Data;
import java.util.List;
/**
* @author: xuxd
* @date: 2023/1/9 21:53
**/
@Data
public class QueryClientQuotaDTO {
private List<String> types;
private List<String> names;
}

View File

@@ -0,0 +1,82 @@
package com.xuxd.kafka.console.beans.vo;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.apache.kafka.common.config.internals.QuotaConfigs;
import org.apache.kafka.common.quota.ClientQuotaEntity;
import java.util.List;
import java.util.Map;
/**
* @author 晓东哥哥
*/
@Data
public class ClientQuotaEntityVO {
private String user;
private String client;
private String ip;
private String consumerRate;
private String producerRate;
private String requestPercentage;
public static ClientQuotaEntityVO from(ClientQuotaEntity entity, List<String> entityTypes, Map<String, Object> config) {
ClientQuotaEntityVO entityVO = new ClientQuotaEntityVO();
Map<String, String> entries = entity.entries();
entityTypes.forEach(type -> {
switch (type) {
case ClientQuotaEntity.USER:
entityVO.setUser(entries.get(type));
break;
case ClientQuotaEntity.CLIENT_ID:
entityVO.setClient(entries.get(type));
break;
case ClientQuotaEntity.IP:
entityVO.setIp(entries.get(type));
break;
default:
break;
}
});
entityVO.setConsumerRate(convert(config.getOrDefault(QuotaConfigs.CONSUMER_BYTE_RATE_OVERRIDE_CONFIG, "")));
entityVO.setProducerRate(convert(config.getOrDefault(QuotaConfigs.PRODUCER_BYTE_RATE_OVERRIDE_CONFIG, "")));
entityVO.setRequestPercentage(config.getOrDefault(QuotaConfigs.REQUEST_PERCENTAGE_OVERRIDE_CONFIG, "").toString());
return entityVO;
}
public static String convert(Object num) {
if (num == null) {
return null;
}
if (num instanceof String) {
if ((StringUtils.isBlank((String) num))) {
return (String) num;
}
}
if (num instanceof Number) {
Number number = (Number) num;
double value = number.doubleValue();
double _1kb = 1024;
double _1mb = 1024 * _1kb;
if (value < _1kb) {
return value + " Byte";
}
if (value < _1mb) {
return String.format("%.1f KB", (value / _1kb));
}
if (value >= _1mb) {
return String.format("%.1f MB", (value / _1mb));
}
}
return String.valueOf(num);
}
}

View File

@@ -0,0 +1,33 @@
package com.xuxd.kafka.console.cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalListener;
import kafka.console.KafkaConsole;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
public class TimeBasedCache<K, V> {
private LoadingCache<K, V> cache;
private KafkaConsole console;
public TimeBasedCache(CacheLoader<K, V> loader, RemovalListener<K, V> listener) {
cache = CacheBuilder.newBuilder()
.maximumSize(50) // maximum 100 records can be cached
.expireAfterAccess(30, TimeUnit.MINUTES) // cache will expire after 30 minutes of access
.removalListener(listener)
.build(loader);
}
public V get(K k) {
try {
return cache.get(k);
} catch (ExecutionException e) {
throw new RuntimeException("Get connection from cache error.", e);
}
}
}

View File

@@ -20,6 +20,12 @@ public class KafkaConfig {
private Properties properties;
private boolean cacheAdminConnection;
private boolean cacheProducerConnection;
private boolean cacheConsumerConnection;
public String getBootstrapServer() {
return bootstrapServer;
}
@@ -43,4 +49,28 @@ public class KafkaConfig {
public void setProperties(Properties properties) {
this.properties = properties;
}
public boolean isCacheAdminConnection() {
return cacheAdminConnection;
}
public void setCacheAdminConnection(boolean cacheAdminConnection) {
this.cacheAdminConnection = cacheAdminConnection;
}
public boolean isCacheProducerConnection() {
return cacheProducerConnection;
}
public void setCacheProducerConnection(boolean cacheProducerConnection) {
this.cacheProducerConnection = cacheProducerConnection;
}
public boolean isCacheConsumerConnection() {
return cacheConsumerConnection;
}
public void setCacheConsumerConnection(boolean cacheConsumerConnection) {
this.cacheConsumerConnection = cacheConsumerConnection;
}
}

View File

@@ -1,13 +1,6 @@
package com.xuxd.kafka.console.config;
import kafka.console.ClusterConsole;
import kafka.console.ConfigConsole;
import kafka.console.ConsumerConsole;
import kafka.console.KafkaAclConsole;
import kafka.console.KafkaConfigConsole;
import kafka.console.MessageConsole;
import kafka.console.OperationConsole;
import kafka.console.TopicConsole;
import kafka.console.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -52,7 +45,7 @@ public class KafkaConfiguration {
@Bean
public OperationConsole operationConsole(KafkaConfig config, TopicConsole topicConsole,
ConsumerConsole consumerConsole) {
ConsumerConsole consumerConsole) {
return new OperationConsole(config, topicConsole, consumerConsole);
}
@@ -60,4 +53,9 @@ public class KafkaConfiguration {
public MessageConsole messageConsole(KafkaConfig config) {
return new MessageConsole(config);
}
@Bean
public ClientQuotaConsole clientQuotaConsole(KafkaConfig config) {
return new ClientQuotaConsole(config);
}
}

View File

@@ -118,4 +118,14 @@ public class AclAuthController {
return aclService.deleteConsumerAcl(param.toTopicEntry(), param.toGroupEntry());
}
/**
* clear principal acls.
*
* @param param acl principal.
* @return true or false.
*/
@DeleteMapping("/clear")
public Object clearAcl(@RequestBody DeleteAclDTO param) {
return aclService.clearAcl(param.toUserEntry());
}
}

View File

@@ -1,7 +1,9 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.beans.AclEntry;
import com.xuxd.kafka.console.beans.AclUser;
import com.xuxd.kafka.console.service.AclService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
@@ -12,7 +14,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* kafka-console-ui.
* kafka-console-ui. sasl scram user.
*
* @author xuxd
* @date 2021-08-28 21:13:05
@@ -49,4 +51,11 @@ public class AclUserController {
public Object getUserDetail(@RequestParam String username) {
return aclService.getUserDetail(username);
}
@GetMapping("/scram")
public Object getSaslScramUserList(@RequestParam(required = false) String username) {
AclEntry entry = new AclEntry();
entry.setPrincipal(StringUtils.isNotBlank(username) ? username : null);
return aclService.getSaslScramUserList(entry);
}
}

View File

@@ -0,0 +1,52 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.dto.AlterClientQuotaDTO;
import com.xuxd.kafka.console.beans.dto.QueryClientQuotaDTO;
import com.xuxd.kafka.console.service.ClientQuotaService;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.web.bind.annotation.*;
/**
* @author: xuxd
* @date: 2023/1/9 21:50
**/
@RestController
@RequestMapping("/client/quota")
public class ClientQuotaController {
private final ClientQuotaService clientQuotaService;
public ClientQuotaController(ClientQuotaService clientQuotaService) {
this.clientQuotaService = clientQuotaService;
}
@PostMapping("/list")
public Object getClientQuotaConfigs(@RequestBody QueryClientQuotaDTO request) {
return clientQuotaService.getClientQuotaConfigs(request.getTypes(), request.getNames());
}
@PostMapping
public Object alterClientQuotaConfigs(@RequestBody AlterClientQuotaDTO request) {
if (request.getTypes().size() != 2) {
if (CollectionUtils.isEmpty(request.getTypes())
|| CollectionUtils.isEmpty(request.getNames())
|| request.getTypes().size() != request.getNames().size()) {
return ResponseData.create().failed("types length and names length is invalid.");
}
}
return clientQuotaService.alterClientQuotaConfigs(request);
}
@DeleteMapping
public Object deleteClientQuotaConfigs(@RequestBody AlterClientQuotaDTO request) {
if (request.getTypes().size() != 2) {
if (CollectionUtils.isEmpty(request.getTypes())
|| CollectionUtils.isEmpty(request.getNames())
|| request.getTypes().size() != request.getNames().size()) {
return ResponseData.create().failed("types length and names length is invalid.");
}
}
return clientQuotaService.deleteClientQuotaConfigs(request);
}
}

View File

@@ -1,14 +1,15 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.beans.QueryMessage;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.SendMessage;
import com.xuxd.kafka.console.beans.dto.QueryMessageDTO;
import com.xuxd.kafka.console.service.MessageService;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* kafka-console-ui.
@@ -52,4 +53,12 @@ public class MessageController {
public Object resend(@RequestBody SendMessage message) {
return messageService.resend(message);
}
@DeleteMapping
public Object delete(@RequestBody List<QueryMessage> messages) {
if (CollectionUtils.isEmpty(messages)) {
return ResponseData.create().failed("params is null");
}
return messageService.delete(messages);
}
}

View File

@@ -43,8 +43,8 @@ public class TopicController {
}
@DeleteMapping
public Object deleteTopic(@RequestParam String topic) {
return topicService.deleteTopic(topic);
public Object deleteTopic(@RequestBody List<String> topics) {
return topicService.deleteTopics(topics);
}
@GetMapping("/partition")

View File

@@ -6,28 +6,25 @@ import com.xuxd.kafka.console.config.ContextConfig;
import com.xuxd.kafka.console.config.ContextConfigHolder;
import com.xuxd.kafka.console.dao.ClusterInfoMapper;
import com.xuxd.kafka.console.utils.ConvertUtil;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
/**
* kafka-console-ui.
*
* @author xuxd
* @date 2022-01-05 19:56:25
**/
@WebFilter(filterName = "context-set-filter", urlPatterns = {"/acl/*","/user/*","/cluster/*","/config/*","/consumer/*","/message/*","/topic/*","/op/*"})
@WebFilter(filterName = "context-set-filter", urlPatterns = {"/acl/*", "/user/*", "/cluster/*", "/config/*", "/consumer/*", "/message/*", "/topic/*", "/op/*", "/client/*"})
@Slf4j
public class ContextSetFilter implements Filter {
@@ -42,8 +39,9 @@ public class ContextSetFilter implements Filter {
@Autowired
private ClusterInfoMapper clusterInfoMapper;
@Override public void doFilter(ServletRequest req, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
@Override
public void doFilter(ServletRequest req, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
try {
HttpServletRequest request = (HttpServletRequest) req;
String uri = request.getRequestURI();
@@ -72,6 +70,7 @@ public class ContextSetFilter implements Filter {
config.setProperties(ConvertUtil.toProperties(infoDO.getProperties()));
}
ContextConfigHolder.CONTEXT_CONFIG.set(config);
// log.info("current kafka config: {}", config);
}
}
chain.doFilter(req, response);

View File

@@ -42,4 +42,7 @@ public interface AclService {
ResponseData getUserDetail(String username);
ResponseData clearAcl(AclEntry entry);
ResponseData getSaslScramUserList(AclEntry entry);
}

View File

@@ -0,0 +1,18 @@
package com.xuxd.kafka.console.service;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.dto.AlterClientQuotaDTO;
import java.util.List;
/**
* @author 晓东哥哥
*/
public interface ClientQuotaService {
ResponseData getClientQuotaConfigs(List<String> types, List<String> names);
ResponseData alterClientQuotaConfigs(AlterClientQuotaDTO request);
ResponseData deleteClientQuotaConfigs(AlterClientQuotaDTO request);
}

View File

@@ -4,6 +4,8 @@ import com.xuxd.kafka.console.beans.QueryMessage;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.SendMessage;
import java.util.List;
/**
* kafka-console-ui.
*
@@ -23,4 +25,6 @@ public interface MessageService {
ResponseData send(SendMessage message);
ResponseData resend(SendMessage message);
ResponseData delete(List<QueryMessage> messages);
}

View File

@@ -4,6 +4,8 @@ import com.xuxd.kafka.console.beans.ReplicaAssignment;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.enums.TopicThrottleSwitch;
import com.xuxd.kafka.console.beans.enums.TopicType;
import java.util.Collection;
import java.util.List;
import org.apache.kafka.clients.admin.NewTopic;
@@ -19,7 +21,7 @@ public interface TopicService {
ResponseData getTopicList(String topic, TopicType type);
ResponseData deleteTopic(String topic);
ResponseData deleteTopics(Collection<String> topics);
ResponseData getTopicPartitionInfo(String topic);

View File

@@ -10,30 +10,23 @@ import com.xuxd.kafka.console.config.ContextConfigHolder;
import com.xuxd.kafka.console.dao.KafkaUserMapper;
import com.xuxd.kafka.console.service.AclService;
import com.xuxd.kafka.console.utils.SaslUtil;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;
import kafka.console.KafkaAclConsole;
import kafka.console.KafkaConfigConsole;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.kafka.clients.CommonClientConfigs;
import org.apache.kafka.clients.admin.ScramMechanism;
import org.apache.kafka.clients.admin.UserScramCredentialsDescription;
import org.apache.kafka.common.acl.AclBinding;
import org.apache.kafka.common.acl.AclOperation;
import org.apache.kafka.common.config.SaslConfigs;
import org.apache.kafka.common.security.auth.SecurityProtocol;
import org.apache.kafka.common.errors.SecurityDisabledException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import scala.Tuple2;
import java.util.*;
import java.util.stream.Collectors;
import static com.xuxd.kafka.console.utils.SaslUtil.isEnableSasl;
import static com.xuxd.kafka.console.utils.SaslUtil.isEnableScram;
@@ -139,39 +132,52 @@ public class AclServiceImpl implements AclService {
}
@Override public ResponseData getAclList(AclEntry entry) {
List<AclBinding> aclBindingList = entry.isNull() ? aclConsole.getAclList(null) : aclConsole.getAclList(entry);
List<AclBinding> aclBindingList = Collections.emptyList();
try {
aclBindingList = entry.isNull() ? aclConsole.getAclList(null) : aclConsole.getAclList(entry);
}catch (Exception ex) {
if (ex.getCause() instanceof SecurityDisabledException) {
Throwable e = ex.getCause();
log.info("SecurityDisabledException: {}", e.getMessage());
Map<String, String> hint = new HashMap<>(2);
hint.put("hint", "Security Disabled: " + e.getMessage());
return ResponseData.create().data(hint).success();
}
throw new RuntimeException(ex.getCause());
}
// List<AclBinding> aclBindingList = entry.isNull() ? aclConsole.getAclList(null) : aclConsole.getAclList(entry);
List<AclEntry> entryList = aclBindingList.stream().map(x -> AclEntry.valueOf(x)).collect(Collectors.toList());
Map<String, List<AclEntry>> entryMap = entryList.stream().collect(Collectors.groupingBy(AclEntry::getPrincipal));
Map<String, Object> resultMap = new HashMap<>();
entryMap.forEach((k, v) -> {
Map<String, List<AclEntry>> map = v.stream().collect(Collectors.groupingBy(e -> e.getResourceType() + "#" + e.getName()));
String username = SaslUtil.findUsername(ContextConfigHolder.CONTEXT_CONFIG.get().getProperties().getProperty(SaslConfigs.SASL_JAAS_CONFIG));
if (k.equals(username)) {
Map<String, Object> map2 = new HashMap<>(map);
Map<String, Object> userMap = new HashMap<>();
userMap.put("role", "admin");
map2.put("USER", userMap);
}
// String username = SaslUtil.findUsername(ContextConfigHolder.CONTEXT_CONFIG.get().getProperties().getProperty(SaslConfigs.SASL_JAAS_CONFIG));
// if (k.equals(username)) {
// Map<String, Object> map2 = new HashMap<>(map);
// Map<String, Object> userMap = new HashMap<>();
// userMap.put("role", "admin");
// map2.put("USER", userMap);
// }
resultMap.put(k, map);
});
if (entry.isNull() || StringUtils.isNotBlank(entry.getPrincipal())) {
Map<String, UserScramCredentialsDescription> detailList = configConsole.getUserDetailList(StringUtils.isNotBlank(entry.getPrincipal()) ? Collections.singletonList(entry.getPrincipal()) : null);
detailList.values().forEach(u -> {
if (!resultMap.containsKey(u.name()) && !u.credentialInfos().isEmpty()) {
String username = SaslUtil.findUsername(ContextConfigHolder.CONTEXT_CONFIG.get().getProperties().getProperty(SaslConfigs.SASL_JAAS_CONFIG));
if (!u.name().equals(username)) {
resultMap.put(u.name(), Collections.emptyMap());
} else {
Map<String, Object> map2 = new HashMap<>();
Map<String, Object> userMap = new HashMap<>();
userMap.put("role", "admin");
map2.put("USER", userMap);
resultMap.put(u.name(), map2);
}
}
});
}
// if (entry.isNull() || StringUtils.isNotBlank(entry.getPrincipal())) {
// Map<String, UserScramCredentialsDescription> detailList = configConsole.getUserDetailList(StringUtils.isNotBlank(entry.getPrincipal()) ? Collections.singletonList(entry.getPrincipal()) : null);
//
// detailList.values().forEach(u -> {
// if (!resultMap.containsKey(u.name()) && !u.credentialInfos().isEmpty()) {
// String username = SaslUtil.findUsername(ContextConfigHolder.CONTEXT_CONFIG.get().getProperties().getProperty(SaslConfigs.SASL_JAAS_CONFIG));
// if (!u.name().equals(username)) {
// resultMap.put(u.name(), Collections.emptyMap());
// } else {
// Map<String, Object> map2 = new HashMap<>();
// Map<String, Object> userMap = new HashMap<>();
// userMap.put("role", "admin");
// map2.put("USER", userMap);
// resultMap.put(u.name(), map2);
// }
// }
// });
// }
return ResponseData.create().data(new CounterMap<>(resultMap)).success();
}
@@ -236,6 +242,37 @@ public class AclServiceImpl implements AclService {
return ResponseData.create().data(vo).success();
}
@Override
public ResponseData clearAcl(AclEntry entry) {
log.info("Start clear acl, principal: {}", entry);
return aclConsole.deleteUserAcl(entry) ? ResponseData.create().success() : ResponseData.create().failed("操作失败");
}
@Override
public ResponseData getSaslScramUserList(AclEntry entry) {
Map<String, Object> resultMap = new HashMap<>();
if (entry.isNull() || StringUtils.isNotBlank(entry.getPrincipal())) {
Map<String, UserScramCredentialsDescription> detailList = configConsole.getUserDetailList(StringUtils.isNotBlank(entry.getPrincipal()) ? Collections.singletonList(entry.getPrincipal()) : null);
detailList.values().forEach(u -> {
if (!resultMap.containsKey(u.name()) && !u.credentialInfos().isEmpty()) {
String username = SaslUtil.findUsername(ContextConfigHolder.CONTEXT_CONFIG.get().getProperties().getProperty(SaslConfigs.SASL_JAAS_CONFIG));
if (!u.name().equals(username)) {
resultMap.put(u.name(), Collections.emptyMap());
} else {
Map<String, Object> map2 = new HashMap<>();
Map<String, Object> userMap = new HashMap<>();
userMap.put("role", "admin");
map2.put("USER", userMap);
resultMap.put(u.name(), map2);
}
}
});
}
return ResponseData.create().data(new CounterMap<>(resultMap)).success();
}
// @Override public void afterSingletonsInstantiated() {
// if (kafkaConfig.isEnableAcl() && kafkaConfig.isAdminCreate()) {
// log.info("Start create admin user, username: {}, password: {}", kafkaConfig.getAdminUsername(), kafkaConfig.getAdminPassword());

View File

@@ -0,0 +1,172 @@
package com.xuxd.kafka.console.service.impl;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.dto.AlterClientQuotaDTO;
import com.xuxd.kafka.console.beans.vo.ClientQuotaEntityVO;
import com.xuxd.kafka.console.service.ClientQuotaService;
import kafka.console.ClientQuotaConsole;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.kafka.common.config.internals.QuotaConfigs;
import org.apache.kafka.common.quota.ClientQuotaEntity;
import org.springframework.stereotype.Service;
import scala.Tuple2;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author 晓东哥哥
*/
@Slf4j
@Service
public class ClientQuotaServiceImpl implements ClientQuotaService {
private final ClientQuotaConsole clientQuotaConsole;
private final Map<String, String> typeDict = new HashMap<>();
private final Map<String, String> configDict = new HashMap<>();
private final String USER = "user";
private final String CLIENT_ID = "client-id";
private final String IP = "ip";
private final String USER_CLIENT = "user&client-id";
{
typeDict.put(USER, ClientQuotaEntity.USER);
typeDict.put(CLIENT_ID, ClientQuotaEntity.CLIENT_ID);
typeDict.put(IP, ClientQuotaEntity.IP);
typeDict.put(USER_CLIENT, USER_CLIENT);
configDict.put("producerRate", QuotaConfigs.PRODUCER_BYTE_RATE_OVERRIDE_CONFIG);
configDict.put("consumerRate", QuotaConfigs.CONSUMER_BYTE_RATE_OVERRIDE_CONFIG);
configDict.put("requestPercentage", QuotaConfigs.REQUEST_PERCENTAGE_OVERRIDE_CONFIG);
}
public ClientQuotaServiceImpl(ClientQuotaConsole clientQuotaConsole) {
this.clientQuotaConsole = clientQuotaConsole;
}
@Override
public ResponseData getClientQuotaConfigs(List<String> types, List<String> names) {
List<String> entityNames = names == null ? Collections.emptyList() : new ArrayList<>(names);
List<String> entityTypes = types.stream().map(e -> typeDict.get(e)).filter(e -> e != null).collect(Collectors.toList());
if (entityTypes.isEmpty() || entityTypes.size() != types.size()) {
throw new IllegalArgumentException("types illegal.");
}
boolean userAndClientFilterClientOnly = false;
// only type: [user and client-id], type.size == 2
if (entityTypes.size() == 2) {
if (names.size() == 2 && StringUtils.isBlank(names.get(0)) && StringUtils.isNotBlank(names.get(1))) {
userAndClientFilterClientOnly = true;
}
}
Map<ClientQuotaEntity, Map<String, Object>> clientQuotasConfigs = clientQuotaConsole.getClientQuotasConfigs(entityTypes,
userAndClientFilterClientOnly ? Collections.emptyList() : entityNames);
List<ClientQuotaEntityVO> voList = clientQuotasConfigs.entrySet().stream().map(entry -> ClientQuotaEntityVO.from(
entry.getKey(), entityTypes, entry.getValue())).collect(Collectors.toList());
if (!userAndClientFilterClientOnly) {
return ResponseData.create().data(voList).success();
}
List<ClientQuotaEntityVO> list = voList.stream().filter(e -> names.get(1).equals(e.getClient())).collect(Collectors.toList());
return ResponseData.create().data(list).success();
}
@Override
public ResponseData alterClientQuotaConfigs(AlterClientQuotaDTO request) {
if (StringUtils.isEmpty(request.getType()) || !typeDict.containsKey(request.getType())) {
return ResponseData.create().failed("Unknown type.");
}
List<String> types = new ArrayList<>();
List<String> names = new ArrayList<>();
parseTypesAndNames(request, types, names, request.getType());
Map<String, String> configsToBeAddedMap = new HashMap<>();
if (StringUtils.isNotEmpty(request.getProducerRate())) {
configsToBeAddedMap.put(QuotaConfigs.PRODUCER_BYTE_RATE_OVERRIDE_CONFIG, String.valueOf(Math.floor(Double.valueOf(request.getProducerRate()))));
}
if (StringUtils.isNotEmpty(request.getConsumerRate())) {
configsToBeAddedMap.put(QuotaConfigs.CONSUMER_BYTE_RATE_OVERRIDE_CONFIG, String.valueOf(Math.floor(Double.valueOf(request.getConsumerRate()))));
}
if (StringUtils.isNotEmpty(request.getRequestPercentage())) {
configsToBeAddedMap.put(QuotaConfigs.REQUEST_PERCENTAGE_OVERRIDE_CONFIG, String.valueOf(Math.floor(Double.valueOf(request.getRequestPercentage()))));
}
Tuple2<Object, String> tuple2 = clientQuotaConsole.addQuotaConfigs(types, names, configsToBeAddedMap);
if (!(Boolean) tuple2._1) {
return ResponseData.create().failed(tuple2._2);
}
if (CollectionUtils.isNotEmpty(request.getDeleteConfigs())) {
List<String> delete = request.getDeleteConfigs().stream().map(key -> configDict.get(key)).collect(Collectors.toList());
Tuple2<Object, String> tuple2Del = clientQuotaConsole.deleteQuotaConfigs(types, names, delete);
if (!(Boolean) tuple2Del._1) {
return ResponseData.create().failed(tuple2Del._2);
}
}
return ResponseData.create().success();
}
@Override
public ResponseData deleteClientQuotaConfigs(AlterClientQuotaDTO request) {
if (StringUtils.isEmpty(request.getType()) || !typeDict.containsKey(request.getType())) {
return ResponseData.create().failed("Unknown type.");
}
List<String> types = new ArrayList<>();
List<String> names = new ArrayList<>();
parseTypesAndNames(request, types, names, request.getType());
List<String> configs = new ArrayList<>();
configs.add(QuotaConfigs.PRODUCER_BYTE_RATE_OVERRIDE_CONFIG);
configs.add(QuotaConfigs.CONSUMER_BYTE_RATE_OVERRIDE_CONFIG);
configs.add(QuotaConfigs.REQUEST_PERCENTAGE_OVERRIDE_CONFIG);
Tuple2<Object, String> tuple2 = clientQuotaConsole.deleteQuotaConfigs(types, names, configs);
if (!(Boolean) tuple2._1) {
return ResponseData.create().failed(tuple2._2);
}
return ResponseData.create().success();
}
private void parseTypesAndNames(AlterClientQuotaDTO request, List<String> types, List<String> names, String type) {
switch (request.getType()) {
case USER:
getTypesAndNames(request, types, names, USER);
break;
case CLIENT_ID:
getTypesAndNames(request, types, names, CLIENT_ID);
break;
case IP:
getTypesAndNames(request, types, names, IP);
break;
case USER_CLIENT:
getTypesAndNames(request, types, names, USER);
getTypesAndNames(request, types, names, CLIENT_ID);
break;
}
}
private void getTypesAndNames(AlterClientQuotaDTO request, List<String> types, List<String> names, String type) {
int index = -1;
for (int i = 0; i < request.getTypes().size(); i++) {
if (type.equals(request.getTypes().get(i))) {
index = i;
break;
}
}
if (index < 0) {
throw new IllegalArgumentException("Does not contain the type" + type);
}
types.add(request.getTypes().get(index));
if (CollectionUtils.isNotEmpty(request.getNames()) && request.getNames().size() > index) {
names.add(request.getNames().get(index));
} else {
names.add("");
}
}
}

View File

@@ -1,6 +1,7 @@
package com.xuxd.kafka.console.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xuxd.kafka.console.beans.BrokerNode;
import com.xuxd.kafka.console.beans.ClusterInfo;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.dos.ClusterInfoDO;
@@ -8,15 +9,11 @@ import com.xuxd.kafka.console.beans.vo.BrokerApiVersionVO;
import com.xuxd.kafka.console.beans.vo.ClusterInfoVO;
import com.xuxd.kafka.console.dao.ClusterInfoMapper;
import com.xuxd.kafka.console.service.ClusterService;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.TreeSet;
import java.util.*;
import java.util.stream.Collectors;
import kafka.console.ClusterConsole;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.kafka.clients.NodeApiVersions;
@@ -30,6 +27,7 @@ import org.springframework.stereotype.Service;
* @author xuxd
* @date 2021-10-08 14:23:09
**/
@Slf4j
@Service
public class ClusterServiceImpl implements ClusterService {
@@ -45,7 +43,12 @@ public class ClusterServiceImpl implements ClusterService {
@Override public ResponseData getClusterInfo() {
ClusterInfo clusterInfo = clusterConsole.clusterInfo();
clusterInfo.setNodes(new TreeSet<>(clusterInfo.getNodes()));
Set<BrokerNode> nodes = clusterInfo.getNodes();
if (nodes == null) {
log.error("集群节点信息为空,集群地址可能不正确或集群内没有活跃节点");
return ResponseData.create().failed("集群节点信息为空,集群地址可能不正确或集群内没有活跃节点");
}
clusterInfo.setNodes(new TreeSet<>(nodes));
return ResponseData.create().data(clusterInfo).success();
}
@@ -70,6 +73,10 @@ public class ClusterServiceImpl implements ClusterService {
}
@Override public ResponseData updateClusterInfo(ClusterInfoDO infoDO) {
if (infoDO.getProperties() == null) {
// null 的话不更新这个是bug设置为空字符串解决
infoDO.setProperties("");
}
clusterInfoMapper.updateById(infoDO);
return ResponseData.create().success();
}

View File

@@ -24,6 +24,7 @@ import kafka.console.TopicConsole;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.kafka.clients.admin.RecordsToDelete;
import org.apache.kafka.clients.admin.TopicDescription;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.producer.ProducerRecord;
@@ -242,6 +243,18 @@ public class MessageServiceImpl implements MessageService, ApplicationContextAwa
return success ? ResponseData.create().success("success: " + tuple2._2()) : ResponseData.create().failed(tuple2._2());
}
@Override
public ResponseData delete(List<QueryMessage> messages) {
Map<TopicPartition, RecordsToDelete> params = new HashMap<>(messages.size(), 1f);
messages.forEach(message -> {
params.put(new TopicPartition(message.getTopic(), message.getPartition()), RecordsToDelete.beforeOffset(message.getOffset()));
});
Tuple2<Object, String> tuple2 = messageConsole.delete(params);
boolean success = (boolean) tuple2._1();
return success ? ResponseData.create().success() : ResponseData.create().failed(tuple2._2());
}
private Map<TopicPartition, ConsumerRecord<byte[], byte[]>> searchRecordByOffset(QueryMessage queryMessage) {
Set<TopicPartition> partitions = getPartitions(queryMessage);

View File

@@ -9,16 +9,6 @@ import com.xuxd.kafka.console.beans.vo.TopicDescriptionVO;
import com.xuxd.kafka.console.beans.vo.TopicPartitionVO;
import com.xuxd.kafka.console.service.TopicService;
import com.xuxd.kafka.console.utils.GsonUtil;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
import kafka.console.MessageConsole;
import kafka.console.TopicConsole;
import lombok.extern.slf4j.Slf4j;
@@ -33,6 +23,10 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import scala.Tuple2;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
/**
* kafka-console-ui.
*
@@ -87,8 +81,8 @@ public class TopicServiceImpl implements TopicService {
return ResponseData.create().data(topicDescriptions.stream().map(d -> TopicDescriptionVO.from(d))).success();
}
@Override public ResponseData deleteTopic(String topic) {
Tuple2<Object, String> tuple2 = topicConsole.deleteTopic(topic);
@Override public ResponseData deleteTopics(Collection<String> topics) {
Tuple2<Object, String> tuple2 = topicConsole.deleteTopics(topics);
return (Boolean) tuple2._1 ? ResponseData.create().success() : ResponseData.create().failed(tuple2._2);
}

View File

@@ -12,6 +12,14 @@ kafka:
# 集群其它属性配置
properties:
# request.timeout.ms: 5000
# 缓存连接,不缓存的情况下,每次请求建立连接. 即使每次请求建立连接其实也很快某些情况下开启ACL查询可能很慢可以设置连接缓存为true
# 或者想提高查询速度也可以设置下面连接缓存为true
# 缓存 admin client的连接
cache-admin-connection: false
# 缓存 producer的连接
cache-producer-connection: false
# 缓存 consumer的连接
cache-consumer-connection: false
spring:
application:

View File

@@ -29,8 +29,8 @@ CREATE TABLE IF NOT EXISTS T_CLUSTER_INFO
(
ID IDENTITY NOT NULL COMMENT '主键ID',
CLUSTER_NAME VARCHAR(128) NOT NULL DEFAULT '' COMMENT '集群名',
ADDRESS VARCHAR(256) NOT NULL DEFAULT '' COMMENT '集群地址',
PROPERTIES VARCHAR(512) NOT NULL DEFAULT '' COMMENT '集群的其它属性配置',
ADDRESS VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '集群地址',
PROPERTIES VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '集群的其它属性配置',
UPDATE_TIME TIMESTAMP NOT NULL DEFAULT NOW() COMMENT '更新时间',
PRIMARY KEY (ID),
UNIQUE (CLUSTER_NAME)

View File

@@ -18,6 +18,7 @@ import org.apache.kafka.common.network.Selector
import org.apache.kafka.common.protocol.Errors
import org.apache.kafka.common.requests._
import org.apache.kafka.common.utils.{KafkaThread, LogContext, Time}
import org.slf4j.{Logger, LoggerFactory}
import java.io.IOException
import java.util.Properties
@@ -34,7 +35,9 @@ import scala.util.{Failure, Success, Try}
* @author xuxd
* @date 2022-01-22 15:15:57
* */
object BrokerApiVersion extends Logging {
object BrokerApiVersion{
protected lazy val log : Logger = LoggerFactory.getLogger(this.getClass)
def listAllBrokerApiVersionInfo(): java.util.HashMap[Node, NodeApiVersions] = {
val res = new java.util.HashMap[Node, NodeApiVersions]()
@@ -48,7 +51,7 @@ object BrokerApiVersion extends Logging {
case Success(v) => {
res.put(broker, v)
}
case Failure(v) => logger.error(s"${broker} -> ERROR: ${v}\n")
case Failure(v) => log.error(s"${broker} -> ERROR: ${v}\n")
}
}
} finally {
@@ -149,12 +152,12 @@ object BrokerApiVersion extends Logging {
val response = sendAnyNode(request).asInstanceOf[MetadataResponse]
val errors = response.errors
if (!errors.isEmpty) {
logger.info(s"Metadata request contained errors: $errors")
log.info(s"Metadata request contained errors: $errors")
}
// 在3.x版本中这个方法是buildCluster 代替cluster()了
// response.buildCluster.nodes.asScala.toList
response.cluster().nodes.asScala.toList
response.buildCluster.nodes.asScala.toList
// response.cluster().nodes.asScala.toList
}
def listAllBrokerVersionInfo(): Map[Node, Try[NodeApiVersions]] =
@@ -277,40 +280,40 @@ object BrokerApiVersion extends Logging {
// 版本不一样,这个地方的兼容性问题也不一样了
// 3.x版本用这个
// val networkClient = new NetworkClient(
// selector,
// metadata,
// clientId,
// DefaultMaxInFlightRequestsPerConnection,
// DefaultReconnectBackoffMs,
// DefaultReconnectBackoffMax,
// DefaultSendBufferBytes,
// DefaultReceiveBufferBytes,
// requestTimeoutMs,
// connectionSetupTimeoutMs,
// connectionSetupTimeoutMaxMs,
// time,
// true,
// new ApiVersions,
// logContext)
val networkClient = new NetworkClient(
selector,
metadata,
clientId,
DefaultMaxInFlightRequestsPerConnection,
DefaultReconnectBackoffMs,
DefaultReconnectBackoffMax,
DefaultSendBufferBytes,
DefaultReceiveBufferBytes,
requestTimeoutMs,
connectionSetupTimeoutMs,
connectionSetupTimeoutMaxMs,
time,
true,
new ApiVersions,
logContext)
val networkClient = new NetworkClient(
selector,
metadata,
clientId,
DefaultMaxInFlightRequestsPerConnection,
DefaultReconnectBackoffMs,
DefaultReconnectBackoffMax,
DefaultSendBufferBytes,
DefaultReceiveBufferBytes,
requestTimeoutMs,
connectionSetupTimeoutMs,
connectionSetupTimeoutMaxMs,
ClientDnsLookup.USE_ALL_DNS_IPS,
time,
true,
new ApiVersions,
logContext)
// val networkClient = new NetworkClient(
// selector,
// metadata,
// clientId,
// DefaultMaxInFlightRequestsPerConnection,
// DefaultReconnectBackoffMs,
// DefaultReconnectBackoffMax,
// DefaultSendBufferBytes,
// DefaultReceiveBufferBytes,
// requestTimeoutMs,
// connectionSetupTimeoutMs,
// connectionSetupTimeoutMaxMs,
// ClientDnsLookup.USE_ALL_DNS_IPS,
// time,
// true,
// new ApiVersions,
// logContext)
val highLevelClient = new ConsumerNetworkClient(
logContext,

View File

@@ -0,0 +1,84 @@
package kafka.console
import com.xuxd.kafka.console.config.KafkaConfig
import org.apache.kafka.clients.admin.{Admin, AlterClientQuotasOptions}
import org.apache.kafka.common.quota.{ClientQuotaAlteration, ClientQuotaEntity, ClientQuotaFilter, ClientQuotaFilterComponent}
import java.util.Collections
import java.util.concurrent.TimeUnit
import scala.jdk.CollectionConverters.{IterableHasAsJava, ListHasAsScala, MapHasAsScala, SeqHasAsJava}
/**
* client quota console.
*
* @author xuxd
* @date 2022-12-30 10:55:56
* */
class ClientQuotaConsole(config: KafkaConfig) extends KafkaConsole(config: KafkaConfig) with Logging {
def getClientQuotasConfigs(entityTypes: java.util.List[String], entityNames: java.util.List[String]): java.util.Map[ClientQuotaEntity, java.util.Map[String, Double]] = {
withAdminClientAndCatchError(admin => getAllClientQuotasConfigs(admin, entityTypes.asScala.toList, entityNames.asScala.toList),
e => {
log.error("getAllClientQuotasConfigs error.", e)
java.util.Collections.emptyMap()
})
}.asInstanceOf[java.util.Map[ClientQuotaEntity, java.util.Map[String, Double]]]
def addQuotaConfigs(entityTypes: java.util.List[String], entityNames: java.util.List[String], configsToBeAddedMap: java.util.Map[String, String]): (Boolean, String) = {
alterQuotaConfigs(entityTypes, entityNames, configsToBeAddedMap, Collections.emptyList())
}
def deleteQuotaConfigs(entityTypes: java.util.List[String], entityNames: java.util.List[String], configsToBeDeleted: java.util.List[String]): (Boolean, String) = {
alterQuotaConfigs(entityTypes, entityNames, Collections.emptyMap(), configsToBeDeleted)
}
def alterQuotaConfigs(entityTypes: java.util.List[String], entityNames: java.util.List[String], configsToBeAddedMap: java.util.Map[String, String], configsToBeDeleted: java.util.List[String]): (Boolean, String) = {
withAdminClientAndCatchError(admin => {
alterQuotaConfigsInner(admin, entityTypes.asScala.toList, entityNames.asScala.toList, configsToBeAddedMap.asScala.toMap, configsToBeDeleted.asScala.toSeq)
(true, "")
},
e => {
log.error("getAllClientQuotasConfigs error.", e)
(false, e.getMessage)
}).asInstanceOf[(Boolean, String)]
}
private def getAllClientQuotasConfigs(adminClient: Admin, entityTypes: List[String], entityNames: List[String]): java.util.Map[ClientQuotaEntity, java.util.Map[String, Double]] = {
val components = entityTypes.map(Some(_)).zipAll(entityNames.map(Some(_)), None, None).map { case (entityType, entityNameOpt) =>
entityNameOpt match {
case Some("") => ClientQuotaFilterComponent.ofDefaultEntity(entityType.get)
case Some(name) => ClientQuotaFilterComponent.ofEntity(entityType.get, name)
case None => ClientQuotaFilterComponent.ofEntityType(entityType.get)
}
}
adminClient.describeClientQuotas(ClientQuotaFilter.containsOnly(components.asJava)).entities.get(30, TimeUnit.SECONDS)
}.asInstanceOf[java.util.Map[ClientQuotaEntity, java.util.Map[String, Double]]]
private def alterQuotaConfigsInner(adminClient: Admin, entityTypes: List[String], entityNames: List[String], configsToBeAddedMap: Map[String, String], configsToBeDeleted: Seq[String]) = {
// handle altering client/user quota configs
// val oldConfig = getAllClientQuotasConfigs(adminClient, entityTypes, entityNames)
// val invalidConfigs = configsToBeDeleted.filterNot(oldConfig.asScala.toMap.contains)
// if (invalidConfigs.nonEmpty)
// throw new InvalidConfigurationException(s"Invalid config(s): ${invalidConfigs.mkString(",")}")
val alterEntityNames = entityNames.map(en => if (en.nonEmpty) en else null)
// Explicitly populate a HashMap to ensure nulls are recorded properly.
val alterEntityMap = new java.util.HashMap[String, String]
entityTypes.zip(alterEntityNames).foreach { case (k, v) => alterEntityMap.put(k, v) }
val entity = new ClientQuotaEntity(alterEntityMap)
val alterOptions = new AlterClientQuotasOptions().validateOnly(false)
val alterOps = (configsToBeAddedMap.map { case (key, value) =>
val doubleValue = try value.toDouble catch {
case _: NumberFormatException =>
throw new IllegalArgumentException(s"Cannot parse quota configuration value for $key: $value")
}
new ClientQuotaAlteration.Op(key, doubleValue)
} ++ configsToBeDeleted.map(key => new ClientQuotaAlteration.Op(key, null))).asJavaCollection
adminClient.alterClientQuotas(Collections.singleton(new ClientQuotaAlteration(entity, alterOps)), alterOptions)
.all().get(60, TimeUnit.SECONDS)
}
}

View File

@@ -1,18 +1,21 @@
package kafka.console
import com.google.common.cache.{CacheLoader, RemovalListener, RemovalNotification}
import com.xuxd.kafka.console.cache.TimeBasedCache
import com.xuxd.kafka.console.config.{ContextConfigHolder, KafkaConfig}
import kafka.zk.{AdminZkClient, KafkaZkClient}
import kafka.zk.AdminZkClient
import org.apache.kafka.clients.admin._
import org.apache.kafka.clients.consumer.{ConsumerConfig, KafkaConsumer, OffsetAndMetadata}
import org.apache.kafka.clients.producer.KafkaProducer
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.requests.ListOffsetsResponse
import org.apache.kafka.common.serialization.{ByteArrayDeserializer, ByteArraySerializer, StringSerializer}
import org.apache.kafka.common.utils.Time
import org.slf4j.{Logger, LoggerFactory}
import java.util.Properties
import java.util.concurrent.Executors
import scala.collection.{Map, Seq}
import scala.concurrent.{ExecutionContext, Future}
import scala.jdk.CollectionConverters.{MapHasAsJava, MapHasAsScala}
/**
@@ -27,11 +30,13 @@ class KafkaConsole(config: KafkaConfig) {
protected def withAdminClient(f: Admin => Any): Any = {
val admin = createAdminClient()
val admin = if (config.isCacheAdminConnection()) AdminCache.cache.get(ContextConfigHolder.CONTEXT_CONFIG.get().getBootstrapServer()) else createAdminClient()
try {
f(admin)
} finally {
admin.close()
if (!config.isCacheAdminConnection) {
admin.close()
}
}
}
@@ -45,33 +50,40 @@ class KafkaConsole(config: KafkaConfig) {
protected def withConsumerAndCatchError(f: KafkaConsumer[Array[Byte], Array[Byte]] => Any, eh: Exception => Any,
extra: Properties = new Properties()): Any = {
val props = getProps()
props.putAll(extra)
props.put(ConsumerConfig.CLIENT_ID_CONFIG, String.valueOf(System.currentTimeMillis()))
val consumer = new KafkaConsumer(props, new ByteArrayDeserializer, new ByteArrayDeserializer)
// val props = getProps()
// props.putAll(extra)
// props.put(ConsumerConfig.CLIENT_ID_CONFIG, String.valueOf(System.currentTimeMillis()))
// val consumer = new KafkaConsumer(props, new ByteArrayDeserializer, new ByteArrayDeserializer)
ConsumerCache.setProperties(extra)
val consumer = if (config.isCacheConsumerConnection) ConsumerCache.cache.get(ContextConfigHolder.CONTEXT_CONFIG.get().getBootstrapServer()) else KafkaConsole.createByteArrayKVConsumer(extra)
try {
f(consumer)
} catch {
case er: Exception => eh(er)
}
finally {
consumer.close()
ConsumerCache.clearProperties()
if (!config.isCacheConsumerConnection) {
consumer.close()
}
}
}
protected def withProducerAndCatchError(f: KafkaProducer[String, String] => Any, eh: Exception => Any,
extra: Properties = new Properties()): Any = {
val props = getProps()
props.putAll(extra)
props.put(ConsumerConfig.CLIENT_ID_CONFIG, String.valueOf(System.currentTimeMillis()))
val producer = new KafkaProducer[String, String](props, new StringSerializer, new StringSerializer)
ProducerCache.setProperties(extra)
val producer = if (config.isCacheProducerConnection) ProducerCache.cache.get(ContextConfigHolder.CONTEXT_CONFIG.get().getBootstrapServer) else KafkaConsole.createProducer(extra)
try {
f(producer)
} catch {
case er: Exception => eh(er)
}
finally {
producer.close()
ProducerCache.clearProperties()
if (!config.isCacheProducerConnection) {
producer.close()
}
}
}
@@ -79,7 +91,6 @@ class KafkaConsole(config: KafkaConfig) {
extra: Properties = new Properties()): Any = {
val props = getProps()
props.putAll(extra)
props.put(ConsumerConfig.CLIENT_ID_CONFIG, String.valueOf(System.currentTimeMillis()))
val producer = new KafkaProducer[Array[Byte], Array[Byte]](props, new ByteArraySerializer, new ByteArraySerializer)
try {
f(producer)
@@ -91,14 +102,17 @@ class KafkaConsole(config: KafkaConfig) {
}
}
@Deprecated
protected def withZKClient(f: AdminZkClient => Any): Any = {
val zkClient = KafkaZkClient(config.getZookeeperAddr, false, 30000, 30000, Int.MaxValue, Time.SYSTEM)
val adminZkClient = new AdminZkClient(zkClient)
try {
f(adminZkClient)
} finally {
zkClient.close()
}
// val zkClient = KafkaZkClient(config.getZookeeperAddr, false, 30000, 30000, Int.MaxValue, Time.SYSTEM)
// 3.x
// val zkClient = KafkaZkClient(config.getZookeeperAddr, false, 30000, 30000, Int.MaxValue, Time.SYSTEM, new ZKClientConfig(), "KafkaZkClient")
// val adminZkClient = new AdminZkClient(zkClient)
// try {
// f(adminZkClient)
// } finally {
// zkClient.close()
// }
}
protected def createAdminClient(props: Properties): Admin = {
@@ -110,20 +124,47 @@ class KafkaConsole(config: KafkaConfig) {
}
private def createAdminClient(): Admin = {
Admin.create(getProps())
KafkaConsole.createAdminClient()
}
private def getProps(): Properties = {
KafkaConsole.getProps()
}
}
object KafkaConsole {
val log: Logger = LoggerFactory.getLogger(this.getClass)
def createAdminClient(): Admin = {
Admin.create(getProps())
}
def createByteArrayKVConsumer(extra: Properties) : KafkaConsumer[Array[Byte], Array[Byte]] = {
val props = getProps()
props.putAll(extra)
props.put(ConsumerConfig.CLIENT_ID_CONFIG, String.valueOf(System.currentTimeMillis()))
new KafkaConsumer(props, new ByteArrayDeserializer, new ByteArrayDeserializer)
}
def createProducer(extra: Properties) : KafkaProducer[String, String] = {
val props = getProps()
props.putAll(extra)
new KafkaProducer(props, new StringSerializer, new StringSerializer)
}
def createByteArrayStringProducer(extra: Properties) : KafkaProducer[Array[Byte], Array[Byte]] = {
val props = getProps()
props.putAll(extra)
new KafkaProducer(props, new ByteArraySerializer, new ByteArraySerializer)
}
def getProps(): Properties = {
val props: Properties = new Properties();
props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, ContextConfigHolder.CONTEXT_CONFIG.get().getBootstrapServer())
props.put(AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, ContextConfigHolder.CONTEXT_CONFIG.get().getRequestTimeoutMs())
props.putAll(ContextConfigHolder.CONTEXT_CONFIG.get().getProperties())
props
}
}
object KafkaConsole {
val log: Logger = LoggerFactory.getLogger(this.getClass)
def getCommittedOffsets(admin: Admin, groupId: String,
timeoutMs: Integer): Map[TopicPartition, OffsetAndMetadata] = {
@@ -174,4 +215,88 @@ object KafkaConsole {
}.toMap
res
}
implicit val ec = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(2))
}
object AdminCache {
private val log: Logger = LoggerFactory.getLogger(this.getClass)
private val cacheLoader = new CacheLoader[String, Admin] {
override def load(key: String): Admin = KafkaConsole.createAdminClient()
}
private val removeListener = new RemovalListener[String, Admin] {
override def onRemoval(notification: RemovalNotification[String, Admin]): Unit = {
Future {
log.warn("Close expired admin connection: {}", notification.getKey)
notification.getValue.close()
log.warn("Close expired admin connection complete: {}", notification.getKey)
}(KafkaConsole.ec)
}
}
val cache = new TimeBasedCache[String, Admin](cacheLoader, removeListener)
}
object ConsumerCache {
private val log: Logger = LoggerFactory.getLogger(this.getClass)
private val threadLocal = new ThreadLocal[Properties]
private val cacheLoader = new CacheLoader[String, KafkaConsumer[Array[Byte], Array[Byte]]] {
override def load(key: String): KafkaConsumer[Array[Byte], Array[Byte]] = KafkaConsole.createByteArrayKVConsumer(threadLocal.get())
}
private val removeListener = new RemovalListener[String, KafkaConsumer[Array[Byte], Array[Byte]]] {
override def onRemoval(notification: RemovalNotification[String, KafkaConsumer[Array[Byte], Array[Byte]]]): Unit = {
Future {
log.warn("Close expired consumer connection: {}", notification.getKey)
notification.getValue.close()
log.warn("Close expired consumer connection complete: {}", notification.getKey)
}(KafkaConsole.ec)
}
}
val cache = new TimeBasedCache[String, KafkaConsumer[Array[Byte], Array[Byte]]](cacheLoader, removeListener)
def setProperties(props : Properties) : Unit = {
threadLocal.set(props)
}
def clearProperties() : Unit = {
threadLocal.remove()
}
}
object ProducerCache {
private val log: Logger = LoggerFactory.getLogger(this.getClass)
private val threadLocal = new ThreadLocal[Properties]
private val cacheLoader = new CacheLoader[String, KafkaProducer[String, String]] {
override def load(key: String): KafkaProducer[String, String] = KafkaConsole.createProducer(threadLocal.get())
}
private val removeListener = new RemovalListener[String, KafkaProducer[String, String]] {
override def onRemoval(notification: RemovalNotification[String, KafkaProducer[String, String]]): Unit = {
Future {
log.warn("Close expired producer connection: {}", notification.getKey)
notification.getValue.close()
log.warn("Close expired producer connection complete: {}", notification.getKey)
}(KafkaConsole.ec)
}
}
val cache = new TimeBasedCache[String, KafkaProducer[String, String]](cacheLoader, removeListener)
def setProperties(props : Properties) : Unit = {
threadLocal.set(props)
}
def clearProperties() : Unit = {
threadLocal.remove()
}
}

View File

@@ -4,13 +4,14 @@ import com.xuxd.kafka.console.beans.MessageFilter
import com.xuxd.kafka.console.beans.enums.FilterType
import com.xuxd.kafka.console.config.{ContextConfigHolder, KafkaConfig}
import org.apache.commons.lang3.StringUtils
import org.apache.kafka.clients.admin.{DeleteRecordsOptions, RecordsToDelete}
import org.apache.kafka.clients.consumer.{ConsumerConfig, ConsumerRecord}
import org.apache.kafka.clients.producer.ProducerRecord
import org.apache.kafka.common.TopicPartition
import java.time.Duration
import java.util
import java.util.Properties
import java.util.{Properties}
import scala.collection.immutable
import scala.jdk.CollectionConverters.{CollectionHasAsScala, MapHasAsScala, SeqHasAsJava}
@@ -127,7 +128,7 @@ class MessageConsole(config: KafkaConfig) extends KafkaConsole(config: KafkaConf
record.offset(),
record.timestamp(),
record.timestampType(),
record.checksum(),
// record.checksum(),
record.serializedKeySize(),
record.serializedValueSize(),
record.key(),
@@ -236,4 +237,14 @@ class MessageConsole(config: KafkaConfig) extends KafkaConsole(config: KafkaConf
(false, e.getMessage)
}).asInstanceOf[(Boolean, String)]
}
def delete(recordsToDelete: util.Map[TopicPartition, RecordsToDelete]): (Boolean, String) = {
withAdminClientAndCatchError(admin => {
admin.deleteRecords(recordsToDelete, withTimeoutMs(new DeleteRecordsOptions())).all().get()
(true, "")
}, e => {
log.error("delete message error.", e)
(false, "delete error :" + e.getMessage)
}).asInstanceOf[(Boolean, String)]
}
}

View File

@@ -66,17 +66,17 @@ class TopicConsole(config: KafkaConfig) extends KafkaConsole(config: KafkaConfig
/**
* delete topic by topic name.
*
* @param topic topic name.
* @param topics topic name list.
* @return result or : fail message.
*/
def deleteTopic(topic: String): (Boolean, String) = {
def deleteTopics(topics: util.Collection[String]): (Boolean, String) = {
withAdminClientAndCatchError(admin => {
val timeoutMs = ContextConfigHolder.CONTEXT_CONFIG.get().getRequestTimeoutMs()
admin.deleteTopics(Collections.singleton(topic), new DeleteTopicsOptions().retryOnQuotaViolation(false)).all().get(timeoutMs, TimeUnit.MILLISECONDS)
admin.deleteTopics(topics, new DeleteTopicsOptions().retryOnQuotaViolation(false)).all().get(timeoutMs, TimeUnit.MILLISECONDS)
(true, "")
},
e => {
log.error("delete topic error, topic: " + topic, e)
log.error("delete topic error, topic: " + topics, e)
(false, e.getMessage)
}).asInstanceOf[(Boolean, String)]
}

View File

@@ -0,0 +1,48 @@
package com.xuxd.kafka.console.scala;
import com.xuxd.kafka.console.config.ContextConfig;
import com.xuxd.kafka.console.config.ContextConfigHolder;
import com.xuxd.kafka.console.config.KafkaConfig;
import kafka.console.ClientQuotaConsole;
import org.apache.kafka.common.config.internals.QuotaConfigs;
import org.apache.kafka.common.quota.ClientQuotaEntity;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class ClientQuotaConsoleTest {
String bootstrapServer = "localhost:9092";
@Test
void testGetClientQuotasConfigs() {
ClientQuotaConsole console = new ClientQuotaConsole(new KafkaConfig());
ContextConfig config = new ContextConfig();
config.setBootstrapServer(bootstrapServer);
ContextConfigHolder.CONTEXT_CONFIG.set(config);
Map<ClientQuotaEntity, Map<String, Object>> configs = console.getClientQuotasConfigs(Arrays.asList(ClientQuotaEntity.USER, ClientQuotaEntity.CLIENT_ID), Arrays.asList("user1", "clientA"));
configs.forEach((k, v) -> {
System.out.println(k);
System.out.println(v);
});
}
@Test
void testAlterClientQuotasConfigs() {
ClientQuotaConsole console = new ClientQuotaConsole(new KafkaConfig());
ContextConfig config = new ContextConfig();
config.setBootstrapServer(bootstrapServer);
ContextConfigHolder.CONTEXT_CONFIG.set(config);
Map<String, String> configsToBeAddedMap = new HashMap<>();
configsToBeAddedMap.put(QuotaConfigs.PRODUCER_BYTE_RATE_OVERRIDE_CONFIG, "1024000000");
console.addQuotaConfigs(Arrays.asList(ClientQuotaEntity.USER), Arrays.asList("user-test"), configsToBeAddedMap);
console.addQuotaConfigs(Arrays.asList(ClientQuotaEntity.USER), Arrays.asList(""), configsToBeAddedMap);
console.addQuotaConfigs(Arrays.asList(ClientQuotaEntity.CLIENT_ID), Arrays.asList(""), configsToBeAddedMap);
console.addQuotaConfigs(Arrays.asList(ClientQuotaEntity.CLIENT_ID), Arrays.asList("clientA"), configsToBeAddedMap);
console.addQuotaConfigs(Arrays.asList(ClientQuotaEntity.USER, ClientQuotaEntity.CLIENT_ID), Arrays.asList("", ""), configsToBeAddedMap);
// console.deleteQuotaConfigs(Arrays.asList(ClientQuotaEntity.CLIENT_ID), Arrays.asList(""), Arrays.asList(QuotaConfigs.CONSUMER_BYTE_RATE_OVERRIDE_CONFIG));
}
}

6
ui/package-lock.json generated
View File

@@ -1866,9 +1866,9 @@
"optional": true
},
"loader-utils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
"dev": true,
"optional": true,
"requires": {

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
ui/public/vue.ico Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -11,10 +11,10 @@
><router-link to="/group-page" class="pad-l-r">消费组</router-link>
<span>|</span
><router-link to="/message-page" class="pad-l-r">消息</router-link>
<span v-show="enableSasl">|</span
><router-link to="/acl-page" class="pad-l-r" v-show="enableSasl"
>Acl</router-link
>
<span>|</span
><router-link to="/client-quota-page" class="pad-l-r">限流</router-link>
<span>|</span
><router-link to="/acl-page" class="pad-l-r">Acl</router-link>
<span>|</span
><router-link to="/op-page" class="pad-l-r">运维</router-link>
<span class="right">集群{{ clusterName }}</span>

View File

@@ -49,6 +49,12 @@ const routes = [
component: () =>
import(/* webpackChunkName: "cluster" */ "../views/message/Message.vue"),
},
{
path: "/client-quota-page",
name: "ClientQuota",
component: () =>
import(/* webpackChunkName: "cluster" */ "../views/quota/ClientQuota.vue"),
},
];
const router = new VueRouter({

View File

@@ -47,6 +47,18 @@ export const KafkaAclApi = {
url: "/acl",
method: "delete",
},
clearAcl: {
url: "/acl/clear",
method: "delete",
},
getSaslScramUserList: {
url: "/user/scram",
method: "get",
},
deleteSaslScramUser: {
url: "/user",
method: "delete",
},
};
export const KafkaConfigApi = {
@@ -276,4 +288,23 @@ export const KafkaMessageApi = {
url: "/message/resend",
method: "post",
},
delete: {
url: "/message",
method: "delete",
},
};
export const KafkaClientQuotaApi = {
getClientQuotaConfigs: {
url: "/client/quota/list",
method: "post",
},
alterClientQuotaConfigs: {
url: "/client/quota",
method: "post",
},
deleteClientQuotaConfigs: {
url: "/client/quota",
method: "delete",
},
};

View File

@@ -1,461 +1,25 @@
<template>
<div class="content">
<a-spin :spinning="loading">
<div class="acl">
<div id="components-form-acl-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item :label="`用户名`">
<a-input
placeholder="username"
class="input-w"
v-decorator="['username']"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="`topic`">
<a-input
placeholder="topic"
class="input-w"
v-decorator="['topic']"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="`消费组`">
<a-input
placeholder="groupId"
class="input-w"
v-decorator="['groupId']"
/>
</a-form-item>
</a-col>
<a-col :span="24" :style="{ textAlign: 'right' }">
<a-button type="primary" html-type="submit"> 搜索</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleReset">
重置
</a-button>
</a-col>
</a-row>
</a-form>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="updateUser">新增/更新用户</a-button>
<UpdateUser
:visible="showUpdateUser"
@updateUserDialogData="closeUpdateUserDialog"
></UpdateUser>
</div>
<a-table :columns="columns" :data-source="data" bordered>
<div slot="username" slot-scope="username">
<span>{{ username }}</span
><a-button
size="small"
shape="round"
type="dashed"
style="float: right"
@click="onUserDetail(username)"
>详情</a-button
>
</div>
<div slot="topicList" slot-scope="topicList, record">
<a
href="#"
v-for="t in topicList"
:key="t"
@click="onTopicDetail(t, record.username)"
><div style="border-bottom: 1px solid #e5e1e1">{{ t }}</div>
</a>
</div>
<div slot="groupList" slot-scope="groupList, record">
<a
href="#"
v-for="t in groupList"
:key="t"
@click="onGroupDetail(t, record.username)"
><div style="border-bottom: 1px solid #e5e1e1">{{ t }}</div>
</a>
</div>
<div
slot="operation"
slot-scope="record"
v-show="!record.user || record.user.role != 'admin'"
>
<a-popconfirm
:title="'删除用户: ' + record.username + '及相关权限?'"
ok-text="确认"
cancel-text="取消"
@confirm="onDeleteUser(record)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
>删除</a-button
>
</a-popconfirm>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onManageProducerAuth(record)"
>管理生产权限
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onManageConsumerAuth(record)"
>管理消费权限
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onAddAuth(record)"
>增加权限
</a-button>
</div>
</a-table>
<UserDetail
:visible="openUserDetailDialog"
:username="selectDetail.username"
@userDetailDialog="closeUserDetailDialog"
></UserDetail>
<AclDetail
:visible="openAclDetailDialog"
:selectDetail="selectDetail"
@aclDetailDialog="closeAclDetailDialog"
></AclDetail>
<ManageProducerAuth
:visible="openManageProducerAuthDialog"
:record="selectRow"
@manageProducerAuthDialog="closeManageProducerAuthDialog"
></ManageProducerAuth>
<ManageConsumerAuth
:visible="openManageConsumerAuthDialog"
:record="selectRow"
@manageConsumerAuthDialog="closeManageConsumerAuthDialog"
></ManageConsumerAuth>
<AddAuth
:visible="openAddAuthDialog"
:record="selectRow"
@addAuthDialog="closeAddAuthDialog"
></AddAuth>
</div>
</a-spin>
<a-tabs default-active-key="1" size="large" tabPosition="top">
<a-tab-pane key="1" tab="资源授权">
<acl-list></acl-list>
</a-tab-pane>
<a-tab-pane key="2" tab="SaslScram用户管理">
<sasl-scram></sasl-scram>
</a-tab-pane>
</a-tabs>
</div>
</template>
<script>
import request from "@/utils/request";
import notification from "ant-design-vue/es/notification";
import UpdateUser from "@/views/acl/UpdateUser";
import { KafkaAclApi } from "@/utils/api";
import ManageProducerAuth from "@/views/acl/ManageProducerAuth";
import ManageConsumerAuth from "@/views/acl/ManageConsumerAuth";
import AddAuth from "@/views/acl/AddAuth";
import AclDetail from "@/views/acl/AclDetail";
import UserDetail from "@/views/acl/UserDetail";
import AclList from "@/views/acl/AclList";
import SaslScram from "@/views/acl/SaslScram";
export default {
name: "Acl",
components: {
UpdateUser,
ManageProducerAuth,
ManageConsumerAuth,
AddAuth,
AclDetail,
UserDetail,
},
data() {
return {
queryParam: {},
data: [],
columns,
selectRow: {},
form: this.$form.createForm(this, { name: "advanced_search" }),
showUpdateUser: false,
deleteUserConfirm: false,
openManageProducerAuthDialog: false,
openManageConsumerAuthDialog: false,
openAddAuthDialog: false,
openAclDetailDialog: false,
openUserDetailDialog: false,
selectDetail: {
resourceName: "",
resourceType: "",
username: "",
},
loading: false,
};
},
methods: {
handleSearch(e) {
e.preventDefault();
this.form.validateFields((error, values) => {
let queryParam = {};
if (values.username) {
queryParam.username = values.username;
}
if (values.topic) {
queryParam.resourceType = "TOPIC";
queryParam.resourceName = values.topic;
} else if (values.groupId) {
queryParam.resourceType = "GROUP";
queryParam.resourceName = values.groupId;
}
Object.assign(this.queryParam, queryParam);
this.getAclList();
});
},
handleReset() {
this.form.resetFields();
},
updateUser() {
this.showUpdateUser = true;
},
closeUpdateUserDialog(data) {
this.showUpdateUser = data.show;
if (data.ok) {
this.getAclList();
}
},
onDeleteUser(row) {
this.loading = true;
request({
url: KafkaAclApi.deleteKafkaUser.url,
method: KafkaAclApi.deleteKafkaUser.method,
data: { username: row.username },
}).then((res) => {
this.loading = false;
this.getAclList();
if (res.code == 0) {
this.$message.success(res.msg);
} else {
this.$message.error(res.msg);
}
});
},
onManageProducerAuth(row) {
this.openManageProducerAuthDialog = true;
const rowData = {};
Object.assign(rowData, row);
this.selectRow = rowData;
},
onManageConsumerAuth(row) {
this.openManageConsumerAuthDialog = true;
const rowData = {};
Object.assign(rowData, row);
this.selectRow = rowData;
},
onAddAuth(row) {
this.openAddAuthDialog = true;
const rowData = {};
Object.assign(rowData, row);
this.selectRow = rowData;
},
onTopicDetail(topic, username) {
this.selectDetail.resourceType = "TOPIC";
this.selectDetail.resourceName = topic;
this.selectDetail.username = username;
this.openAclDetailDialog = true;
},
onGroupDetail(group, username) {
this.selectDetail.resourceType = "GROUP";
this.selectDetail.resourceName = group;
this.selectDetail.username = username;
this.openAclDetailDialog = true;
},
onUserDetail(username) {
this.selectDetail.username = username;
this.openUserDetailDialog = true;
},
closeManageProducerAuthDialog() {
this.openManageProducerAuthDialog = false;
this.getAclList();
},
closeManageConsumerAuthDialog() {
this.openManageConsumerAuthDialog = false;
this.getAclList();
},
closeAddAuthDialog(p) {
this.openAddAuthDialog = false;
if (p.refresh) {
this.getAclList();
}
},
closeAclDetailDialog(p) {
this.openAclDetailDialog = false;
if (p.refresh) {
this.getAclList();
}
},
closeUserDetailDialog() {
this.openUserDetailDialog = false;
},
getAclList() {
this.loading = true;
request({
url: KafkaAclApi.getAclList.url,
method: KafkaAclApi.getAclList.method,
data: this.queryParam,
}).then((response) => {
this.loading = false;
this.data.splice(0, this.data.length);
if (response.code != 0) {
notification.error({
message: response.msg,
});
return;
}
for (let k in response.data.map) {
let v = response.data.map[k];
let topicList = Object.keys(v)
.filter((e) => e.startsWith("TOPIC"))
.map((e) => e.split("#")[1]);
let groupList = Object.keys(v)
.filter((e) => e.startsWith("GROUP"))
.map((e) => e.split("#")[1]);
this.data.push({
key: k,
username: k,
topicList: topicList,
groupList: groupList,
user: response.data.map[k]["USER"],
});
this.data.sort((a, b) => a.username.localeCompare(b.username));
}
});
},
},
created() {
this.getAclList();
AclList,
SaslScram,
},
};
// function getAclList(data, requestParameters) {
// request({
// url: KafkaAclApi.getAclList.url,
// method: KafkaAclApi.getAclList.method,
// data: requestParameters,
// }).then((response) => {
// data.splice(0, data.length);
// if (response.code != 0) {
// notification.error({
// message: response.msg,
// });
// return;
// }
// for (let k in response.data.map) {
// let v = response.data.map[k];
// let topicList = Object.keys(v)
// .filter((e) => e.startsWith("TOPIC"))
// .map((e) => e.split("#")[1]);
// let groupList = Object.keys(v)
// .filter((e) => e.startsWith("GROUP"))
// .map((e) => e.split("#")[1]);
// data.push({
// key: k,
// username: k,
// topicList: topicList,
// groupList: groupList,
// user: response.data.map[k]["USER"],
// });
// data.sort((a, b) => a.username.localeCompare(b.username));
// }
// });
// }
const columns = [
{
title: "用户名",
dataIndex: "username",
key: "username",
width: 300,
slots: { title: "username" },
scopedSlots: { customRender: "username" },
},
{
title: "topic列表",
dataIndex: "topicList",
key: "topicList",
slots: { title: "topicList" },
scopedSlots: { customRender: "topicList" },
},
{
title: "消费组列表",
dataIndex: "groupList",
key: "groupList",
slots: { title: "groupList" },
scopedSlots: { customRender: "groupList" },
},
{
title: "操作",
key: "operation",
scopedSlots: { customRender: "operation" },
width: 500,
},
];
</script>
<style scoped>
.acl {
width: 100%;
height: 100%;
}
.ant-advanced-search-form {
padding: 24px;
background: #fbfbfb;
border: 1px solid #d9d9d9;
border-radius: 6px;
}
.ant-advanced-search-form .ant-form-item {
display: flex;
}
.ant-advanced-search-form .ant-form-item-control-wrapper {
flex: 1;
}
#components-form-acl-advanced-search .ant-form {
max-width: none;
margin-bottom: 1%;
}
#components-form-acl-advanced-search .search-result-list {
margin-top: 16px;
border: 1px dashed #e9e9e9;
border-radius: 6px;
background-color: #fafafa;
min-height: 200px;
text-align: center;
padding-top: 80px;
}
.input-w {
width: 400px;
}
.operation-row-button {
height: 4%;
text-align: left;
margin-bottom: 8px;
}
.operation-btn {
margin-right: 3%;
}
</style>

View File

@@ -0,0 +1,449 @@
<template>
<div class="acl">
<a-spin :spinning="loading">
<div class="acl">
<div id="components-form-acl-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item :label="`主体`">
<a-input
placeholder="比如, 用户名"
class="input-w"
v-decorator="['username']"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="`topic`">
<a-input
placeholder="topic"
class="input-w"
v-decorator="['topic']"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="`消费组`">
<a-input
placeholder="groupId"
class="input-w"
v-decorator="['groupId']"
/>
</a-form-item>
</a-col>
<a-col :span="24" :style="{ textAlign: 'right' }">
<a-button type="primary" html-type="submit"> 搜索</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleReset">
重置
</a-button>
</a-col>
</a-row>
</a-form>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="onAddPrincipalAuth"
>新增主体权限</a-button
>
<span v-show="hint != ''" class="hint"
>broker未启用权限管理所以不支持授权相关操作[{{ hint }}]</span
>
</div>
<a-table :columns="columns" :data-source="data" bordered>
<div slot="username" slot-scope="username">
<span>{{ username }}</span>
</div>
<div slot="topicList" slot-scope="topicList, record">
<a
href="#"
v-for="t in topicList"
:key="t"
@click="onTopicDetail(t, record.username)"
><div style="border-bottom: 1px solid #e5e1e1">{{ t }}</div>
</a>
</div>
<div slot="groupList" slot-scope="groupList, record">
<a
href="#"
v-for="t in groupList"
:key="t"
@click="onGroupDetail(t, record.username)"
><div style="border-bottom: 1px solid #e5e1e1">{{ t }}</div>
</a>
</div>
<div slot="operation" slot-scope="record">
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onManageProducerAuth(record)"
>管理生产权限
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onManageConsumerAuth(record)"
>管理消费权限
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onAddAuth(record)"
>增加权限
</a-button>
<a-popconfirm
:title="'清除: ' + record.username + '所有资源权限?'"
ok-text="确认"
cancel-text="取消"
@confirm="onClearUserAcl(record)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
>清除权限</a-button
>
</a-popconfirm>
</div>
</a-table>
<AclDetail
:visible="openAclDetailDialog"
:selectDetail="selectDetail"
@aclDetailDialog="closeAclDetailDialog"
></AclDetail>
<ManageProducerAuth
:visible="openManageProducerAuthDialog"
:record="selectRow"
@manageProducerAuthDialog="closeManageProducerAuthDialog"
></ManageProducerAuth>
<ManageConsumerAuth
:visible="openManageConsumerAuthDialog"
:record="selectRow"
@manageConsumerAuthDialog="closeManageConsumerAuthDialog"
></ManageConsumerAuth>
<AddAuth
:visible="openAddAuthDialog"
:record="selectRow"
@addAuthDialog="closeAddAuthDialog"
></AddAuth>
<AddPrincipalAuth
:visible="openAddPrincipalAuthDialog"
@closeAddPrincipalAuthDialog="closeAddPrincipalAuthDialog"
></AddPrincipalAuth>
</div>
</a-spin>
</div>
</template>
<script>
import request from "@/utils/request";
import notification from "ant-design-vue/es/notification";
import { KafkaAclApi } from "@/utils/api";
import ManageProducerAuth from "@/views/acl/ManageProducerAuth";
import ManageConsumerAuth from "@/views/acl/ManageConsumerAuth";
import AddAuth from "@/views/acl/AddAuth";
import AclDetail from "@/views/acl/AclDetail";
import AddPrincipalAuth from "@/views/acl/AddPrincipalAuth";
export default {
name: "AclList",
components: {
ManageProducerAuth,
ManageConsumerAuth,
AddAuth,
AclDetail,
AddPrincipalAuth,
},
data() {
return {
queryParam: {},
data: [],
columns,
selectRow: {},
form: this.$form.createForm(this, { name: "advanced_search" }),
openManageProducerAuthDialog: false,
openManageConsumerAuthDialog: false,
openAddAuthDialog: false,
openAclDetailDialog: false,
openAddPrincipalAuthDialog: false,
selectDetail: {
resourceName: "",
resourceType: "",
username: "",
},
loading: false,
hint: "",
};
},
methods: {
handleSearch(e) {
e.preventDefault();
this.form.validateFields((error, values) => {
let queryParam = {};
queryParam.username = values.username ? values.username : null;
// if (values.username) {
// queryParam.username = values.username;
// }
if (values.topic) {
queryParam.resourceType = "TOPIC";
queryParam.resourceName = values.topic;
} else if (values.groupId) {
queryParam.resourceType = "GROUP";
queryParam.resourceName = values.groupId;
}
Object.assign(this.queryParam, queryParam);
this.getAclList();
});
},
handleReset() {
this.form.resetFields();
},
onClearUserAcl(row) {
this.loading = true;
request({
url: KafkaAclApi.clearAcl.url,
method: KafkaAclApi.clearAcl.method,
data: { username: row.username },
}).then((res) => {
this.loading = false;
this.getAclList();
if (res.code == 0) {
this.$message.success(res.msg);
} else {
this.$message.error(res.msg);
}
});
},
onManageProducerAuth(row) {
this.openManageProducerAuthDialog = true;
const rowData = {};
Object.assign(rowData, row);
this.selectRow = rowData;
},
onManageConsumerAuth(row) {
this.openManageConsumerAuthDialog = true;
const rowData = {};
Object.assign(rowData, row);
this.selectRow = rowData;
},
onAddAuth(row) {
this.openAddAuthDialog = true;
const rowData = {};
Object.assign(rowData, row);
this.selectRow = rowData;
},
onTopicDetail(topic, username) {
this.selectDetail.resourceType = "TOPIC";
this.selectDetail.resourceName = topic;
this.selectDetail.username = username;
this.openAclDetailDialog = true;
},
onGroupDetail(group, username) {
this.selectDetail.resourceType = "GROUP";
this.selectDetail.resourceName = group;
this.selectDetail.username = username;
this.openAclDetailDialog = true;
},
onAddPrincipalAuth() {
this.openAddPrincipalAuthDialog = true;
},
closeManageProducerAuthDialog() {
this.openManageProducerAuthDialog = false;
this.getAclList();
},
closeManageConsumerAuthDialog() {
this.openManageConsumerAuthDialog = false;
this.getAclList();
},
closeAddAuthDialog(p) {
this.openAddAuthDialog = false;
if (p.refresh) {
this.getAclList();
}
},
closeAddPrincipalAuthDialog(p) {
this.openAddPrincipalAuthDialog = false;
if (p.refresh) {
this.getAclList();
}
},
closeAclDetailDialog(p) {
this.openAclDetailDialog = false;
if (p.refresh) {
this.getAclList();
}
},
getAclList() {
this.loading = true;
request({
url: KafkaAclApi.getAclList.url,
method: KafkaAclApi.getAclList.method,
data: this.queryParam,
}).then((response) => {
this.loading = false;
this.data.splice(0, this.data.length);
if (response.code != 0) {
notification.error({
message: response.msg,
});
return;
}
if (!response.data.total && response.data.hint) {
this.hint = response.data.hint;
return;
}
this.hint = "";
for (let k in response.data.map) {
let v = response.data.map[k];
let topicList = Object.keys(v)
.filter((e) => e.startsWith("TOPIC"))
.map((e) => e.split("#")[1]);
let groupList = Object.keys(v)
.filter((e) => e.startsWith("GROUP"))
.map((e) => e.split("#")[1]);
this.data.push({
key: k,
username: k,
topicList: topicList,
groupList: groupList,
user: response.data.map[k]["USER"],
});
this.data.sort((a, b) => a.username.localeCompare(b.username));
}
});
},
},
created() {
this.getAclList();
},
};
// function getAclList(data, requestParameters) {
// request({
// url: KafkaAclApi.getAclList.url,
// method: KafkaAclApi.getAclList.method,
// data: requestParameters,
// }).then((response) => {
// data.splice(0, data.length);
// if (response.code != 0) {
// notification.error({
// message: response.msg,
// });
// return;
// }
// for (let k in response.data.map) {
// let v = response.data.map[k];
// let topicList = Object.keys(v)
// .filter((e) => e.startsWith("TOPIC"))
// .map((e) => e.split("#")[1]);
// let groupList = Object.keys(v)
// .filter((e) => e.startsWith("GROUP"))
// .map((e) => e.split("#")[1]);
// data.push({
// key: k,
// username: k,
// topicList: topicList,
// groupList: groupList,
// user: response.data.map[k]["USER"],
// });
// data.sort((a, b) => a.username.localeCompare(b.username));
// }
// });
// }
const columns = [
{
title: "主体标识",
dataIndex: "username", //历史原因使用变量username
key: "username",
width: 300,
slots: { title: "username" },
scopedSlots: { customRender: "username" },
},
{
title: "topic列表",
dataIndex: "topicList",
key: "topicList",
slots: { title: "topicList" },
scopedSlots: { customRender: "topicList" },
},
{
title: "消费组列表",
dataIndex: "groupList",
key: "groupList",
slots: { title: "groupList" },
scopedSlots: { customRender: "groupList" },
},
{
title: "操作",
key: "operation",
scopedSlots: { customRender: "operation" },
width: 500,
},
];
</script>
<style scoped>
.acl {
width: 100%;
height: 100%;
}
.ant-advanced-search-form {
padding: 24px;
background: #fbfbfb;
border: 1px solid #d9d9d9;
border-radius: 6px;
}
.ant-advanced-search-form .ant-form-item {
display: flex;
}
.ant-advanced-search-form .ant-form-item-control-wrapper {
flex: 1;
}
#components-form-acl-advanced-search .ant-form {
max-width: none;
margin-bottom: 1%;
}
#components-form-acl-advanced-search .search-result-list {
margin-top: 16px;
border: 1px dashed #e9e9e9;
border-radius: 6px;
background-color: #fafafa;
min-height: 200px;
text-align: center;
padding-top: 80px;
}
.input-w {
width: 400px;
}
.operation-row-button {
height: 4%;
text-align: left;
margin-bottom: 8px;
}
.operation-btn {
margin-right: 3%;
}
.hint {
margin-left: 1%;
color: red;
}
</style>

View File

@@ -0,0 +1,154 @@
<template>
<a-modal
title="增加权限"
:visible="show"
:confirm-loading="confirmLoading"
:width="800"
@ok="handleOk"
@cancel="handleCancel"
okText="提交"
cancelText="取消"
:mask="false"
:destroyOnClose="true"
>
<a-form :form="form" :label-col="{ span: 5 }" :wrapper-col="{ span: 12 }">
<a-form-item label="主体标识">
<a-input
v-decorator="[
'username',
{ rules: [{ required: true, message: '请输入!' }] },
]"
/>
</a-form-item>
<a-form-item label="资源类型">
<a-radio-group
v-decorator="['resourceType', { initialValue: 'TOPIC' }]"
>
<a-radio value="TOPIC"> topic</a-radio>
<a-radio value="GROUP"> 消费组</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="资源名称">
<a-input
v-decorator="[
'resourceName',
{ rules: [{ required: true, message: '请输入!' }] },
]"
placeholder="请输入topic或消费组名称"
/>
</a-form-item>
<a-form-item label="主机">
<a-input
v-decorator="[
'host',
{
rules: [{ required: true, message: '请输入!' }],
initialValue: '*',
},
]"
placeholder="请输入主机地址,比如:*,全部匹配"
/>
</a-form-item>
<a-form-item label="操作类型" has-feedback>
<a-select
v-decorator="[
'operation',
{ rules: [{ required: true, message: '请选择!' }] },
]"
placeholder="请选择!"
>
<a-select-option v-for="i in operations" :key="i">
{{ i }}</a-select-option
>
</a-select>
</a-form-item>
<a-form-item label="权限类型">
<a-radio-group
v-decorator="['permissionType', { initialValue: 'ALLOW' }]"
>
<a-radio value="ALLOW"> 允许</a-radio>
<a-radio value="DENY"> 拒绝</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</a-modal>
</template>
<script>
import { KafkaAclApi } from "@/utils/api";
import request from "@/utils/request";
export default {
name: "AddPrincipalAuth",
props: {
visible: {
type: Boolean,
default: false,
},
},
data() {
return {
formLayout: "horizontal",
form: this.$form.createForm(this, { name: "AddPrincipalAuthForm" }),
confirmLoading: false,
show: this.visible,
operations: operationList,
};
},
watch: {
visible(v) {
if (this.show != v) {
this.show = v;
if (this.show) {
this.getOperationList();
}
}
},
},
methods: {
handleOk() {
const form = this.form;
form.validateFields((e, v) => {
if (e) {
return;
}
const param = Object.assign({}, v);
const api = KafkaAclApi.addAclAuth;
this.confirmLoading = true;
request({
url: api.url,
method: api.method,
data: param,
}).then((res) => {
this.confirmLoading = false;
if (res.code == 0) {
this.$message.success(res.msg);
this.$emit("closeAddPrincipalAuthDialog", { refresh: true });
} else {
this.$message.error(res.msg);
}
});
});
},
handleCancel() {
this.$emit("closeAddPrincipalAuthDialog", { refresh: false });
},
getOperationList() {
request({
url: KafkaAclApi.getOperationList.url,
method: KafkaAclApi.getOperationList.method,
}).then((res) => {
if (res.code != 0) {
this.$message.error(res.msg);
} else {
operationList.splice(0, operationList.length);
operationList.push(...res.data);
}
});
},
},
};
const operationList = [];
</script>
<style scoped></style>

View File

@@ -0,0 +1,411 @@
<template>
<a-spin :spinning="loading">
<div class="acl">
<div id="components-form-acl-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item :label="`用户名`">
<a-input
placeholder="username"
class="input-w"
v-decorator="['username']"
/>
</a-form-item>
</a-col>
<a-col :span="12" :style="{ textAlign: 'right' }">
<a-button type="primary" html-type="submit"> 搜索</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleReset">
重置
</a-button>
</a-col>
</a-row>
</a-form>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="updateUser">新增/更新用户</a-button>
<span class="hint" v-show="!enableSasl"
>未启用SASL SCRAM认证不支持相关操作</span
>
<UpdateUser
:visible="showUpdateUser"
@updateUserDialogData="closeUpdateUserDialog"
></UpdateUser>
</div>
<a-table :columns="columns" :data-source="data" bordered>
<div slot="username" slot-scope="username">
<span>{{ username }}</span
><a-button
size="small"
shape="round"
type="dashed"
style="float: right"
@click="onUserDetail(username)"
>详情</a-button
>
</div>
<div
slot="operation"
slot-scope="record"
v-show="!record.user || record.user.role != 'admin'"
>
<a-popconfirm
:title="'删除用户: ' + record.username + ''"
ok-text="确认"
cancel-text="取消"
@confirm="onDeleteUser(record)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
>删除</a-button
>
</a-popconfirm>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onManageProducerAuth(record)"
>管理生产权限
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onManageConsumerAuth(record)"
>管理消费权限
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onAddAuth(record)"
>增加权限
</a-button>
<a-popconfirm
:title="'删除用户: ' + record.username + '及相关权限?'"
ok-text="确认"
cancel-text="取消"
@confirm="onDeleteUserAndAuth(record)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
>彻底删除</a-button
>
</a-popconfirm>
</div>
</a-table>
<UserDetail
:visible="openUserDetailDialog"
:username="selectDetail.username"
@userDetailDialog="closeUserDetailDialog"
></UserDetail>
<AclDetail
:visible="openAclDetailDialog"
:selectDetail="selectDetail"
@aclDetailDialog="closeAclDetailDialog"
></AclDetail>
<ManageProducerAuth
:visible="openManageProducerAuthDialog"
:record="selectRow"
@manageProducerAuthDialog="closeManageProducerAuthDialog"
></ManageProducerAuth>
<ManageConsumerAuth
:visible="openManageConsumerAuthDialog"
:record="selectRow"
@manageConsumerAuthDialog="closeManageConsumerAuthDialog"
></ManageConsumerAuth>
<AddAuth
:visible="openAddAuthDialog"
:record="selectRow"
@addAuthDialog="closeAddAuthDialog"
></AddAuth>
</div>
</a-spin>
</template>
<script>
import request from "@/utils/request";
import notification from "ant-design-vue/es/notification";
import UpdateUser from "@/views/acl/UpdateUser";
import { KafkaAclApi } from "@/utils/api";
import ManageProducerAuth from "@/views/acl/ManageProducerAuth";
import ManageConsumerAuth from "@/views/acl/ManageConsumerAuth";
import AddAuth from "@/views/acl/AddAuth";
import AclDetail from "@/views/acl/AclDetail";
import UserDetail from "@/views/acl/UserDetail";
import { mapState } from "vuex";
export default {
name: "SaslScram",
components: {
UpdateUser,
ManageProducerAuth,
ManageConsumerAuth,
AddAuth,
AclDetail,
UserDetail,
},
data() {
return {
queryParam: {},
data: [],
columns,
selectRow: {},
form: this.$form.createForm(this, { name: "advanced_search" }),
showUpdateUser: false,
deleteUserConfirm: false,
openManageProducerAuthDialog: false,
openManageConsumerAuthDialog: false,
openAddAuthDialog: false,
openAclDetailDialog: false,
openUserDetailDialog: false,
selectDetail: {
resourceName: "",
resourceType: "",
username: "",
},
loading: false,
};
},
methods: {
handleSearch(e) {
e.preventDefault();
this.form.validateFields((error, values) => {
let queryParam = {};
if (values.username) {
queryParam.username = values.username;
}
if (values.topic) {
queryParam.resourceType = "TOPIC";
queryParam.resourceName = values.topic;
} else if (values.groupId) {
queryParam.resourceType = "GROUP";
queryParam.resourceName = values.groupId;
}
this.queryParam = {};
Object.assign(this.queryParam, queryParam);
this.getSaslScramUserList();
});
},
handleReset() {
this.form.resetFields();
},
updateUser() {
this.showUpdateUser = true;
},
closeUpdateUserDialog(data) {
this.showUpdateUser = data.show;
if (data.ok) {
this.getSaslScramUserList();
}
},
onDeleteUser(row) {
this.loading = true;
request({
url: KafkaAclApi.deleteSaslScramUser.url,
method: KafkaAclApi.deleteSaslScramUser.method,
data: { username: row.username },
}).then((res) => {
this.loading = false;
this.getSaslScramUserList();
if (res.code == 0) {
this.$message.success(res.msg);
} else {
this.$message.error(res.msg);
}
});
},
onDeleteUserAndAuth(row) {
this.loading = true;
request({
url: KafkaAclApi.deleteKafkaUser.url,
method: KafkaAclApi.deleteKafkaUser.method,
data: { username: row.username },
}).then((res) => {
this.loading = false;
this.getSaslScramUserList();
if (res.code == 0) {
this.$message.success(res.msg);
} else {
this.$message.error(res.msg);
}
});
},
onManageProducerAuth(row) {
this.openManageProducerAuthDialog = true;
const rowData = {};
Object.assign(rowData, row);
this.selectRow = rowData;
},
onManageConsumerAuth(row) {
this.openManageConsumerAuthDialog = true;
const rowData = {};
Object.assign(rowData, row);
this.selectRow = rowData;
},
onAddAuth(row) {
this.openAddAuthDialog = true;
const rowData = {};
Object.assign(rowData, row);
this.selectRow = rowData;
},
onTopicDetail(topic, username) {
this.selectDetail.resourceType = "TOPIC";
this.selectDetail.resourceName = topic;
this.selectDetail.username = username;
this.openAclDetailDialog = true;
},
onGroupDetail(group, username) {
this.selectDetail.resourceType = "GROUP";
this.selectDetail.resourceName = group;
this.selectDetail.username = username;
this.openAclDetailDialog = true;
},
onUserDetail(username) {
this.selectDetail.username = username;
this.openUserDetailDialog = true;
},
closeManageProducerAuthDialog() {
this.openManageProducerAuthDialog = false;
},
closeManageConsumerAuthDialog() {
this.openManageConsumerAuthDialog = false;
},
closeAddAuthDialog() {
this.openAddAuthDialog = false;
},
closeAclDetailDialog(p) {
this.openAclDetailDialog = false;
if (p.refresh) {
this.getSaslScramUserList();
}
},
closeUserDetailDialog() {
this.openUserDetailDialog = false;
},
getSaslScramUserList() {
if (!this.enableSasl) {
return;
}
this.loading = true;
request({
url: KafkaAclApi.getSaslScramUserList.url,
method: KafkaAclApi.getSaslScramUserList.method,
params: this.queryParam,
}).then((response) => {
this.loading = false;
this.data.splice(0, this.data.length);
if (response.code != 0) {
notification.error({
message: response.msg,
});
return;
}
for (let k in response.data.map) {
let v = response.data.map[k];
let topicList = Object.keys(v)
.filter((e) => e.startsWith("TOPIC"))
.map((e) => e.split("#")[1]);
let groupList = Object.keys(v)
.filter((e) => e.startsWith("GROUP"))
.map((e) => e.split("#")[1]);
this.data.push({
key: k,
username: k,
topicList: topicList,
groupList: groupList,
user: response.data.map[k]["USER"],
});
this.data.sort((a, b) => a.username.localeCompare(b.username));
}
});
},
},
created() {
this.getSaslScramUserList();
},
computed: {
...mapState({
enableSasl: (state) => state.clusterInfo.enableSasl,
}),
},
};
const columns = [
{
title: "用户名",
dataIndex: "username",
key: "username",
width: 300,
slots: { title: "username" },
scopedSlots: { customRender: "username" },
},
{
title: "操作",
key: "operation",
scopedSlots: { customRender: "operation" },
width: 500,
},
];
</script>
<style scoped>
.acl {
width: 100%;
height: 100%;
}
.ant-advanced-search-form {
padding: 24px;
background: #fbfbfb;
border: 1px solid #d9d9d9;
border-radius: 6px;
}
.ant-advanced-search-form .ant-form-item {
display: flex;
}
.ant-advanced-search-form .ant-form-item-control-wrapper {
flex: 1;
}
#components-form-acl-advanced-search .ant-form {
max-width: none;
margin-bottom: 1%;
}
#components-form-acl-advanced-search .search-result-list {
margin-top: 16px;
border: 1px dashed #e9e9e9;
border-radius: 6px;
background-color: #fafafa;
min-height: 200px;
text-align: center;
padding-top: 80px;
}
.input-w {
width: 400px;
}
.operation-row-button {
height: 4%;
text-align: left;
margin-bottom: 8px;
}
.operation-btn {
margin-right: 3%;
}
.hint {
margin-left: 1%;
color: red;
}
</style>

View File

@@ -272,6 +272,7 @@ const columns = [
dataIndex: "clientId",
key: "clientId",
scopedSlots: { customRender: "clientId" },
width: 400,
},
{
title: "日志位点",

View File

@@ -2,7 +2,7 @@
<a-modal
title="消费端成员"
:visible="show"
:width="1500"
:width="1300"
:mask="false"
:destroyOnClose="true"
:footer="null"
@@ -96,16 +96,19 @@ const columns = [
title: "成员ID",
dataIndex: "memberId",
key: "memberId",
width: 300,
},
{
title: "客户端ID",
dataIndex: "clientId",
key: "clientId",
width: 300,
},
{
title: "实例ID",
dataIndex: "groupInstanceId",
key: "groupInstanceId",
width: 150,
},
{
title: "主机",
@@ -117,6 +120,7 @@ const columns = [
dataIndex: "partitions",
key: "partitions",
scopedSlots: { customRender: "partitions" },
width: 300,
},
];
</script>

View File

@@ -0,0 +1,196 @@
<template>
<div class="tab-content">
<a-spin :spinning="loading">
<div>
<h4 class="hint-content">
注意以下删除将删除该分区比该偏移位点小的所有消息不包含该位点
</h4>
<hr />
</div>
<div id="search-offset-form-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<a-row :gutter="24">
<a-col :span="9">
<a-form-item label="topic">
<a-select
class="topic-select"
@change="handleTopicChange"
show-search
option-filter-prop="children"
v-decorator="[
'topic',
{
rules: [{ required: true, message: '请选择一个topic!' }],
},
]"
placeholder="请选择一个topic"
>
<a-select-option v-for="v in topicList" :key="v" :value="v">
{{ v }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item label="分区">
<a-select
class="type-select"
show-search
option-filter-prop="children"
v-model="selectPartition"
placeholder="请选择一个分区"
>
<a-select-option v-for="v in partitions" :key="v" :value="v">
<span v-if="v == -1">全部</span> <span v-else>{{ v }}</span>
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="7">
<a-form-item label="偏移">
<a-input
v-decorator="[
'offset',
{
rules: [{ required: true, message: '请输入消息偏移!' }],
},
]"
placeholder="消息偏移"
/>
</a-form-item>
</a-col>
<a-col :span="2" :style="{ textAlign: 'right' }">
<a-form-item>
<a-button type="primary" html-type="submit"> 执行删除</a-button>
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
</a-spin>
</div>
</template>
<script>
import request from "@/utils/request";
import { KafkaMessageApi, KafkaTopicApi } from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
export default {
name: "DeleteMessage",
props: {
topicList: {
type: Array,
},
},
data() {
return {
loading: false,
form: this.$form.createForm(this, { name: "message_search_offset" }),
partitions: [],
selectPartition: undefined,
};
},
methods: {
handleSearch(e) {
e.preventDefault();
this.form.validateFields((err, values) => {
if (!err) {
const data = Object.assign({}, values, {
partition: this.selectPartition,
});
this.loading = true;
request({
url: KafkaMessageApi.delete.url,
method: KafkaMessageApi.delete.method,
data: [data],
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.$message.success(res.msg);
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
}
});
},
getPartitionInfo(topic) {
this.loading = true;
request({
url: KafkaTopicApi.getPartitionInfo.url + "?topic=" + topic,
method: KafkaTopicApi.getPartitionInfo.method,
}).then((res) => {
this.loading = false;
if (res.code != 0) {
notification.error({
message: "error",
description: res.msg,
});
} else {
this.partitions = res.data.map((v) => v.partition);
}
});
},
handleTopicChange(topic) {
this.selectPartition =
this.partitions.length > 0 ? this.partitions[0] : 0;
this.getPartitionInfo(topic);
},
},
};
</script>
<style scoped>
.tab-content {
width: 100%;
height: 100%;
}
.ant-advanced-search-form {
padding: 24px;
background: #fbfbfb;
border: 1px solid #d9d9d9;
border-radius: 6px;
}
.ant-advanced-search-form .ant-form-item {
display: flex;
}
.ant-advanced-search-form .ant-form-item-control-wrapper {
flex: 1;
}
#components-form-topic-advanced-search .ant-form {
max-width: none;
margin-bottom: 1%;
}
#search-offset-form-advanced-search .search-result-list {
margin-top: 16px;
border: 1px dashed #e9e9e9;
border-radius: 6px;
background-color: #fafafa;
min-height: 200px;
text-align: center;
padding-top: 80px;
}
.topic-select {
width: 400px !important;
}
.type-select {
width: 200px !important;
}
.hint-content {
color: red;
}
</style>

View File

@@ -11,6 +11,9 @@
<a-tab-pane key="3" tab="在线发送">
<SendMessage :topic-list="topicList"></SendMessage>
</a-tab-pane>
<a-tab-pane key="4" tab="在线删除">
<DeleteMessage :topic-list="topicList"></DeleteMessage>
</a-tab-pane>
</a-tabs>
</a-spin>
</div>
@@ -23,9 +26,10 @@ import request from "@/utils/request";
import { KafkaTopicApi } from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
import SendMessage from "@/views/message/SendMessage";
import DeleteMessage from "./DeleteMessage";
export default {
name: "Message",
components: { SearchByTime, SearchByOffset, SendMessage },
components: { DeleteMessage, SearchByTime, SearchByOffset, SendMessage },
data() {
return {
loading: false,

View File

@@ -132,9 +132,9 @@ export default {
if (
clusterInfo &&
clusterInfo.id &&
clusterInfo.id == this.clusterInfo.id &&
clusterInfo.clusterName != data.clusterName
clusterInfo.id == this.clusterInfo.id
) {
// &&clusterInfo.clusterName != data.clusterName
this.switchCluster(data);
}
}

View File

@@ -0,0 +1,207 @@
<script src="../../store/index.js"></script>
<template>
<a-modal
title="新增配置"
:visible="show"
:width="800"
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
@cancel="handleCancel"
>
<div>
<a-spin :spinning="loading">
<a-form
:form="form"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 12 }"
@submit="handleSubmit"
>
<a-form-item label="用户" v-show="showUser">
<a-input
v-decorator="[
'user',
]"
placeholder="输入用户主体标识,比如:用户名,未指定表示用户默认设置"
/>
</a-form-item>
<a-form-item label="客户端ID" v-show="showClientId">
<a-input
v-decorator="[
'client',
]"
placeholder="输入用户客户端ID未指定表示默认客户端设置"
/>
</a-form-item>
<a-form-item label="IP" v-show="showIP">
<a-input
v-decorator="[
'ip',
]"
placeholder="输入客户端IP"
/>
</a-form-item>
<a-form-item label="生产速率">
<a-input-number
:min="1"
:max="102400000"
v-decorator="[
'producerRate',
]"
/>
<a-select default-value="MB" v-model="producerRateUnit" style="width: 100px">
<a-select-option value="MB"> MB/s</a-select-option>
<a-select-option value="KB"> KB/s</a-select-option>
<a-select-option value="Byte"> Byte/s</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="消费速率">
<a-input-number
:min="1"
:max="102400000"
v-decorator="[
'consumerRate',
]"
/>
<a-select default-value="MB" v-model="consumerRateUnit" style="width: 100px">
<a-select-option value="MB"> MB/s</a-select-option>
<a-select-option value="KB"> KB/s</a-select-option>
<a-select-option value="Byte"> Byte/s</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="吞吐量">
<a-input-number
:min="1"
:max="102400000"
v-decorator="[
'requestPercentage',
]"
/>
</a-form-item>
<a-form-item :wrapper-col="{ span: 12, offset: 5 }">
<a-button type="primary" html-type="submit"> 提交</a-button>
</a-form-item>
</a-form>
</a-spin>
</div>
</a-modal>
</template>
<script>
import request from "@/utils/request";
import {KafkaClientQuotaApi} from "@/utils/api";
import notification from "ant-design-vue/es/notification";
export default {
name: "AddQuotaConfig",
props: {
visible: {
type: Boolean,
default: false,
},
type: {
type: String,
default: "",
},
showClientId: {
type: Boolean,
default: false,
},
showUser: {
type: Boolean,
default: false,
},
showIP: {
type: Boolean,
default: false,
},
},
data() {
return {
show: this.visible,
data: [],
loading: false,
form: this.$form.createForm(this, {name: "coordinated"}),
producerRateUnit: "MB",
consumerRateUnit: "MB",
};
},
watch: {
visible(v) {
this.show = v;
},
},
methods: {
handleSubmit() {
this.form.validateFields((err, values) => {
if (!err) {
const params = Object.assign({type: this.type}, values);
const unitMap = {MB: 1024 * 1024, KB: 1024, Byte: 1};
if (values.consumerRate) {
params.consumerRate = params.consumerRate * unitMap[this.consumerRateUnit];
}
if (values.producerRate) {
params.producerRate = params.producerRate * unitMap[this.producerRateUnit];
}
params.types = [];
params.names = [];
if (this.showUser) {
params.types.push("user");
if (params.user) {
params.names.push(params.user.trim());
} else {
params.names.push("");
}
}
if (this.showClientId) {
params.types.push("client-id");
if (params.client) {
params.names.push(params.client.trim());
} else {
params.names.push("");
}
}
if (this.showIP) {
params.types.push("ip");
if (params.ip) {
params.names.push(params.ip.trim());
} else {
params.names.push("");
}
}
this.loading = true;
request({
url: KafkaClientQuotaApi.alterClientQuotaConfigs.url,
method: KafkaClientQuotaApi.alterClientQuotaConfigs.method,
data: params,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.$message.success(res.msg);
this.$emit("closeAddQuotaDialog", {refresh: true});
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
}
});
},
handleCancel() {
this.data = [];
this.$emit("closeAddQuotaDialog", {refresh: false});
this.producerRateUnit = "MB";
this.consumerRateUnit = "MB";
},
create() {
this.producerRateUnit = "MB";
this.consumerRateUnit = "MB";
},
},
};
</script>
<style scoped></style>

View File

@@ -0,0 +1,180 @@
<template>
<div class="tab-content">
<a-spin :spinning="loading">
<div id="search-offset-form-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<a-row :gutter="24">
<a-col :span="16">
<a-form-item label="客户端ID">
<a-input
v-decorator="[
'id',
]"
placeholder="请输入生产者/消费者客户端ID!"
/>
</a-form-item>
</a-col>
<a-col :span="2" :style="{ textAlign: 'right' }">
<a-form-item>
<a-button type="primary" html-type="submit"> 搜索</a-button>
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="openAddQuotaDialog"
>新增配置
</a-button>
</div>
<QuotaList type="client-id" :columns="columns" :data="data" @refreshQuotaList="refresh"></QuotaList>
<AddQuotaConfig type="client-id" :visible="showAddQuotaDialog" :showClientId="true" @closeAddQuotaDialog="closeAddQuotaDialog"></AddQuotaConfig>
</a-spin>
</div>
</template>
<script>
import request from "@/utils/request";
import {KafkaClientQuotaApi} from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
import QuotaList from "@/views/quota/QuotaList.vue";
import AddQuotaConfig from "@/views/quota/AddQuotaConfig.vue";
export default {
name: "ClientIDQuota",
components: {QuotaList, AddQuotaConfig},
props: {
topicList: {
type: Array,
},
},
data() {
return {
loading: false,
form: this.$form.createForm(this, {name: "client_id_quota"}),
data: [],
showAlterQuotaDialog: false,
showAddQuotaDialog: false,
columns: [
{
title: "客户端ID",
dataIndex: "client",
key: "client",
slots: {title: "client"},
scopedSlots: {customRender: "client"},
},
{
title: "生产速率(带宽/秒)",
dataIndex: "producerRate",
key: "producerRate",
},
{
title: "消费速率(带宽/秒)",
dataIndex: "consumerRate",
key: "consumerRate",
},
{
title: "吞吐量(请求占比*100)",
dataIndex: "requestPercentage",
key: "requestPercentage",
},
],
};
},
methods: {
handleSearch() {
this.form.validateFields((err, values) => {
if (!err) {
this.loading = true;
const params = {types: ["client-id"]};
if (values.id) {
params.names = [values.id.trim()];
}
request({
url: KafkaClientQuotaApi.getClientQuotaConfigs.url,
method: KafkaClientQuotaApi.getClientQuotaConfigs.method,
data: params,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.data = res.data;
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
}
});
},
openAddQuotaDialog() {
this.showAddQuotaDialog = true;
},
closeAddQuotaDialog(p) {
if (p.refresh) {
this.handleSearch();
}
this.showAddQuotaDialog = false;
},
refresh() {
this.handleSearch();
},
},
created() {
this.handleSearch();
},
};
</script>
<style scoped>
.tab-content {
width: 100%;
height: 100%;
}
.ant-advanced-search-form {
padding: 24px;
background: #fbfbfb;
border: 1px solid #d9d9d9;
border-radius: 6px;
}
.ant-advanced-search-form .ant-form-item {
display: flex;
}
.ant-advanced-search-form input {
width: 400px;
}
.ant-advanced-search-form .ant-form-item-control-wrapper {
flex: 1;
}
#components-form-topic-advanced-search .ant-form {
max-width: none;
margin-bottom: 1%;
}
#search-offset-form-advanced-search .search-result-list {
margin-top: 16px;
border: 1px dashed #e9e9e9;
border-radius: 6px;
background-color: #fafafa;
min-height: 200px;
text-align: center;
padding-top: 80px;
}
.operation-row-button {
height: 4%;
text-align: left;
margin-bottom: 5px;
margin-top: 5px;
}
</style>

View File

@@ -0,0 +1,42 @@
<template>
<div class="content">
<a-spin :spinning="loading">
<a-tabs default-active-key="1" size="large" tabPosition="top">
<a-tab-pane key="1" tab="使用说明">
<ClientQuotaIntroduce></ClientQuotaIntroduce>
</a-tab-pane>
<a-tab-pane key="2" tab="用户">
<UserQuota></UserQuota>
</a-tab-pane>
<a-tab-pane key="3" tab="客户端ID">
<ClientIDQuota></ClientIDQuota>
</a-tab-pane>
<a-tab-pane key="4" tab="用户_客户端ID">
<UserAndClientIDQuota></UserAndClientIDQuota>
</a-tab-pane>
<!-- <a-tab-pane key="5" tab="IP">-->
<!-- <IpQuota></IpQuota>-->
<!-- </a-tab-pane>-->
</a-tabs>
</a-spin>
</div>
</template>
<script>
import ClientIDQuota from "@/views/quota/ClientIDQuota.vue";
import UserQuota from "@/views/quota/UserQuota.vue";
import UserAndClientIDQuota from "@/views/quota/UserAndClientIDQuota.vue";
import ClientQuotaIntroduce from "@/views/quota/ClientQuotaIntroduce.vue";
export default {
name: "ClientQuota",
components: {ClientIDQuota, UserQuota, UserAndClientIDQuota, ClientQuotaIntroduce},
data() {
return {
loading: false,
};
},
};
</script>
<style scoped></style>

View File

@@ -0,0 +1,46 @@
<template>
<div class="content">
<h1>客户端限流说明资源限额</h1>
<hr/>
<hr/>
<h2>支持类型</h2>
<ul>
<li>基于用户主体标识的配置</li>
<li>基于客户端ID的配置</li>
<li>基于用户主体+客户端ID的配置</li>
</ul>
<hr/>
<h2>默认配置</h2>
<p>比如基于用户的配置在新增配置的时候未设置用户名称则默认应用于所有用户</p>
<p>基于客户端ID的配置新增的时候未指定客户端ID则应用于所有客户端</p>
<hr/>
<h2>配置优先级</h2>
下面的展示中数字越小即越靠上的优先级越高相同的用户名称或者客户端ID优先级最高的配置生效下面的未指定表示 默认配置
<ol>
<li>[用户+客户端ID] 指定用户名称并且指定客户端ID优先级最高</li>
<li>[用户+客户端ID] 指定用户名称未指定客户端ID</li>
<li>[用户] 指定用户名称</li>
<li>[用户+客户端ID] 未指定用户名称但指定客户端ID</li>
<li>[用户+客户端ID] 未指定用户名称未指定客户端ID</li>
<li>[用户] 未指定用户名称</li>
<li>[客户端ID] 指定客户端ID</li>
<li>[客户端ID] 未指定客户端ID优先级最低</li>
</ol>
<hr/>
<h2>使用注意</h2>
大多数集群一般没有开启认证所以用户配置可能不支持限流可以使用基于客户端ID的配置但是对于优先级最低的"[客户端ID]
未指定客户端ID"默认对于所有客户端生效万一速率配置过小可能无意间影响生产业务所以尽量避免默认配置
<hr/>
<h2>查询</h2>
查询默认配置在查询项的输入框内输入一个空格即可
</div>
</template>
<script>
export default {
name: "ClientQuotaIntroduce",
};
</script>
<style scoped></style>

View File

@@ -0,0 +1,180 @@
<template>
<div class="tab-content">
<a-spin :spinning="loading">
<div id="search-offset-form-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<a-row :gutter="24">
<a-col :span="16">
<a-form-item label="IP">
<a-input
v-decorator="[
'ip',
]"
placeholder="请输入ip!"
/>
</a-form-item>
</a-col>
<a-col :span="2" :style="{ textAlign: 'right' }">
<a-form-item>
<a-button type="primary" html-type="submit"> 搜索</a-button>
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="openAddQuotaDialog"
>新增配置
</a-button>
</div>
<QuotaList type="ip" :columns="columns" :data="data" @refreshQuotaList="refresh"></QuotaList>
<AddQuotaConfig type="ip" :visible="showAddQuotaDialog" :showIP="true" @closeAddQuotaDialog="closeAddQuotaDialog"></AddQuotaConfig>
</a-spin>
</div>
</template>
<script>
import request from "@/utils/request";
import {KafkaClientQuotaApi} from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
import QuotaList from "@/views/quota/QuotaList.vue";
import AddQuotaConfig from "@/views/quota/AddQuotaConfig.vue";
export default {
name: "IpQuota",
components: {QuotaList, AddQuotaConfig},
props: {
topicList: {
type: Array,
},
},
data() {
return {
loading: false,
form: this.$form.createForm(this, {name: "ip_quota"}),
data: [],
showAlterQuotaDialog: false,
showAddQuotaDialog: false,
columns: [
{
title: "IP",
dataIndex: "ip",
key: "ip",
slots: {title: "ip"},
scopedSlots: {customRender: "ip"},
},
{
title: "生产速率(带宽/秒)",
dataIndex: "producerRate",
key: "producerRate",
},
{
title: "消费速率(带宽/秒)",
dataIndex: "consumerRate",
key: "consumerRate",
},
{
title: "吞吐量(请求占比*100)",
dataIndex: "requestPercentage",
key: "requestPercentage",
},
],
};
},
methods: {
handleSearch() {
this.form.validateFields((err, values) => {
if (!err) {
this.loading = true;
const params = {types: ["ip"]};
if (values.ip) {
params.names = [values.ip.trim()];
}
request({
url: KafkaClientQuotaApi.getClientQuotaConfigs.url,
method: KafkaClientQuotaApi.getClientQuotaConfigs.method,
data: params,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.data = res.data;
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
}
});
},
openAddQuotaDialog() {
this.showAddQuotaDialog = true;
},
closeAddQuotaDialog(p) {
if (p.refresh) {
this.handleSearch();
}
this.showAddQuotaDialog = false;
},
refresh() {
this.handleSearch();
},
},
created() {
this.handleSearch();
},
};
</script>
<style scoped>
.tab-content {
width: 100%;
height: 100%;
}
.ant-advanced-search-form {
padding: 24px;
background: #fbfbfb;
border: 1px solid #d9d9d9;
border-radius: 6px;
}
.ant-advanced-search-form .ant-form-item {
display: flex;
}
.ant-advanced-search-form input {
width: 400px;
}
.ant-advanced-search-form .ant-form-item-control-wrapper {
flex: 1;
}
#components-form-topic-advanced-search .ant-form {
max-width: none;
margin-bottom: 1%;
}
#search-offset-form-advanced-search .search-result-list {
margin-top: 16px;
border: 1px dashed #e9e9e9;
border-radius: 6px;
background-color: #fafafa;
min-height: 200px;
text-align: center;
padding-top: 80px;
}
.operation-row-button {
height: 4%;
text-align: left;
margin-bottom: 5px;
margin-top: 5px;
}
</style>

View File

@@ -0,0 +1,174 @@
<template>
<div>
<a-spin :spinning="loading">
<a-table
:columns="columns"
:data-source="data"
bordered
:row-key="
(record, index) => {
return index;
}
"
@change="handleChange"
>
<div slot="client" slot-scope="text">
<span v-if="text">{{ text }}</span><span v-else style="color: red">默认配置</span>
</div>
<div slot="user" slot-scope="text">
<span v-if="text">{{ text }}</span><span v-else style="color: red">默认配置</span>
</div>
<div slot="operation" slot-scope="record">
<a-popconfirm
:title="'删除当前配置?'"
ok-text="确认"
cancel-text="取消"
@confirm="deleteConfig(record)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
>删除
</a-button>
</a-popconfirm>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openUpdateDialog(record)"
>修改
</a-button>
</div>
</a-table>
<UpdateQuotaConfig :type="type" :record="selectRow" :visible="showUpdateDialog"
@closeUpdateQuotaDialog="closeUpdateQuotaDialog"></UpdateQuotaConfig>
</a-spin>
</div>
</template>
<script>
import {KafkaClientQuotaApi} from "@/utils/api";
import request from "@/utils/request";
import notification from "ant-design-vue/lib/notification";
import UpdateQuotaConfig from "@/views/quota/UpdateQuotaConfig.vue";
export default {
name: "QuotaList",
components: {UpdateQuotaConfig},
props: {
columns: {
type: Array,
},
data: {
type: Array,
},
type: {
type: String,
default: "",
},
},
data() {
return {
record: {},
sortedInfo: null,
loading: false,
selectRow: {},
showUpdateDialog: false,
};
},
methods: {
openDetailDialog(record) {
this.record = record;
this.showDetailDialog = true;
},
closeDetailDialog() {
this.showDetailDialog = false;
},
handleChange() {
this.sortedInfo = arguments[2];
},
deleteConfig(record) {
this.loading = true;
const params = {type: this.type};
params.types = [];
params.names = [];
if (this.type == "user") {
params.types.push("user");
if (record.user) {
params.names.push(record.user.trim());
} else {
params.names.push("");
}
} else if (this.type == "client-id") {
params.types.push("client-id");
if (record.client) {
params.names.push(record.client.trim());
} else {
params.names.push("");
}
}
if (this.type == "ip") {
params.types.push("ip");
if (record.ip) {
params.names.push(record.ip.trim());
} else {
params.names.push("");
}
}
if (this.type == "user&client-id") {
params.types.push("user");
params.types.push("client-id");
if (record.user) {
params.names.push(record.user.trim());
} else {
params.names.push("");
}
if (record.client) {
params.names.push(record.client.trim());
} else {
params.names.push("");
}
}
request({
url: KafkaClientQuotaApi.deleteClientQuotaConfigs.url,
method: KafkaClientQuotaApi.deleteClientQuotaConfigs.method,
data: params,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.$message.success(res.msg);
this.$emit("refreshQuotaList");
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
},
openUpdateDialog(record) {
this.selectRow = record;
this.showUpdateDialog = true;
},
closeUpdateQuotaDialog(event) {
this.selectRow = {};
this.showUpdateDialog = false;
if (event.refresh) {
this.$emit("refreshQuotaList");
}
},
},
created() {
this.columns.push({
title: "操作",
key: "operation",
scopedSlots: {customRender: "operation"},
});
},
};
</script>
<style scoped>
.operation-btn {
margin-right: 3%;
}
</style>

View File

@@ -0,0 +1,252 @@
<script src="../../store/index.js"></script>
<template>
<a-modal
title="修改配置"
:visible="show"
:width="800"
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
@cancel="handleCancel"
>
<div>
<a-spin :spinning="loading">
<a-form
:form="form"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 12 }"
@submit="handleSubmit"
>
<a-form-item label="用户" v-show="showUser">
<a-input
:disabled="true"
v-decorator="[
'user', { initialValue: record.user }
]"
placeholder="输入用户主体标识,比如:用户名,未指定表示用户默认设置"
/>
</a-form-item>
<a-form-item label="客户端ID" v-show="showClientId">
<a-input
:disabled="true"
v-decorator="[
'client', { initialValue: record.client }
]"
placeholder="输入用户客户端ID未指定表示默认客户端设置"
/>
</a-form-item>
<a-form-item label="IP" v-show="showIP">
<a-input
:disabled="true"
v-decorator="[
'ip', { initialValue: record.ip }
]"
placeholder="输入客户端IP"
/>
</a-form-item>
<a-form-item label="生产速率">
<a-input-number
:min="1"
:max="102400000"
v-decorator="[
'producerRate', { initialValue: record.producerRate }
]"
/>
<a-select default-value="MB" v-model="producerRateUnit" style="width: 100px">
<a-select-option value="MB"> MB/s</a-select-option>
<a-select-option value="KB"> KB/s</a-select-option>
<a-select-option value="Byte"> Byte/s</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="消费速率">
<a-input-number
:min="1"
:max="102400000"
v-decorator="[
'consumerRate', { initialValue: record.consumerRate }
]"
/>
<a-select default-value="MB" v-model="consumerRateUnit" style="width: 100px">
<a-select-option value="MB"> MB/s</a-select-option>
<a-select-option value="KB"> KB/s</a-select-option>
<a-select-option value="Byte"> Byte/s</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="吞吐量">
<a-input-number
:min="1"
:max="102400000"
v-decorator="[
'requestPercentage', { initialValue: record.requestPercentage }
]"
/>
</a-form-item>
<a-form-item :wrapper-col="{ span: 12, offset: 5 }">
<a-button type="primary" html-type="submit"> 提交</a-button>
</a-form-item>
</a-form>
</a-spin>
</div>
</a-modal>
</template>
<script>
import request from "@/utils/request";
import {KafkaClientQuotaApi} from "@/utils/api";
import notification from "ant-design-vue/es/notification";
export default {
name: "UpdateQuotaConfig",
props: {
visible: {
type: Boolean,
default: false,
},
type: {
type: String,
default: "",
},
record: {
type: Object,
default: function () {
return {}
},
},
},
data() {
return {
show: this.visible,
data: [],
loading: false,
form: this.$form.createForm(this, {name: "coordinated"}),
producerRateUnit: "MB",
consumerRateUnit: "MB",
showUser: false,
showIP: false,
showClientId: false,
};
},
watch: {
visible(v) {
this.show = v;
if (this.show) {
this.init();
}
},
},
methods: {
handleSubmit() {
this.form.validateFields((err, values) => {
if (!err) {
const params = {type: this.type, deleteConfigs: []};
const unitMap = {MB: 1024 * 1024, KB: 1024, Byte: 1};
if (values.consumerRate) {
const num = typeof (values.consumerRate) == "string" && values.consumerRate.indexOf(" ") > 0 ? values.consumerRate.split(" ")[0] : values.consumerRate;
params.consumerRate = num * unitMap[this.consumerRateUnit];
} else {
params.deleteConfigs.push("consumerRate");
}
if (values.producerRate) {
const num = typeof (values.producerRate) == "string" && values.producerRate.indexOf(" ") > 0 ? values.producerRate.split(" ")[0] : values.producerRate;
params.producerRate = num * unitMap[this.producerRateUnit];
} else {
params.deleteConfigs.push("producerRate");
}
if (values.requestPercentage) {
params.requestPercentage = values.requestPercentage;
} else {
params.deleteConfigs.push("requestPercentage");
}
params.types = [];
params.names = [];
if (this.showUser) {
params.types.push("user");
if (values.user) {
params.names.push(values.user.trim());
} else {
params.names.push("");
}
}
if (this.showClientId) {
params.types.push("client-id");
if (values.client) {
params.names.push(values.client.trim());
} else {
params.names.push("");
}
}
if (this.showIP) {
params.types.push("ip");
if (values.ip) {
params.names.push(values.ip.trim());
} else {
params.names.push("");
}
}
if (this.showUser && this.showClientId) {
params.types.push("user");
params.types.push("client-id");
if (values.user) {
params.names.push(values.user.trim());
} else {
params.names.push("");
}
if (values.client) {
params.names.push(values.client.trim());
} else {
params.names.push("");
}
}
this.loading = true;
request({
url: KafkaClientQuotaApi.alterClientQuotaConfigs.url,
method: KafkaClientQuotaApi.alterClientQuotaConfigs.method,
data: params,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.$message.success(res.msg);
this.$emit("closeUpdateQuotaDialog", {refresh: true});
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
}
});
},
handleCancel() {
this.data = [];
this.$emit("closeUpdateQuotaDialog", {refresh: false});
},
init() {
this.producerRateUnit = "MB";
if (this.record.producerRate) {
this.producerRateUnit = this.record.producerRate.split(" ")[1];
}
this.consumerRateUnit = "MB";
if (this.record.consumerRate) {
this.consumerRateUnit = this.record.consumerRate.split(" ")[1];
}
if (this.type == "user") {
this.showUser = true;
} else if (this.type == "client-id") {
this.showClientId = true;
} else if (this.type == "ip") {
this.showIP = true;
} else if (this.type == "user&client-id") {
this.showUser = true;
this.showClientId = true;
}
},
},
created() {
},
};
</script>
<style scoped></style>

View File

@@ -0,0 +1,204 @@
<template>
<div class="tab-content">
<a-spin :spinning="loading">
<div id="search-offset-form-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<a-row :gutter="24">
<a-col :span="10">
<a-form-item label="用户标识">
<a-input
v-decorator="[
'user',
]"
placeholder="请输入用户标识,如:用户名!"
/>
</a-form-item>
</a-col>
<a-col :span="10">
<a-form-item label="客户端ID">
<a-input
v-decorator="[
'client',
]"
placeholder="请输入客户端ID!"
/>
</a-form-item>
</a-col>
<a-col :span="2" :style="{ textAlign: 'right' }">
<a-form-item>
<a-button type="primary" html-type="submit"> 搜索</a-button>
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="openAddQuotaDialog"
>新增配置
</a-button>
</div>
<QuotaList type="user&client-id" :columns="columns" :data="data" @refreshQuotaList="refresh"></QuotaList>
<AddQuotaConfig type="user&client-id" :visible="showAddQuotaDialog" :showUser="true" :showClientId="true"
@closeAddQuotaDialog="closeAddQuotaDialog"></AddQuotaConfig>
</a-spin>
</div>
</template>
<script>
import request from "@/utils/request";
import {KafkaClientQuotaApi} from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
import QuotaList from "@/views/quota/QuotaList.vue";
import AddQuotaConfig from "@/views/quota/AddQuotaConfig.vue";
export default {
name: "UserAndClientIDQuota",
components: {QuotaList, AddQuotaConfig},
props: {
topicList: {
type: Array,
},
},
data() {
return {
loading: false,
form: this.$form.createForm(this, {name: "user_client_id_quota"}),
data: [],
showAlterQuotaDialog: false,
showAddQuotaDialog: false,
columns: [
{
title: "用户标识",
dataIndex: "user",
key: "user",
slots: {title: "user"},
scopedSlots: {customRender: "user"},
},
{
title: "客户端ID",
dataIndex: "client",
key: "client",
slots: {title: "client"},
scopedSlots: {customRender: "client"},
},
{
title: "生产速率(带宽/秒)",
dataIndex: "producerRate",
key: "producerRate",
},
{
title: "消费速率(带宽/秒)",
dataIndex: "consumerRate",
key: "consumerRate",
},
{
title: "吞吐量(请求占比*100)",
dataIndex: "requestPercentage",
key: "requestPercentage",
},
],
};
},
methods: {
handleSearch() {
this.form.validateFields((err, values) => {
if (!err) {
this.loading = true;
const params = {types: ["user", "client-id"], names: []};
if (values.user) {
params.names.push(values.user.trim());
}
if (values.client) {
if (params.names.length == 0) {
params.names.push("");
}
params.names.push(values.client.trim());
}
request({
url: KafkaClientQuotaApi.getClientQuotaConfigs.url,
method: KafkaClientQuotaApi.getClientQuotaConfigs.method,
data: params,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.data = res.data;
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
}
});
},
openAddQuotaDialog() {
this.showAddQuotaDialog = true;
},
closeAddQuotaDialog(p) {
if (p.refresh) {
this.handleSearch();
}
this.showAddQuotaDialog = false;
},
refresh() {
this.handleSearch();
},
},
created() {
this.handleSearch();
},
};
</script>
<style scoped>
.tab-content {
width: 100%;
height: 100%;
}
.ant-advanced-search-form {
padding: 24px;
background: #fbfbfb;
border: 1px solid #d9d9d9;
border-radius: 6px;
}
.ant-advanced-search-form .ant-form-item {
display: flex;
}
.ant-advanced-search-form input {
width: 400px;
}
.ant-advanced-search-form .ant-form-item-control-wrapper {
flex: 1;
}
#components-form-topic-advanced-search .ant-form {
max-width: none;
margin-bottom: 1%;
}
#search-offset-form-advanced-search .search-result-list {
margin-top: 16px;
border: 1px dashed #e9e9e9;
border-radius: 6px;
background-color: #fafafa;
min-height: 200px;
text-align: center;
padding-top: 80px;
}
.operation-row-button {
height: 4%;
text-align: left;
margin-bottom: 5px;
margin-top: 5px;
}
</style>

View File

@@ -0,0 +1,181 @@
<template>
<div class="tab-content">
<a-spin :spinning="loading">
<div id="search-offset-form-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<a-row :gutter="24">
<a-col :span="16">
<a-form-item label="用户标识">
<a-input
v-decorator="[
'user',
]"
placeholder="请输入用户标识,如:用户名!"
/>
</a-form-item>
</a-col>
<a-col :span="2" :style="{ textAlign: 'right' }">
<a-form-item>
<a-button type="primary" html-type="submit"> 搜索</a-button>
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="openAddQuotaDialog"
>新增配置
</a-button>
</div>
<QuotaList type="user" :columns="columns" :data="data" @refreshQuotaList="refresh"></QuotaList>
<AddQuotaConfig type="user" :visible="showAddQuotaDialog" :showUser="true" @closeAddQuotaDialog="closeAddQuotaDialog"></AddQuotaConfig>
</a-spin>
</div>
</template>
<script>
import request from "@/utils/request";
import {KafkaClientQuotaApi} from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
import QuotaList from "@/views/quota/QuotaList.vue";
import AddQuotaConfig from "@/views/quota/AddQuotaConfig.vue";
export default {
name: "UserQuota",
components: {QuotaList, AddQuotaConfig},
props: {
topicList: {
type: Array,
},
},
data() {
return {
loading: false,
form: this.$form.createForm(this, {name: "user_quota"}),
data: [],
showAlterQuotaDialog: false,
showAddQuotaDialog: false,
columns: [
{
title: "用户标识",
dataIndex: "user",
key: "user",
width: 300,
slots: {title: "user"},
scopedSlots: {customRender: "user"},
},
{
title: "生产速率(带宽/秒)",
dataIndex: "producerRate",
key: "producerRate",
},
{
title: "消费速率(带宽/秒)",
dataIndex: "consumerRate",
key: "consumerRate",
},
{
title: "吞吐量(请求占比*100)",
dataIndex: "requestPercentage",
key: "requestPercentage",
},
],
};
},
methods: {
handleSearch() {
this.form.validateFields((err, values) => {
if (!err) {
this.loading = true;
const params = {types: ["user"]};
if (values.user) {
params.names = [values.user.trim()];
}
request({
url: KafkaClientQuotaApi.getClientQuotaConfigs.url,
method: KafkaClientQuotaApi.getClientQuotaConfigs.method,
data: params,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.data = res.data;
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
}
});
},
openAddQuotaDialog() {
this.showAddQuotaDialog = true;
},
closeAddQuotaDialog(p) {
if (p.refresh) {
this.handleSearch();
}
this.showAddQuotaDialog = false;
},
refresh() {
this.handleSearch();
},
},
created() {
this.handleSearch();
},
};
</script>
<style scoped>
.tab-content {
width: 100%;
height: 100%;
}
.ant-advanced-search-form {
padding: 24px;
background: #fbfbfb;
border: 1px solid #d9d9d9;
border-radius: 6px;
}
.ant-advanced-search-form .ant-form-item {
display: flex;
}
.ant-advanced-search-form input {
width: 400px;
}
.ant-advanced-search-form .ant-form-item-control-wrapper {
flex: 1;
}
#components-form-topic-advanced-search .ant-form {
max-width: none;
margin-bottom: 1%;
}
#search-offset-form-advanced-search .search-result-list {
margin-top: 16px;
border: 1px dashed #e9e9e9;
border-radius: 6px;
background-color: #fafafa;
min-height: 200px;
text-align: center;
padding-top: 80px;
}
.operation-row-button {
height: 4%;
text-align: left;
margin-bottom: 5px;
margin-top: 5px;
}
</style>

View File

@@ -161,6 +161,7 @@ const columns = [
dataIndex: "clientId",
key: "clientId",
scopedSlots: { customRender: "clientId" },
width: 400,
},
{
title: "日志位点",

View File

@@ -49,10 +49,34 @@
<a-button type="primary" @click="openCreateTopicDialog"
>新增</a-button
>
<a-popconfirm
title="删除这些Topic?"
ok-text="确认"
cancel-text="取消"
@confirm="deleteTopics(selectedRowKeys)"
>
<a-button
type="danger"
class="btn-left"
:disabled="!hasSelected"
:loading="loading"
>
批量删除
</a-button>
</a-popconfirm>
<span style="margin-left: 8px">
<template v-if="hasSelected">
{{ `已选择 ${selectedRowKeys.length} 个Topic` }}
</template>
</span>
</div>
<a-table
:columns="columns"
:data-source="filteredData"
:row-selection="{
selectedRowKeys: selectedRowKeys,
onChange: onSelectChange,
}"
bordered
row-key="name"
>
@@ -225,8 +249,14 @@ export default {
filterTopic: "",
filteredData: [],
type: "normal",
selectedRowKeys: [], // Check here to configure the default column
};
},
computed: {
hasSelected() {
return this.selectedRowKeys.length > 0;
},
},
methods: {
handleSearch(e) {
e.preventDefault();
@@ -256,14 +286,16 @@ export default {
}
});
},
deleteTopic(topic) {
deleteTopics(topics) {
request({
url: KafkaTopicApi.deleteTopic.url + "?topic=" + topic,
url: KafkaTopicApi.deleteTopic.url,
method: KafkaTopicApi.deleteTopic.method,
data: topics,
}).then((res) => {
if (res.code == 0) {
this.$message.success(res.msg);
this.getTopicList();
this.selectedRowKeys = [];
} else {
notification.error({
message: "error",
@@ -272,6 +304,9 @@ export default {
}
});
},
deleteTopic(topic) {
this.deleteTopics([topic]);
},
onTopicUpdate(input) {
this.filterTopic = input.target.value;
this.filter();
@@ -342,9 +377,13 @@ export default {
closeThrottleDialog() {
this.showThrottleDialog = false;
},
onSelectChange(selectedRowKeys) {
this.selectedRowKeys = selectedRowKeys;
},
},
created() {
this.getTopicList();
this.selectedRowKeys = [];
},
};
@@ -431,4 +470,8 @@ const columns = [
.type-select {
width: 200px !important;
}
.btn-left {
margin-left: 1%;
}
</style>