23 Commits

Author SHA1 Message Date
许晓东
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
85 changed files with 14793 additions and 2386 deletions

View File

@@ -1,6 +1,6 @@
# kafka可视化管理平台
一款轻量级的kafka可视化管理平台安装配置快捷、简单易用。
为了开发的省事,没有国际化支持,只支持中文展示。
为了开发的省事,没有国际化支持,页面只支持中文展示。
用过rocketmq-console吧前端展示风格跟那个有点类似。
## 页面预览
@@ -22,7 +22,9 @@ acl配置说明如果kafka集群启用了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.4版本)[kafka-console-ui.zip](https://github.com/xxd763795151/kafka-console-ui/releases/download/v1.0.4/kafka-console-ui.zip)
如果安装包下载的比较慢可以查看下面的源码打包说明把代码下载下来快速打包不过最新main分支代码刚升级了kafka版本到3.2.0,还没有充分测试,如果需要稳定版本,可以下载 1.0.4-release分支代码
## 快速使用
### Windows
@@ -61,7 +63,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
@@ -71,10 +73,21 @@ 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="https://github.com/dongyinuo/kafka-console-ui/blob/feature/dongyinuo/add/contact/document/contact/weixin_contact.jpeg" width="40%"/>
<img src="./document/contact/weixin_contact.jpg" width="40%"/>
[//]: # (<img src="https://github.com/xxd763795151/kafka-console-ui/blob/main/document/contact/weixin_contact.jpeg" width="40%"/>)
+ 若联系方式失效, 请联系加一下微信, 说明意图
- xxd763795151
- wxid_7jy2ezljvebt12
- 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 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放入文件夹的根目录
**Dockefile**
```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: 103 KiB

View File

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

22
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.5</version>
<name>kafka-console-ui</name>
<description>Kafka console manage ui</description>
<properties>
@@ -21,18 +21,12 @@
<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>
<jwt.version>0.9.0</jwt.version>
</properties>
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
@@ -82,6 +76,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>

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

@@ -1,7 +0,0 @@
package com.xuxd.kafka.console.beans;
public class KafkaConsoleException extends RuntimeException{
public KafkaConsoleException(String msg){
super(msg);
}
}

View File

@@ -11,7 +11,7 @@ import lombok.Setter;
**/
public class ResponseData<T> {
public static final int SUCCESS_CODE = 0, TOKEN_ILLEGAL = -5000, FAILED_CODE = -9999;
public static final int SUCCESS_CODE = 0, FAILED_CODE = -9999;
public static final String SUCCESS_MSG = "success", FAILED_MSG = "failed";
@@ -58,12 +58,6 @@ public class ResponseData<T> {
return this;
}
public ResponseData<T> failed(int code) {
this.code = code;
this.msg = FAILED_MSG;
return this;
}
public ResponseData<T> failed(String msg) {
this.code = FAILED_CODE;
this.msg = msg;

View File

@@ -1,9 +0,0 @@
package com.xuxd.kafka.console.beans.annotation;
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequiredAuthorize {
}

View File

@@ -1,29 +0,0 @@
package com.xuxd.kafka.console.beans.dos;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.xuxd.kafka.console.beans.enums.Role;
import lombok.Data;
import java.util.Date;
@Data
@TableName("t_devops_user")
public class DevOpsUserDO {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private Role role;
private boolean delete;
private Date createTime;
private Date updateTime;
}

View File

@@ -1,11 +0,0 @@
package com.xuxd.kafka.console.beans.dto.user;
import com.xuxd.kafka.console.beans.enums.Role;
import lombok.Data;
@Data
public class AddUserDTO {
private String username;
private String password;
private Role role;
}

View File

@@ -1,9 +0,0 @@
package com.xuxd.kafka.console.beans.dto.user;
import lombok.Data;
@Data
public class ListUserDTO {
private Long id;
private String username;
}

View File

@@ -1,9 +0,0 @@
package com.xuxd.kafka.console.beans.dto.user;
import lombok.Data;
@Data
public class LoginDTO {
private String username;
private String password;
}

View File

@@ -1,9 +0,0 @@
package com.xuxd.kafka.console.beans.dto.user;
import lombok.Data;
@Data
public class PasswordDTO {
private Long userId;
private String password;
}

View File

@@ -1,11 +0,0 @@
package com.xuxd.kafka.console.beans.dto.user;
import com.xuxd.kafka.console.beans.enums.Role;
import lombok.Data;
@Data
public class UpdateUserDTO {
private String username;
private String password;
private Role role;
}

View File

@@ -1,6 +0,0 @@
package com.xuxd.kafka.console.beans.enums;
public enum Role {
developer,
manager
}

View File

@@ -1,16 +0,0 @@
package com.xuxd.kafka.console.beans.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.xuxd.kafka.console.beans.enums.Role;
import lombok.Data;
import java.util.Date;
@Data
public class DevOpsUserVO {
private Long id;
private String username;
private Role role;
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
private Date createTime;
}

View File

@@ -1,16 +0,0 @@
package com.xuxd.kafka.console.beans.vo;
import com.xuxd.kafka.console.beans.enums.Role;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class LoginVO {
private String token;
private Role role;
}

View File

@@ -2,20 +2,15 @@ package com.xuxd.kafka.console.boot;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xuxd.kafka.console.beans.dos.ClusterInfoDO;
import com.xuxd.kafka.console.beans.dos.DevOpsUserDO;
import com.xuxd.kafka.console.config.KafkaConfig;
import com.xuxd.kafka.console.dao.ClusterInfoMapper;
import com.xuxd.kafka.console.dao.DevOpsUserMapper;
import com.xuxd.kafka.console.utils.ConvertUtil;
import java.util.List;
import com.xuxd.kafka.console.utils.Md5Utils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**

View File

@@ -1,42 +0,0 @@
package com.xuxd.kafka.console.boot;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xuxd.kafka.console.beans.dos.DevOpsUserDO;
import com.xuxd.kafka.console.beans.enums.Role;
import com.xuxd.kafka.console.dao.DevOpsUserMapper;
import com.xuxd.kafka.console.utils.Md5Utils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
@Slf4j
@RequiredArgsConstructor
public class InitSuperDevOpsUser implements SmartInitializingSingleton {
private final DevOpsUserMapper devOpsUserMapper;
public final static String SUPER_USERNAME = "admin";
@Value("${devops.password:kafka-console-ui521}")
private String password;
@Override
public void afterSingletonsInstantiated() {
QueryWrapper<DevOpsUserDO> userDOQueryWrapper = new QueryWrapper<>();
userDOQueryWrapper.eq("username", SUPER_USERNAME);
DevOpsUserDO userDO = devOpsUserMapper.selectOne(userDOQueryWrapper);
if (userDO == null){
DevOpsUserDO devOpsUserDO = new DevOpsUserDO();
devOpsUserDO.setUsername(SUPER_USERNAME);
devOpsUserDO.setPassword(Md5Utils.MD5(password));
devOpsUserDO.setRole(Role.manager);
devOpsUserMapper.insert(devOpsUserDO);
} else {
userDO.setPassword(Md5Utils.MD5(password));
devOpsUserMapper.updateById(userDO);
}
log.info("init super devops user done, username = {}", SUPER_USERNAME);
}
}

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,7 +1,6 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.beans.AclUser;
import com.xuxd.kafka.console.beans.annotation.RequiredAuthorize;
import com.xuxd.kafka.console.service.AclService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
@@ -31,20 +30,17 @@ public class AclUserController {
}
@PostMapping
@RequiredAuthorize
public Object addOrUpdateUser(@RequestBody AclUser user) {
return aclService.addOrUpdateUser(user.getUsername(), user.getPassword());
}
@DeleteMapping
@RequiredAuthorize
public Object deleteUser(@RequestBody AclUser user) {
return aclService.deleteUser(user.getUsername());
}
@DeleteMapping("/auth")
@RequiredAuthorize
public Object deleteUserAndAuth(@RequestBody AclUser user) {
return aclService.deleteUserAndAuth(user.getUsername());
}

View File

@@ -3,7 +3,13 @@ package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.beans.dto.ClusterInfoDTO;
import com.xuxd.kafka.console.service.ClusterService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* kafka-console-ui.

View File

@@ -1,7 +1,6 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.annotation.RequiredAuthorize;
import com.xuxd.kafka.console.beans.dto.AlterConfigDTO;
import com.xuxd.kafka.console.beans.enums.AlterType;
import com.xuxd.kafka.console.config.KafkaConfig;
@@ -48,13 +47,11 @@ public class ConfigController {
}
@PostMapping("/topic")
@RequiredAuthorize
public Object setTopicConfig(@RequestBody AlterConfigDTO dto) {
return configService.alterTopicConfig(dto.getEntity(), dto.to(), AlterType.SET);
}
@DeleteMapping("/topic")
@RequiredAuthorize
public Object deleteTopicConfig(@RequestBody AlterConfigDTO dto) {
return configService.alterTopicConfig(dto.getEntity(), dto.to(), AlterType.DELETE);
}
@@ -65,13 +62,11 @@ public class ConfigController {
}
@PostMapping("/broker")
@RequiredAuthorize
public Object setBrokerConfig(@RequestBody AlterConfigDTO dto) {
return configService.alterBrokerConfig(dto.getEntity(), dto.to(), AlterType.SET);
}
@DeleteMapping("/broker")
@RequiredAuthorize
public Object deleteBrokerConfig(@RequestBody AlterConfigDTO dto) {
return configService.alterBrokerConfig(dto.getEntity(), dto.to(), AlterType.DELETE);
}
@@ -82,13 +77,11 @@ public class ConfigController {
}
@PostMapping("/broker/logger")
@RequiredAuthorize
public Object setBrokerLoggerConfig(@RequestBody AlterConfigDTO dto) {
return configService.alterBrokerLoggerConfig(dto.getEntity(), dto.to(), AlterType.SET);
}
@DeleteMapping("/broker/logger")
@RequiredAuthorize
public Object deleteBrokerLoggerConfig(@RequestBody AlterConfigDTO dto) {
return configService.alterBrokerLoggerConfig(dto.getEntity(), dto.to(), AlterType.DELETE);
}

View File

@@ -1,7 +1,6 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.annotation.RequiredAuthorize;
import com.xuxd.kafka.console.beans.dto.AddSubscriptionDTO;
import com.xuxd.kafka.console.beans.dto.QueryConsumerGroupDTO;
import com.xuxd.kafka.console.beans.dto.ResetOffsetDTO;
@@ -68,13 +67,11 @@ public class ConsumerController {
}
@PostMapping("/subscription")
@RequiredAuthorize
public Object addSubscription(@RequestBody AddSubscriptionDTO subscriptionDTO) {
return consumerService.addSubscription(subscriptionDTO.getGroupId(), subscriptionDTO.getTopic());
}
@PostMapping("/reset/offset")
@RequiredAuthorize
public Object restOffset(@RequestBody ResetOffsetDTO offsetDTO) {
ResponseData res = ResponseData.create().failed("unknown");
switch (offsetDTO.getLevel()) {

View File

@@ -1,53 +0,0 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.annotation.RequiredAuthorize;
import com.xuxd.kafka.console.beans.dto.user.*;
import com.xuxd.kafka.console.beans.vo.DevOpsUserVO;
import com.xuxd.kafka.console.beans.vo.LoginVO;
import com.xuxd.kafka.console.service.DevOpsUserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 用户管理
* @author dongyinuo
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/devops/user")
public class DevOpsUserController {
private final DevOpsUserService devOpsUserService;
@PostMapping("add")
@RequiredAuthorize
public ResponseData<Boolean> add(@RequestBody AddUserDTO addUserDTO){
return devOpsUserService.add(addUserDTO);
}
@PostMapping("update")
@RequiredAuthorize
public ResponseData<Boolean> update(@RequestBody UpdateUserDTO updateUserDTO){
return devOpsUserService.update(updateUserDTO);
}
@DeleteMapping
@RequiredAuthorize
public ResponseData<Boolean> delete(@RequestParam Long id){
return devOpsUserService.delete(id);
}
@GetMapping("list")
@RequiredAuthorize
public ResponseData<List<DevOpsUserVO>> list(@ModelAttribute ListUserDTO listUserDTO){
return devOpsUserService.list(listUserDTO);
}
@PostMapping("login")
public ResponseData<LoginVO> login(@RequestBody LoginDTO loginDTO){
return devOpsUserService.login(loginDTO.getUsername(), loginDTO.getPassword());
}
}

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

@@ -1,7 +1,6 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.beans.TopicPartition;
import com.xuxd.kafka.console.beans.annotation.RequiredAuthorize;
import com.xuxd.kafka.console.beans.dto.BrokerThrottleDTO;
import com.xuxd.kafka.console.beans.dto.ProposedAssignmentDTO;
import com.xuxd.kafka.console.beans.dto.ReplicationDTO;
@@ -31,14 +30,12 @@ public class OperationController {
private OperationService operationService;
@PostMapping("/sync/consumer/offset")
@RequiredAuthorize
public Object syncConsumerOffset(@RequestBody SyncDataDTO dto) {
dto.getProperties().put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, dto.getAddress());
return operationService.syncConsumerOffset(dto.getGroupId(), dto.getTopic(), dto.getProperties());
}
@PostMapping("/sync/min/offset/alignment")
@RequiredAuthorize
public Object minOffsetAlignment(@RequestBody SyncDataDTO dto) {
dto.getProperties().put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, dto.getAddress());
return operationService.minOffsetAlignment(dto.getGroupId(), dto.getTopic(), dto.getProperties());
@@ -50,7 +47,6 @@ public class OperationController {
}
@DeleteMapping("/sync/alignment")
@RequiredAuthorize
public Object deleteAlignment(@RequestParam Long id) {
return operationService.deleteAlignmentById(id);
}
@@ -61,7 +57,6 @@ public class OperationController {
}
@PostMapping("/broker/throttle")
@RequiredAuthorize
public Object configThrottle(@RequestBody BrokerThrottleDTO dto) {
return operationService.configThrottle(dto.getBrokerList(), dto.getUnit().toKb(dto.getThrottle()));
}
@@ -82,7 +77,6 @@ public class OperationController {
}
@PostMapping("/replication/reassignments/proposed")
@RequiredAuthorize
public Object proposedAssignments(@RequestBody ProposedAssignmentDTO dto) {
return operationService.proposedAssignments(dto.getTopic(), dto.getBrokers());
}

View File

@@ -1,7 +1,6 @@
package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.beans.ReplicaAssignment;
import com.xuxd.kafka.console.beans.annotation.RequiredAuthorize;
import com.xuxd.kafka.console.beans.dto.AddPartitionDTO;
import com.xuxd.kafka.console.beans.dto.NewTopicDTO;
import com.xuxd.kafka.console.beans.dto.TopicThrottleDTO;
@@ -44,9 +43,8 @@ public class TopicController {
}
@DeleteMapping
@RequiredAuthorize
public Object deleteTopic(@RequestParam String topic) {
return topicService.deleteTopic(topic);
public Object deleteTopic(@RequestBody List<String> topics) {
return topicService.deleteTopics(topics);
}
@GetMapping("/partition")
@@ -55,13 +53,11 @@ public class TopicController {
}
@PostMapping("/new")
@RequiredAuthorize
public Object createNewTopic(@RequestBody NewTopicDTO topicDTO) {
return topicService.createTopic(topicDTO.toNewTopic());
}
@PostMapping("/partition/new")
@RequiredAuthorize
public Object addPartition(@RequestBody AddPartitionDTO partitionDTO) {
String topic = partitionDTO.getTopic().trim();
int addNum = partitionDTO.getAddNum();
@@ -84,13 +80,11 @@ public class TopicController {
}
@PostMapping("/replica/assignment")
@RequiredAuthorize
public Object updateReplicaAssignment(@RequestBody ReplicaAssignment assignment) {
return topicService.updateReplicaAssignment(assignment);
}
@PostMapping("/replica/throttle")
@RequiredAuthorize
public Object configThrottle(@RequestBody TopicThrottleDTO dto) {
return topicService.configThrottle(dto.getTopic(), dto.getPartitions(), dto.getOperation());
}

View File

@@ -1,7 +0,0 @@
package com.xuxd.kafka.console.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xuxd.kafka.console.beans.dos.DevOpsUserDO;
public interface DevOpsUserMapper extends BaseMapper<DevOpsUserDO> {
}

View File

@@ -1,84 +0,0 @@
package com.xuxd.kafka.console.interceptor;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.annotation.RequiredAuthorize;
import com.xuxd.kafka.console.beans.enums.Role;
import com.xuxd.kafka.console.beans.vo.DevOpsUserVO;
import com.xuxd.kafka.console.service.DevOpsUserService;
import com.xuxd.kafka.console.utils.ContextUtil;
import com.xuxd.kafka.console.utils.ConvertUtil;
import com.xuxd.kafka.console.utils.JwtUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.AsyncHandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import static com.xuxd.kafka.console.beans.ResponseData.TOKEN_ILLEGAL;
@Component
@Slf4j
@RequiredArgsConstructor
public class TokenInterceptor implements AsyncHandlerInterceptor {
private final static String TOKEN = "token";
private final DevOpsUserService devOpsUserService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod){
String token = request.getHeader(TOKEN);
if (StringUtils.isBlank(token)){
log.info("token not exist");
write(response);
return false;
}
String username = JwtUtils.parse(token);
if (StringUtils.isBlank(username)){
log.info("{} is wrongful", token);
write(response);
return false;
}
ResponseData<DevOpsUserVO> userVORsp = devOpsUserService.detail(username);
if (userVORsp == null || userVORsp.getData() == null){
log.info("{} not exist", username);
write(response);
return false;
}
ContextUtil.set(ContextUtil.USERNAME, username);
HandlerMethod method = (HandlerMethod)handler;
RequiredAuthorize annotation = method.getMethodAnnotation(RequiredAuthorize.class);
if (annotation != null){
DevOpsUserVO userVO = userVORsp.getData();
if (!userVO.getRole().equals(Role.manager)){
log.info("{},{} no permission", username, request.getRequestURI());
write(response);
return false;
}
}
}
return true;
}
private void write(HttpServletResponse response){
PrintWriter writer = null;
try {
writer = response.getWriter();
writer.write(ConvertUtil.toJsonString(ResponseData.create().failed(TOKEN_ILLEGAL)));
} catch (Exception ignored){
} finally {
if (writer != null){
writer.flush();
writer.close();
}
}
}
}

View File

@@ -1,20 +0,0 @@
package com.xuxd.kafka.console.interceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
private final TokenInterceptor tokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/devops/user/login");
}
}

View File

@@ -1,26 +0,0 @@
package com.xuxd.kafka.console.service;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.dto.user.AddUserDTO;
import com.xuxd.kafka.console.beans.dto.user.ListUserDTO;
import com.xuxd.kafka.console.beans.dto.user.UpdateUserDTO;
import com.xuxd.kafka.console.beans.vo.DevOpsUserVO;
import com.xuxd.kafka.console.beans.vo.LoginVO;
import java.util.List;
public interface DevOpsUserService {
ResponseData<Boolean> add(AddUserDTO addUserDTO);
ResponseData<Boolean> update(UpdateUserDTO updateUserDTO);
ResponseData<Boolean> delete(Long id);
ResponseData<List<DevOpsUserVO>> list(ListUserDTO listUserDTO);
ResponseData<DevOpsUserVO> detail(String username);
ResponseData<LoginVO> login(String username, String password);
}

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

@@ -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();
}

View File

@@ -1,99 +0,0 @@
package com.xuxd.kafka.console.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.xuxd.kafka.console.beans.KafkaConsoleException;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.dos.DevOpsUserDO;
import com.xuxd.kafka.console.beans.dto.user.AddUserDTO;
import com.xuxd.kafka.console.beans.dto.user.ListUserDTO;
import com.xuxd.kafka.console.beans.dto.user.UpdateUserDTO;
import com.xuxd.kafka.console.beans.vo.DevOpsUserVO;
import com.xuxd.kafka.console.beans.vo.LoginVO;
import com.xuxd.kafka.console.boot.InitSuperDevOpsUser;
import com.xuxd.kafka.console.dao.DevOpsUserMapper;
import com.xuxd.kafka.console.service.DevOpsUserService;
import com.xuxd.kafka.console.utils.ConvertUtil;
import com.xuxd.kafka.console.utils.JwtUtils;
import com.xuxd.kafka.console.utils.Md5Utils;
import com.xuxd.kafka.console.utils.ResponseUtil;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class DevOpsServiceImpl implements DevOpsUserService {
private final DevOpsUserMapper devOpsUserMapper;
@Override
public ResponseData<Boolean> add(AddUserDTO addUserDTO) {
QueryWrapper<DevOpsUserDO> queryWrapper = new QueryWrapper<DevOpsUserDO>();
queryWrapper.eq("username", addUserDTO.getUsername());
if (devOpsUserMapper.selectOne(queryWrapper) != null){
throw new KafkaConsoleException("账号已存在");
}
addUserDTO.setPassword(Md5Utils.MD5(addUserDTO.getPassword()));
int ret = devOpsUserMapper.insert(ConvertUtil.copy(addUserDTO, DevOpsUserDO.class));
return ResponseUtil.success(ret > 0);
}
@Override
public ResponseData<Boolean> update(UpdateUserDTO updateUserDTO) {
UpdateWrapper<DevOpsUserDO> updateWrapper = new UpdateWrapper<>();
if (updateUserDTO.getRole() != null){
updateWrapper.set("role", updateUserDTO.getRole());
}
if (StringUtils.isNotBlank(updateUserDTO.getPassword())){
updateWrapper.set("password", Md5Utils.MD5(updateUserDTO.getPassword()));
}
updateWrapper.eq("username", updateUserDTO.getUsername());
int ret = devOpsUserMapper.update(null, updateWrapper);
return ResponseUtil.success(ret > 0);
}
@Override
public ResponseData<Boolean> delete(Long id) {
int ret = devOpsUserMapper.deleteById(id);
return ResponseUtil.success(ret > 0);
}
@Override
public ResponseData<List<DevOpsUserVO>> list(ListUserDTO listUserDTO) {
QueryWrapper<DevOpsUserDO> queryWrapper = new QueryWrapper<DevOpsUserDO>();
if (listUserDTO.getId() != null){
queryWrapper.eq("id", listUserDTO.getId());
}
if (StringUtils.isNotBlank(listUserDTO.getUsername())){
queryWrapper.eq("username", listUserDTO.getUsername());
}
queryWrapper.ne("username", InitSuperDevOpsUser.SUPER_USERNAME);
List<DevOpsUserDO> userDOS = devOpsUserMapper.selectList(queryWrapper);
return ResponseUtil.success(ConvertUtil.copyList(userDOS, DevOpsUserVO.class));
}
@Override
public ResponseData<DevOpsUserVO> detail(String username) {
QueryWrapper<DevOpsUserDO> queryWrapper = new QueryWrapper<DevOpsUserDO>();
queryWrapper.eq("username", username);
DevOpsUserDO userDO = devOpsUserMapper.selectOne(queryWrapper);
return ResponseUtil.success(ConvertUtil.copy(userDO, DevOpsUserVO.class));
}
@Override
public ResponseData<LoginVO> login(String username, String password) {
QueryWrapper<DevOpsUserDO> queryWrapper = new QueryWrapper<DevOpsUserDO>();
queryWrapper.eq("username", username);
queryWrapper.eq("password", Md5Utils.MD5(password));
DevOpsUserDO userDO = devOpsUserMapper.selectOne(queryWrapper);
if (userDO == null){
throw new KafkaConsoleException("用户名或密码错误");
}
LoginVO loginVO = LoginVO.builder().role(userDO.getRole()).token(JwtUtils.sign(username)).build();
return ResponseUtil.success(loginVO);
}
}

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

@@ -1,4 +1,4 @@
package com.xuxd.kafka.console.interceptor;
package com.xuxd.kafka.console.service.interceptor;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.dos.ClusterInfoDO;

View File

@@ -1,6 +1,7 @@
package com.xuxd.kafka.console.interceptor;
package com.xuxd.kafka.console.service.interceptor;
import com.xuxd.kafka.console.utils.ResponseUtil;
import com.xuxd.kafka.console.beans.ResponseData;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -13,13 +14,14 @@ import org.springframework.web.bind.annotation.ResponseBody;
* @date 2021-10-19 14:32:18
**/
@Slf4j
@ControllerAdvice(basePackages = "com.xuxd.kafka.console")
@ControllerAdvice(basePackages = "com.xuxd.kafka.console.controller")
public class GlobalExceptionHandler {
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Object exceptionHandler(Exception ex) {
public Object exceptionHandler(HttpServletRequest req, Exception ex) throws Exception {
log.error("exception handle: ", ex);
return ResponseUtil.error(ex.getMessage());
return ResponseData.create().failed(ex.getMessage());
}
}

View File

@@ -1,23 +0,0 @@
package com.xuxd.kafka.console.utils;
import java.util.HashMap;
import java.util.Map;
public class ContextUtil {
public static final String USERNAME = "username" ;
private static ThreadLocal<Map<String, Object>> context = ThreadLocal.withInitial(() -> new HashMap<>());
public static void set(String key, Object value){
context.get().put(key, value);
}
public static String get(String key){
return (String) context.get().get(key);
}
public static void clear(){
context.remove();
}
}

View File

@@ -1,15 +1,17 @@
package com.xuxd.kafka.console.utils;
import com.google.common.base.Preconditions;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cglib.beans.BeanCopier;
import org.springframework.objenesis.ObjenesisStd;
import org.springframework.util.ClassUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ClassUtils;
/**
* kafka-console-ui.
@@ -20,47 +22,6 @@ import java.util.concurrent.ConcurrentHashMap;
@Slf4j
public class ConvertUtil {
private static ThreadLocal<ObjenesisStd> objenesisStdThreadLocal = ThreadLocal.withInitial(ObjenesisStd::new);
private static ConcurrentHashMap<Class<?>, ConcurrentHashMap<Class<?>, BeanCopier>> cache = new ConcurrentHashMap<>();
public static <T> T copy(Object source, Class<T> target) {
return copy(source, objenesisStdThreadLocal.get().newInstance(target));
}
public static <T> T copy(Object source, T target) {
if (null == source) {
return null;
}
BeanCopier beanCopier = getCacheBeanCopier(source.getClass(), target.getClass());
beanCopier.copy(source, target, null);
return target;
}
public static <T> List<T> copyList(List<?> sources, Class<T> target) {
if (sources.isEmpty()) {
return Collections.emptyList();
}
ArrayList<T> list = new ArrayList<>(sources.size());
ObjenesisStd objenesisStd = objenesisStdThreadLocal.get();
for (Object source : sources) {
if (source == null) {
break;
}
T newInstance = objenesisStd.newInstance(target);
BeanCopier beanCopier = getCacheBeanCopier(source.getClass(), target);
beanCopier.copy(source, newInstance, null);
list.add(newInstance);
}
return list;
}
private static <S, T> BeanCopier getCacheBeanCopier(Class<S> source, Class<T> target) {
ConcurrentHashMap<Class<?>, BeanCopier> copierConcurrentHashMap =
cache.computeIfAbsent(source, aClass -> new ConcurrentHashMap<>(16));
return copierConcurrentHashMap.computeIfAbsent(target, aClass -> BeanCopier.create(source, target, false));
}
public static Map<String, Object> toMap(Object src) {
Preconditions.checkNotNull(src);
Map<String, Object> res = new HashMap<>();

View File

@@ -1,43 +0,0 @@
package com.xuxd.kafka.console.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtUtils {
private static final String ISSUER = "kafka-console-ui";
private static final long EXPIRE_TIME = 5 * 24 * 60 * 60 * 1000;
private static final String PRIVATE_KEY = "~hello!kafka=console^ui";
public static String sign(String username){
Map<String,Object> header = new HashMap<>();
header.put("typ","JWT");
header.put("alg","HS256");
Map<String,Object> claims = new HashMap<>();
claims.put("username", username);
return Jwts.builder()
.setIssuer(ISSUER)
.setHeader(header)
.setClaims(claims)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME))
.signWith(SignatureAlgorithm.HS256, PRIVATE_KEY)
.compact();
}
public static String parse(String token){
try{
Claims claims = Jwts.parser()
.setSigningKey(PRIVATE_KEY)
.parseClaimsJws(token).getBody();
return (String) claims.get("username");
}catch (Exception e){
return null;
}
}
}

View File

@@ -1,12 +0,0 @@
package com.xuxd.kafka.console.utils;
import org.springframework.util.DigestUtils;
import java.nio.charset.StandardCharsets;
public class Md5Utils {
public static String MD5(String s) {
return DigestUtils.md5DigestAsHex(s.getBytes(StandardCharsets.UTF_8));
}
}

View File

@@ -1,15 +0,0 @@
package com.xuxd.kafka.console.utils;
import com.xuxd.kafka.console.beans.ResponseData;
public class ResponseUtil {
public static <T> ResponseData<T> success(T data) {
return ResponseData.create().data(data);
}
public static ResponseData<String> error(String msg) {
return ResponseData.create().failed(msg);
}
}

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

@@ -34,18 +34,4 @@ CREATE TABLE IF NOT EXISTS T_CLUSTER_INFO
UPDATE_TIME TIMESTAMP NOT NULL DEFAULT NOW() COMMENT '更新时间',
PRIMARY KEY (ID),
UNIQUE (CLUSTER_NAME)
);
-- 用户表
CREATE TABLE IF NOT EXISTS T_DEVOPS_USER
(
ID IDENTITY NOT NULL COMMENT '主键ID',
USERNAME VARCHAR(50) NOT NULL DEFAULT '' COMMENT '用户名',
PASSWORD VARCHAR(50) NOT NULL DEFAULT '' COMMENT '密码',
`ROLE` VARCHAR(16) NOT NULL DEFAULT 'developer' COMMENT '角色',
`DELETE` TINYINT(1) NOT NULL DEFAULT '' COMMENT '删除标记',
CREATE_TIME TIMESTAMP NOT NULL DEFAULT NOW() COMMENT '创建时间',
UPDATE_TIME TIMESTAMP NOT NULL DEFAULT NOW() COMMENT '更新时间',
PRIMARY KEY (ID),
UNIQUE (USERNAME)
);

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

@@ -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)]
}

13127
ui/package-lock.json generated Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,11 @@
"vuex": "^3.4.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/eslint-config-prettier": "^6.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",

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

@@ -1,5 +1,127 @@
<template>
<div id="app">
<router-view />
<div id="nav">
<h2 class="logo">Kafka 控制台</h2>
<router-link to="/" class="pad-l-r">主页</router-link>
<span>|</span
><router-link to="/cluster-page" class="pad-l-r">集群</router-link>
<span>|</span
><router-link to="/topic-page" class="pad-l-r">Topic</router-link>
<span>|</span
><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="/op-page" class="pad-l-r">运维</router-link>
<span class="right">集群{{ clusterName }}</span>
</div>
<router-view class="content" />
</div>
</template>
</template>
<script>
import { KafkaClusterApi } from "@/utils/api";
import request from "@/utils/request";
import { mapMutations, mapState } from "vuex";
import { getClusterInfo } from "@/utils/local-cache";
import notification from "ant-design-vue/lib/notification";
import { CLUSTER } from "@/store/mutation-types";
export default {
data() {
return {
config: {},
};
},
created() {
const clusterInfo = getClusterInfo();
if (!clusterInfo) {
request({
url: KafkaClusterApi.peekClusterInfo.url,
method: KafkaClusterApi.peekClusterInfo.method,
}).then((res) => {
if (res.code == 0) {
this.switchCluster(res.data);
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
} else {
this.switchCluster(clusterInfo);
}
},
computed: {
...mapState({
clusterName: (state) => state.clusterInfo.clusterName,
enableSasl: (state) => state.clusterInfo.enableSasl,
}),
},
methods: {
...mapMutations({
switchCluster: CLUSTER.SWITCH,
}),
},
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
#app {
width: 100%;
height: 100%;
}
#nav {
background-color: #9fe0e0;
font-size: large;
padding-top: 1%;
padding-bottom: 1%;
margin-bottom: 1%;
text-align: center;
}
#nav a {
font-weight: bold;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #61c126;
}
.pad-l-r {
padding-left: 10px;
padding-right: 10px;
}
.content {
padding-left: 2%;
padding-right: 2%;
height: 90%;
width: 100%;
}
.logo {
float: left;
left: 1%;
top: 1%;
position: absolute;
}
.right {
float: right;
right: 1%;
top: 2%;
position: absolute;
}
</style>

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

View File

@@ -1,162 +0,0 @@
<template>
<div id="main">
<div id="nav">
<h2 class="logo">Kafka 控制台</h2>
<span v-show="manager">
<router-link to="/home" class="pad-l-r">主页</router-link>
<span>|</span>
</span>
<span>
<router-link to="/cluster-page" class="pad-l-r">集群</router-link>
</span>
<span>
<span>|</span>
<router-link to="/topic-page" class="pad-l-r">Topic</router-link>
</span>
<span>
<span>|</span>
<router-link to="/group-page" class="pad-l-r">消费组</router-link>
</span>
<span>
<span>|</span>
<router-link to="/message-page" class="pad-l-r">消息</router-link>
</span>
<span v-show="manager && enableSasl">
<span>|</span>
<router-link to="/acl-page" class="pad-l-r">Acl</router-link>
</span>
<span>
<span>|</span>
<router-link to="/op-page" class="pad-l-r">运维</router-link>
</span>
<span v-show="manager">
<span>|</span>
<router-link to="/devops/user" class="pad-l-r">用户</router-link>
</span>
<span class="right">
<span>集群{{ clusterName }}</span>
<span> | </span>
<span @click="logout" style="cursor: pointer">登出</span>
</span>
</div>
<router-view class="content" />
</div>
</template>
<script>
import { KafkaClusterApi } from "@/utils/api";
import request from "@/utils/request";
import { mapMutations, mapState } from "vuex";
import { getClusterInfo } from "@/utils/local-cache";
import notification from "ant-design-vue/lib/notification";
import { CLUSTER } from "@/store/mutation-types";
import {isManager} from "../utils/role";
import router from "../router";
export default {
name: "Header",
data() {
return {
manager: isManager(),
config: {},
};
},
created() {
const clusterInfo = getClusterInfo();
if (!clusterInfo) {
request({
url: KafkaClusterApi.peekClusterInfo.url,
method: KafkaClusterApi.peekClusterInfo.method,
}).then((res) => {
if (res.code == 0) {
this.switchCluster(res.data);
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
} else {
this.switchCluster(clusterInfo);
}
},
computed: {
...mapState({
clusterName: (state) => state.clusterInfo.clusterName,
enableSasl: (state) => state.clusterInfo.enableSasl,
}),
},
methods: {
...mapMutations({
switchCluster: CLUSTER.SWITCH,
}),
logout: function (){
localStorage.clear();
router.push("/")
}
},
};
</script>
<style>
#main {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
#main {
width: 100%;
height: 100%;
}
#nav {
background-color: #9fe0e0;
font-size: large;
padding-top: 1%;
padding-bottom: 1%;
margin-bottom: 1%;
text-align: center;
}
#nav a {
font-weight: bold;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #61c126;
}
.pad-l-r {
padding-left: 10px;
padding-right: 10px;
}
.content {
padding-left: 2%;
padding-right: 2%;
height: 90%;
width: 100%;
}
.logo {
float: left;
left: 1%;
top: 1%;
position: absolute;
}
.cluster {
float: right;
right: 8%;
top: 2%;
position: absolute;
}
.right {
float: right;
right: 2%;
top: 2%;
position: absolute;
}
</style>

View File

@@ -0,0 +1,32 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String,
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@@ -1,30 +1,12 @@
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/home/Home.vue";
import Login from "@/views/login/index";
import Home from "../views/Home.vue";
Vue.use(VueRouter);
const routes = [
{
path: "/",
name: "Login",
component: Login,
},
{
path: "/main",
name: "Main",
component: () =>
import( "../components/Header"),
},
{
path: "/devops/user",
name: "DevOpsUser",
component: () =>
import( "../views/user/index"),
},
{
path: "/home",
name: "Home",
component: Home,
},

View File

@@ -92,29 +92,6 @@ export const KafkaConfigApi = {
},
};
export const DevOpsUserAPi = {
createUser: {
url: "/devops/user/add",
method: "post",
},
userList: {
url: "/devops/user/list",
method: "get",
},
deleteUser: {
url: "/devops/user/",
method: "delete",
},
updateUser: {
url: "/devops/user/update",
method: "post",
},
login: {
url: "/devops/user/login",
method: "post",
},
}
export const KafkaTopicApi = {
getTopicNameList: {
url: "/topic",
@@ -299,4 +276,8 @@ export const KafkaMessageApi = {
url: "/message/resend",
method: "post",
},
delete: {
url: "/message",
method: "delete",
},
};

View File

@@ -1,5 +1,4 @@
import axios from "axios";
import router from "../router";
import notification from "ant-design-vue/es/notification";
import { VueAxios } from "./axios";
import { getClusterInfo } from "@/utils/local-cache";
@@ -26,7 +25,6 @@ const errorHandler = (error) => {
// request interceptor
request.interceptors.request.use((config) => {
const clusterInfo = getClusterInfo();
config.headers["token"] = localStorage.getItem('token');
if (clusterInfo) {
config.headers["X-Cluster-Info-Id"] = clusterInfo.id;
// config.headers["X-Cluster-Info-Name"] = encodeURIComponent(clusterInfo.clusterName);
@@ -36,10 +34,6 @@ request.interceptors.request.use((config) => {
// response interceptor
request.interceptors.response.use((response) => {
if (response.data.code === -5000){
router.push({ path:'/'})
return
}
return response.data;
}, errorHandler);

View File

@@ -1,3 +0,0 @@
export function isManager() {
return 'manager' === localStorage.getItem("role");
}

View File

@@ -1,87 +0,0 @@
/**
* Created by PanJiaChen on 16/11/18.
*/
/**
* @param {string} path
* @returns {Boolean}
*/
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUsername(str) {
const valid_map = ['admin', 'editor']
return valid_map.indexOf(str.trim()) >= 0
}
/**
* @param {string} url
* @returns {Boolean}
*/
export function validURL(url) {
const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
return reg.test(url)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validLowerCase(str) {
const reg = /^[a-z]+$/
return reg.test(str)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUpperCase(str) {
const reg = /^[A-Z]+$/
return reg.test(str)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validAlphabets(str) {
const reg = /^[A-Za-z]+$/
return reg.test(str)
}
/**
* @param {string} email
* @returns {Boolean}
*/
export function validEmail(email) {
const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
return reg.test(email)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function isString(str) {
if (typeof str === 'string' || str instanceof String) {
return true
}
return false
}
/**
* @param {Array} arg
* @returns {Boolean}
*/
export function isArray(arg) {
if (typeof Array.isArray === 'undefined') {
return Object.prototype.toString.call(arg) === '[object Array]'
}
return Array.isArray(arg)
}

View File

@@ -1,38 +1,35 @@
<template>
<div>
<Header/>
<div class="content">
<a-card title="控制台默认配置" class="card-style">
<p v-for="(v, k) in config" :key="k">{{ k }}={{ v }}</p>
</a-card>
<p></p>
<hr />
<h3>kafka API 版本兼容性</h3>
<a-spin :spinning="apiVersionInfoLoading">
<a-table
:columns="columns"
:data-source="brokerApiVersionInfo"
bordered
row-key="brokerId"
>
<div slot="operation" slot-scope="record">
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openApiVersionInfoDialog(record)"
>详情
</a-button>
</div>
</a-table>
</a-spin>
<VersionInfo
:version-info="apiVersionInfo"
:visible="showApiVersionInfoDialog"
@closeApiVersionInfoDialog="closeApiVersionInfoDialog"
<div class="home">
<a-card title="控制台默认配置" class="card-style">
<p v-for="(v, k) in config" :key="k">{{ k }}={{ v }}</p>
</a-card>
<p></p>
<hr />
<h3>kafka API 版本兼容性</h3>
<a-spin :spinning="apiVersionInfoLoading">
<a-table
:columns="columns"
:data-source="brokerApiVersionInfo"
bordered
row-key="brokerId"
>
</VersionInfo>
</div>
<div slot="operation" slot-scope="record">
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openApiVersionInfoDialog(record)"
>详情
</a-button>
</div>
</a-table>
</a-spin>
<VersionInfo
:version-info="apiVersionInfo"
:visible="showApiVersionInfoDialog"
@closeApiVersionInfoDialog="closeApiVersionInfoDialog"
>
</VersionInfo>
</div>
</template>
@@ -42,10 +39,9 @@ import request from "@/utils/request";
import { KafkaConfigApi, KafkaClusterApi } from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
import VersionInfo from "@/views/home/VersionInfo";
import Header from "@/components/Header"
export default {
name: "Home",
components: { VersionInfo, Header },
components: { VersionInfo },
data() {
return {
config: {},

View File

@@ -1,160 +1,157 @@
<template>
<div>
<Header/>
<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>
<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">
<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
size="small"
shape="round"
type="dashed"
style="float: right"
@click="onUserDetail(username)"
>详情</a-button
>
</div>
</div>
<div slot="topicList" slot-scope="topicList, record">
<a
href="#"
v-for="t in topicList"
:key="t"
@click="onTopicDetail(t, record.username)"
<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>
</a>
</div>
<div slot="groupList" slot-scope="groupList, record">
<a
href="#"
v-for="t in groupList"
:key="t"
@click="onGroupDetail(t, record.username)"
<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>
</a>
</div>
<div
slot="operation"
slot-scope="record"
v-show="!record.user || record.user.role != 'admin'"
<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-popconfirm
:title="'删除用户: ' + record.username + '及相关权限?'"
ok-text="确认"
cancel-text="取消"
@confirm="onDeleteUser(record)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
<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-popconfirm>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onManageProducerAuth(record)"
>管理生产权限
</a-button>
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onManageConsumerAuth(record)"
<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-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>
</div>
</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>
</div>
</template>
@@ -168,7 +165,7 @@ 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 Header from "@/components/Header"
export default {
name: "Acl",
components: {
@@ -178,7 +175,6 @@ export default {
AddAuth,
AclDetail,
UserDetail,
Header
},
data() {
return {

View File

@@ -1,47 +1,43 @@
<template>
<div>
<Header/>
<div class="content">
<a-spin :spinning="loading">
<div class="body-c">
<div class="cluster-id">
<h3>集群ID{{ clusterId }}</h3>
</div>
<a-table :columns="columns" :data-source="data" bordered row-key="id">
<div slot="addr" slot-scope="text, record">
{{ record.host }}:{{ record.port }}
</div>
<div slot="controller" slot-scope="text">
<span v-if="text" style="color: red"></span><span v-else></span>
</div>
<div slot="operation" slot-scope="record" v-show="!record.internal">
<a-button
v-show="manager"
size="small"
href="javascript:;"
class="operation-btn"
@click="openBrokerConfigDialog(record, false)"
>属性配置
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openBrokerConfigDialog(record, true)"
>日志配置
</a-button>
</div>
</a-table>
<div class="content">
<a-spin :spinning="loading">
<div class="body-c">
<div class="cluster-id">
<h3>集群ID{{ clusterId }}</h3>
</div>
<BrokerConfig
:visible="showBrokerConfigDialog"
:id="this.select.idString"
:is-logger-config="isLoggerConfig"
@closeBrokerConfigDialog="closeBrokerConfigDialog"
></BrokerConfig>
</a-spin>
</div>
<a-table :columns="columns" :data-source="data" bordered row-key="id">
<div slot="addr" slot-scope="text, record">
{{ record.host }}:{{ record.port }}
</div>
<div slot="controller" slot-scope="text">
<span v-if="text" style="color: red"></span><span v-else></span>
</div>
<div slot="operation" slot-scope="record" v-show="!record.internal">
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openBrokerConfigDialog(record, false)"
>属性配置
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openBrokerConfigDialog(record, true)"
>日志配置
</a-button>
</div>
</a-table>
</div>
<BrokerConfig
:visible="showBrokerConfigDialog"
:id="this.select.idString"
:is-logger-config="isLoggerConfig"
@closeBrokerConfigDialog="closeBrokerConfigDialog"
></BrokerConfig>
</a-spin>
</div>
</template>
@@ -50,14 +46,12 @@ import request from "@/utils/request";
import { KafkaClusterApi } from "@/utils/api";
import BrokerConfig from "@/views/cluster/BrokerConfig";
import notification from "ant-design-vue/lib/notification";
import Header from "@/components/Header"
import {isManager} from "../../utils/role";
export default {
name: "Topic",
components: { BrokerConfig, Header },
components: { BrokerConfig },
data() {
return {
manager: isManager(),
data: [],
columns,
loading: false,

View File

@@ -1,148 +1,144 @@
<template>
<div>
<Header/>
<div class="content">
<a-spin :spinning="loading">
<div class="topic">
<div id="form-consumer-group-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="groupId"
class="input-w"
v-decorator="['groupId']"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :label="`状态`">
<a-checkbox-group
v-decorator="['states']"
style="width: 100%"
>
<a-row>
<a-col :span="8">
<a-checkbox value="Empty"> Empty</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="PreparingRebalance">
PreparingRebalance
</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="CompletingRebalance">
CompletingRebalance
</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="Stable"> Stable</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="Dead"> Dead</a-checkbox>
</a-col>
</a-row>
</a-checkbox-group>
</a-form-item>
</a-col>
<a-col :span="4" :style="{ textAlign: 'right' }">
<a-form-item>
<a-button type="primary" html-type="submit"> 搜索</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleReset">
重置
</a-button>
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
<div v-show="manager" class="operation-row-button">
<a-button type="primary" @click="openAddSubscriptionDialog"
>新增订阅</a-button
>
</div>
<a-table
:columns="columns"
:data-source="data"
bordered
row-key="groupId"
<div class="content">
<a-spin :spinning="loading">
<div class="topic">
<div id="form-consumer-group-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<div slot="members" slot-scope="text, record">
<a href="#" @click="openConsumerMemberDialog(record.groupId)"
>{{ text }}
</a>
</div>
<a-row :gutter="24">
<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="12">
<a-form-item :label="`状态`">
<a-checkbox-group
v-decorator="['states']"
style="width: 100%"
>
<a-row>
<a-col :span="8">
<a-checkbox value="Empty"> Empty</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="PreparingRebalance">
PreparingRebalance
</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="CompletingRebalance">
CompletingRebalance
</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="Stable"> Stable</a-checkbox>
</a-col>
<a-col :span="8">
<a-checkbox value="Dead"> Dead</a-checkbox>
</a-col>
</a-row>
</a-checkbox-group>
</a-form-item>
</a-col>
<div slot="state" slot-scope="text">
{{ text }}
<!-- <span v-if="text" style="color: red"></span><span v-else></span>-->
</div>
<div slot="operation" slot-scope="record" v-show="!record.internal">
<a-popconfirm
:title="'删除消费组: ' + record.groupId + ''"
ok-text="确认"
cancel-text="取消"
@confirm="deleteGroup(record.groupId)"
>
<a-button v-show="manager" size="small" href="javascript:;" class="operation-btn"
>删除
</a-button>
</a-popconfirm>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openConsumerMemberDialog(record.groupId)"
>消费端
</a-button>
<a-button
v-show="manager"
size="small"
href="javascript:;"
class="operation-btn"
@click="openConsumerDetailDialog(record.groupId)"
>消费详情
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openOffsetPartitionDialog(record.groupId)"
>位移分区
</a-button>
</div>
</a-table>
<Member
:visible="showConsumerGroupDialog"
:group="selectDetail.resourceName"
@closeConsumerMemberDialog="closeConsumerDialog"
></Member>
<ConsumerDetail
:visible="showConsumerDetailDialog"
:group="selectDetail.resourceName"
@closeConsumerDetailDialog="closeConsumerDetailDialog"
>
</ConsumerDetail>
<AddSupscription
:visible="showAddSubscriptionDialog"
@closeAddSubscriptionDialog="closeAddSubscriptionDialog"
>
</AddSupscription>
<OffsetTopicPartition
:visible="showOffsetPartitionDialog"
:group="selectDetail.resourceName"
@closeOffsetPartitionDialog="closeOffsetPartitionDialog"
></OffsetTopicPartition>
<a-col :span="4" :style="{ textAlign: 'right' }">
<a-form-item>
<a-button type="primary" html-type="submit"> 搜索</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleReset">
重置
</a-button>
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
</a-spin>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="openAddSubscriptionDialog"
>新增订阅</a-button
>
</div>
<a-table
:columns="columns"
:data-source="data"
bordered
row-key="groupId"
>
<div slot="members" slot-scope="text, record">
<a href="#" @click="openConsumerMemberDialog(record.groupId)"
>{{ text }}
</a>
</div>
<div slot="state" slot-scope="text">
{{ text }}
<!-- <span v-if="text" style="color: red"></span><span v-else></span>-->
</div>
<div slot="operation" slot-scope="record" v-show="!record.internal">
<a-popconfirm
:title="'删除消费组: ' + record.groupId + ''"
ok-text="确认"
cancel-text="取消"
@confirm="deleteGroup(record.groupId)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
>删除
</a-button>
</a-popconfirm>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openConsumerMemberDialog(record.groupId)"
>消费端
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openConsumerDetailDialog(record.groupId)"
>消费详情
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openOffsetPartitionDialog(record.groupId)"
>位移分区
</a-button>
</div>
</a-table>
<Member
:visible="showConsumerGroupDialog"
:group="selectDetail.resourceName"
@closeConsumerMemberDialog="closeConsumerDialog"
></Member>
<ConsumerDetail
:visible="showConsumerDetailDialog"
:group="selectDetail.resourceName"
@closeConsumerDetailDialog="closeConsumerDetailDialog"
>
</ConsumerDetail>
<AddSupscription
:visible="showAddSubscriptionDialog"
@closeAddSubscriptionDialog="closeAddSubscriptionDialog"
>
</AddSupscription>
<OffsetTopicPartition
:visible="showOffsetPartitionDialog"
:group="selectDetail.resourceName"
@closeOffsetPartitionDialog="closeOffsetPartitionDialog"
></OffsetTopicPartition>
</div>
</a-spin>
</div>
</template>
@@ -154,14 +150,12 @@ import Member from "@/views/group/Member";
import ConsumerDetail from "@/views/group/ConsumerDetail";
import AddSupscription from "@/views/group/AddSupscription";
import OffsetTopicPartition from "@/views/group/OffsetTopicPartition";
import Header from "@/components/Header"
import {isManager} from "../../utils/role";
export default {
name: "ConsumerGroup",
components: { Member, ConsumerDetail, AddSupscription, OffsetTopicPartition, Header },
components: { Member, ConsumerDetail, AddSupscription, OffsetTopicPartition },
data() {
return {
manager: isManager(),
queryParam: {},
data: [],
columns,

View File

@@ -1,99 +0,0 @@
<template>
<div id="login">
<div class="kafka-console-ui">
<span style="font-size: xxx-large; font-weight: bold">kafka-console-ui</span>
</div>
<div>
<a-form
:form="form"
:label-col="{ span: 10 }"
:wrapper-col="{ span: 4 }"
@submit="handleSubmit"
>
<a-form-item label="账号">
<a-input
v-decorator="[
'username',
{ rules: [{ required: true, message: '请输入账号' }] },
]"
placeholder="请输入账号"
/>
</a-form-item>
<a-form-item label="密码">
<a-input
v-decorator="[
'password',
{ rules: [{ required: true, message: '请输入密码' }] },
]"
placeholder="请输入密码"
/>
</a-form-item>
<a-form-item :wrapper-col="{ span: 10, offset: 10 }">
<a-button type="primary" html-type="submit"> 提交 </a-button>
</a-form-item>
</a-form>
</div>
<router-view></router-view>
</div>
</template>
<script>
import notification from "ant-design-vue/lib/notification";
import request from "@/utils/request";
import { DevOpsUserAPi } from "@/utils/api";
export default {
name: 'login',
data(){
return{
form: this.$form.createForm(this, { name: "coordinated" }),
}
},methods:{
handleSubmit(e){
e.preventDefault();
this.form.validateFields((err, values) => {
if (!err) {
if (values.configs) {
const config = {};
values.configs.split("\n").forEach((e) => {
const c = e.split("=");
if (c.length > 1) {
let k = c[0].trim(),
v = c[1].trim();
if (k && v) {
config[k] = v;
}
}
});
values.configs = config;
}
request({
url: DevOpsUserAPi.login.url,
method: DevOpsUserAPi.login.method,
data: values,
}).then((res) => {
if (res.code == 0) {
localStorage.setItem('token', res.data.token)
localStorage.setItem('role', res.data.role)
this.$router.push({ path:'/main'})
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
}
});
}
}
}
</script>
<style>
.kafka-console-ui{
text-align: center;
height: 100px;
margin-top: 50px;
margin-bottom: 20px;
}
</style>

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

@@ -1,21 +1,21 @@
<template>
<div>
<Header/>
<div class="content">
<a-spin :spinning="loading">
<a-tabs default-active-key="1" size="large" tabPosition="top">
<a-tab-pane key="1" tab="根据时间查询消息">
<SearchByTime :topic-list="topicList"></SearchByTime>
</a-tab-pane>
<a-tab-pane key="2" tab="根据偏移查询消息">
<SearchByOffset :topic-list="topicList"></SearchByOffset>
</a-tab-pane>
<a-tab-pane key="3" tab="在线发送">
<SendMessage :topic-list="topicList"></SendMessage>
</a-tab-pane>
</a-tabs>
</a-spin>
</div>
<div class="content">
<a-spin :spinning="loading">
<a-tabs default-active-key="1" size="large" tabPosition="top">
<a-tab-pane key="1" tab="根据时间查询消息">
<SearchByTime :topic-list="topicList"></SearchByTime>
</a-tab-pane>
<a-tab-pane key="2" tab="根据偏移查询消息">
<SearchByOffset :topic-list="topicList"></SearchByOffset>
</a-tab-pane>
<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>
</template>
@@ -26,10 +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 Header from "@/components/Header"
import DeleteMessage from "./DeleteMessage";
export default {
name: "Message",
components: { SearchByTime, SearchByOffset, SendMessage, Header },
components: { DeleteMessage, SearchByTime, SearchByOffset, SendMessage },
data() {
return {
loading: false,

View File

@@ -1,147 +1,144 @@
<template>
<div>
<Header/>
<div class="content">
<div class="content-module">
<a-card title="集群管理" style="width: 100%; text-align: left">
<p>
<a-button type="primary" @click="openClusterInfoDialog">
集群切换
</a-button>
<label>说明</label>
<span
<div class="content">
<div class="content-module">
<a-card title="集群管理" style="width: 100%; text-align: left">
<p>
<a-button type="primary" @click="openClusterInfoDialog">
集群切换
</a-button>
<label>说明</label>
<span
>多集群管理增加删除集群配置切换选中集群为当前操作集群</span
>
</p>
</a-card>
</div>
<div class="content-module" v-show="manager">
<a-card title="Broker管理" style="width: 100%; text-align: left">
<p>
<a-button type="primary" @click="openConfigThrottleDialog">
配置限流
</a-button>
<label>说明</label>
<span
>
</p>
</a-card>
</div>
<div class="content-module">
<a-card title="Broker管理" style="width: 100%; text-align: left">
<p>
<a-button type="primary" @click="openConfigThrottleDialog">
配置限流
</a-button>
<label>说明</label>
<span
>设置指定broker上的topic的副本之间数据同步占用的带宽这个设置是broker级别的但是设置后还要去对应的topic上进行限流配置指定对这个topic的相关副本进行限制</span
>
</p>
<p>
<a-button type="primary" @click="openRemoveThrottleDialog">
解除限流
</a-button>
<label>说明</label>
<span>解除指定broker上的topic副本之间数据同步占用的带宽限制</span>
</p>
</a-card>
</div>
<div class="content-module" v-show="manager">
<a-card title="副本管理" style="width: 100%; text-align: left">
<p>
<a-button type="primary" @click="openElectPreferredLeaderDialog">
首选副本作为leader
</a-button>
<label>说明</label>
<span>将集群中所有分区leader副本设置为首选副本</span>
</p>
<p>
<a-button type="primary" @click="openCurrentReassignmentsDialog">
副本变更详情
</a-button>
<label>说明</label>
<span>查看正在进行副本变更/重分配的任务或者将其取消</span>
</p>
<p>
<a-button type="primary" @click="openReplicaReassignDialog">
副本重分配
</a-button>
<label>说明</label>
<span
>
</p>
<p>
<a-button type="primary" @click="openRemoveThrottleDialog">
解除限流
</a-button>
<label>说明</label>
<span>解除指定broker上的topic副本之间数据同步占用的带宽限制</span>
</p>
</a-card>
</div>
<div class="content-module">
<a-card title="副本管理" style="width: 100%; text-align: left">
<p>
<a-button type="primary" @click="openElectPreferredLeaderDialog">
首选副本作为leader
</a-button>
<label>说明</label>
<span>将集群中所有分区leader副本设置为首选副本</span>
</p>
<p>
<a-button type="primary" @click="openCurrentReassignmentsDialog">
副本变更详情
</a-button>
<label>说明</label>
<span>查看正在进行副本变更/重分配的任务或者将其取消</span>
</p>
<p>
<a-button type="primary" @click="openReplicaReassignDialog">
副本重分配
</a-button>
<label>说明</label>
<span
>副本所在节点重新分配打个比方集群有6个节点分区1的3个副本在节点123现在将它们重新分配到345</span
>
</p>
</a-card>
</div>
<!-- 隐藏数据同步相关-->
<div class="content-module" v-show="false">
<a-card title="数据同步" style="width: 100%; text-align: left">
<p v-show="true">
<a-button type="primary" @click="openDataSyncSchemeDialog">
数据同步方案
</a-button>
<label>说明</label>
<span>新老集群迁移数据同步解决方案</span>
</p>
<p>
<a-button type="primary" @click="openMinOffsetAlignmentDialog">
最小位移对齐
</a-button>
<label>说明</label>
<span
>
</p>
</a-card>
</div>
<!-- 隐藏数据同步相关-->
<div class="content-module" v-show="false">
<a-card title="数据同步" style="width: 100%; text-align: left">
<p v-show="true">
<a-button type="primary" @click="openDataSyncSchemeDialog">
数据同步方案
</a-button>
<label>说明</label>
<span>新老集群迁移数据同步解决方案</span>
</p>
<p>
<a-button type="primary" @click="openMinOffsetAlignmentDialog">
最小位移对齐
</a-button>
<label>说明</label>
<span
>同步消费位点时需要获取两端集群中订阅分区的最小位移进行消费位点计算如需后面同步消费位点在进行数据同步前先进行最小位移对齐
点击右侧查看</span
><a href="javascript:;" @click="openOffsetAlignmentInfoDialog"
>对齐信息</a
><a href="javascript:;" @click="openOffsetAlignmentInfoDialog"
>对齐信息</a
>
</p>
<p>
<a-button type="primary" @click="openSyncConsumerOffsetDialog">
同步消费位点
</a-button>
<label>说明</label>
<span
</p>
<p>
<a-button type="primary" @click="openSyncConsumerOffsetDialog">
同步消费位点
</a-button>
<label>说明</label>
<span
>同步其它集群中指定消费组与订阅的topic的消费位点到当前集群上该消费组在当前集群已存在且双方订阅的topic分区信息一致</span
>
</p>
</a-card>
</div>
<SyncConsumerOffset
:visible="syncData.showSyncConsumerOffsetDialog"
@closeSyncConsumerOffsetDialog="closeSyncConsumerOffsetDialog"
>
</SyncConsumerOffset>
<MinOffsetAlignment
:visible="syncData.showMinOffsetAlignmentDialog"
@closeMinOffsetAlignmentDialog="closeMinOffsetAlignmentDialog"
>
</MinOffsetAlignment>
<OffsetAlignmentTable
:visible="syncData.showOffsetAlignmentInfoDialog"
@closeOffsetAlignmentInfoDialog="closeOffsetAlignmentInfoDialog"
></OffsetAlignmentTable>
<ElectPreferredLeader
:visible="replicationManager.showElectPreferredLeaderDialog"
@closeElectPreferredLeaderDialog="closeElectPreferredLeaderDialog"
></ElectPreferredLeader>
<DataSyncScheme
:visible="syncData.showDataSyncSchemeDialog"
@closeDataSyncSchemeDialog="closeDataSyncSchemeDialog"
>
</DataSyncScheme>
<ConfigThrottle
:visible="brokerManager.showConfigThrottleDialog"
@closeConfigThrottleDialog="closeConfigThrottleDialog"
>
</ConfigThrottle>
<RemoveThrottle
:visible="brokerManager.showRemoveThrottleDialog"
@closeRemoveThrottleDialog="closeRemoveThrottleDialog"
>
</RemoveThrottle>
<CurrentReassignments
:visible="replicationManager.showCurrentReassignmentsDialog"
@closeCurrentReassignmentsDialog="closeCurrentReassignmentsDialog"
></CurrentReassignments>
<ClusterInfo
:visible="clusterManager.showClusterInfoDialog"
@closeClusterInfoDialog="closeClusterInfoDialog"
></ClusterInfo>
<ReplicaReassign
:visible="replicationManager.showReplicaReassignDialog"
@closeReplicaReassignDialog="closeReplicaReassignDialog"
>
</ReplicaReassign>
>
</p>
</a-card>
</div>
<SyncConsumerOffset
:visible="syncData.showSyncConsumerOffsetDialog"
@closeSyncConsumerOffsetDialog="closeSyncConsumerOffsetDialog"
>
</SyncConsumerOffset>
<MinOffsetAlignment
:visible="syncData.showMinOffsetAlignmentDialog"
@closeMinOffsetAlignmentDialog="closeMinOffsetAlignmentDialog"
>
</MinOffsetAlignment>
<OffsetAlignmentTable
:visible="syncData.showOffsetAlignmentInfoDialog"
@closeOffsetAlignmentInfoDialog="closeOffsetAlignmentInfoDialog"
></OffsetAlignmentTable>
<ElectPreferredLeader
:visible="replicationManager.showElectPreferredLeaderDialog"
@closeElectPreferredLeaderDialog="closeElectPreferredLeaderDialog"
></ElectPreferredLeader>
<DataSyncScheme
:visible="syncData.showDataSyncSchemeDialog"
@closeDataSyncSchemeDialog="closeDataSyncSchemeDialog"
>
</DataSyncScheme>
<ConfigThrottle
:visible="brokerManager.showConfigThrottleDialog"
@closeConfigThrottleDialog="closeConfigThrottleDialog"
>
</ConfigThrottle>
<RemoveThrottle
:visible="brokerManager.showRemoveThrottleDialog"
@closeRemoveThrottleDialog="closeRemoveThrottleDialog"
>
</RemoveThrottle>
<CurrentReassignments
:visible="replicationManager.showCurrentReassignmentsDialog"
@closeCurrentReassignmentsDialog="closeCurrentReassignmentsDialog"
></CurrentReassignments>
<ClusterInfo
:visible="clusterManager.showClusterInfoDialog"
@closeClusterInfoDialog="closeClusterInfoDialog"
></ClusterInfo>
<ReplicaReassign
:visible="replicationManager.showReplicaReassignDialog"
@closeReplicaReassignDialog="closeReplicaReassignDialog"
>
</ReplicaReassign>
</div>
</template>
@@ -156,8 +153,6 @@ import RemoveThrottle from "@/views/op/RemoveThrottle";
import CurrentReassignments from "@/views/op/CurrentReassignments";
import ClusterInfo from "@/views/op/ClusterInfo";
import ReplicaReassign from "@/views/op/ReplicaReassign";
import Header from "@/components/Header"
import {isManager} from "../../utils/role";
export default {
name: "Operation",
components: {
@@ -171,11 +166,9 @@ export default {
CurrentReassignments,
ClusterInfo,
ReplicaReassign,
Header
},
data() {
return {
manager: isManager(),
syncData: {
showSyncConsumerOffsetDialog: false,
showMinOffsetAlignmentDialog: false,

View File

@@ -31,7 +31,7 @@
{{ i }}
</span>
</div>
<div slot="operation" slot-scope="record" v-show="!record.internal && manager">
<div slot="operation" slot-scope="record" v-show="!record.internal">
<a-popconfirm
:title="
'topic: ' +
@@ -68,7 +68,6 @@ import request from "@/utils/request";
import { KafkaOpApi, KafkaTopicApi } from "@/utils/api";
import notification from "ant-design-vue/es/notification";
import moment from "moment";
import {isManager} from "../../utils/role";
export default {
name: "PartitionInfo",
props: {
@@ -83,7 +82,6 @@ export default {
},
data() {
return {
manager: isManager(),
columns: columns,
show: this.visible,
data: [],

View File

@@ -1,185 +1,192 @@
<template>
<div>
<Header/>
<div class="content">
<a-spin :spinning="loading">
<div class="topic">
<div id="components-form-topic-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="`topic`">
<a-input
placeholder="topic"
class="input-w"
v-decorator="['topic']"
@change="onTopicUpdate"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="`类型`">
<a-select
class="type-select"
v-model="type"
placeholder="选择类型"
@change="getTopicList"
>
<a-select-option value="all"> 所有</a-select-option>
<a-select-option value="normal"> 普通</a-select-option>
<a-select-option value="system"> 系统</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="8" :style="{ textAlign: 'right' }">
<a-form-item>
<a-button type="primary" html-type="submit"> 刷新</a-button>
<!-- <a-button :style="{ marginLeft: '8px' }" @click="handleReset">-->
<!-- 重置-->
<!-- </a-button>-->
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
<div v-show="manager" class="operation-row-button">
<a-button type="primary" @click="openCreateTopicDialog"
>新增</a-button
>
</div>
<a-table
:columns="columns"
:data-source="filteredData"
bordered
row-key="name"
<div class="content">
<a-spin :spinning="loading">
<div class="topic">
<div id="components-form-topic-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<div slot="partitions" slot-scope="text, record">
<a href="#" @click="openPartitionInfoDialog(record.name)"
>{{ text }}
</a>
</div>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item :label="`topic`">
<a-input
placeholder="topic"
class="input-w"
v-decorator="['topic']"
@change="onTopicUpdate"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="`类型`">
<a-select
class="type-select"
v-model="type"
placeholder="选择类型"
@change="getTopicList"
>
<a-select-option value="all"> 所有</a-select-option>
<a-select-option value="normal"> 普通</a-select-option>
<a-select-option value="system"> 系统</a-select-option>
</a-select>
</a-form-item>
</a-col>
<div slot="internal" slot-scope="text">
<span v-if="text" style="color: red"></span><span v-else></span>
</div>
<div slot="operation" slot-scope="record" v-show="!record.internal">
<a-popconfirm
v-show="manager"
:title="'删除topic: ' + record.name + ''"
ok-text="确认"
cancel-text="取消"
@confirm="deleteTopic(record.name)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
>删除
</a-button>
</a-popconfirm>
<a-button
v-show="manager"
size="small"
href="javascript:;"
class="operation-btn"
@click="openPartitionInfoDialog(record.name)"
>分区详情
</a-button>
<a-button
v-show="manager"
size="small"
href="javascript:;"
class="operation-btn"
@click="openAddPartitionDialog(record.name)"
>增加分区
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openConsumedDetailDialog(record.name)"
>消费详情
</a-button>
<a-button
v-show="manager"
size="small"
href="javascript:;"
class="operation-btn"
@click="openTopicConfigDialog(record.name)"
>属性配置
</a-button>
<a-button
v-show="manager"
size="small"
href="javascript:;"
class="operation-btn"
@click="openUpdateReplicaDialog(record.name)"
>变更副本
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openMessageStatsDialog(record.name)"
>发送统计
</a-button>
<a-button
v-show="manager"
size="small"
href="javascript:;"
class="operation-btn"
@click="openThrottleDialog(record.name)"
>限流
</a-button>
</div>
</a-table>
<PartitionInfo
:topic="selectDetail.resourceName"
:visible="showPartitionInfo"
@closePartitionInfoDialog="closePartitionInfoDialog"
></PartitionInfo>
<CreateTopic
:visible="showCreateTopic"
@closeCreateTopicDialog="closeCreateTopicDialog"
>
</CreateTopic>
<AddPartition
:visible="showAddPartition"
:topic="selectDetail.resourceName"
@closeAddPartitionDialog="closeAddPartitionDialog"
></AddPartition>
<ConsumedDetail
:visible="showConsumedDetailDialog"
:topic="selectDetail.resourceName"
@closeConsumedDetailDialog="closeConsumedDetailDialog"
>
</ConsumedDetail>
<TopicConfig
:visible="showTopicConfigDialog"
:topic="selectDetail.resourceName"
@closeTopicConfigDialog="closeTopicConfigDialog"
></TopicConfig>
<UpdateReplica
:visible="showUpdateReplicaDialog"
:topic="selectDetail.resourceName"
@closeUpdateReplicaDialog="closeUpdateReplicaDialog"
></UpdateReplica>
<ConfigTopicThrottle
:visible="showThrottleDialog"
:topic="selectDetail.resourceName"
@closeThrottleDialog="closeThrottleDialog"
></ConfigTopicThrottle>
<SendStats
:visible="showSendStatsDialog"
:topic="selectDetail.resourceName"
@closeMessageStatsDialog="closeMessageStatsDialog"
></SendStats>
<a-col :span="8" :style="{ textAlign: 'right' }">
<a-form-item>
<a-button type="primary" html-type="submit"> 刷新</a-button>
<!-- <a-button :style="{ marginLeft: '8px' }" @click="handleReset">-->
<!-- 重置-->
<!-- </a-button>-->
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
</a-spin>
</div>
<div class="operation-row-button">
<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"
>
<div slot="partitions" slot-scope="text, record">
<a href="#" @click="openPartitionInfoDialog(record.name)"
>{{ text }}
</a>
</div>
<div slot="internal" slot-scope="text">
<span v-if="text" style="color: red"></span><span v-else></span>
</div>
<div slot="operation" slot-scope="record" v-show="!record.internal">
<a-popconfirm
:title="'删除topic: ' + record.name + ''"
ok-text="确认"
cancel-text="取消"
@confirm="deleteTopic(record.name)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
>删除
</a-button>
</a-popconfirm>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openPartitionInfoDialog(record.name)"
>分区详情
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openAddPartitionDialog(record.name)"
>增加分区
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openConsumedDetailDialog(record.name)"
>消费详情
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openTopicConfigDialog(record.name)"
>属性配置
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openUpdateReplicaDialog(record.name)"
>变更副本
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openMessageStatsDialog(record.name)"
>发送统计
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="openThrottleDialog(record.name)"
>限流
</a-button>
</div>
</a-table>
<PartitionInfo
:topic="selectDetail.resourceName"
:visible="showPartitionInfo"
@closePartitionInfoDialog="closePartitionInfoDialog"
></PartitionInfo>
<CreateTopic
:visible="showCreateTopic"
@closeCreateTopicDialog="closeCreateTopicDialog"
>
</CreateTopic>
<AddPartition
:visible="showAddPartition"
:topic="selectDetail.resourceName"
@closeAddPartitionDialog="closeAddPartitionDialog"
></AddPartition>
<ConsumedDetail
:visible="showConsumedDetailDialog"
:topic="selectDetail.resourceName"
@closeConsumedDetailDialog="closeConsumedDetailDialog"
>
</ConsumedDetail>
<TopicConfig
:visible="showTopicConfigDialog"
:topic="selectDetail.resourceName"
@closeTopicConfigDialog="closeTopicConfigDialog"
></TopicConfig>
<UpdateReplica
:visible="showUpdateReplicaDialog"
:topic="selectDetail.resourceName"
@closeUpdateReplicaDialog="closeUpdateReplicaDialog"
></UpdateReplica>
<ConfigTopicThrottle
:visible="showThrottleDialog"
:topic="selectDetail.resourceName"
@closeThrottleDialog="closeThrottleDialog"
></ConfigTopicThrottle>
<SendStats
:visible="showSendStatsDialog"
:topic="selectDetail.resourceName"
@closeMessageStatsDialog="closeMessageStatsDialog"
></SendStats>
</div>
</a-spin>
</div>
</template>
@@ -195,8 +202,7 @@ import TopicConfig from "@/views/topic/TopicConfig";
import UpdateReplica from "@/views/topic/UpdateReplica";
import ConfigTopicThrottle from "@/views/topic/ConfigTopicThrottle";
import SendStats from "@/views/topic/SendStats";
import Header from "@/components/Header"
import {isManager} from "../../utils/role";
export default {
name: "Topic",
components: {
@@ -208,11 +214,9 @@ export default {
UpdateReplica,
ConfigTopicThrottle,
SendStats,
Header
},
data() {
return {
manager: isManager(),
queryParam: { type: "normal" },
data: [],
columns,
@@ -237,8 +241,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();
@@ -268,14 +278,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",
@@ -284,6 +296,9 @@ export default {
}
});
},
deleteTopic(topic) {
this.deleteTopics([topic])
},
onTopicUpdate(input) {
this.filterTopic = input.target.value;
this.filter();
@@ -354,9 +369,13 @@ export default {
closeThrottleDialog() {
this.showThrottleDialog = false;
},
onSelectChange(selectedRowKeys) {
this.selectedRowKeys = selectedRowKeys;
},
},
created() {
this.getTopicList();
this.selectedRowKeys = [];
},
};
@@ -443,4 +462,8 @@ const columns = [
.type-select {
width: 200px !important;
}
.btn-left {
margin-left: 1%;
}
</style>

View File

@@ -1,135 +0,0 @@
<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="账号">
<a-input
v-decorator="[
'username',
{ rules: [{ required: true, message: '请输入用户名!' }] },
]"
placeholder="请输入用户名"
/>
</a-form-item>
<a-form-item label="密码">
<a-input
v-decorator="[
'password',
{ rules: [{ required: true, message: '请输入密码!' }] },
]"
placeholder="请输入密码"
/>
</a-form-item>
<a-form-item label="角色">
<a-select
option-filter-prop="role"
v-decorator="[
'role',
{ rules: [{ required: true, message: '请选择一个角色!' }] },
]"
placeholder="请选择一个角色"
>
<a-select-option v-for="v in roleList" :key="v" :value="v">
{{ v }}
</a-select-option>
</a-select>
</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 notification from "ant-design-vue/es/notification";
import {DevOpsUserAPi} from "../../utils/api";
export default {
name: "CreateUser",
props: {
visible: {
type: Boolean,
default: false,
},
},
data() {
return {
show: this.visible,
data: [],
roleList: roleList,
loading: false,
form: this.$form.createForm(this, { name: "coordinated" }),
};
},
watch: {
visible(v) {
this.show = v;
},
},
methods: {
handleSubmit(e) {
e.preventDefault();
this.form.validateFields((err, values) => {
if (!err) {
if (values.configs) {
const config = {};
values.configs.split("\n").forEach((e) => {
const c = e.split("=");
if (c.length > 1) {
let k = c[0].trim(),
v = c[1].trim();
if (k && v) {
config[k] = v;
}
}
});
values.configs = config;
}
this.loading = true;
request({
url: DevOpsUserAPi.createUser.url,
method: DevOpsUserAPi.createUser.method,
data: values,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.$message.success(res.msg);
this.$emit("closeCreateUserDialog", { refresh: true });
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
}
});
},
handleCancel() {
this.data = [];
this.$emit("closeCreateUserDialog", { refresh: false });
},
},
};
const roleList = ["developer", "manager"]
</script>
<style scoped></style>

View File

@@ -1,123 +0,0 @@
<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="账号">
<a-input
:disabled="true"
v-decorator="['username', { initialValue: username }]"
placeholder="username"
/>
</a-form-item>
<a-form-item label="密码">
<a-input
v-decorator="[
'password',
{ rules: [{ required: true, message: '请输入密码!' }] },
]"
placeholder="请输入密码"
/>
</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 { DevOpsUserAPi } from "@/utils/api";
import notification from "ant-design-vue/es/notification";
export default {
name: "UpdatePassword",
props: {
username: {
type: String,
default: "",
},
visible: {
type: Boolean,
default: false,
},
},
data() {
return {
show: this.visible,
data: [],
loading: false,
form: this.$form.createForm(this, { name: "coordinated" }),
};
},
watch: {
visible(v) {
this.show = v;
},
},
methods: {
handleSubmit(e) {
e.preventDefault();
this.form.validateFields((err, values) => {
if (!err) {
if (values.assignment) {
const assignment = {};
values.assignment.split("\n").forEach((e) => {
const c = e.split("=");
if (c.length > 1) {
let k = c[0];
let v = c[1];
let arr = v.split(",");
if (arr.length > 0) {
assignment[k] = arr;
}
}
});
values.assignment = assignment;
}
this.loading = true;
request({
url: DevOpsUserAPi.updateUser.url,
method: DevOpsUserAPi.updateUser.method,
data: values,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.$message.success(res.msg);
this.$emit("closeResetPasswordDialog", { refresh: true });
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
}
});
},
handleCancel() {
this.data = [];
this.$emit("closeResetPasswordDialog", { refresh: false });
},
},
};
</script>
<style scoped></style>

View File

@@ -1,227 +0,0 @@
<template>
<div>
<Header/>
<div class="content">
<a-spin :spinning="loading">
<div class="user">
<div class="operation-row-button">
<a-button type="primary" @click="openCreateUserDialog"
>新增</a-button
>
</div>
<a-table
:columns="columns"
:data-source="data"
bordered
row-key="name"
>
<div slot="role" slot-scope="text, record">
<a-select
@change="handleRoleChange(record.username, text)"
v-model="text"
option-filter-prop="role"
v-decorator="['role']"
style="width: 200px"
>
<a-select-option v-for="v in roleList" :key="v" :value="v">
{{ v }}
</a-select-option>
</a-select>
</div>
<div slot="operation" slot-scope="record">
<a-popconfirm
:title="'删除用户: ' + record.username + ' '"
ok-text="确认"
cancel-text="取消"
@confirm="deleteUser(record.id)"
>
<a-button size="small" href="javascript:;" class="operation-btn"
>删除
</a-button>
</a-popconfirm>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="resetPassword(record.username)"
>重置密码
</a-button>
</div>
</a-table>
<CreateUser
:visible="showCreateUser"
@closeCreateUserDialog="closeCreateUserDialog"
>
</CreateUser>
<ResetPassword
:visible="showResetPassword"
:username="selectDetail.resourceName"
@closeResetPasswordDialog="closeResetPasswordDialog"
></ResetPassword>
</div>
</a-spin>
</div>
</div>
</template>
<script>
import request from "@/utils/request";
import notification from "ant-design-vue/es/notification";
import CreateUser from "@/views/user/CreateUser";
import ResetPassword from "@/views/user/ResetPassword"
import Header from "@/components/Header"
import {DevOpsUserAPi} from "../../utils/api";
export default {
name: "DevOpsUser",
components: {
CreateUser,
ResetPassword,
Header
},
data() {
return {
queryParam: { type: "normal" },
roleList: ["developer", "manager"],
columns,
showUpdateUser: false,
deleteUserConfirm: false,
selectDetail: {
resourceName: "",
resourceType: "",
username: "",
},
loading: false,
showCreateUser: false,
showResetPassword: false,
type: "normal",
};
},
methods: {
handleRoleChange(username, role) {
this.loading = true;
request({
url: DevOpsUserAPi.updateUser.url,
method: DevOpsUserAPi.updateUser.method,
data: {
"username": username,
"role": role
}
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.$message.success(res.msg);
this.getDevOpsUserList();
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
},
getDevOpsUserList() {
Object.assign(this.queryParam, { type: this.type });
this.loading = true;
request({
url: DevOpsUserAPi.userList.url,
method: DevOpsUserAPi.userList.method,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.data = res.data;
//this.filter();
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
},
deleteUser(id) {
request({
url: DevOpsUserAPi.deleteUser.url + "?id=" + id,
method: DevOpsUserAPi.deleteUser.method,
}).then((res) => {
if (res.code == 0) {
this.$message.success(res.msg);
this.getDevOpsUserList();
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
},
openCreateUserDialog() {
this.showCreateUser = true;
},
closeCreateUserDialog(res) {
this.showCreateUser = false;
if (res.refresh) {
this.getDevOpsUserList();
}
},
resetPassword(username) {
this.selectDetail.resourceName = username;
this.showResetPassword = true;
},
closeResetPasswordDialog(res) {
this.showResetPassword = false;
if (res.refresh) {
this.getDevOpsUserList();
}
},
},
created() {
this.getDevOpsUserList();
},
};
const columns = [
{
title: "账号",
dataIndex: "username",
key: "username",
},
{
title: "角色",
dataIndex: "role",
key: "role",
slots: { title: "role" },
scopedSlots: { customRender: "role" },
width: 300
},
{
title: "创建时间",
dataIndex: "createTime",
key: "createTime",
slots: { title: "createTime" },
width: 300
},
{
title: "操作",
key: "operation",
scopedSlots: { customRender: "operation" },
},
];
</script>
<style scoped>
.user {
width: 100%;
height: 100%;
}
.operation-row-button {
height: 4%;
text-align: left;
margin-bottom: 8px;
}
.operation-btn {
margin-right: 3%;
}
</style>