Compare commits
17 Commits
v1.0.6
...
user-auth-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
571efe6ddc | ||
|
|
7e98a58f60 | ||
|
|
b08be2aa65 | ||
|
|
238507de19 | ||
|
|
435a5ca2bc | ||
|
|
be8e567684 | ||
|
|
da1ddeb1e7 | ||
|
|
38ca2cfc52 | ||
|
|
cc8f671fdb | ||
|
|
e3b0dd5d2a | ||
|
|
a37664f6d5 | ||
|
|
af52e6bc61 | ||
|
|
fce1154c36 | ||
|
|
0bf5c6f46c | ||
|
|
fe759aaf74 | ||
|
|
1e6a7bb269 | ||
|
|
fb440ae153 |
@@ -18,17 +18,18 @@ v1.0.6版本之前,如果kafka集群启用了ACL,但是控制台没看到Acl
|
||||
* 消费组管理
|
||||
* 消息管理
|
||||
* ACL
|
||||
* 客户端限流
|
||||
* 运维
|
||||
|
||||
功能明细看这个脑图:
|
||||

|
||||
|
||||
## 安装包下载
|
||||
点击下载(v1.0.5版本):[kafka-console-ui.zip](https://github.com/xxd763795151/kafka-console-ui/releases/download/v1.0.5/kafka-console-ui.zip)
|
||||
点击下载(v1.0.7版本):[kafka-console-ui.zip](https://github.com/xxd763795151/kafka-console-ui/releases/download/v1.0.7/kafka-console-ui.zip)
|
||||
|
||||
如果安装包下载的比较慢,可以查看下面的源码打包说明,把代码下载下来,快速打包,不过最新main分支代码刚升级了kafka版本到3.2.0,还没有充分测试,如果需要稳定版本,可以下载 1.0.4-release分支代码
|
||||
如果安装包下载的比较慢,可以查看下面的源码打包说明,把代码下载下来,本地快速打包.
|
||||
|
||||
github下载慢也可以试试从gitee下载,点击下载[gitee来源kafka-console-ui.zip](https://gitee.com/xiaodong_xu/kafka-console-ui/attach_files/969018/download/kafka-console-ui.zip)
|
||||
github下载慢也可以试试从gitee下载,点击下载[gitee来源kafka-console-ui.zip](https://gitee.com/xiaodong_xu/kafka-console-ui/releases/download/v1.0.7/kafka-console-ui.zip)
|
||||
|
||||
## 快速使用
|
||||
### Windows
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 238 KiB After Width: | Height: | Size: 178 KiB |
@@ -85,7 +85,7 @@ docker run -d -p 7766:7766 -v $PWD/data:/app/data -v $PWD/log:/app/log wdkang/ka
|
||||
|
||||
解压后 将Dockerfile放入文件夹的根目录
|
||||
|
||||
**Dockefile**
|
||||
**Dockerfile**
|
||||
|
||||
```dockerfile
|
||||
# jdk
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 439 KiB |
8
pom.xml
8
pom.xml
@@ -10,7 +10,7 @@
|
||||
</parent>
|
||||
<groupId>com.xuxd</groupId>
|
||||
<artifactId>kafka-console-ui</artifactId>
|
||||
<version>1.0.6</version>
|
||||
<version>1.0.7</version>
|
||||
<name>kafka-console-ui</name>
|
||||
<description>Kafka console manage ui</description>
|
||||
<properties>
|
||||
@@ -25,6 +25,7 @@
|
||||
<maven.assembly.plugin.version>3.0.0</maven.assembly.plugin.version>
|
||||
<mybatis-plus-boot-starter.version>3.4.2</mybatis-plus-boot-starter.version>
|
||||
<scala.version>2.13.6</scala.version>
|
||||
<spring-framework.version>5.3.26</spring-framework.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
@@ -48,6 +49,11 @@
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
|
||||
@@ -6,6 +6,9 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.web.servlet.ServletComponentScan;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* @author 晓东哥哥
|
||||
*/
|
||||
@MapperScan("com.xuxd.kafka.console.dao")
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.xuxd.kafka.console.aspect;
|
||||
|
||||
import com.xuxd.kafka.console.aspect.annotation.ControllerLog;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* @author 晓东哥哥
|
||||
*/
|
||||
@Slf4j
|
||||
@Order(-1)
|
||||
@Aspect
|
||||
@Component
|
||||
public class ControllerLogAspect {
|
||||
|
||||
private Map<String, String> descMap = new HashMap<>();
|
||||
|
||||
private ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
@Pointcut("@annotation(com.xuxd.kafka.console.aspect.annotation.ControllerLog)")
|
||||
private void pointcut() {
|
||||
|
||||
}
|
||||
|
||||
@Around("pointcut()")
|
||||
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
StringBuilder params = new StringBuilder("[");
|
||||
try {
|
||||
String methodName = getMethodFullName(joinPoint.getTarget().getClass().getName(), joinPoint.getSignature().getName());
|
||||
|
||||
if (!descMap.containsKey(methodName)) {
|
||||
cacheDescInfo(joinPoint);
|
||||
}
|
||||
|
||||
Object[] args = joinPoint.getArgs();
|
||||
long startTime = System.currentTimeMillis();
|
||||
Object res = joinPoint.proceed();
|
||||
long endTime = System.currentTimeMillis();
|
||||
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
params.append(args[i]);
|
||||
}
|
||||
params.append("]");
|
||||
|
||||
String resStr = "[" + (res != null ? res.toString() : "") + "]";
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String shortMethodName = descMap.getOrDefault(methodName, ".-");
|
||||
shortMethodName = shortMethodName.substring(shortMethodName.lastIndexOf(".") + 1);
|
||||
sb.append("[").append(shortMethodName)
|
||||
.append("调用完成: ")
|
||||
.append("请求参数=").append(params).append(", ")
|
||||
.append("响应值=").append(resStr).append(", ")
|
||||
.append("耗时=").append(endTime - startTime)
|
||||
.append(" ms");
|
||||
log.info(sb.toString());
|
||||
return res;
|
||||
} catch (Throwable e) {
|
||||
log.error("调用方法异常, 请求参数:" + params, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void cacheDescInfo(ProceedingJoinPoint joinPoint) {
|
||||
lock.lock();
|
||||
try {
|
||||
String methodName = joinPoint.getSignature().getName();
|
||||
Class<?> aClass = joinPoint.getTarget().getClass();
|
||||
|
||||
Method method = null;
|
||||
try {
|
||||
Object[] args = joinPoint.getArgs();
|
||||
|
||||
Class<?>[] clzArr = new Class[args.length];
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
clzArr[i] = args[i].getClass();
|
||||
}
|
||||
method = aClass.getDeclaredMethod(methodName, clzArr);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("cacheDescInfo error: {}", e.getMessage());
|
||||
}
|
||||
|
||||
String fullMethodName = getMethodFullName(aClass.getName(), methodName);
|
||||
String desc = "[" + fullMethodName + "]";
|
||||
if (method == null) {
|
||||
descMap.put(fullMethodName, desc);
|
||||
return;
|
||||
}
|
||||
|
||||
ControllerLog controllerLog = method.getAnnotation(ControllerLog.class);
|
||||
String value = controllerLog.value();
|
||||
if (StringUtils.isBlank(value)) {
|
||||
descMap.put(fullMethodName, desc);
|
||||
} else {
|
||||
descMap.put(fullMethodName, value);
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private String getMethodFullName(String className, String methodName) {
|
||||
return className + "#" + methodName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.xuxd.kafka.console.aspect;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.xuxd.kafka.console.aspect.annotation.Permission;
|
||||
import com.xuxd.kafka.console.beans.Credentials;
|
||||
import com.xuxd.kafka.console.beans.dos.SysUserDO;
|
||||
import com.xuxd.kafka.console.cache.RolePermCache;
|
||||
import com.xuxd.kafka.console.config.AuthConfig;
|
||||
import com.xuxd.kafka.console.dao.SysPermissionMapper;
|
||||
import com.xuxd.kafka.console.dao.SysRoleMapper;
|
||||
import com.xuxd.kafka.console.dao.SysUserMapper;
|
||||
import com.xuxd.kafka.console.exception.UnAuthorizedException;
|
||||
import com.xuxd.kafka.console.filter.CredentialsContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/5/17 22:32
|
||||
**/
|
||||
@Slf4j
|
||||
@Order(1)
|
||||
@Aspect
|
||||
@Component
|
||||
public class PermissionAspect {
|
||||
|
||||
|
||||
private Map<String, Set<String>> permMap = new HashMap<>();
|
||||
|
||||
private final AuthConfig authConfig;
|
||||
|
||||
private final SysUserMapper userMapper;
|
||||
|
||||
private final SysRoleMapper roleMapper;
|
||||
|
||||
private final SysPermissionMapper permissionMapper;
|
||||
|
||||
private final RolePermCache rolePermCache;
|
||||
|
||||
public PermissionAspect(AuthConfig authConfig,
|
||||
SysUserMapper userMapper,
|
||||
SysRoleMapper roleMapper,
|
||||
SysPermissionMapper permissionMapper,
|
||||
RolePermCache rolePermCache) {
|
||||
this.authConfig = authConfig;
|
||||
this.userMapper = userMapper;
|
||||
this.roleMapper = roleMapper;
|
||||
this.permissionMapper = permissionMapper;
|
||||
this.rolePermCache = rolePermCache;
|
||||
}
|
||||
|
||||
@Pointcut("@annotation(com.xuxd.kafka.console.aspect.annotation.Permission)")
|
||||
private void pointcut() {
|
||||
|
||||
}
|
||||
|
||||
@Before(value = "pointcut()")
|
||||
public void before(JoinPoint joinPoint) {
|
||||
if (!authConfig.isEnable()) {
|
||||
return;
|
||||
}
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
Permission permission = method.getAnnotation(Permission.class);
|
||||
if (permission == null) {
|
||||
return;
|
||||
}
|
||||
String[] value = permission.value();
|
||||
if (value == null || value.length == 0) {
|
||||
return;
|
||||
}
|
||||
String name = method.getName() + "@" + method.hashCode();
|
||||
|
||||
Map<String, Set<String>> pm = checkPermMap(name, value);
|
||||
|
||||
Set<String> allowPermSet = pm.get(name);
|
||||
if (allowPermSet == null) {
|
||||
log.error("解析权限出现意外啦!!!");
|
||||
return;
|
||||
}
|
||||
|
||||
Credentials credentials = CredentialsContext.get();
|
||||
if (credentials == null || credentials.isInvalid()) {
|
||||
throw new UnAuthorizedException("credentials is invalid");
|
||||
}
|
||||
QueryWrapper<SysUserDO> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("username", credentials.getUsername());
|
||||
SysUserDO userDO = userMapper.selectOne(queryWrapper);
|
||||
if (userDO == null) {
|
||||
throw new UnAuthorizedException(credentials.getUsername() + ":" + allowPermSet);
|
||||
}
|
||||
|
||||
String roleIds = userDO.getRoleIds();
|
||||
List<Long> roleIdList = Arrays.stream(roleIds.split(",")).map(String::trim).filter(StringUtils::isNotEmpty).map(Long::valueOf).collect(Collectors.toList());
|
||||
for (Long roleId : roleIdList) {
|
||||
Set<String> permSet = rolePermCache.getRolePermCache().getOrDefault(roleId, Collections.emptySet());
|
||||
for (String p : allowPermSet) {
|
||||
if (permSet.contains(p)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new UnAuthorizedException(credentials.getUsername() + ":" + allowPermSet);
|
||||
}
|
||||
|
||||
private Map<String, Set<String>> checkPermMap(String methodName, String[] value) {
|
||||
if (!permMap.containsKey(methodName)) {
|
||||
Map<String, Set<String>> map = new HashMap<>(permMap);
|
||||
map.put(methodName, new HashSet<>(Arrays.asList(value)));
|
||||
permMap = map;
|
||||
return map;
|
||||
}
|
||||
return permMap;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.xuxd.kafka.console.aspect.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 该注解用到controller层的方法上
|
||||
* @author 晓东哥哥
|
||||
*/
|
||||
@Inherited
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface ControllerLog {
|
||||
|
||||
String value() default "";
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.xuxd.kafka.console.aspect.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 权限注解,开启认证的时候拥有该权限的用户才能访问对应接口.
|
||||
*
|
||||
* @author: xuxd
|
||||
* @date: 2023/5/17 22:30
|
||||
**/
|
||||
@Inherited
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface Permission {
|
||||
|
||||
String[] value() default {};
|
||||
}
|
||||
21
src/main/java/com/xuxd/kafka/console/beans/Credentials.java
Normal file
21
src/main/java/com/xuxd/kafka/console/beans/Credentials.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.xuxd.kafka.console.beans;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/5/14 19:37
|
||||
**/
|
||||
@Data
|
||||
public class Credentials {
|
||||
|
||||
public static final Credentials INVALID = new Credentials();
|
||||
|
||||
private String username;
|
||||
|
||||
private long expiration;
|
||||
|
||||
public boolean isInvalid() {
|
||||
return this == INVALID;
|
||||
}
|
||||
}
|
||||
17
src/main/java/com/xuxd/kafka/console/beans/LoginResult.java
Normal file
17
src/main/java/com/xuxd/kafka/console/beans/LoginResult.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.xuxd.kafka.console.beans;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/5/14 20:44
|
||||
**/
|
||||
@Data
|
||||
public class LoginResult {
|
||||
|
||||
private String token;
|
||||
|
||||
private List<String> permissions;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.xuxd.kafka.console.beans;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/5/18 15:49
|
||||
**/
|
||||
@ToString
|
||||
public class RolePermUpdateEvent extends ApplicationEvent {
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private boolean reload = false;
|
||||
|
||||
public RolePermUpdateEvent(Object source) {
|
||||
super(source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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 lombok.Data;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/4/11 21:17
|
||||
**/
|
||||
@Data
|
||||
@TableName("t_sys_permission")
|
||||
public class SysPermissionDO {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 权限类型: 0:菜单,1:按钮
|
||||
*/
|
||||
private Integer type;
|
||||
|
||||
private Long parentId;
|
||||
|
||||
private String permission;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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 lombok.Data;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/4/11 21:17
|
||||
**/
|
||||
@Data
|
||||
@TableName("t_sys_role")
|
||||
public class SysRoleDO {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private String roleName;
|
||||
|
||||
private String description;
|
||||
|
||||
private String permissionIds;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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 lombok.Data;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/4/11 21:17
|
||||
**/
|
||||
@Data
|
||||
@TableName("t_sys_user")
|
||||
public class SysUserDO {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private String username;
|
||||
|
||||
private String password;
|
||||
|
||||
private String salt;
|
||||
|
||||
private String roleIds;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.xuxd.kafka.console.beans.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/5/14 18:59
|
||||
**/
|
||||
@Data
|
||||
public class LoginUserDTO {
|
||||
|
||||
private String username;
|
||||
|
||||
private String password;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.xuxd.kafka.console.beans.dto;
|
||||
|
||||
import com.xuxd.kafka.console.beans.dos.SysPermissionDO;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/4/11 21:17
|
||||
**/
|
||||
@Data
|
||||
public class SysPermissionDTO {
|
||||
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 权限类型: 0:菜单,1:按钮
|
||||
*/
|
||||
private Integer type;
|
||||
|
||||
private Long parentId;
|
||||
|
||||
private String permission;
|
||||
|
||||
public SysPermissionDO toSysPermissionDO() {
|
||||
SysPermissionDO permissionDO = new SysPermissionDO();
|
||||
permissionDO.setName(this.name);
|
||||
permissionDO.setType(this.type);
|
||||
permissionDO.setParentId(this.parentId);
|
||||
permissionDO.setPermission(this.permission);
|
||||
return permissionDO;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.xuxd.kafka.console.beans.dto;
|
||||
|
||||
import com.xuxd.kafka.console.beans.dos.SysRoleDO;
|
||||
import lombok.Data;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/4/11 21:17
|
||||
**/
|
||||
@Data
|
||||
public class SysRoleDTO {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String roleName;
|
||||
|
||||
private String description;
|
||||
|
||||
private List<String> permissionIds;
|
||||
|
||||
public SysRoleDO toDO() {
|
||||
SysRoleDO roleDO = new SysRoleDO();
|
||||
roleDO.setId(this.id);
|
||||
roleDO.setRoleName(this.roleName);
|
||||
roleDO.setDescription(this.description);
|
||||
if (CollectionUtils.isNotEmpty(permissionIds)) {
|
||||
roleDO.setPermissionIds(StringUtils.join(this.permissionIds, ","));
|
||||
}
|
||||
return roleDO;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.xuxd.kafka.console.beans.dto;
|
||||
|
||||
import com.xuxd.kafka.console.beans.dos.SysUserDO;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/4/11 21:17
|
||||
**/
|
||||
@Data
|
||||
public class SysUserDTO {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String username;
|
||||
|
||||
private String password;
|
||||
|
||||
private String salt;
|
||||
|
||||
private String roleIds;
|
||||
|
||||
private Boolean resetPassword = false;
|
||||
|
||||
public SysUserDO toDO() {
|
||||
SysUserDO userDO = new SysUserDO();
|
||||
userDO.setId(this.id);
|
||||
userDO.setUsername(this.username);
|
||||
userDO.setPassword(this.password);
|
||||
userDO.setSalt(this.salt);
|
||||
userDO.setRoleIds(this.roleIds);
|
||||
return userDO;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.xuxd.kafka.console.beans.vo;
|
||||
|
||||
import com.xuxd.kafka.console.beans.dos.SysPermissionDO;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/4/17 21:18
|
||||
**/
|
||||
@Data
|
||||
public class SysPermissionVO {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 权限类型: 0:菜单,1:按钮
|
||||
*/
|
||||
private Integer type;
|
||||
|
||||
private Long parentId;
|
||||
|
||||
private String permission;
|
||||
|
||||
private Long key;
|
||||
|
||||
private List<SysPermissionVO> children;
|
||||
|
||||
public static SysPermissionVO from(SysPermissionDO permissionDO) {
|
||||
SysPermissionVO permissionVO = new SysPermissionVO();
|
||||
|
||||
permissionVO.setPermission(permissionDO.getPermission());
|
||||
permissionVO.setType(permissionDO.getType());
|
||||
permissionVO.setName(permissionDO.getName());
|
||||
permissionVO.setParentId(permissionDO.getParentId());
|
||||
permissionVO.setKey(permissionDO.getId());
|
||||
permissionVO.setId(permissionDO.getId());
|
||||
return permissionVO;
|
||||
}
|
||||
}
|
||||
38
src/main/java/com/xuxd/kafka/console/beans/vo/SysRoleVO.java
Normal file
38
src/main/java/com/xuxd/kafka/console/beans/vo/SysRoleVO.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package com.xuxd.kafka.console.beans.vo;
|
||||
|
||||
import com.xuxd.kafka.console.beans.dos.SysRoleDO;
|
||||
import lombok.Data;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/4/19 21:12
|
||||
**/
|
||||
@Data
|
||||
public class SysRoleVO {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String roleName;
|
||||
|
||||
private String description;
|
||||
|
||||
private List<Long> permissionIds;
|
||||
|
||||
public static SysRoleVO from(SysRoleDO roleDO) {
|
||||
SysRoleVO roleVO = new SysRoleVO();
|
||||
roleVO.setId(roleDO.getId());
|
||||
roleVO.setRoleName(roleDO.getRoleName());
|
||||
roleVO.setDescription(roleDO.getDescription());
|
||||
if (StringUtils.isNotEmpty(roleDO.getPermissionIds())) {
|
||||
List<Long> list = Arrays.stream(roleDO.getPermissionIds().split(",")).
|
||||
filter(StringUtils::isNotEmpty).map(e -> Long.valueOf(e.trim())).collect(Collectors.toList());
|
||||
roleVO.setPermissionIds(list);
|
||||
}
|
||||
return roleVO;
|
||||
}
|
||||
}
|
||||
31
src/main/java/com/xuxd/kafka/console/beans/vo/SysUserVO.java
Normal file
31
src/main/java/com/xuxd/kafka/console/beans/vo/SysUserVO.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.xuxd.kafka.console.beans.vo;
|
||||
|
||||
import com.xuxd.kafka.console.beans.dos.SysUserDO;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/5/6 13:06
|
||||
**/
|
||||
@Data
|
||||
public class SysUserVO {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String username;
|
||||
|
||||
private String password;
|
||||
|
||||
private String roleIds;
|
||||
|
||||
private String roleNames;
|
||||
|
||||
public static SysUserVO from(SysUserDO userDO) {
|
||||
SysUserVO userVO = new SysUserVO();
|
||||
userVO.setId(userDO.getId());
|
||||
userVO.setUsername(userDO.getUsername());
|
||||
userVO.setRoleIds(userDO.getRoleIds());
|
||||
userVO.setPassword(userDO.getPassword());
|
||||
return userVO;
|
||||
}
|
||||
}
|
||||
91
src/main/java/com/xuxd/kafka/console/cache/RolePermCache.java
vendored
Normal file
91
src/main/java/com/xuxd/kafka/console/cache/RolePermCache.java
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
package com.xuxd.kafka.console.cache;
|
||||
|
||||
import com.xuxd.kafka.console.beans.RolePermUpdateEvent;
|
||||
import com.xuxd.kafka.console.beans.dos.SysPermissionDO;
|
||||
import com.xuxd.kafka.console.beans.dos.SysRoleDO;
|
||||
import com.xuxd.kafka.console.dao.SysPermissionMapper;
|
||||
import com.xuxd.kafka.console.dao.SysRoleMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/5/18 15:47
|
||||
**/
|
||||
@DependsOn("dataInit")
|
||||
@Slf4j
|
||||
@Component
|
||||
public class RolePermCache implements ApplicationListener<RolePermUpdateEvent>, SmartInitializingSingleton {
|
||||
|
||||
private Map<Long, SysPermissionDO> permCache = new HashMap<>();
|
||||
|
||||
private Map<Long, Set<String>> rolePermCache = new HashMap<>();
|
||||
|
||||
private final SysPermissionMapper permissionMapper;
|
||||
|
||||
private final SysRoleMapper roleMapper;
|
||||
|
||||
public RolePermCache(SysPermissionMapper permissionMapper, SysRoleMapper roleMapper) {
|
||||
this.permissionMapper = permissionMapper;
|
||||
this.roleMapper = roleMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(RolePermUpdateEvent event) {
|
||||
log.info("更新角色权限信息:{}", event);
|
||||
if (event.isReload()) {
|
||||
this.loadPermCache();
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
public Map<Long, SysPermissionDO> getPermCache() {
|
||||
return permCache;
|
||||
}
|
||||
|
||||
public Map<Long, Set<String>> getRolePermCache() {
|
||||
return rolePermCache;
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
List<SysRoleDO> roleDOS = roleMapper.selectList(null);
|
||||
Map<Long, Set<String>> tmp = new HashMap<>();
|
||||
for (SysRoleDO roleDO : roleDOS) {
|
||||
String permissionIds = roleDO.getPermissionIds();
|
||||
if (StringUtils.isEmpty(permissionIds)) {
|
||||
continue;
|
||||
}
|
||||
List<Long> list = Arrays.stream(permissionIds.split(",")).map(String::trim).filter(StringUtils::isNotEmpty).map(Long::valueOf).collect(Collectors.toList());
|
||||
Set<String> permSet = tmp.getOrDefault(roleDO.getId(), new HashSet<>());
|
||||
for (Long permId : list) {
|
||||
SysPermissionDO permissionDO = permCache.get(permId);
|
||||
if (permissionDO != null) {
|
||||
permSet.add(permissionDO.getPermission());
|
||||
}
|
||||
}
|
||||
tmp.put(roleDO.getId(), permSet);
|
||||
}
|
||||
rolePermCache = tmp;
|
||||
}
|
||||
|
||||
private void loadPermCache() {
|
||||
List<SysPermissionDO> roleDOS = permissionMapper.selectList(null);
|
||||
Map<Long, SysPermissionDO> map = roleDOS.stream().collect(Collectors.toMap(SysPermissionDO::getId, Function.identity(), (e1, e2) -> e1));
|
||||
permCache = map;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void afterSingletonsInstantiated() {
|
||||
this.loadPermCache();
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
21
src/main/java/com/xuxd/kafka/console/config/AuthConfig.java
Normal file
21
src/main/java/com/xuxd/kafka/console/config/AuthConfig.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.xuxd.kafka.console.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/5/9 21:08
|
||||
**/
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "auth")
|
||||
public class AuthConfig {
|
||||
|
||||
private boolean enable;
|
||||
|
||||
private String secret = "kafka-console-ui-default-secret";
|
||||
|
||||
private long expireHours;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.xuxd.kafka.console.controller;
|
||||
|
||||
import com.xuxd.kafka.console.aspect.annotation.Permission;
|
||||
import com.xuxd.kafka.console.beans.AclEntry;
|
||||
import com.xuxd.kafka.console.beans.dto.AddAuthDTO;
|
||||
import com.xuxd.kafka.console.beans.dto.ConsumerAuthDTO;
|
||||
@@ -28,6 +29,7 @@ public class AclAuthController {
|
||||
@Autowired
|
||||
private AclService aclService;
|
||||
|
||||
@Permission({"acl:authority:detail", "acl:sasl-scram:detail"})
|
||||
@PostMapping("/detail")
|
||||
public Object getAclDetailList(@RequestBody QueryAclDTO param) {
|
||||
return aclService.getAclDetailList(param.toEntry());
|
||||
@@ -38,11 +40,13 @@ public class AclAuthController {
|
||||
return aclService.getOperationList();
|
||||
}
|
||||
|
||||
@Permission("acl:authority")
|
||||
@PostMapping("/list")
|
||||
public Object getAclList(@RequestBody QueryAclDTO param) {
|
||||
return aclService.getAclList(param.toEntry());
|
||||
}
|
||||
|
||||
@Permission({"acl:authority:add-principal", "acl:authority:add", "acl:sasl-scram:add-auth"})
|
||||
@PostMapping
|
||||
public Object addAcl(@RequestBody AddAuthDTO param) {
|
||||
return aclService.addAcl(param.toAclEntry());
|
||||
@@ -54,6 +58,7 @@ public class AclAuthController {
|
||||
* @param param entry.topic && entry.username must.
|
||||
* @return
|
||||
*/
|
||||
@Permission({"acl:authority:producer", "acl:sasl-scram:producer"})
|
||||
@PostMapping("/producer")
|
||||
public Object addProducerAcl(@RequestBody ProducerAuthDTO param) {
|
||||
|
||||
@@ -66,6 +71,7 @@ public class AclAuthController {
|
||||
* @param param entry.topic && entry.groupId entry.username must.
|
||||
* @return
|
||||
*/
|
||||
@Permission({"acl:authority:consumer", "acl:sasl-scram:consumer"})
|
||||
@PostMapping("/consumer")
|
||||
public Object addConsumerAcl(@RequestBody ConsumerAuthDTO param) {
|
||||
|
||||
@@ -78,6 +84,7 @@ public class AclAuthController {
|
||||
* @param entry entry
|
||||
* @return
|
||||
*/
|
||||
@Permission({"acl:authority:clean", "acl:sasl-scram:pure"})
|
||||
@DeleteMapping
|
||||
public Object deleteAclByUser(@RequestBody AclEntry entry) {
|
||||
return aclService.deleteAcl(entry);
|
||||
@@ -89,6 +96,7 @@ public class AclAuthController {
|
||||
* @param param entry.username
|
||||
* @return
|
||||
*/
|
||||
@Permission({"acl:authority:clean", "acl:sasl-scram:pure"})
|
||||
@DeleteMapping("/user")
|
||||
public Object deleteAclByUser(@RequestBody DeleteAclDTO param) {
|
||||
return aclService.deleteUserAcl(param.toUserEntry());
|
||||
@@ -100,6 +108,7 @@ public class AclAuthController {
|
||||
* @param param entry.topic && entry.username must.
|
||||
* @return
|
||||
*/
|
||||
@Permission({"acl:authority:clean", "acl:sasl-scram:pure"})
|
||||
@DeleteMapping("/producer")
|
||||
public Object deleteProducerAcl(@RequestBody ProducerAuthDTO param) {
|
||||
|
||||
@@ -112,6 +121,7 @@ public class AclAuthController {
|
||||
* @param param entry.topic && entry.groupId entry.username must.
|
||||
* @return
|
||||
*/
|
||||
@Permission({"acl:authority:clean", "acl:sasl-scram:pure"})
|
||||
@DeleteMapping("/consumer")
|
||||
public Object deleteConsumerAcl(@RequestBody ConsumerAuthDTO param) {
|
||||
|
||||
@@ -124,6 +134,7 @@ public class AclAuthController {
|
||||
* @param param acl principal.
|
||||
* @return true or false.
|
||||
*/
|
||||
@Permission({"acl:authority:clean", "acl:sasl-scram:pure"})
|
||||
@DeleteMapping("/clear")
|
||||
public Object clearAcl(@RequestBody DeleteAclDTO param) {
|
||||
return aclService.clearAcl(param.toUserEntry());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.xuxd.kafka.console.controller;
|
||||
|
||||
import com.xuxd.kafka.console.aspect.annotation.Permission;
|
||||
import com.xuxd.kafka.console.beans.AclEntry;
|
||||
import com.xuxd.kafka.console.beans.AclUser;
|
||||
import com.xuxd.kafka.console.service.AclService;
|
||||
@@ -26,27 +27,32 @@ public class AclUserController {
|
||||
@Autowired
|
||||
private AclService aclService;
|
||||
|
||||
@Permission("acl:sasl-scram")
|
||||
@GetMapping
|
||||
public Object getUserList() {
|
||||
return aclService.getUserList();
|
||||
}
|
||||
|
||||
@Permission({"acl:sasl-scram:add-update", "acl:sasl-scram:add-auth"})
|
||||
@PostMapping
|
||||
public Object addOrUpdateUser(@RequestBody AclUser user) {
|
||||
return aclService.addOrUpdateUser(user.getUsername(), user.getPassword());
|
||||
}
|
||||
|
||||
@Permission({"acl:sasl-scram:del", "acl:sasl-scram:pure"})
|
||||
@DeleteMapping
|
||||
public Object deleteUser(@RequestBody AclUser user) {
|
||||
return aclService.deleteUser(user.getUsername());
|
||||
}
|
||||
|
||||
|
||||
@Permission({"acl:sasl-scram:del", "acl:sasl-scram:pure"})
|
||||
@DeleteMapping("/auth")
|
||||
public Object deleteUserAndAuth(@RequestBody AclUser user) {
|
||||
return aclService.deleteUserAndAuth(user.getUsername());
|
||||
}
|
||||
|
||||
@Permission("acl:sasl-scram:detail")
|
||||
@GetMapping("/detail")
|
||||
public Object getUserDetail(@RequestParam String username) {
|
||||
return aclService.getUserDetail(username);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.xuxd.kafka.console.controller;
|
||||
|
||||
import com.xuxd.kafka.console.beans.ResponseData;
|
||||
import com.xuxd.kafka.console.beans.dto.LoginUserDTO;
|
||||
import com.xuxd.kafka.console.config.AuthConfig;
|
||||
import com.xuxd.kafka.console.service.AuthService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/5/11 18:54
|
||||
**/
|
||||
@RestController
|
||||
@RequestMapping("/auth")
|
||||
public class AuthController {
|
||||
|
||||
|
||||
private final AuthConfig authConfig;
|
||||
|
||||
private final AuthService authService;
|
||||
|
||||
public AuthController(AuthConfig authConfig, AuthService authService) {
|
||||
this.authConfig = authConfig;
|
||||
this.authService = authService;
|
||||
}
|
||||
|
||||
@GetMapping("/enable")
|
||||
public boolean enable() {
|
||||
return authConfig.isEnable();
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public ResponseData login(@RequestBody LoginUserDTO userDTO) {
|
||||
return authService.login(userDTO);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.xuxd.kafka.console.controller;
|
||||
|
||||
import com.xuxd.kafka.console.aspect.annotation.Permission;
|
||||
import com.xuxd.kafka.console.beans.ResponseData;
|
||||
import com.xuxd.kafka.console.beans.dto.AlterClientQuotaDTO;
|
||||
import com.xuxd.kafka.console.beans.dto.QueryClientQuotaDTO;
|
||||
@@ -21,11 +22,13 @@ public class ClientQuotaController {
|
||||
this.clientQuotaService = clientQuotaService;
|
||||
}
|
||||
|
||||
@Permission({"quota:user", "quota:client", "quota:user-client"})
|
||||
@PostMapping("/list")
|
||||
public Object getClientQuotaConfigs(@RequestBody QueryClientQuotaDTO request) {
|
||||
return clientQuotaService.getClientQuotaConfigs(request.getTypes(), request.getNames());
|
||||
}
|
||||
|
||||
@Permission({"quota:user:add", "quota:client:add", "quota:user-client:add", "quota:edit"})
|
||||
@PostMapping
|
||||
public Object alterClientQuotaConfigs(@RequestBody AlterClientQuotaDTO request) {
|
||||
if (request.getTypes().size() != 2) {
|
||||
@@ -38,6 +41,7 @@ public class ClientQuotaController {
|
||||
return clientQuotaService.alterClientQuotaConfigs(request);
|
||||
}
|
||||
|
||||
@Permission("quota:del")
|
||||
@DeleteMapping
|
||||
public Object deleteClientQuotaConfigs(@RequestBody AlterClientQuotaDTO request) {
|
||||
if (request.getTypes().size() != 2) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.xuxd.kafka.console.controller;
|
||||
|
||||
import com.xuxd.kafka.console.aspect.annotation.Permission;
|
||||
import com.xuxd.kafka.console.beans.dto.ClusterInfoDTO;
|
||||
import com.xuxd.kafka.console.service.ClusterService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -34,16 +35,19 @@ public class ClusterController {
|
||||
return clusterService.getClusterInfoList();
|
||||
}
|
||||
|
||||
@Permission("op:cluster-switch:add")
|
||||
@PostMapping("/info")
|
||||
public Object addClusterInfo(@RequestBody ClusterInfoDTO dto) {
|
||||
return clusterService.addClusterInfo(dto.to());
|
||||
}
|
||||
|
||||
@Permission("op:cluster-switch:del")
|
||||
@DeleteMapping("/info")
|
||||
public Object deleteClusterInfo(@RequestBody ClusterInfoDTO dto) {
|
||||
return clusterService.deleteClusterInfo(dto.getId());
|
||||
}
|
||||
|
||||
@Permission("op:cluster-switch:edit")
|
||||
@PutMapping("/info")
|
||||
public Object updateClusterInfo(@RequestBody ClusterInfoDTO dto) {
|
||||
return clusterService.updateClusterInfo(dto.to());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.xuxd.kafka.console.controller;
|
||||
|
||||
import com.xuxd.kafka.console.aspect.annotation.Permission;
|
||||
import com.xuxd.kafka.console.beans.ResponseData;
|
||||
import com.xuxd.kafka.console.beans.dto.AlterConfigDTO;
|
||||
import com.xuxd.kafka.console.beans.enums.AlterType;
|
||||
@@ -41,46 +42,55 @@ public class ConfigController {
|
||||
return ResponseData.create().data(configMap).success();
|
||||
}
|
||||
|
||||
@Permission("topic:property-config")
|
||||
@GetMapping("/topic")
|
||||
public Object getTopicConfig(String topic) {
|
||||
return configService.getTopicConfig(topic);
|
||||
}
|
||||
|
||||
@Permission("topic:property-config:edit")
|
||||
@PostMapping("/topic")
|
||||
public Object setTopicConfig(@RequestBody AlterConfigDTO dto) {
|
||||
return configService.alterTopicConfig(dto.getEntity(), dto.to(), AlterType.SET);
|
||||
}
|
||||
|
||||
@Permission("topic:property-config:del")
|
||||
@DeleteMapping("/topic")
|
||||
public Object deleteTopicConfig(@RequestBody AlterConfigDTO dto) {
|
||||
return configService.alterTopicConfig(dto.getEntity(), dto.to(), AlterType.DELETE);
|
||||
}
|
||||
|
||||
@Permission("cluster:property-config")
|
||||
@GetMapping("/broker")
|
||||
public Object getBrokerConfig(String brokerId) {
|
||||
return configService.getBrokerConfig(brokerId);
|
||||
}
|
||||
|
||||
@Permission("cluster:edit")
|
||||
@PostMapping("/broker")
|
||||
public Object setBrokerConfig(@RequestBody AlterConfigDTO dto) {
|
||||
return configService.alterBrokerConfig(dto.getEntity(), dto.to(), AlterType.SET);
|
||||
}
|
||||
|
||||
@Permission("cluster:edit")
|
||||
@DeleteMapping("/broker")
|
||||
public Object deleteBrokerConfig(@RequestBody AlterConfigDTO dto) {
|
||||
return configService.alterBrokerConfig(dto.getEntity(), dto.to(), AlterType.DELETE);
|
||||
}
|
||||
|
||||
@Permission("cluster:log-config")
|
||||
@GetMapping("/broker/logger")
|
||||
public Object getBrokerLoggerConfig(String brokerId) {
|
||||
return configService.getBrokerLoggerConfig(brokerId);
|
||||
}
|
||||
|
||||
@Permission("cluster:edit")
|
||||
@PostMapping("/broker/logger")
|
||||
public Object setBrokerLoggerConfig(@RequestBody AlterConfigDTO dto) {
|
||||
return configService.alterBrokerLoggerConfig(dto.getEntity(), dto.to(), AlterType.SET);
|
||||
}
|
||||
|
||||
@Permission("cluster:edit")
|
||||
@DeleteMapping("/broker/logger")
|
||||
public Object deleteBrokerLoggerConfig(@RequestBody AlterConfigDTO dto) {
|
||||
return configService.alterBrokerLoggerConfig(dto.getEntity(), dto.to(), AlterType.DELETE);
|
||||
|
||||
@@ -1,28 +1,20 @@
|
||||
package com.xuxd.kafka.console.controller;
|
||||
|
||||
import com.xuxd.kafka.console.aspect.annotation.Permission;
|
||||
import com.xuxd.kafka.console.beans.ResponseData;
|
||||
import com.xuxd.kafka.console.beans.dto.AddSubscriptionDTO;
|
||||
import com.xuxd.kafka.console.beans.dto.QueryConsumerGroupDTO;
|
||||
import com.xuxd.kafka.console.beans.dto.ResetOffsetDTO;
|
||||
import com.xuxd.kafka.console.service.ConsumerService;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.kafka.clients.consumer.OffsetResetStrategy;
|
||||
import org.apache.kafka.common.ConsumerGroupState;
|
||||
import org.apache.kafka.common.TopicPartition;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* kafka-console-ui.
|
||||
@@ -51,26 +43,34 @@ public class ConsumerController {
|
||||
return consumerService.getConsumerGroupList(groupIdList, stateSet);
|
||||
}
|
||||
|
||||
@Permission("group:del")
|
||||
@DeleteMapping("/group")
|
||||
public Object deleteConsumerGroup(@RequestParam String groupId) {
|
||||
return consumerService.deleteConsumerGroup(groupId);
|
||||
}
|
||||
|
||||
@Permission("group:client")
|
||||
@GetMapping("/member")
|
||||
public Object getConsumerMembers(@RequestParam String groupId) {
|
||||
return consumerService.getConsumerMembers(groupId);
|
||||
}
|
||||
|
||||
@Permission("group:consumer-detail")
|
||||
@GetMapping("/detail")
|
||||
public Object getConsumerDetail(@RequestParam String groupId) {
|
||||
return consumerService.getConsumerDetail(groupId);
|
||||
}
|
||||
|
||||
@Permission("group:add")
|
||||
@PostMapping("/subscription")
|
||||
public Object addSubscription(@RequestBody AddSubscriptionDTO subscriptionDTO) {
|
||||
return consumerService.addSubscription(subscriptionDTO.getGroupId(), subscriptionDTO.getTopic());
|
||||
}
|
||||
|
||||
@Permission({"group:consumer-detail:min",
|
||||
"group:consumer-detail:last",
|
||||
"group:consumer-detail:timestamp",
|
||||
"group:consumer-detail:any"})
|
||||
@PostMapping("/reset/offset")
|
||||
public Object restOffset(@RequestBody ResetOffsetDTO offsetDTO) {
|
||||
ResponseData res = ResponseData.create().failed("unknown");
|
||||
@@ -78,7 +78,7 @@ public class ConsumerController {
|
||||
case ResetOffsetDTO.Level.TOPIC:
|
||||
switch (offsetDTO.getType()) {
|
||||
case ResetOffsetDTO.Type
|
||||
.EARLIEST:
|
||||
.EARLIEST:
|
||||
res = consumerService.resetOffsetToEndpoint(offsetDTO.getGroupId(), offsetDTO.getTopic(), OffsetResetStrategy.EARLIEST);
|
||||
break;
|
||||
case ResetOffsetDTO.Type.LATEST:
|
||||
@@ -94,7 +94,7 @@ public class ConsumerController {
|
||||
case ResetOffsetDTO.Level.PARTITION:
|
||||
switch (offsetDTO.getType()) {
|
||||
case ResetOffsetDTO.Type
|
||||
.SPECIAL:
|
||||
.SPECIAL:
|
||||
res = consumerService.resetPartitionToTargetOffset(offsetDTO.getGroupId(), new TopicPartition(offsetDTO.getTopic(), offsetDTO.getPartition()), offsetDTO.getOffset());
|
||||
break;
|
||||
default:
|
||||
@@ -118,11 +118,13 @@ public class ConsumerController {
|
||||
return consumerService.getSubscribeTopicList(groupId);
|
||||
}
|
||||
|
||||
@Permission({"topic:consumer-detail"})
|
||||
@GetMapping("/topic/subscribed")
|
||||
public Object getTopicSubscribedByGroups(@RequestParam String topic) {
|
||||
return consumerService.getTopicSubscribedByGroups(topic);
|
||||
}
|
||||
|
||||
@Permission("group:offset-partition")
|
||||
@GetMapping("/offset/partition")
|
||||
public Object getOffsetPartition(@RequestParam String groupId) {
|
||||
return consumerService.getOffsetPartition(groupId);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.xuxd.kafka.console.controller;
|
||||
|
||||
import com.xuxd.kafka.console.aspect.annotation.Permission;
|
||||
import com.xuxd.kafka.console.beans.QueryMessage;
|
||||
import com.xuxd.kafka.console.beans.ResponseData;
|
||||
import com.xuxd.kafka.console.beans.SendMessage;
|
||||
@@ -24,16 +25,19 @@ public class MessageController {
|
||||
@Autowired
|
||||
private MessageService messageService;
|
||||
|
||||
@Permission("message:search-time")
|
||||
@PostMapping("/search/time")
|
||||
public Object searchByTime(@RequestBody QueryMessageDTO dto) {
|
||||
return messageService.searchByTime(dto.toQueryMessage());
|
||||
}
|
||||
|
||||
@Permission("message:search-offset")
|
||||
@PostMapping("/search/offset")
|
||||
public Object searchByOffset(@RequestBody QueryMessageDTO dto) {
|
||||
return messageService.searchByOffset(dto.toQueryMessage());
|
||||
}
|
||||
|
||||
@Permission("message:detail")
|
||||
@PostMapping("/search/detail")
|
||||
public Object searchDetail(@RequestBody QueryMessageDTO dto) {
|
||||
return messageService.searchDetail(dto.toQueryMessage());
|
||||
@@ -44,16 +48,19 @@ public class MessageController {
|
||||
return messageService.deserializerList();
|
||||
}
|
||||
|
||||
@Permission("message:send")
|
||||
@PostMapping("/send")
|
||||
public Object send(@RequestBody SendMessage message) {
|
||||
return messageService.send(message);
|
||||
}
|
||||
|
||||
@Permission("message:resend")
|
||||
@PostMapping("/resend")
|
||||
public Object resend(@RequestBody SendMessage message) {
|
||||
return messageService.resend(message);
|
||||
}
|
||||
|
||||
@Permission("message:del")
|
||||
@DeleteMapping
|
||||
public Object delete(@RequestBody List<QueryMessage> messages) {
|
||||
if (CollectionUtils.isEmpty(messages)) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.xuxd.kafka.console.controller;
|
||||
|
||||
import com.xuxd.kafka.console.aspect.annotation.Permission;
|
||||
import com.xuxd.kafka.console.beans.TopicPartition;
|
||||
import com.xuxd.kafka.console.beans.dto.BrokerThrottleDTO;
|
||||
import com.xuxd.kafka.console.beans.dto.ProposedAssignmentDTO;
|
||||
@@ -8,13 +9,7 @@ import com.xuxd.kafka.console.beans.dto.SyncDataDTO;
|
||||
import com.xuxd.kafka.console.service.OperationService;
|
||||
import org.apache.kafka.clients.admin.AdminClientConfig;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* kafka-console-ui.
|
||||
@@ -51,26 +46,31 @@ public class OperationController {
|
||||
return operationService.deleteAlignmentById(id);
|
||||
}
|
||||
|
||||
@Permission({"topic:partition-detail:preferred", "op:replication-preferred"})
|
||||
@PostMapping("/replication/preferred")
|
||||
public Object electPreferredLeader(@RequestBody ReplicationDTO dto) {
|
||||
return operationService.electPreferredLeader(dto.getTopic(), dto.getPartition());
|
||||
}
|
||||
|
||||
@Permission("op:config-throttle")
|
||||
@PostMapping("/broker/throttle")
|
||||
public Object configThrottle(@RequestBody BrokerThrottleDTO dto) {
|
||||
return operationService.configThrottle(dto.getBrokerList(), dto.getUnit().toKb(dto.getThrottle()));
|
||||
}
|
||||
|
||||
@Permission("op:remove-throttle")
|
||||
@DeleteMapping("/broker/throttle")
|
||||
public Object removeThrottle(@RequestBody BrokerThrottleDTO dto) {
|
||||
return operationService.removeThrottle(dto.getBrokerList());
|
||||
}
|
||||
|
||||
@Permission("op:replication-update-detail")
|
||||
@GetMapping("/replication/reassignments")
|
||||
public Object currentReassignments() {
|
||||
return operationService.currentReassignments();
|
||||
}
|
||||
|
||||
@Permission("op:replication-update-detail:cancel")
|
||||
@DeleteMapping("/replication/reassignments")
|
||||
public Object cancelReassignment(@RequestBody TopicPartition partition) {
|
||||
return operationService.cancelReassignment(new org.apache.kafka.common.TopicPartition(partition.getTopic(), partition.getPartition()));
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
package com.xuxd.kafka.console.controller;
|
||||
|
||||
import com.xuxd.kafka.console.aspect.annotation.Permission;
|
||||
import com.xuxd.kafka.console.beans.ReplicaAssignment;
|
||||
import com.xuxd.kafka.console.beans.dto.AddPartitionDTO;
|
||||
import com.xuxd.kafka.console.beans.dto.NewTopicDTO;
|
||||
import com.xuxd.kafka.console.beans.dto.TopicThrottleDTO;
|
||||
import com.xuxd.kafka.console.beans.enums.TopicType;
|
||||
import com.xuxd.kafka.console.service.TopicService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* kafka-console-ui.
|
||||
@@ -37,26 +33,31 @@ public class TopicController {
|
||||
return topicService.getTopicNameList(false);
|
||||
}
|
||||
|
||||
@Permission("topic:load")
|
||||
@GetMapping("/list")
|
||||
public Object getTopicList(@RequestParam(required = false) String topic, @RequestParam String type) {
|
||||
return topicService.getTopicList(topic, TopicType.valueOf(type.toUpperCase()));
|
||||
}
|
||||
|
||||
@Permission({"topic:batch-del", "topic:del"})
|
||||
@DeleteMapping
|
||||
public Object deleteTopic(@RequestBody List<String> topics) {
|
||||
return topicService.deleteTopics(topics);
|
||||
}
|
||||
|
||||
@Permission("topic:partition-detail")
|
||||
@GetMapping("/partition")
|
||||
public Object getTopicPartitionInfo(@RequestParam String topic) {
|
||||
return topicService.getTopicPartitionInfo(topic.trim());
|
||||
}
|
||||
|
||||
@Permission("topic:add")
|
||||
@PostMapping("/new")
|
||||
public Object createNewTopic(@RequestBody NewTopicDTO topicDTO) {
|
||||
return topicService.createTopic(topicDTO.toNewTopic());
|
||||
}
|
||||
|
||||
@Permission("topic:partition-add")
|
||||
@PostMapping("/partition/new")
|
||||
public Object addPartition(@RequestBody AddPartitionDTO partitionDTO) {
|
||||
String topic = partitionDTO.getTopic().trim();
|
||||
@@ -79,16 +80,19 @@ public class TopicController {
|
||||
return topicService.getCurrentReplicaAssignment(topic);
|
||||
}
|
||||
|
||||
@Permission({"topic:replication-modify", "op:replication-reassign"})
|
||||
@PostMapping("/replica/assignment")
|
||||
public Object updateReplicaAssignment(@RequestBody ReplicaAssignment assignment) {
|
||||
return topicService.updateReplicaAssignment(assignment);
|
||||
}
|
||||
|
||||
@Permission("topic:replication-sync-throttle")
|
||||
@PostMapping("/replica/throttle")
|
||||
public Object configThrottle(@RequestBody TopicThrottleDTO dto) {
|
||||
return topicService.configThrottle(dto.getTopic(), dto.getPartitions(), dto.getOperation());
|
||||
}
|
||||
|
||||
@Permission("topic:send-count")
|
||||
@GetMapping("/send/stats")
|
||||
public Object sendStats(@RequestParam String topic) {
|
||||
return topicService.sendStats(topic);
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.xuxd.kafka.console.controller;
|
||||
|
||||
import com.xuxd.kafka.console.aspect.annotation.ControllerLog;
|
||||
import com.xuxd.kafka.console.aspect.annotation.Permission;
|
||||
import com.xuxd.kafka.console.beans.Credentials;
|
||||
import com.xuxd.kafka.console.beans.dto.SysPermissionDTO;
|
||||
import com.xuxd.kafka.console.beans.dto.SysRoleDTO;
|
||||
import com.xuxd.kafka.console.beans.dto.SysUserDTO;
|
||||
import com.xuxd.kafka.console.service.UserManageService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/4/11 21:34
|
||||
**/
|
||||
@RestController
|
||||
@RequestMapping("/sys/user/manage")
|
||||
public class UserManageController {
|
||||
|
||||
private final UserManageService userManageService;
|
||||
|
||||
public UserManageController(UserManageService userManageService) {
|
||||
this.userManageService = userManageService;
|
||||
}
|
||||
|
||||
@Permission({"user-manage:user:add", "user-manage:user:change-role", "user-manage:user:reset-pass"})
|
||||
@ControllerLog("新增/更新用户")
|
||||
@PostMapping("/user")
|
||||
public Object addOrUpdateUser(@RequestBody SysUserDTO userDTO) {
|
||||
return userManageService.addOrUpdateUser(userDTO);
|
||||
}
|
||||
|
||||
@Permission("user-manage:role:save")
|
||||
@ControllerLog("新增/更新角色")
|
||||
@PostMapping("/role")
|
||||
public Object addOrUpdateRole(@RequestBody SysRoleDTO roleDTO) {
|
||||
return userManageService.addOrUdpateRole(roleDTO);
|
||||
}
|
||||
|
||||
@ControllerLog("新增权限")
|
||||
@PostMapping("/permission")
|
||||
public Object addPermission(@RequestBody SysPermissionDTO permissionDTO) {
|
||||
return userManageService.addPermission(permissionDTO);
|
||||
}
|
||||
|
||||
@Permission("user-manage:role:save")
|
||||
@ControllerLog("更新角色")
|
||||
@PutMapping("/role")
|
||||
public Object updateRole(@RequestBody SysRoleDTO roleDTO) {
|
||||
return userManageService.updateRole(roleDTO);
|
||||
}
|
||||
|
||||
@Permission({"user-manage:role"})
|
||||
@GetMapping("/role")
|
||||
public Object selectRole() {
|
||||
return userManageService.selectRole();
|
||||
}
|
||||
|
||||
@Permission({"user-manage:permission"})
|
||||
@GetMapping("/permission")
|
||||
public Object selectPermission() {
|
||||
return userManageService.selectPermission();
|
||||
}
|
||||
|
||||
@Permission({"user-manage:user"})
|
||||
@GetMapping("/user")
|
||||
public Object selectUser() {
|
||||
return userManageService.selectUser();
|
||||
}
|
||||
|
||||
@Permission("user-manage:role:del")
|
||||
@ControllerLog("删除角色")
|
||||
@DeleteMapping("/role")
|
||||
public Object deleteRole(@RequestParam Long id) {
|
||||
return userManageService.deleteRole(id);
|
||||
}
|
||||
|
||||
@Permission("user-manage:user:del")
|
||||
@ControllerLog("删除用户")
|
||||
@DeleteMapping("/user")
|
||||
public Object deleteUser(@RequestParam Long id) {
|
||||
return userManageService.deleteUser(id);
|
||||
}
|
||||
|
||||
@Permission("user-manage:setting")
|
||||
@ControllerLog("更新密码")
|
||||
@PostMapping("/user/password")
|
||||
public Object updatePassword(@RequestBody SysUserDTO userDTO, HttpServletRequest request) {
|
||||
Credentials credentials = (Credentials) request.getAttribute("credentials");
|
||||
if (credentials != null && !credentials.isInvalid()) {
|
||||
userDTO.setUsername(credentials.getUsername());
|
||||
}
|
||||
return userManageService.updatePassword(userDTO);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.xuxd.kafka.console.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.xuxd.kafka.console.beans.dos.SysPermissionDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 系统权限 .
|
||||
*
|
||||
* @author: xuxd
|
||||
* @date: 2023/4/11 21:21
|
||||
**/
|
||||
@Mapper
|
||||
public interface SysPermissionMapper extends BaseMapper<SysPermissionDO> {
|
||||
}
|
||||
13
src/main/java/com/xuxd/kafka/console/dao/SysRoleMapper.java
Normal file
13
src/main/java/com/xuxd/kafka/console/dao/SysRoleMapper.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.xuxd.kafka.console.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.xuxd.kafka.console.beans.dos.SysRoleDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/4/11 21:22
|
||||
**/
|
||||
@Mapper
|
||||
public interface SysRoleMapper extends BaseMapper<SysRoleDO> {
|
||||
}
|
||||
13
src/main/java/com/xuxd/kafka/console/dao/SysUserMapper.java
Normal file
13
src/main/java/com/xuxd/kafka/console/dao/SysUserMapper.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.xuxd.kafka.console.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.xuxd.kafka.console.beans.dos.SysUserDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/4/11 21:22
|
||||
**/
|
||||
@Mapper
|
||||
public interface SysUserMapper extends BaseMapper<SysUserDO> {
|
||||
}
|
||||
93
src/main/java/com/xuxd/kafka/console/dao/init/DataInit.java
Normal file
93
src/main/java/com/xuxd/kafka/console/dao/init/DataInit.java
Normal file
@@ -0,0 +1,93 @@
|
||||
package com.xuxd.kafka.console.dao.init;
|
||||
|
||||
import com.xuxd.kafka.console.beans.RolePermUpdateEvent;
|
||||
import com.xuxd.kafka.console.config.AuthConfig;
|
||||
import com.xuxd.kafka.console.dao.SysPermissionMapper;
|
||||
import com.xuxd.kafka.console.dao.SysRoleMapper;
|
||||
import com.xuxd.kafka.console.dao.SysUserMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/5/17 13:10
|
||||
**/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DataInit implements SmartInitializingSingleton {
|
||||
|
||||
private final AuthConfig authConfig;
|
||||
|
||||
private final SysUserMapper userMapper;
|
||||
|
||||
private final SysRoleMapper roleMapper;
|
||||
|
||||
private final SysPermissionMapper permissionMapper;
|
||||
|
||||
private final DataSource dataSource;
|
||||
|
||||
private final SqlParse sqlParse;
|
||||
|
||||
private final ApplicationEventPublisher publisher;
|
||||
|
||||
|
||||
public DataInit(AuthConfig authConfig,
|
||||
SysUserMapper userMapper,
|
||||
SysRoleMapper roleMapper,
|
||||
SysPermissionMapper permissionMapper,
|
||||
DataSource dataSource,
|
||||
ApplicationEventPublisher publisher) {
|
||||
this.authConfig = authConfig;
|
||||
this.userMapper = userMapper;
|
||||
this.roleMapper = roleMapper;
|
||||
this.permissionMapper = permissionMapper;
|
||||
this.dataSource = dataSource;
|
||||
this.publisher = publisher;
|
||||
this.sqlParse = new SqlParse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterSingletonsInstantiated() {
|
||||
if (!authConfig.isEnable()) {
|
||||
log.info("Disable login authentication, no longer try to initialize the data");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Connection connection = dataSource.getConnection();
|
||||
Integer userCount = userMapper.selectCount(null);
|
||||
if (userCount == null || userCount == 0) {
|
||||
initData(connection, SqlParse.USER_TABLE);
|
||||
}
|
||||
|
||||
Integer roleCount = roleMapper.selectCount(null);
|
||||
if (roleCount == null || roleCount == 0) {
|
||||
initData(connection, SqlParse.ROLE_TABLE);
|
||||
}
|
||||
|
||||
Integer permCount = permissionMapper.selectCount(null);
|
||||
if (permCount == null || permCount == 0) {
|
||||
initData(connection, SqlParse.PERM_TABLE);
|
||||
}
|
||||
RolePermUpdateEvent event = new RolePermUpdateEvent(this);
|
||||
event.setReload(true);
|
||||
publisher.publishEvent(event);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void initData(Connection connection, String table) throws SQLException {
|
||||
log.info("Init default data for {}", table);
|
||||
String sql = sqlParse.getMergeSql(table);
|
||||
PreparedStatement statement = connection.prepareStatement(sql);
|
||||
statement.execute();
|
||||
}
|
||||
}
|
||||
85
src/main/java/com/xuxd/kafka/console/dao/init/SqlParse.java
Normal file
85
src/main/java/com/xuxd/kafka/console/dao/init/SqlParse.java
Normal file
@@ -0,0 +1,85 @@
|
||||
package com.xuxd.kafka.console.dao.init;
|
||||
|
||||
import com.google.common.io.Files;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
import scala.collection.mutable.StringBuilder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/5/17 21:22
|
||||
**/
|
||||
@Slf4j
|
||||
public class SqlParse {
|
||||
|
||||
private final String FILE = "classpath:db/data-h2.sql";
|
||||
|
||||
private final Map<String, List<String>> sqlMap = new HashMap<>();
|
||||
|
||||
public static final String ROLE_TABLE = "t_sys_role";
|
||||
public static final String USER_TABLE = "t_sys_user";
|
||||
public static final String PERM_TABLE = "t_sys_permission";
|
||||
|
||||
public SqlParse() {
|
||||
sqlMap.put(ROLE_TABLE, new ArrayList<>());
|
||||
sqlMap.put(USER_TABLE, new ArrayList<>());
|
||||
sqlMap.put(PERM_TABLE, new ArrayList<>());
|
||||
|
||||
String table = null;
|
||||
try {
|
||||
File file = ResourceUtils.getFile(FILE);
|
||||
List<String> lines = Files.readLines(file, Charset.forName("UTF-8"));
|
||||
for (String str : lines) {
|
||||
if (StringUtils.isNotEmpty(str)) {
|
||||
if (str.indexOf("start--") > 0) {
|
||||
if (str.indexOf(ROLE_TABLE) > 0) {
|
||||
table = ROLE_TABLE;
|
||||
}
|
||||
if (str.indexOf(USER_TABLE) > 0) {
|
||||
table = USER_TABLE;
|
||||
}
|
||||
if (str.indexOf(PERM_TABLE) > 0) {
|
||||
table = PERM_TABLE;
|
||||
}
|
||||
}
|
||||
if (isSql(str)) {
|
||||
if (table == null) {
|
||||
log.error("Table is null, can not load sql: {}", str);
|
||||
continue;
|
||||
}
|
||||
sqlMap.get(table).add(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getSqlList(String table) {
|
||||
return sqlMap.get(table);
|
||||
}
|
||||
|
||||
public String getMergeSql(String table) {
|
||||
List<String> list = getSqlList(table);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
list.forEach(sql -> sb.append(sql));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private boolean isSql(String str) {
|
||||
return StringUtils.isNotEmpty(str) && str.startsWith("insert");
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
package com.xuxd.kafka.console.interceptor;
|
||||
package com.xuxd.kafka.console.exception;
|
||||
|
||||
import com.xuxd.kafka.console.beans.ResponseData;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* kafka-console-ui.
|
||||
@@ -17,6 +20,14 @@ import org.springframework.web.bind.annotation.ResponseBody;
|
||||
@ControllerAdvice(basePackages = "com.xuxd.kafka.console.controller")
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ResponseStatus(code = HttpStatus.FORBIDDEN)
|
||||
@ExceptionHandler(value = UnAuthorizedException.class)
|
||||
@ResponseBody
|
||||
public Object unAuthorizedExceptionHandler(HttpServletRequest req, Exception ex) throws Exception {
|
||||
log.error("unAuthorized: {}", ex.getMessage());
|
||||
return ResponseData.create().failed("UnAuthorized: " + ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = Exception.class)
|
||||
@ResponseBody
|
||||
public Object exceptionHandler(HttpServletRequest req, Exception ex) throws Exception {
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.xuxd.kafka.console.exception;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/5/17 23:08
|
||||
**/
|
||||
public class UnAuthorizedException extends RuntimeException{
|
||||
|
||||
public UnAuthorizedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
70
src/main/java/com/xuxd/kafka/console/filter/AuthFilter.java
Normal file
70
src/main/java/com/xuxd/kafka/console/filter/AuthFilter.java
Normal file
@@ -0,0 +1,70 @@
|
||||
package com.xuxd.kafka.console.filter;
|
||||
|
||||
import com.xuxd.kafka.console.beans.Credentials;
|
||||
import com.xuxd.kafka.console.config.AuthConfig;
|
||||
import com.xuxd.kafka.console.utils.AuthUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import javax.servlet.*;
|
||||
import javax.servlet.annotation.WebFilter;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/5/9 21:20
|
||||
**/
|
||||
@Order(1)
|
||||
@WebFilter(filterName = "auth-filter", urlPatterns = {"/*"})
|
||||
@Slf4j
|
||||
public class AuthFilter implements Filter {
|
||||
|
||||
private final AuthConfig authConfig;
|
||||
|
||||
private final String TOKEN_HEADER = "X-Auth-Token";
|
||||
|
||||
private final String AUTH_URI_PREFIX = "/auth";
|
||||
|
||||
public AuthFilter(AuthConfig authConfig) {
|
||||
this.authConfig = authConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
|
||||
if (!authConfig.isEnable()) {
|
||||
filterChain.doFilter(servletRequest, servletResponse);
|
||||
return;
|
||||
}
|
||||
HttpServletRequest request = (HttpServletRequest) servletRequest;
|
||||
HttpServletResponse response = (HttpServletResponse) servletResponse;
|
||||
String accessToken = request.getHeader(TOKEN_HEADER);
|
||||
|
||||
String requestURI = request.getRequestURI();
|
||||
if (requestURI.startsWith(AUTH_URI_PREFIX)) {
|
||||
filterChain.doFilter(servletRequest, servletResponse);
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isEmpty(accessToken)) {
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
return;
|
||||
}
|
||||
|
||||
Credentials credentials = AuthUtil.parseToken(authConfig.getSecret(), accessToken);
|
||||
if (credentials.isInvalid()) {
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
return;
|
||||
}
|
||||
request.setAttribute("credentials", credentials);
|
||||
|
||||
try {
|
||||
CredentialsContext.set(credentials);
|
||||
filterChain.doFilter(servletRequest, servletResponse);
|
||||
}finally {
|
||||
CredentialsContext.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.xuxd.kafka.console.interceptor;
|
||||
package com.xuxd.kafka.console.filter;
|
||||
|
||||
import com.xuxd.kafka.console.beans.ResponseData;
|
||||
import com.xuxd.kafka.console.beans.dos.ClusterInfoDO;
|
||||
@@ -9,6 +9,7 @@ import com.xuxd.kafka.console.utils.ConvertUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import javax.servlet.*;
|
||||
@@ -24,6 +25,7 @@ import java.util.Set;
|
||||
* @author xuxd
|
||||
* @date 2022-01-05 19:56:25
|
||||
**/
|
||||
@Order(100)
|
||||
@WebFilter(filterName = "context-set-filter", urlPatterns = {"/acl/*", "/user/*", "/cluster/*", "/config/*", "/consumer/*", "/message/*", "/topic/*", "/op/*", "/client/*"})
|
||||
@Slf4j
|
||||
public class ContextSetFilter implements Filter {
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.xuxd.kafka.console.filter;
|
||||
|
||||
import com.xuxd.kafka.console.beans.Credentials;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/5/17 23:02
|
||||
**/
|
||||
public class CredentialsContext {
|
||||
private static final ThreadLocal<Credentials> CREDENTIALS = new ThreadLocal<>();
|
||||
|
||||
public static void set(Credentials credentials) {
|
||||
CREDENTIALS.set(credentials);
|
||||
}
|
||||
|
||||
public static Credentials get() {
|
||||
return CREDENTIALS.get();
|
||||
}
|
||||
|
||||
public static void remove() {
|
||||
CREDENTIALS.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.xuxd.kafka.console.service;
|
||||
|
||||
import com.xuxd.kafka.console.beans.ResponseData;
|
||||
import com.xuxd.kafka.console.beans.dto.LoginUserDTO;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/5/14 19:00
|
||||
**/
|
||||
public interface AuthService {
|
||||
|
||||
ResponseData login(LoginUserDTO userDTO);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.xuxd.kafka.console.service;
|
||||
|
||||
import com.xuxd.kafka.console.beans.ResponseData;
|
||||
import com.xuxd.kafka.console.beans.dto.SysPermissionDTO;
|
||||
import com.xuxd.kafka.console.beans.dto.SysRoleDTO;
|
||||
import com.xuxd.kafka.console.beans.dto.SysUserDTO;
|
||||
|
||||
/**
|
||||
* 登录用户权限管理.
|
||||
*
|
||||
* @author: xuxd
|
||||
* @date: 2023/4/11 21:24
|
||||
**/
|
||||
public interface UserManageService {
|
||||
|
||||
/**
|
||||
* 增加权限
|
||||
*/
|
||||
ResponseData addPermission(SysPermissionDTO permissionDTO);
|
||||
|
||||
ResponseData addOrUdpateRole(SysRoleDTO roleDTO);
|
||||
|
||||
ResponseData addOrUpdateUser(SysUserDTO userDTO);
|
||||
|
||||
ResponseData selectRole();
|
||||
|
||||
ResponseData selectPermission();
|
||||
|
||||
ResponseData selectUser();
|
||||
|
||||
ResponseData updateUser(SysUserDTO userDTO);
|
||||
|
||||
ResponseData updateRole(SysRoleDTO roleDTO);
|
||||
|
||||
ResponseData deleteRole(Long id);
|
||||
|
||||
ResponseData deleteUser(Long id);
|
||||
|
||||
ResponseData updatePassword(SysUserDTO userDTO);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.xuxd.kafka.console.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.xuxd.kafka.console.beans.Credentials;
|
||||
import com.xuxd.kafka.console.beans.LoginResult;
|
||||
import com.xuxd.kafka.console.beans.ResponseData;
|
||||
import com.xuxd.kafka.console.beans.dos.SysRoleDO;
|
||||
import com.xuxd.kafka.console.beans.dos.SysUserDO;
|
||||
import com.xuxd.kafka.console.beans.dto.LoginUserDTO;
|
||||
import com.xuxd.kafka.console.cache.RolePermCache;
|
||||
import com.xuxd.kafka.console.config.AuthConfig;
|
||||
import com.xuxd.kafka.console.dao.SysRoleMapper;
|
||||
import com.xuxd.kafka.console.dao.SysUserMapper;
|
||||
import com.xuxd.kafka.console.service.AuthService;
|
||||
import com.xuxd.kafka.console.utils.AuthUtil;
|
||||
import com.xuxd.kafka.console.utils.UUIDStrUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/5/14 19:01
|
||||
**/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AuthServiceImpl implements AuthService {
|
||||
|
||||
private final SysUserMapper userMapper;
|
||||
|
||||
private final SysRoleMapper roleMapper;
|
||||
|
||||
private final AuthConfig authConfig;
|
||||
|
||||
private final RolePermCache rolePermCache;
|
||||
|
||||
public AuthServiceImpl(SysUserMapper userMapper,
|
||||
SysRoleMapper roleMapper,
|
||||
AuthConfig authConfig,
|
||||
RolePermCache rolePermCache) {
|
||||
this.userMapper = userMapper;
|
||||
this.roleMapper = roleMapper;
|
||||
this.authConfig = authConfig;
|
||||
this.rolePermCache = rolePermCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseData login(LoginUserDTO userDTO) {
|
||||
QueryWrapper<SysUserDO> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("username", userDTO.getUsername());
|
||||
SysUserDO userDO = userMapper.selectOne(queryWrapper);
|
||||
if (userDO == null) {
|
||||
return ResponseData.create().failed("用户名/密码不正确");
|
||||
}
|
||||
String encrypt = UUIDStrUtil.generate(userDTO.getPassword(), userDO.getSalt());
|
||||
if (!userDO.getPassword().equals(encrypt)) {
|
||||
return ResponseData.create().failed("用户名/密码不正确");
|
||||
}
|
||||
Credentials credentials = new Credentials();
|
||||
credentials.setUsername(userDO.getUsername());
|
||||
credentials.setExpiration(System.currentTimeMillis() + authConfig.getExpireHours() * 3600 * 1000);
|
||||
String token = AuthUtil.generateToken(authConfig.getSecret(), credentials);
|
||||
LoginResult loginResult = new LoginResult();
|
||||
List<String> permissions = new ArrayList<>();
|
||||
String roleIds = userDO.getRoleIds();
|
||||
if (StringUtils.isNotEmpty(roleIds)) {
|
||||
List<String> roleIdList = Arrays.stream(roleIds.split(",")).map(String::trim).filter(StringUtils::isNotEmpty).collect(Collectors.toList());
|
||||
roleIdList.forEach(roleId -> {
|
||||
Long rId = Long.valueOf(roleId);
|
||||
SysRoleDO roleDO = roleMapper.selectById(rId);
|
||||
String permissionIds = roleDO.getPermissionIds();
|
||||
if (StringUtils.isNotEmpty(permissionIds)) {
|
||||
List<Long> permIds = Arrays.stream(permissionIds.split(",")).map(String::trim).
|
||||
filter(StringUtils::isNotEmpty).map(Long::valueOf).collect(Collectors.toList());
|
||||
permIds.forEach(id -> {
|
||||
String permission = rolePermCache.getPermCache().get(id).getPermission();
|
||||
if (StringUtils.isNotEmpty(permission)) {
|
||||
permissions.add(permission);
|
||||
} else {
|
||||
log.error("角色:{},权限id: {},不存在", roleId, id);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
loginResult.setToken(token);
|
||||
loginResult.setPermissions(permissions);
|
||||
return ResponseData.create().data(loginResult).success();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package com.xuxd.kafka.console.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.xuxd.kafka.console.beans.ResponseData;
|
||||
import com.xuxd.kafka.console.beans.RolePermUpdateEvent;
|
||||
import com.xuxd.kafka.console.beans.dos.SysPermissionDO;
|
||||
import com.xuxd.kafka.console.beans.dos.SysRoleDO;
|
||||
import com.xuxd.kafka.console.beans.dos.SysUserDO;
|
||||
import com.xuxd.kafka.console.beans.dto.SysPermissionDTO;
|
||||
import com.xuxd.kafka.console.beans.dto.SysRoleDTO;
|
||||
import com.xuxd.kafka.console.beans.dto.SysUserDTO;
|
||||
import com.xuxd.kafka.console.beans.vo.SysPermissionVO;
|
||||
import com.xuxd.kafka.console.beans.vo.SysRoleVO;
|
||||
import com.xuxd.kafka.console.beans.vo.SysUserVO;
|
||||
import com.xuxd.kafka.console.dao.SysPermissionMapper;
|
||||
import com.xuxd.kafka.console.dao.SysRoleMapper;
|
||||
import com.xuxd.kafka.console.dao.SysUserMapper;
|
||||
import com.xuxd.kafka.console.service.UserManageService;
|
||||
import com.xuxd.kafka.console.utils.RandomStringUtil;
|
||||
import com.xuxd.kafka.console.utils.UUIDStrUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/4/11 21:24
|
||||
**/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class UserManageServiceImpl implements UserManageService {
|
||||
|
||||
private final SysUserMapper userMapper;
|
||||
|
||||
private final SysRoleMapper roleMapper;
|
||||
|
||||
private final SysPermissionMapper permissionMapper;
|
||||
|
||||
private final ApplicationEventPublisher publisher;
|
||||
|
||||
public UserManageServiceImpl(ObjectProvider<SysUserMapper> userMapper,
|
||||
ObjectProvider<SysRoleMapper> roleMapper,
|
||||
ObjectProvider<SysPermissionMapper> permissionMapper,
|
||||
ApplicationEventPublisher publisher) {
|
||||
this.userMapper = userMapper.getIfAvailable();
|
||||
this.roleMapper = roleMapper.getIfAvailable();
|
||||
this.permissionMapper = permissionMapper.getIfAvailable();
|
||||
this.publisher = publisher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseData addPermission(SysPermissionDTO permissionDTO) {
|
||||
permissionMapper.insert(permissionDTO.toSysPermissionDO());
|
||||
return ResponseData.create().success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseData addOrUdpateRole(SysRoleDTO roleDTO) {
|
||||
SysRoleDO roleDO = roleDTO.toDO();
|
||||
if (roleDO.getId() == null) {
|
||||
roleMapper.insert(roleDO);
|
||||
} else {
|
||||
roleMapper.updateById(roleDO);
|
||||
}
|
||||
publisher.publishEvent(new RolePermUpdateEvent(this));
|
||||
return ResponseData.create().success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseData addOrUpdateUser(SysUserDTO userDTO) {
|
||||
|
||||
if (userDTO.getId() == null) {
|
||||
if (StringUtils.isEmpty(userDTO.getPassword())) {
|
||||
userDTO.setPassword(RandomStringUtil.random6Str());
|
||||
}
|
||||
SysUserDO userDO = userDTO.toDO();
|
||||
QueryWrapper<SysUserDO> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq(true, "username", userDO.getUsername());
|
||||
SysUserDO exist = userMapper.selectOne(queryWrapper);
|
||||
if (exist != null) {
|
||||
return ResponseData.create().failed("用户已存在:" + userDO.getUsername());
|
||||
}
|
||||
userDO.setSalt(UUIDStrUtil.random());
|
||||
userDO.setPassword(UUIDStrUtil.generate(userDTO.getPassword(), userDO.getSalt()));
|
||||
userMapper.insert(userDO);
|
||||
} else {
|
||||
SysUserDO userDO = userMapper.selectById(userDTO.getId());
|
||||
if (userDO == null) {
|
||||
log.error("查不到用户: {}", userDTO.getId());
|
||||
return ResponseData.create().failed("Unknown User.");
|
||||
}
|
||||
// 判断是否更新密码
|
||||
if (userDTO.getResetPassword()) {
|
||||
userDTO.setPassword(RandomStringUtil.random6Str());
|
||||
userDO.setSalt(UUIDStrUtil.random());
|
||||
userDO.setPassword(UUIDStrUtil.generate(userDTO.getPassword(), userDO.getSalt()));
|
||||
}
|
||||
userDO.setRoleIds(userDTO.getRoleIds());
|
||||
userDO.setUsername(userDTO.getUsername());
|
||||
userMapper.updateById(userDO);
|
||||
}
|
||||
return ResponseData.create().data(userDTO.getPassword()).success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseData selectRole() {
|
||||
List<SysRoleDO> dos = roleMapper.selectList(new QueryWrapper<>());
|
||||
return ResponseData.create().data(dos.stream().map(SysRoleVO::from).collect(Collectors.toList())).success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseData selectPermission() {
|
||||
QueryWrapper<SysPermissionDO> queryWrapper = new QueryWrapper<>();
|
||||
|
||||
List<SysPermissionDO> permissionDOS = permissionMapper.selectList(queryWrapper);
|
||||
List<SysPermissionVO> vos = new ArrayList<>();
|
||||
Map<Long, Integer> posMap = new HashMap<>();
|
||||
Map<Long, SysPermissionVO> voMap = new HashMap<>();
|
||||
|
||||
Iterator<SysPermissionDO> iterator = permissionDOS.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
SysPermissionDO permissionDO = iterator.next();
|
||||
if (permissionDO.getParentId() == null) {
|
||||
// 菜单
|
||||
SysPermissionVO vo = SysPermissionVO.from(permissionDO);
|
||||
vos.add(vo);
|
||||
int index = vos.size() - 1;
|
||||
// 记录位置
|
||||
posMap.put(permissionDO.getId(), index);
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
// 上面把菜单都处理过了
|
||||
while (!permissionDOS.isEmpty()) {
|
||||
iterator = permissionDOS.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
SysPermissionDO permissionDO = iterator.next();
|
||||
Long parentId = permissionDO.getParentId();
|
||||
if (posMap.containsKey(parentId)) {
|
||||
// 菜单下的按扭
|
||||
SysPermissionVO vo = SysPermissionVO.from(permissionDO);
|
||||
Integer index = posMap.get(parentId);
|
||||
SysPermissionVO menuVO = vos.get(index);
|
||||
if (menuVO.getChildren() == null) {
|
||||
menuVO.setChildren(new ArrayList<>());
|
||||
}
|
||||
menuVO.getChildren().add(vo);
|
||||
voMap.put(permissionDO.getId(), vo);
|
||||
iterator.remove();
|
||||
} else if (voMap.containsKey(parentId)) {
|
||||
// 按钮下的按扭
|
||||
SysPermissionVO vo = SysPermissionVO.from(permissionDO);
|
||||
SysPermissionVO buttonVO = voMap.get(parentId);
|
||||
if (buttonVO.getChildren() == null) {
|
||||
buttonVO.setChildren(new ArrayList<>());
|
||||
}
|
||||
buttonVO.getChildren().add(vo);
|
||||
voMap.put(permissionDO.getId(), vo);
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
return ResponseData.create().data(vos).success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseData selectUser() {
|
||||
QueryWrapper<SysUserDO> queryWrapper = new QueryWrapper<>();
|
||||
List<SysUserDO> userDOS = userMapper.selectList(queryWrapper);
|
||||
List<SysRoleDO> roleDOS = roleMapper.selectList(null);
|
||||
Map<Long, SysRoleDO> roleDOMap = roleDOS.stream().collect(Collectors.toMap(SysRoleDO::getId, Function.identity(), (e1, e2) -> e1));
|
||||
List<SysUserVO> voList = userDOS.stream().map(SysUserVO::from).collect(Collectors.toList());
|
||||
voList.forEach(vo -> {
|
||||
if (vo.getRoleIds() != null) {
|
||||
Long roleId = Long.valueOf(vo.getRoleIds());
|
||||
vo.setRoleNames(roleDOMap.containsKey(roleId) ? roleDOMap.get(roleId).getRoleName() : null);
|
||||
}
|
||||
});
|
||||
return ResponseData.create().data(voList).success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseData updateUser(SysUserDTO userDTO) {
|
||||
userMapper.updateById(userDTO.toDO());
|
||||
return ResponseData.create().success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseData updateRole(SysRoleDTO roleDTO) {
|
||||
roleMapper.updateById(roleDTO.toDO());
|
||||
publisher.publishEvent(new RolePermUpdateEvent(this));
|
||||
return ResponseData.create().success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseData deleteRole(Long id) {
|
||||
QueryWrapper<SysUserDO> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq(true, "role_ids", id);
|
||||
Integer count = userMapper.selectCount(queryWrapper);
|
||||
if (count > 0) {
|
||||
return ResponseData.create().failed("存在用户被分配为当前角色,不允许删除");
|
||||
}
|
||||
roleMapper.deleteById(id);
|
||||
publisher.publishEvent(new RolePermUpdateEvent(this));
|
||||
return ResponseData.create().success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseData deleteUser(Long id) {
|
||||
userMapper.deleteById(id);
|
||||
return ResponseData.create().success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseData updatePassword(SysUserDTO userDTO) {
|
||||
SysUserDO userDO = userDTO.toDO();
|
||||
userDO.setSalt(UUIDStrUtil.random());
|
||||
userDO.setPassword(UUIDStrUtil.generate(userDTO.getPassword(), userDO.getSalt()));
|
||||
QueryWrapper<SysUserDO> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("username", userDTO.getUsername());
|
||||
userMapper.update(userDO, wrapper);
|
||||
return ResponseData.create().success();
|
||||
}
|
||||
}
|
||||
54
src/main/java/com/xuxd/kafka/console/utils/AuthUtil.java
Normal file
54
src/main/java/com/xuxd/kafka/console/utils/AuthUtil.java
Normal file
@@ -0,0 +1,54 @@
|
||||
package com.xuxd.kafka.console.utils;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.xuxd.kafka.console.beans.Credentials;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.Base64Utils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/5/14 19:34
|
||||
**/
|
||||
@Slf4j
|
||||
public class AuthUtil {
|
||||
|
||||
private static Gson gson = GsonUtil.INSTANCE.get();
|
||||
|
||||
public static String generateToken(String secret, Credentials info) {
|
||||
String json = gson.toJson(info);
|
||||
String str = json + secret;
|
||||
String signature = MD5Util.md5(str);
|
||||
return Base64Utils.encodeToString(json.getBytes(StandardCharsets.UTF_8)) + "." +
|
||||
Base64Utils.encodeToString(signature.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public static boolean isToken(String token) {
|
||||
return token.split("\\.").length == 2;
|
||||
}
|
||||
|
||||
public static Credentials parseToken(String secret, String token) {
|
||||
if (!isToken(token)) {
|
||||
return Credentials.INVALID;
|
||||
}
|
||||
String[] arr = token.split("\\.");
|
||||
String infoStr = new String(Base64Utils.decodeFromString(arr[0]), StandardCharsets.UTF_8);
|
||||
String signature = new String(Base64Utils.decodeFromString(arr[1]), StandardCharsets.UTF_8);
|
||||
|
||||
String encrypt = MD5Util.md5(infoStr + secret);
|
||||
if (!encrypt.equals(signature)) {
|
||||
return Credentials.INVALID;
|
||||
}
|
||||
try {
|
||||
Credentials credentials = gson.fromJson(infoStr, Credentials.class);
|
||||
if (credentials.getExpiration() < System.currentTimeMillis()) {
|
||||
return Credentials.INVALID;
|
||||
}
|
||||
return credentials;
|
||||
} catch (Exception e) {
|
||||
log.error("解析token失败: {}", token, e);
|
||||
return Credentials.INVALID;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/main/java/com/xuxd/kafka/console/utils/MD5Util.java
Normal file
32
src/main/java/com/xuxd/kafka/console/utils/MD5Util.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package com.xuxd.kafka.console.utils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/5/14 20:25
|
||||
**/
|
||||
@Slf4j
|
||||
public class MD5Util {
|
||||
|
||||
public static MessageDigest getInstance() {
|
||||
try {
|
||||
MessageDigest md5 = MessageDigest.getInstance("MD5");
|
||||
return md5;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String md5(String str) {
|
||||
MessageDigest digest = getInstance();
|
||||
if (digest == null) {
|
||||
return null;
|
||||
}
|
||||
return new String(digest.digest(str.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.xuxd.kafka.console.utils;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/5/8 9:19
|
||||
**/
|
||||
public class RandomStringUtil {
|
||||
|
||||
private final static String ALLOWED_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
|
||||
public static String random6Str() {
|
||||
return generateRandomString(6);
|
||||
}
|
||||
|
||||
public static String generateRandomString(int length) {
|
||||
Random random = new Random();
|
||||
StringBuilder sb = new StringBuilder(length);
|
||||
for (int i = 0; i < length; i++) {
|
||||
int index = random.nextInt(ALLOWED_CHARS.length());
|
||||
sb.append(ALLOWED_CHARS.charAt(index));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
22
src/main/java/com/xuxd/kafka/console/utils/UUIDStrUtil.java
Normal file
22
src/main/java/com/xuxd/kafka/console/utils/UUIDStrUtil.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.xuxd.kafka.console.utils;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author: xuxd
|
||||
* @date: 2023/5/6 13:30
|
||||
**/
|
||||
public class UUIDStrUtil {
|
||||
|
||||
public static String random() {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
public static String generate(String ... strs) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String str : strs) {
|
||||
sb.append(str);
|
||||
}
|
||||
return UUID.nameUUIDFromBytes(sb.toString().getBytes()).toString();
|
||||
}
|
||||
}
|
||||
@@ -47,3 +47,9 @@ logging:
|
||||
cron:
|
||||
# clear-dirty-user: 0 * * * * ?
|
||||
clear-dirty-user: 0 0 1 * * ?
|
||||
|
||||
# 权限认证设置,设置为true,需要先登录才能访问
|
||||
auth:
|
||||
enable: false
|
||||
# 登录用户token的过期时间,单位:小时
|
||||
expire-hours: 24
|
||||
@@ -1,5 +1,109 @@
|
||||
-- DELETE FROM t_kafka_user;
|
||||
--
|
||||
-- INSERT INTO t_kafka_user (id, username, password) VALUES
|
||||
-- (1, 'Jone', 'p1'),
|
||||
-- (2, 'Jack', 'p2');
|
||||
-- 不要随便修改下面的注释,要根据这个注释初始化加载数据
|
||||
-- t_sys_permission start--
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(0,'主页',0,null,'home');
|
||||
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(11,'集群',0,null,'cluster');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(12,'属性配置',1,11,'cluster:property-config');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(13,'日志配置',1,11,'cluster:log-config');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(14,'编辑配置',1,11,'cluster:edit');
|
||||
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(21,'Topic',0,null,'topic');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(22,'刷新',1,21,'topic:load');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(23,'新增',1,21,'topic:add');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(24,'批量删除',1,21,'topic:batch-del');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(25,'删除',1,21,'topic:del');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(26,'分区详情',1,21,'topic:partition-detail');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(27,'首选副本作leader',1,26,'topic:partition-detail:preferred');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(28,'增加分区',1,21,'topic:partition-add');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(29,'消费详情',1,21,'topic:consumer-detail');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(30,'属性配置',1,21,'topic:property-config');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(31,'变更副本',1,21,'topic:replication-modify');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(32,'发送统计',1,21,'topic:send-count');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(33,'限流',1,21,'topic:replication-sync-throttle');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(34,'编辑属性配置',1,30,'topic:property-config:edit');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(35,'删除属性配置',1,30,'topic:property-config:del');
|
||||
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(41,'消费组',0,null,'group');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(42,'新增订阅',1,41,'group:add');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(43,'删除',1,41,'group:del');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(44,'消费端',1,41,'group:client');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(45,'消费详情',1,41,'group:consumer-detail');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(46,'最小位点',1,45,'group:consumer-detail:min');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(47,'最新位点',1,45,'group:consumer-detail:last');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(48,'时间戳',1,45,'group:consumer-detail:timestamp');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(49,'重置位点',1,45,'group:consumer-detail:any');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(50,'位移分区',1,41,'group:offset-partition');
|
||||
|
||||
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(61,'消息',0,null,'message');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(62,'根据时间查询',1,61,'message:search-time');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(63,'根据偏移查询',1,61,'message:search-offset');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(64,'在线发送',1,61,'message:send');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(65,'在线删除',1,61,'message:del');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(66,'消息详情',1,61,'message:detail');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(67,'重新发送',1,61,'message:resend');
|
||||
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(80,'限流',0,null,'quota');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(81,'用户',1,80,'quota:user');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(82,'新增配置',1,81,'quota:user:add');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(83,'客户端ID',1,80,'quota:client');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(84,'新增配置',1,83,'quota:client:add');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(85,'用户和客户端ID',1,80,'quota:user-client');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(86,'新增配置',1,85,'quota:user-client:add');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(87,'删除',1,80,'quota:del');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(88,'修改',1,80,'quota:edit');
|
||||
|
||||
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(100,'Acl',0,null,'acl');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(101,'资源授权',1,100,'acl:authority');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(102,'新增主体权限',1,101,'acl:authority:add-principal');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(103,'权限详情',1,101,'acl:authority:detail');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(104,'管理生产权限',1,101,'acl:authority:producer');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(105,'管理消费权限',1,101,'acl:authority:consumer');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(106,'增加权限',1,101,'acl:authority:add');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(107,'清除权限',1,101,'acl:authority:clean');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(108,'SaslScram用户管理',1,100,'acl:sasl-scram');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(109,'新增/更新用户',1,108,'acl:sasl-scram:add-update');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(110,'详情',1,108,'acl:sasl-scram:detail');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(111,'删除',1,108,'acl:sasl-scram:del');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(112,'管理生产权限',1,108,'acl:sasl-scram:producer');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(113,'管理消费权限',1,108,'acl:sasl-scram:consumer');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(114,'增加权限',1,108,'acl:sasl-scram:add-auth');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(115,'彻底删除',1,108,'acl:sasl-scram:pure');
|
||||
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(140,'用户',0,null,'user-manage');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(141,'用户列表',1,140,'user-manage:user');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(142,'新增',1,141,'user-manage:user:add');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(143,'删除',1,141,'user-manage:user:del');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(144,'重置密码',1,141,'user-manage:user:reset-pass');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(145,'分配角色',1,141,'user-manage:user:change-role');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(146,'角色列表',1,140,'user-manage:role');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(147,'保存',1,146,'user-manage:role:save');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(148,'删除',1,146,'user-manage:role:del');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(149,'权限列表',1,140,'user-manage:permission');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(150,'个人设置',1,140,'user-manage:setting');
|
||||
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(160,'运维',0,null,'op');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(161,'集群切换',1,160,'op:cluster-switch');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(162,'新增集群',1,161,'op:cluster-switch:add');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(163,'切换',1,161,'op:cluster-switch:switch');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(164,'编辑',1,161,'op:cluster-switch:edit');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(165,'删除',1,161,'op:cluster-switch:del');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(166,'配置限流',1,160,'op:config-throttle');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(167,'解除限流',1,160,'op:remove-throttle');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(168,'首选副本作leader',1,160,'op:replication-preferred');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(169,'副本变更详情',1,160,'op:replication-update-detail');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(170,'副本重分配',1,160,'op:replication-reassign');
|
||||
insert into t_sys_permission(id, name,type,parent_id,permission) values(171,'取消副本重分配',1,169,'op:replication-update-detail:cancel');
|
||||
-- t_sys_permission end--
|
||||
|
||||
-- t_sys_role start--
|
||||
insert into t_sys_role(id, role_name, description, permission_ids) VALUES (1,'超级管理员','超级管理员','12,13,14,22,23,24,25,26,27,28,29,30,34,35,31,32,33,42,43,44,45,46,47,48,49,50,62,63,64,65,66,67,81,82,83,84,85,86,87,88,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,141,142,143,144,145,146,147,148,149,150,161,162,163,164,165,166,167,168,169,171,170');
|
||||
insert into t_sys_role(id, role_name, description, permission_ids) VALUES (2,'普通管理员','普通管理员,不能更改用户信息','12,13,14,22,23,24,25,26,27,28,29,30,34,35,31,32,33,42,43,44,45,46,47,48,49,50,62,63,64,65,66,67,81,82,83,84,85,86,87,88,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,141,146,149,150,161,162,163,164,165,166,167,168,169,171,170');
|
||||
-- insert into t_sys_role(id, role_name, description, permission_ids) VALUES (2,'访客','访客','12,13,22,26,29,32,44,45,50,62,63,81,83,85,141,146,149,150,161,163');
|
||||
-- t_sys_role end--
|
||||
|
||||
-- t_sys_user start--
|
||||
insert into t_sys_user(id, username, password, salt, role_ids) VALUES (1,'super-admin','3a3e4d32-5247-321b-9efb-9cbf60b2bf6c','e6973cfc-7583-4baa-8802-65ded1268ab6','1' );
|
||||
insert into t_sys_user(id, username, password, salt, role_ids) VALUES (2,'admin','3a3e4d32-5247-321b-9efb-9cbf60b2bf6c','e6973cfc-7583-4baa-8802-65ded1268ab6','2' );
|
||||
-- t_sys_user end--
|
||||
@@ -29,9 +29,40 @@ CREATE TABLE IF NOT EXISTS T_CLUSTER_INFO
|
||||
(
|
||||
ID IDENTITY NOT NULL COMMENT '主键ID',
|
||||
CLUSTER_NAME VARCHAR(128) NOT NULL DEFAULT '' COMMENT '集群名',
|
||||
ADDRESS VARCHAR(256) NOT NULL DEFAULT '' COMMENT '集群地址',
|
||||
PROPERTIES VARCHAR(512) NOT NULL DEFAULT '' COMMENT '集群的其它属性配置',
|
||||
ADDRESS VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '集群地址',
|
||||
PROPERTIES VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '集群的其它属性配置',
|
||||
UPDATE_TIME TIMESTAMP NOT NULL DEFAULT NOW() COMMENT '更新时间',
|
||||
PRIMARY KEY (ID),
|
||||
UNIQUE (CLUSTER_NAME)
|
||||
);
|
||||
|
||||
-- 登录用户的角色权限配置
|
||||
CREATE TABLE IF NOT EXISTS t_sys_permission
|
||||
(
|
||||
ID IDENTITY NOT NULL COMMENT '主键ID',
|
||||
name varchar(100) DEFAULT NULL COMMENT '权限名称',
|
||||
type tinyint(1) NOT NULL DEFAULT 0 COMMENT '权限类型: 0:菜单,1:按钮',
|
||||
parent_id bigint(20) DEFAULT NULL COMMENT '所属父权限ID',
|
||||
permission varchar(100) DEFAULT NULL COMMENT '权限字符串',
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS t_sys_role
|
||||
(
|
||||
ID IDENTITY NOT NULL COMMENT '主键ID',
|
||||
role_name varchar(100) NOT NULL COMMENT '角色名称',
|
||||
description varchar(100) DEFAULT NULL COMMENT '角色描述',
|
||||
permission_ids varchar(500) DEFAULT NULL COMMENT '分配的权限ID',
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS t_sys_user
|
||||
(
|
||||
ID IDENTITY NOT NULL COMMENT '主键ID',
|
||||
username varchar(100) DEFAULT NULL COMMENT '用户名',
|
||||
password varchar(100) DEFAULT NULL COMMENT '用户密码',
|
||||
salt varchar(100) DEFAULT NULL COMMENT '加密的盐值',
|
||||
role_ids varchar(100) DEFAULT NULL COMMENT '分配角色的ID',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (username)
|
||||
);
|
||||
@@ -3,9 +3,9 @@
|
||||
<springProperty scope="context" name="LOGGING_HOME" source="logging.home"/>
|
||||
|
||||
<!-- 日志目录 -->
|
||||
<property name="LOG_HOME" value="${LOGGING_HOME}/logs"/>
|
||||
<property name="LOG_HOME" value="${LOGGING_HOME:-${user.dir}}/logs"/>
|
||||
<!-- 日志文件名-->
|
||||
<property name="APP_NAME" value="${APPLICATION_NAME:-.}"/>
|
||||
<property name="APP_NAME" value="${APPLICATION_NAME:-kafka-console-ui}"/>
|
||||
|
||||
<!-- 使用默认的输出格式-->
|
||||
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
|
||||
|
||||
104
ui/src/App.vue
104
ui/src/App.vue
@@ -15,20 +15,46 @@
|
||||
><router-link to="/client-quota-page" class="pad-l-r">限流</router-link>
|
||||
<span>|</span
|
||||
><router-link to="/acl-page" class="pad-l-r">Acl</router-link>
|
||||
<span v-show="showUserMenu">|</span
|
||||
><router-link to="/user-page" class="pad-l-r" v-show="showUserMenu"
|
||||
>用户</router-link
|
||||
>
|
||||
<span>|</span
|
||||
><router-link to="/op-page" class="pad-l-r">运维</router-link>
|
||||
<span class="right">集群:{{ clusterName }}</span>
|
||||
<div class="right">
|
||||
<span>集群:{{ clusterName }} </span>
|
||||
<a-dropdown v-show="showUsername">
|
||||
<span>
|
||||
<span> | </span><a-icon type="smile"></a-icon>
|
||||
<span>{{ username }}</span>
|
||||
</span>
|
||||
<a-menu slot="overlay">
|
||||
<a-menu-item key="1">
|
||||
<a href="javascript:;" @click="logout">
|
||||
<!-- <a-icon type="logout"/>-->
|
||||
<span>退出</span>
|
||||
</a>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<router-view class="content" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { KafkaClusterApi } from "@/utils/api";
|
||||
import { KafkaClusterApi, AuthApi } from "@/utils/api";
|
||||
import request from "@/utils/request";
|
||||
import { mapMutations, mapState } from "vuex";
|
||||
import { getClusterInfo } from "@/utils/local-cache";
|
||||
import {
|
||||
deleteToken,
|
||||
deleteUsername,
|
||||
getClusterInfo,
|
||||
getPermissions,
|
||||
getUsername,
|
||||
} from "@/utils/local-cache";
|
||||
import notification from "ant-design-vue/lib/notification";
|
||||
import { CLUSTER } from "@/store/mutation-types";
|
||||
import { AUTH, CLUSTER } from "@/store/mutation-types";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -37,35 +63,69 @@ export default {
|
||||
};
|
||||
},
|
||||
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);
|
||||
}
|
||||
this.intAuthState();
|
||||
this.initClusterInfo();
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
clusterName: (state) => state.clusterInfo.clusterName,
|
||||
enableSasl: (state) => state.clusterInfo.enableSasl,
|
||||
showUsername: (state) => state.auth.enable && state.auth.username,
|
||||
username: (state) => state.auth.username,
|
||||
showUserMenu: (state) => state.auth.enable,
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
switchCluster: CLUSTER.SWITCH,
|
||||
enableAuth: AUTH.ENABLE,
|
||||
setUsername: AUTH.SET_USERNAME,
|
||||
setPermissions: AUTH.SET_PERMISSIONS,
|
||||
}),
|
||||
beforeLoadFn() {
|
||||
this.setUsername(getUsername());
|
||||
this.setPermissions(getPermissions());
|
||||
},
|
||||
intAuthState() {
|
||||
request({
|
||||
url: AuthApi.enable.url,
|
||||
method: AuthApi.enable.method,
|
||||
}).then((res) => {
|
||||
const enable = res;
|
||||
this.enableAuth(enable);
|
||||
// if (!enable){
|
||||
// this.initClusterInfo();
|
||||
// }
|
||||
});
|
||||
},
|
||||
initClusterInfo() {
|
||||
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);
|
||||
}
|
||||
},
|
||||
logout() {
|
||||
deleteToken();
|
||||
deleteUsername();
|
||||
this.$router.push("/login-page");
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.beforeLoadFn();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
<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>
|
||||
40
ui/src/components/MessageBox.vue
Normal file
40
ui/src/components/MessageBox.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<a-modal
|
||||
title="消息提示"
|
||||
:visible="visible"
|
||||
:width="400"
|
||||
:mask="false"
|
||||
:destroyOnClose="true"
|
||||
:footer="null"
|
||||
:maskClosable="false"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<div>
|
||||
{{ message }}
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "MessageBox",
|
||||
props: {
|
||||
message: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleCancel() {
|
||||
this.visible = false;
|
||||
this.$emit("closeMessageBox", {});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
16
ui/src/directives/action.js
Normal file
16
ui/src/directives/action.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import Vue from "vue";
|
||||
import Store from "@/store";
|
||||
|
||||
const action = Vue.directive("action", {
|
||||
inserted: function (el, binding) {
|
||||
const actionName = binding.arg;
|
||||
const enableAuth = Store.state.auth.enable;
|
||||
const permissions = Store.state.auth.permissions;
|
||||
if (enableAuth && (!permissions || permissions.indexOf(actionName) < 0)) {
|
||||
(el.parentNode && el.parentNode.removeChild(el)) ||
|
||||
(el.style.display = "none");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default action;
|
||||
@@ -6,6 +6,7 @@ import store from "./store";
|
||||
import Antd from "ant-design-vue";
|
||||
import "ant-design-vue/dist/antd.css";
|
||||
import { VueAxios } from "./utils/request";
|
||||
import "@/directives/action";
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
Vue.use(Antd);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Vue from "vue";
|
||||
import VueRouter from "vue-router";
|
||||
import Home from "../views/Home.vue";
|
||||
import Store from "@/store";
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
@@ -53,7 +54,21 @@ const routes = [
|
||||
path: "/client-quota-page",
|
||||
name: "ClientQuota",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "cluster" */ "../views/quota/ClientQuota.vue"),
|
||||
import(
|
||||
/* webpackChunkName: "cluster" */ "../views/quota/ClientQuota.vue"
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/user-page",
|
||||
name: "UserManage",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "cluster" */ "../views/user/UserManage.vue"),
|
||||
},
|
||||
{
|
||||
path: "/login-page",
|
||||
name: "Login",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "cluster" */ "../views/login/Login.vue"),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -64,4 +79,49 @@ const router = new VueRouter({
|
||||
routes,
|
||||
});
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const enableAuth = Store.state.auth.enable;
|
||||
if (!enableAuth) {
|
||||
next();
|
||||
} else {
|
||||
if (to.path === "/login-page") {
|
||||
next();
|
||||
} else {
|
||||
let token = localStorage.getItem("access_token");
|
||||
if (token === null || token === "") {
|
||||
next("/login-page");
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let originPush = VueRouter.prototype.push;
|
||||
let originReplace = VueRouter.prototype.replace;
|
||||
VueRouter.prototype.push = function (location, resolve, reject) {
|
||||
if (resolve && reject) {
|
||||
originPush.call(this, location, resolve, reject);
|
||||
} else {
|
||||
originPush.call(
|
||||
this,
|
||||
location,
|
||||
() => {},
|
||||
() => {}
|
||||
);
|
||||
}
|
||||
};
|
||||
VueRouter.prototype.replace = function (location, resolve, reject) {
|
||||
if (resolve && reject) {
|
||||
originReplace.call(this, location, resolve, reject);
|
||||
} else {
|
||||
originReplace.call(
|
||||
this,
|
||||
location,
|
||||
() => {},
|
||||
() => {}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import Vue from "vue";
|
||||
import Vuex from "vuex";
|
||||
import { CLUSTER } from "@/store/mutation-types";
|
||||
import { setClusterInfo } from "@/utils/local-cache";
|
||||
import { CLUSTER, AUTH } from "@/store/mutation-types";
|
||||
import {
|
||||
setClusterInfo,
|
||||
setPermissions,
|
||||
setToken,
|
||||
setUsername,
|
||||
} from "@/utils/local-cache";
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
@@ -12,6 +17,11 @@ export default new Vuex.Store({
|
||||
clusterName: undefined,
|
||||
enableSasl: false,
|
||||
},
|
||||
auth: {
|
||||
enable: false,
|
||||
username: "",
|
||||
permissions: [],
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
[CLUSTER.SWITCH](state, clusterInfo) {
|
||||
@@ -28,6 +38,20 @@ export default new Vuex.Store({
|
||||
state.clusterInfo.enableSasl = enableSasl;
|
||||
setClusterInfo(clusterInfo);
|
||||
},
|
||||
[AUTH.ENABLE](state, enable) {
|
||||
state.auth.enable = enable;
|
||||
},
|
||||
[AUTH.SET_TOKEN](state, info) {
|
||||
setToken(info);
|
||||
},
|
||||
[AUTH.SET_USERNAME](state, username) {
|
||||
setUsername(username);
|
||||
state.auth.username = username;
|
||||
},
|
||||
[AUTH.SET_PERMISSIONS](state, permissions) {
|
||||
setPermissions(permissions);
|
||||
state.auth.permissions = permissions;
|
||||
},
|
||||
},
|
||||
actions: {},
|
||||
modules: {},
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
export const CLUSTER = {
|
||||
SWITCH: "switchCluster",
|
||||
};
|
||||
|
||||
export const AUTH = {
|
||||
ENABLE: "enable",
|
||||
SET_TOKEN: "setToken",
|
||||
SET_USERNAME: "setUsername",
|
||||
SET_PERMISSIONS: "setPermissions",
|
||||
};
|
||||
|
||||
@@ -307,4 +307,54 @@ export const KafkaClientQuotaApi = {
|
||||
url: "/client/quota",
|
||||
method: "delete",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const UserManageApi = {
|
||||
getPermissions: {
|
||||
url: "/sys/user/manage/permission",
|
||||
method: "get",
|
||||
},
|
||||
addPermission: {
|
||||
url: "/sys/user/manage/permission",
|
||||
method: "post",
|
||||
},
|
||||
getRole: {
|
||||
url: "/sys/user/manage/role",
|
||||
method: "get",
|
||||
},
|
||||
addOrUpdateRole: {
|
||||
url: "/sys/user/manage/role",
|
||||
method: "post",
|
||||
},
|
||||
deleteRole: {
|
||||
url: "/sys/user/manage/role",
|
||||
method: "delete",
|
||||
},
|
||||
getUsers: {
|
||||
url: "/sys/user/manage/user",
|
||||
method: "get",
|
||||
},
|
||||
addOrUpdateUser: {
|
||||
url: "/sys/user/manage/user",
|
||||
method: "post",
|
||||
},
|
||||
deleteUser: {
|
||||
url: "/sys/user/manage/user",
|
||||
method: "delete",
|
||||
},
|
||||
updatePassword: {
|
||||
url: "/sys/user/manage/user/password",
|
||||
method: "post",
|
||||
},
|
||||
};
|
||||
|
||||
export const AuthApi = {
|
||||
enable: {
|
||||
url: "/auth/enable",
|
||||
method: "get",
|
||||
},
|
||||
login: {
|
||||
url: "/auth/login",
|
||||
method: "post",
|
||||
},
|
||||
};
|
||||
|
||||
16
ui/src/utils/auth.js
Normal file
16
ui/src/utils/auth.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import Store from "@/store";
|
||||
|
||||
export function isUnauthorized(permission) {
|
||||
const enableAuth = Store.state.auth.enable;
|
||||
const permissions = Store.state.auth.permissions;
|
||||
return enableAuth && (!permissions || permissions.indexOf(permission) < 0);
|
||||
}
|
||||
|
||||
export function isAuthorized(permission) {
|
||||
const enableAuth = Store.state.auth.enable;
|
||||
if (!enableAuth) {
|
||||
return true;
|
||||
}
|
||||
const permissions = Store.state.auth.permissions;
|
||||
return permissions && permissions.indexOf(permission) >= 0;
|
||||
}
|
||||
@@ -4,4 +4,9 @@ export const ConstantEvent = {
|
||||
|
||||
export const Cache = {
|
||||
clusterInfo: "clusterInfo",
|
||||
auth: "auth",
|
||||
token: "access_token",
|
||||
username: "login_user",
|
||||
enableAuth: "enable_auth",
|
||||
permissions: "permissions",
|
||||
};
|
||||
|
||||
@@ -8,3 +8,43 @@ export function getClusterInfo() {
|
||||
const str = localStorage.getItem(Cache.clusterInfo);
|
||||
return str ? JSON.parse(str) : undefined;
|
||||
}
|
||||
|
||||
// export function setAuth(auth) {
|
||||
// localStorage.setItem(Cache.auth, JSON.stringify(auth));
|
||||
// }
|
||||
|
||||
export function setToken(token) {
|
||||
localStorage.setItem(Cache.token, token);
|
||||
}
|
||||
|
||||
export function getToken() {
|
||||
return localStorage.getItem(Cache.token);
|
||||
}
|
||||
|
||||
export function deleteToken() {
|
||||
localStorage.removeItem(Cache.token);
|
||||
}
|
||||
|
||||
export function deleteUsername() {
|
||||
localStorage.removeItem(Cache.username);
|
||||
}
|
||||
|
||||
export function setUsername(username) {
|
||||
localStorage.setItem(Cache.username, username);
|
||||
}
|
||||
|
||||
export function getUsername() {
|
||||
return localStorage.getItem(Cache.username);
|
||||
}
|
||||
|
||||
export function setPermissions(permissions) {
|
||||
localStorage.setItem(Cache.permissions, permissions);
|
||||
}
|
||||
|
||||
export function getPermissions() {
|
||||
return localStorage.getItem(Cache.permissions);
|
||||
}
|
||||
|
||||
// export function setEnableAuth(enable) {
|
||||
// localStorage.setItem()
|
||||
// }
|
||||
|
||||
@@ -2,7 +2,7 @@ import axios from "axios";
|
||||
import notification from "ant-design-vue/es/notification";
|
||||
import { VueAxios } from "./axios";
|
||||
import { getClusterInfo } from "@/utils/local-cache";
|
||||
|
||||
import Router from "@/router";
|
||||
// 创建 axios 实例
|
||||
const request = axios.create({
|
||||
// API 请求的默认前缀
|
||||
@@ -10,14 +10,30 @@ const request = axios.create({
|
||||
timeout: 120000, // 请求超时时间
|
||||
});
|
||||
|
||||
// axios.defaults.headers.common['X-Auth-Token'] = localStorage.getItem('access_token');
|
||||
|
||||
// 异常拦截处理器
|
||||
const errorHandler = (error) => {
|
||||
if (error.response) {
|
||||
const data = error.response.data;
|
||||
notification.error({
|
||||
message: error.response.status,
|
||||
description: JSON.stringify(data),
|
||||
});
|
||||
if (error.response.status == 401) {
|
||||
notification.error({
|
||||
message: error.response.status,
|
||||
description: "请登录",
|
||||
});
|
||||
Router.push({ path: "/login-page" });
|
||||
} else if (error.response.status == 403) {
|
||||
// const data = error.response.data;
|
||||
// notification.error({
|
||||
// message: error.response.status,
|
||||
// description: data.msg,
|
||||
// });
|
||||
} else {
|
||||
const data = error.response.data;
|
||||
notification.error({
|
||||
message: error.response.status,
|
||||
description: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
};
|
||||
@@ -29,6 +45,10 @@ request.interceptors.request.use((config) => {
|
||||
config.headers["X-Cluster-Info-Id"] = clusterInfo.id;
|
||||
// config.headers["X-Cluster-Info-Name"] = encodeURIComponent(clusterInfo.clusterName);
|
||||
}
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (token) {
|
||||
config.headers["X-Auth-Token"] = token;
|
||||
}
|
||||
return config;
|
||||
}, errorHandler);
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<a-tabs default-active-key="1" size="large" tabPosition="top">
|
||||
<a-tab-pane key="1" tab="资源授权">
|
||||
<a-tab-pane key="1" tab="资源授权" v-if="isAuthorized('acl:authority')">
|
||||
<acl-list></acl-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab="SaslScram用户管理">
|
||||
<a-tab-pane
|
||||
key="2"
|
||||
tab="SaslScram用户管理"
|
||||
v-if="isAuthorized('acl:sasl-scram')"
|
||||
>
|
||||
<sasl-scram></sasl-scram>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
@@ -14,9 +18,11 @@
|
||||
<script>
|
||||
import AclList from "@/views/acl/AclList";
|
||||
import SaslScram from "@/views/acl/SaslScram";
|
||||
import { isAuthorized } from "@/utils/auth";
|
||||
|
||||
export default {
|
||||
name: "Acl",
|
||||
methods: { isAuthorized },
|
||||
components: {
|
||||
AclList,
|
||||
SaslScram,
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="onDelete(record)"
|
||||
v-action:acl:authority:clean
|
||||
>
|
||||
<a-button>删除</a-button>
|
||||
</a-popconfirm>
|
||||
|
||||
@@ -47,7 +47,10 @@
|
||||
</a-form>
|
||||
</div>
|
||||
<div class="operation-row-button">
|
||||
<a-button type="primary" @click="onAddPrincipalAuth"
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="onAddPrincipalAuth"
|
||||
v-action:acl:authority:add-principal
|
||||
>新增主体权限</a-button
|
||||
>
|
||||
<span v-show="hint != ''" class="hint"
|
||||
|
||||
@@ -27,7 +27,12 @@
|
||||
</a-form>
|
||||
</div>
|
||||
<div class="operation-row-button">
|
||||
<a-button type="primary" @click="updateUser">新增/更新用户</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="updateUser"
|
||||
v-action:acl:sasl-scram:add-update
|
||||
>新增/更新用户</a-button
|
||||
>
|
||||
<span class="hint" v-show="!enableSasl"
|
||||
>未启用SASL SCRAM认证,不支持相关操作</span
|
||||
>
|
||||
@@ -45,6 +50,7 @@
|
||||
type="dashed"
|
||||
style="float: right"
|
||||
@click="onUserDetail(username)"
|
||||
v-action:acl:sasl-scram:detail
|
||||
>详情</a-button
|
||||
>
|
||||
</div>
|
||||
@@ -58,6 +64,7 @@
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="onDeleteUser(record)"
|
||||
v-action:acl:sasl-scram:del
|
||||
>
|
||||
<a-button size="small" href="javascript:;" class="operation-btn"
|
||||
>删除</a-button
|
||||
@@ -68,6 +75,7 @@
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
@click="onManageProducerAuth(record)"
|
||||
v-action:acl:sasl-scram:producer
|
||||
>管理生产权限
|
||||
</a-button>
|
||||
|
||||
@@ -76,6 +84,7 @@
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
@click="onManageConsumerAuth(record)"
|
||||
v-action:acl:sasl-scram:consumer
|
||||
>管理消费权限
|
||||
</a-button>
|
||||
<a-button
|
||||
@@ -83,6 +92,7 @@
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
@click="onAddAuth(record)"
|
||||
v-action:acl:sasl-scram:add-auth
|
||||
>增加权限
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
@@ -91,7 +101,11 @@
|
||||
cancel-text="取消"
|
||||
@confirm="onDeleteUserAndAuth(record)"
|
||||
>
|
||||
<a-button size="small" href="javascript:;" class="operation-btn"
|
||||
<a-button
|
||||
size="small"
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
v-action:acl:sasl-scram:pure
|
||||
>彻底删除</a-button
|
||||
>
|
||||
</a-popconfirm>
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
class="operation-btn"
|
||||
v-show="!record.readOnly"
|
||||
@click="openEditConfigDialog(record)"
|
||||
v-action:cluster:edit
|
||||
>编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
@@ -44,7 +45,11 @@
|
||||
v-show="isDynamic(record.source)"
|
||||
@confirm="deleteBrokerConfig(record)"
|
||||
>
|
||||
<a-button size="small" href="javascript:;" class="operation-btn"
|
||||
<a-button
|
||||
size="small"
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
v-action:cluster:edit
|
||||
>删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
@click="openBrokerConfigDialog(record, false)"
|
||||
v-action:cluster:property-config
|
||||
>属性配置
|
||||
</a-button>
|
||||
<a-button
|
||||
@@ -26,6 +27,7 @@
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
@click="openBrokerConfigDialog(record, true)"
|
||||
v-action:cluster:log-config
|
||||
>日志配置
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,11 @@
|
||||
cancel-text="取消"
|
||||
@confirm="resetTopicOffsetToEndpoint(group, k, 1)"
|
||||
>
|
||||
<a-button size="small" type="danger" style="margin-right: 1%"
|
||||
<a-button
|
||||
size="small"
|
||||
type="danger"
|
||||
style="margin-right: 1%"
|
||||
v-action:group:consumer-detail:min
|
||||
>最小位点
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
@@ -35,7 +39,11 @@
|
||||
cancel-text="取消"
|
||||
@confirm="resetTopicOffsetToEndpoint(group, k, 2)"
|
||||
>
|
||||
<a-button size="small" type="danger" style="margin-right: 1%"
|
||||
<a-button
|
||||
size="small"
|
||||
type="danger"
|
||||
style="margin-right: 1%"
|
||||
v-action:group:consumer-detail:last
|
||||
>最新位点
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
@@ -45,6 +53,7 @@
|
||||
type="danger"
|
||||
style="margin-right: 1%"
|
||||
@click="openResetOffsetByTimeDialog(k)"
|
||||
v-action:group:consumer-detail:timestamp
|
||||
>时间戳
|
||||
</a-button>
|
||||
<a-button
|
||||
@@ -75,6 +84,7 @@
|
||||
@click="
|
||||
openResetPartitionOffsetDialog(record.topic, record.partition)
|
||||
"
|
||||
v-action:group:consumer-detail:any
|
||||
>重置位点
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,10 @@
|
||||
</a-form>
|
||||
</div>
|
||||
<div class="operation-row-button">
|
||||
<a-button type="primary" @click="openAddSubscriptionDialog"
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="openAddSubscriptionDialog"
|
||||
v-action:group:add
|
||||
>新增订阅</a-button
|
||||
>
|
||||
</div>
|
||||
@@ -89,7 +92,11 @@
|
||||
cancel-text="取消"
|
||||
@confirm="deleteGroup(record.groupId)"
|
||||
>
|
||||
<a-button size="small" href="javascript:;" class="operation-btn"
|
||||
<a-button
|
||||
size="small"
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
v-action:group:del
|
||||
>删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
@@ -98,6 +105,7 @@
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
@click="openConsumerMemberDialog(record.groupId)"
|
||||
v-action:group:client
|
||||
>消费端
|
||||
</a-button>
|
||||
<a-button
|
||||
@@ -105,6 +113,7 @@
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
@click="openConsumerDetailDialog(record.groupId)"
|
||||
v-action:group:consumer-detail
|
||||
>消费详情
|
||||
</a-button>
|
||||
<a-button
|
||||
@@ -112,6 +121,7 @@
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
@click="openOffsetPartitionDialog(record.groupId)"
|
||||
v-action:group:offset-partition
|
||||
>位移分区
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
107
ui/src/views/login/Login.vue
Normal file
107
ui/src/views/login/Login.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<a-form
|
||||
:form="form"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 12 }"
|
||||
@submit="handleSubmit"
|
||||
class="login-box"
|
||||
>
|
||||
<h3 class="login-title">登录kafka-console-ui</h3>
|
||||
<a-form-item label="账号">
|
||||
<a-input
|
||||
style="width: 200px"
|
||||
allowClear
|
||||
v-decorator="[
|
||||
'username',
|
||||
{ rules: [{ required: true, message: '请输入账号' }] },
|
||||
]"
|
||||
>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="密码">
|
||||
<a-input-password
|
||||
style="width: 200px"
|
||||
v-decorator="[
|
||||
'password',
|
||||
{ rules: [{ required: true, message: '请输入密码' }] },
|
||||
]"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{ span: 16, offset: 5 }">
|
||||
<a-button type="primary" @click="handleSubmit" :loading="loading"
|
||||
>登录</a-button
|
||||
>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from "@/utils/request";
|
||||
import { AuthApi } from "@/utils/api";
|
||||
import notification from "ant-design-vue/lib/notification";
|
||||
import { mapMutations } from "vuex";
|
||||
import { AUTH } from "@/store/mutation-types";
|
||||
|
||||
export default {
|
||||
name: "Login",
|
||||
data() {
|
||||
return {
|
||||
form: this.$form.createForm(this, { name: "login-form" }),
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
this.loading = true;
|
||||
const params = Object.assign({}, values);
|
||||
request({
|
||||
url: AuthApi.login.url,
|
||||
method: AuthApi.login.method,
|
||||
data: params,
|
||||
}).then((res) => {
|
||||
this.loading = false;
|
||||
if (res.code == 0) {
|
||||
this.setToken(res.data.token);
|
||||
this.setUsername(params.username);
|
||||
this.setPermissions(res.data.permissions);
|
||||
this.$router.push("/");
|
||||
} else {
|
||||
notification.error({
|
||||
message: "error",
|
||||
description: res.msg,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
...mapMutations({
|
||||
setToken: AUTH.SET_TOKEN,
|
||||
setUsername: AUTH.SET_USERNAME,
|
||||
setPermissions: AUTH.SET_PERMISSIONS,
|
||||
}),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-box {
|
||||
border: 1px solid #dcdfe6;
|
||||
width: 350px;
|
||||
height: 300px;
|
||||
margin: 120px auto;
|
||||
padding: 35px 35px 15px 35px;
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
box-shadow: 0 0 25px #909399;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
text-align: center;
|
||||
margin: 0 auto 40px auto;
|
||||
color: #303133;
|
||||
}
|
||||
</style>
|
||||
@@ -2,16 +2,25 @@
|
||||
<div class="content">
|
||||
<a-spin :spinning="loading">
|
||||
<a-tabs default-active-key="1" size="large" tabPosition="top">
|
||||
<a-tab-pane key="1" tab="根据时间查询消息">
|
||||
<a-tab-pane
|
||||
v-if="isAuthorized('message:search-time')"
|
||||
key="1"
|
||||
tab="根据时间查询消息"
|
||||
>
|
||||
<SearchByTime :topic-list="topicList"></SearchByTime>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab="根据偏移查询消息">
|
||||
<a-tab-pane
|
||||
key="2"
|
||||
v-if="isAuthorized('message:search-offset')"
|
||||
tab="根据偏移查询消息"
|
||||
>
|
||||
<SearchByOffset :topic-list="topicList"></SearchByOffset>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3" tab="在线发送">
|
||||
<a-tab-pane key="3" tab="在线发送" v-if="isAuthorized('message:send')">
|
||||
<SendMessage :topic-list="topicList"></SendMessage>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="4" tab="在线删除">
|
||||
|
||||
<a-tab-pane key="4" tab="在线删除" v-if="isAuthorized('message:del')">
|
||||
<DeleteMessage :topic-list="topicList"></DeleteMessage>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
@@ -27,6 +36,7 @@ import { KafkaTopicApi } from "@/utils/api";
|
||||
import notification from "ant-design-vue/lib/notification";
|
||||
import SendMessage from "@/views/message/SendMessage";
|
||||
import DeleteMessage from "./DeleteMessage";
|
||||
import { isAuthorized, isUnauthorized } from "@/utils/auth";
|
||||
export default {
|
||||
name: "Message",
|
||||
components: { DeleteMessage, SearchByTime, SearchByOffset, SendMessage },
|
||||
@@ -37,6 +47,8 @@ export default {
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
isAuthorized,
|
||||
isUnauthorized,
|
||||
getTopicNameList() {
|
||||
request({
|
||||
url: KafkaTopicApi.getTopicNameList.url,
|
||||
|
||||
@@ -116,7 +116,9 @@
|
||||
cancel-text="取消"
|
||||
@confirm="resend"
|
||||
>
|
||||
<a-button type="primary" icon="reload"> 重新发送 </a-button>
|
||||
<a-button type="primary" icon="reload" v-action:message:resend>
|
||||
重新发送
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</a-spin>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
@click="openDetailDialog(record)"
|
||||
v-action:message:detail
|
||||
>消息详情
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
@click="openAddClusterInfoDialog"
|
||||
v-action:op:cluster-switch:add
|
||||
>新增集群
|
||||
</a-button>
|
||||
<br /><br />
|
||||
@@ -38,6 +39,7 @@
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
@click="switchCluster(record)"
|
||||
v-action:op:cluster-switch:switch
|
||||
>切换
|
||||
</a-button>
|
||||
<a-button
|
||||
@@ -45,6 +47,7 @@
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
@click="openUpdateClusterInfoDialog(record)"
|
||||
v-action:op:cluster-switch:edit
|
||||
>编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
@@ -58,6 +61,7 @@
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
type="danger"
|
||||
v-action:op:cluster-switch:del
|
||||
>删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
|
||||
@@ -39,7 +39,11 @@
|
||||
cancel-text="取消"
|
||||
@confirm="cancelReassignment(record)"
|
||||
>
|
||||
<a-button size="small" href="javascript:;" class="operation-btn"
|
||||
<a-button
|
||||
size="small"
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
v-action:op:replication-update-detail:cancel
|
||||
>取消
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="content">
|
||||
<div class="content-module">
|
||||
<a-card title="集群管理" style="width: 100%; text-align: left">
|
||||
<p>
|
||||
<p v-action:op:cluster-switch>
|
||||
<a-button type="primary" @click="openClusterInfoDialog">
|
||||
集群切换
|
||||
</a-button>
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
<div class="content-module">
|
||||
<a-card title="Broker管理" style="width: 100%; text-align: left">
|
||||
<p>
|
||||
<p v-action:op:config-throttle>
|
||||
<a-button type="primary" @click="openConfigThrottleDialog">
|
||||
配置限流
|
||||
</a-button>
|
||||
@@ -24,7 +24,7 @@
|
||||
>设置指定broker上的topic的副本之间数据同步占用的带宽,这个设置是broker级别的,但是设置后还要去对应的topic上进行限流配置,指定对这个topic的相关副本进行限制</span
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<p v-action:op:remove-throttle>
|
||||
<a-button type="primary" @click="openRemoveThrottleDialog">
|
||||
解除限流
|
||||
</a-button>
|
||||
@@ -35,21 +35,21 @@
|
||||
</div>
|
||||
<div class="content-module">
|
||||
<a-card title="副本管理" style="width: 100%; text-align: left">
|
||||
<p>
|
||||
<p v-action:op:replication-preferred>
|
||||
<a-button type="primary" @click="openElectPreferredLeaderDialog">
|
||||
首选副本作为leader
|
||||
</a-button>
|
||||
<label>说明:</label>
|
||||
<span>将集群中所有分区leader副本设置为首选副本</span>
|
||||
</p>
|
||||
<p>
|
||||
<p v-action:op:replication-update-detail>
|
||||
<a-button type="primary" @click="openCurrentReassignmentsDialog">
|
||||
副本变更详情
|
||||
</a-button>
|
||||
<label>说明:</label>
|
||||
<span>查看正在进行副本变更/重分配的任务,或者将其取消</span>
|
||||
</p>
|
||||
<p>
|
||||
<p v-action:op:replication-reassign>
|
||||
<a-button type="primary" @click="openReplicaReassignDialog">
|
||||
副本重分配
|
||||
</a-button>
|
||||
|
||||
@@ -1,56 +1,49 @@
|
||||
<script src="../../store/index.js"></script>
|
||||
<template>
|
||||
<a-modal
|
||||
title="新增配置"
|
||||
:visible="show"
|
||||
:width="800"
|
||||
:mask="false"
|
||||
:destroyOnClose="true"
|
||||
:footer="null"
|
||||
:maskClosable="false"
|
||||
@cancel="handleCancel"
|
||||
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"
|
||||
:form="form"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 12 }"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<a-form-item label="用户" v-show="showUser">
|
||||
<a-input
|
||||
v-decorator="[
|
||||
'user',
|
||||
]"
|
||||
placeholder="输入用户主体标识,比如:用户名,未指定表示用户默认设置"
|
||||
v-decorator="['user']"
|
||||
placeholder="输入用户主体标识,比如:用户名,未指定表示用户默认设置"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="客户端ID" v-show="showClientId">
|
||||
<a-input
|
||||
v-decorator="[
|
||||
'client',
|
||||
]"
|
||||
placeholder="输入用户客户端ID,未指定表示默认客户端设置"
|
||||
v-decorator="['client']"
|
||||
placeholder="输入用户客户端ID,未指定表示默认客户端设置"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="IP" v-show="showIP">
|
||||
<a-input
|
||||
v-decorator="[
|
||||
'ip',
|
||||
]"
|
||||
placeholder="输入客户端IP"
|
||||
/>
|
||||
<a-input v-decorator="['ip']" placeholder="输入客户端IP" />
|
||||
</a-form-item>
|
||||
<a-form-item label="生产速率">
|
||||
<a-input-number
|
||||
:min="1"
|
||||
:max="102400000"
|
||||
v-decorator="[
|
||||
'producerRate',
|
||||
]"
|
||||
:min="1"
|
||||
:max="102400000"
|
||||
v-decorator="['producerRate']"
|
||||
/>
|
||||
<a-select default-value="MB" v-model="producerRateUnit" style="width: 100px">
|
||||
<a-select
|
||||
default-value="MB"
|
||||
v-model="producerRateUnit"
|
||||
style="width: 100px"
|
||||
>
|
||||
<a-select-option value="MB"> MB/s</a-select-option>
|
||||
<a-select-option value="KB"> KB/s</a-select-option>
|
||||
<a-select-option value="Byte"> Byte/s</a-select-option>
|
||||
@@ -58,13 +51,15 @@
|
||||
</a-form-item>
|
||||
<a-form-item label="消费速率">
|
||||
<a-input-number
|
||||
:min="1"
|
||||
:max="102400000"
|
||||
v-decorator="[
|
||||
'consumerRate',
|
||||
]"
|
||||
:min="1"
|
||||
:max="102400000"
|
||||
v-decorator="['consumerRate']"
|
||||
/>
|
||||
<a-select default-value="MB" v-model="consumerRateUnit" style="width: 100px">
|
||||
<a-select
|
||||
default-value="MB"
|
||||
v-model="consumerRateUnit"
|
||||
style="width: 100px"
|
||||
>
|
||||
<a-select-option value="MB"> MB/s</a-select-option>
|
||||
<a-select-option value="KB"> KB/s</a-select-option>
|
||||
<a-select-option value="Byte"> Byte/s</a-select-option>
|
||||
@@ -72,11 +67,9 @@
|
||||
</a-form-item>
|
||||
<a-form-item label="吞吐量">
|
||||
<a-input-number
|
||||
:min="1"
|
||||
:max="102400000"
|
||||
v-decorator="[
|
||||
'requestPercentage',
|
||||
]"
|
||||
:min="1"
|
||||
:max="102400000"
|
||||
v-decorator="['requestPercentage']"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{ span: 12, offset: 5 }">
|
||||
@@ -90,7 +83,7 @@
|
||||
|
||||
<script>
|
||||
import request from "@/utils/request";
|
||||
import {KafkaClientQuotaApi} from "@/utils/api";
|
||||
import { KafkaClientQuotaApi } from "@/utils/api";
|
||||
import notification from "ant-design-vue/es/notification";
|
||||
|
||||
export default {
|
||||
@@ -122,7 +115,7 @@ export default {
|
||||
show: this.visible,
|
||||
data: [],
|
||||
loading: false,
|
||||
form: this.$form.createForm(this, {name: "coordinated"}),
|
||||
form: this.$form.createForm(this, { name: "coordinated" }),
|
||||
producerRateUnit: "MB",
|
||||
consumerRateUnit: "MB",
|
||||
};
|
||||
@@ -136,13 +129,15 @@ export default {
|
||||
handleSubmit() {
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
const params = Object.assign({type: this.type}, values);
|
||||
const unitMap = {MB: 1024 * 1024, KB: 1024, Byte: 1};
|
||||
const params = Object.assign({ type: this.type }, values);
|
||||
const unitMap = { MB: 1024 * 1024, KB: 1024, Byte: 1 };
|
||||
if (values.consumerRate) {
|
||||
params.consumerRate = params.consumerRate * unitMap[this.consumerRateUnit];
|
||||
params.consumerRate =
|
||||
params.consumerRate * unitMap[this.consumerRateUnit];
|
||||
}
|
||||
if (values.producerRate) {
|
||||
params.producerRate = params.producerRate * unitMap[this.producerRateUnit];
|
||||
params.producerRate =
|
||||
params.producerRate * unitMap[this.producerRateUnit];
|
||||
}
|
||||
params.types = [];
|
||||
params.names = [];
|
||||
@@ -179,7 +174,7 @@ export default {
|
||||
this.loading = false;
|
||||
if (res.code == 0) {
|
||||
this.$message.success(res.msg);
|
||||
this.$emit("closeAddQuotaDialog", {refresh: true});
|
||||
this.$emit("closeAddQuotaDialog", { refresh: true });
|
||||
} else {
|
||||
notification.error({
|
||||
message: "error",
|
||||
@@ -192,7 +187,7 @@ export default {
|
||||
},
|
||||
handleCancel() {
|
||||
this.data = [];
|
||||
this.$emit("closeAddQuotaDialog", {refresh: false});
|
||||
this.$emit("closeAddQuotaDialog", { refresh: false });
|
||||
this.producerRateUnit = "MB";
|
||||
this.consumerRateUnit = "MB";
|
||||
},
|
||||
|
||||
@@ -3,18 +3,16 @@
|
||||
<a-spin :spinning="loading">
|
||||
<div id="search-offset-form-advanced-search">
|
||||
<a-form
|
||||
class="ant-advanced-search-form"
|
||||
:form="form"
|
||||
@submit="handleSearch"
|
||||
class="ant-advanced-search-form"
|
||||
:form="form"
|
||||
@submit="handleSearch"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="16">
|
||||
<a-form-item label="客户端ID">
|
||||
<a-input
|
||||
v-decorator="[
|
||||
'id',
|
||||
]"
|
||||
placeholder="请输入生产者/消费者客户端ID!"
|
||||
v-decorator="['id']"
|
||||
placeholder="请输入生产者/消费者客户端ID!"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
@@ -27,26 +25,39 @@
|
||||
</a-form>
|
||||
</div>
|
||||
<div class="operation-row-button">
|
||||
<a-button type="primary" @click="openAddQuotaDialog"
|
||||
>新增配置
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="openAddQuotaDialog"
|
||||
v-action:quota:client:add
|
||||
>新增配置
|
||||
</a-button>
|
||||
</div>
|
||||
<QuotaList type="client-id" :columns="columns" :data="data" @refreshQuotaList="refresh"></QuotaList>
|
||||
<AddQuotaConfig type="client-id" :visible="showAddQuotaDialog" :showClientId="true" @closeAddQuotaDialog="closeAddQuotaDialog"></AddQuotaConfig>
|
||||
<QuotaList
|
||||
type="client-id"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
@refreshQuotaList="refresh"
|
||||
></QuotaList>
|
||||
<AddQuotaConfig
|
||||
type="client-id"
|
||||
:visible="showAddQuotaDialog"
|
||||
:showClientId="true"
|
||||
@closeAddQuotaDialog="closeAddQuotaDialog"
|
||||
></AddQuotaConfig>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from "@/utils/request";
|
||||
import {KafkaClientQuotaApi} from "@/utils/api";
|
||||
import { KafkaClientQuotaApi } from "@/utils/api";
|
||||
import notification from "ant-design-vue/lib/notification";
|
||||
import QuotaList from "@/views/quota/QuotaList.vue";
|
||||
import AddQuotaConfig from "@/views/quota/AddQuotaConfig.vue";
|
||||
|
||||
export default {
|
||||
name: "ClientIDQuota",
|
||||
components: {QuotaList, AddQuotaConfig},
|
||||
components: { QuotaList, AddQuotaConfig },
|
||||
props: {
|
||||
topicList: {
|
||||
type: Array,
|
||||
@@ -55,7 +66,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
form: this.$form.createForm(this, {name: "client_id_quota"}),
|
||||
form: this.$form.createForm(this, { name: "client_id_quota" }),
|
||||
data: [],
|
||||
showAlterQuotaDialog: false,
|
||||
showAddQuotaDialog: false,
|
||||
@@ -64,8 +75,8 @@ export default {
|
||||
title: "客户端ID",
|
||||
dataIndex: "client",
|
||||
key: "client",
|
||||
slots: {title: "client"},
|
||||
scopedSlots: {customRender: "client"},
|
||||
slots: { title: "client" },
|
||||
scopedSlots: { customRender: "client" },
|
||||
},
|
||||
{
|
||||
title: "生产速率(带宽/秒)",
|
||||
@@ -90,7 +101,7 @@ export default {
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
this.loading = true;
|
||||
const params = {types: ["client-id"]};
|
||||
const params = { types: ["client-id"] };
|
||||
if (values.id) {
|
||||
params.names = [values.id.trim()];
|
||||
}
|
||||
|
||||
@@ -5,13 +5,17 @@
|
||||
<a-tab-pane key="1" tab="使用说明">
|
||||
<ClientQuotaIntroduce></ClientQuotaIntroduce>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab="用户">
|
||||
<a-tab-pane v-if="isAuthorized('quota:user')" key="2" tab="用户">
|
||||
<UserQuota></UserQuota>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3" tab="客户端ID">
|
||||
<a-tab-pane key="3" tab="客户端ID" v-if="isAuthorized('quota:client')">
|
||||
<ClientIDQuota></ClientIDQuota>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="4" tab="用户_客户端ID">
|
||||
<a-tab-pane
|
||||
key="4"
|
||||
tab="用户_客户端ID"
|
||||
v-if="isAuthorized('quota:user-client')"
|
||||
>
|
||||
<UserAndClientIDQuota></UserAndClientIDQuota>
|
||||
</a-tab-pane>
|
||||
<!-- <a-tab-pane key="5" tab="IP">-->
|
||||
@@ -27,10 +31,17 @@ import ClientIDQuota from "@/views/quota/ClientIDQuota.vue";
|
||||
import UserQuota from "@/views/quota/UserQuota.vue";
|
||||
import UserAndClientIDQuota from "@/views/quota/UserAndClientIDQuota.vue";
|
||||
import ClientQuotaIntroduce from "@/views/quota/ClientQuotaIntroduce.vue";
|
||||
import { isAuthorized } from "@/utils/auth";
|
||||
|
||||
export default {
|
||||
name: "ClientQuota",
|
||||
components: {ClientIDQuota, UserQuota, UserAndClientIDQuota, ClientQuotaIntroduce},
|
||||
methods: { isAuthorized },
|
||||
components: {
|
||||
ClientIDQuota,
|
||||
UserQuota,
|
||||
UserAndClientIDQuota,
|
||||
ClientQuotaIntroduce,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<h1>客户端限流说明(资源限额)</h1>
|
||||
<hr/>
|
||||
<hr/>
|
||||
<hr />
|
||||
<hr />
|
||||
<h2>支持类型</h2>
|
||||
<ul>
|
||||
<li>基于用户主体标识的配置</li>
|
||||
<li>基于客户端ID的配置</li>
|
||||
<li>基于用户主体+客户端ID的配置</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<hr />
|
||||
<h2>默认配置</h2>
|
||||
<p>比如,基于用户的配置,在新增配置的时候未设置用户名称,则默认应用于所有用户</p>
|
||||
<p>
|
||||
比如,基于用户的配置,在新增配置的时候未设置用户名称,则默认应用于所有用户
|
||||
</p>
|
||||
<p>基于客户端ID的配置,新增的时候,未指定客户端ID,则应用于所有客户端</p>
|
||||
<hr/>
|
||||
<hr />
|
||||
<h2>配置优先级</h2>
|
||||
下面的展示中,数字越小,即越靠上的优先级越高,相同的用户名称或者客户端ID,优先级最高的配置生效,下面的未指定表示 默认配置
|
||||
下面的展示中,数字越小,即越靠上的优先级越高,相同的用户名称或者客户端ID,优先级最高的配置生效,下面的未指定表示
|
||||
默认配置
|
||||
<ol>
|
||||
<li>[用户+客户端ID] 指定用户名称并且指定客户端ID(优先级最高)</li>
|
||||
<li>[用户+客户端ID] 指定用户名称,未指定客户端ID</li>
|
||||
@@ -26,18 +29,17 @@
|
||||
<li>[客户端ID] 指定客户端ID</li>
|
||||
<li>[客户端ID] 未指定客户端ID(优先级最低)</li>
|
||||
</ol>
|
||||
<hr/>
|
||||
<hr />
|
||||
<h2>使用注意</h2>
|
||||
大多数集群一般没有开启认证,所以用户配置可能不支持,限流可以使用基于客户端ID的配置。但是对于优先级最低的:"[客户端ID]
|
||||
未指定客户端ID",默认对于所有客户端生效,万一速率配置过小,可能无意间影响生产业务,所以尽量避免默认配置。
|
||||
<hr/>
|
||||
<hr />
|
||||
<h2>查询</h2>
|
||||
查询默认配置,在查询项的输入框内,输入一个空格即可。
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "ClientQuotaIntroduce",
|
||||
};
|
||||
|
||||
@@ -3,19 +3,14 @@
|
||||
<a-spin :spinning="loading">
|
||||
<div id="search-offset-form-advanced-search">
|
||||
<a-form
|
||||
class="ant-advanced-search-form"
|
||||
:form="form"
|
||||
@submit="handleSearch"
|
||||
class="ant-advanced-search-form"
|
||||
:form="form"
|
||||
@submit="handleSearch"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="16">
|
||||
<a-form-item label="IP">
|
||||
<a-input
|
||||
v-decorator="[
|
||||
'ip',
|
||||
]"
|
||||
placeholder="请输入ip!"
|
||||
/>
|
||||
<a-input v-decorator="['ip']" placeholder="请输入ip!" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="2" :style="{ textAlign: 'right' }">
|
||||
@@ -28,25 +23,35 @@
|
||||
</div>
|
||||
<div class="operation-row-button">
|
||||
<a-button type="primary" @click="openAddQuotaDialog"
|
||||
>新增配置
|
||||
>新增配置
|
||||
</a-button>
|
||||
</div>
|
||||
<QuotaList type="ip" :columns="columns" :data="data" @refreshQuotaList="refresh"></QuotaList>
|
||||
<AddQuotaConfig type="ip" :visible="showAddQuotaDialog" :showIP="true" @closeAddQuotaDialog="closeAddQuotaDialog"></AddQuotaConfig>
|
||||
<QuotaList
|
||||
type="ip"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
@refreshQuotaList="refresh"
|
||||
></QuotaList>
|
||||
<AddQuotaConfig
|
||||
type="ip"
|
||||
:visible="showAddQuotaDialog"
|
||||
:showIP="true"
|
||||
@closeAddQuotaDialog="closeAddQuotaDialog"
|
||||
></AddQuotaConfig>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from "@/utils/request";
|
||||
import {KafkaClientQuotaApi} from "@/utils/api";
|
||||
import { KafkaClientQuotaApi } from "@/utils/api";
|
||||
import notification from "ant-design-vue/lib/notification";
|
||||
import QuotaList from "@/views/quota/QuotaList.vue";
|
||||
import AddQuotaConfig from "@/views/quota/AddQuotaConfig.vue";
|
||||
|
||||
export default {
|
||||
name: "IpQuota",
|
||||
components: {QuotaList, AddQuotaConfig},
|
||||
components: { QuotaList, AddQuotaConfig },
|
||||
props: {
|
||||
topicList: {
|
||||
type: Array,
|
||||
@@ -55,7 +60,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
form: this.$form.createForm(this, {name: "ip_quota"}),
|
||||
form: this.$form.createForm(this, { name: "ip_quota" }),
|
||||
data: [],
|
||||
showAlterQuotaDialog: false,
|
||||
showAddQuotaDialog: false,
|
||||
@@ -64,8 +69,8 @@ export default {
|
||||
title: "IP",
|
||||
dataIndex: "ip",
|
||||
key: "ip",
|
||||
slots: {title: "ip"},
|
||||
scopedSlots: {customRender: "ip"},
|
||||
slots: { title: "ip" },
|
||||
scopedSlots: { customRender: "ip" },
|
||||
},
|
||||
{
|
||||
title: "生产速率(带宽/秒)",
|
||||
@@ -90,7 +95,7 @@ export default {
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
this.loading = true;
|
||||
const params = {types: ["ip"]};
|
||||
const params = { types: ["ip"] };
|
||||
if (values.ip) {
|
||||
params.names = [values.ip.trim()];
|
||||
}
|
||||
|
||||
@@ -2,58 +2,69 @@
|
||||
<div>
|
||||
<a-spin :spinning="loading">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="data"
|
||||
bordered
|
||||
:row-key="
|
||||
(record, index) => {
|
||||
return index;
|
||||
}
|
||||
"
|
||||
@change="handleChange"
|
||||
:columns="columns"
|
||||
:data-source="data"
|
||||
bordered
|
||||
:row-key="
|
||||
(record, index) => {
|
||||
return index;
|
||||
}
|
||||
"
|
||||
@change="handleChange"
|
||||
>
|
||||
<div slot="client" slot-scope="text">
|
||||
<span v-if="text">{{ text }}</span><span v-else style="color: red">默认配置</span>
|
||||
<span v-if="text">{{ text }}</span
|
||||
><span v-else style="color: red">默认配置</span>
|
||||
</div>
|
||||
<div slot="user" slot-scope="text">
|
||||
<span v-if="text">{{ text }}</span><span v-else style="color: red">默认配置</span>
|
||||
<span v-if="text">{{ text }}</span
|
||||
><span v-else style="color: red">默认配置</span>
|
||||
</div>
|
||||
|
||||
<div slot="operation" slot-scope="record">
|
||||
<a-popconfirm
|
||||
:title="'删除当前配置?'"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="deleteConfig(record)"
|
||||
:title="'删除当前配置?'"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="deleteConfig(record)"
|
||||
>
|
||||
<a-button size="small" href="javascript:;" class="operation-btn"
|
||||
>删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<a-button
|
||||
<a-button
|
||||
size="small"
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
@click="openUpdateDialog(record)"
|
||||
>修改
|
||||
v-action:quota:del
|
||||
>删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<a-button
|
||||
size="small"
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
@click="openUpdateDialog(record)"
|
||||
v-action:quota:edit
|
||||
>修改
|
||||
</a-button>
|
||||
</div>
|
||||
</a-table>
|
||||
<UpdateQuotaConfig :type="type" :record="selectRow" :visible="showUpdateDialog"
|
||||
@closeUpdateQuotaDialog="closeUpdateQuotaDialog"></UpdateQuotaConfig>
|
||||
<UpdateQuotaConfig
|
||||
:type="type"
|
||||
:record="selectRow"
|
||||
:visible="showUpdateDialog"
|
||||
@closeUpdateQuotaDialog="closeUpdateQuotaDialog"
|
||||
></UpdateQuotaConfig>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {KafkaClientQuotaApi} from "@/utils/api";
|
||||
import { KafkaClientQuotaApi } from "@/utils/api";
|
||||
import request from "@/utils/request";
|
||||
import notification from "ant-design-vue/lib/notification";
|
||||
import UpdateQuotaConfig from "@/views/quota/UpdateQuotaConfig.vue";
|
||||
|
||||
export default {
|
||||
name: "QuotaList",
|
||||
components: {UpdateQuotaConfig},
|
||||
components: { UpdateQuotaConfig },
|
||||
props: {
|
||||
columns: {
|
||||
type: Array,
|
||||
@@ -88,7 +99,7 @@ export default {
|
||||
},
|
||||
deleteConfig(record) {
|
||||
this.loading = true;
|
||||
const params = {type: this.type};
|
||||
const params = { type: this.type };
|
||||
params.types = [];
|
||||
params.names = [];
|
||||
if (this.type == "user") {
|
||||
@@ -161,7 +172,7 @@ export default {
|
||||
this.columns.push({
|
||||
title: "操作",
|
||||
key: "operation",
|
||||
scopedSlots: {customRender: "operation"},
|
||||
scopedSlots: { customRender: "operation" },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,59 +1,58 @@
|
||||
<script src="../../store/index.js"></script>
|
||||
<template>
|
||||
<a-modal
|
||||
title="修改配置"
|
||||
:visible="show"
|
||||
:width="800"
|
||||
:mask="false"
|
||||
:destroyOnClose="true"
|
||||
:footer="null"
|
||||
:maskClosable="false"
|
||||
@cancel="handleCancel"
|
||||
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"
|
||||
:form="form"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 12 }"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<a-form-item label="用户" v-show="showUser">
|
||||
<a-input
|
||||
:disabled="true"
|
||||
v-decorator="[
|
||||
'user', { initialValue: record.user }
|
||||
]"
|
||||
placeholder="输入用户主体标识,比如:用户名,未指定表示用户默认设置"
|
||||
:disabled="true"
|
||||
v-decorator="['user', { initialValue: record.user }]"
|
||||
placeholder="输入用户主体标识,比如:用户名,未指定表示用户默认设置"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="客户端ID" v-show="showClientId">
|
||||
<a-input
|
||||
:disabled="true"
|
||||
v-decorator="[
|
||||
'client', { initialValue: record.client }
|
||||
]"
|
||||
placeholder="输入用户客户端ID,未指定表示默认客户端设置"
|
||||
:disabled="true"
|
||||
v-decorator="['client', { initialValue: record.client }]"
|
||||
placeholder="输入用户客户端ID,未指定表示默认客户端设置"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="IP" v-show="showIP">
|
||||
<a-input
|
||||
:disabled="true"
|
||||
v-decorator="[
|
||||
'ip', { initialValue: record.ip }
|
||||
]"
|
||||
placeholder="输入客户端IP"
|
||||
:disabled="true"
|
||||
v-decorator="['ip', { initialValue: record.ip }]"
|
||||
placeholder="输入客户端IP"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="生产速率">
|
||||
<a-input-number
|
||||
:min="1"
|
||||
:max="102400000"
|
||||
v-decorator="[
|
||||
'producerRate', { initialValue: record.producerRate }
|
||||
:min="1"
|
||||
:max="102400000"
|
||||
v-decorator="[
|
||||
'producerRate',
|
||||
{ initialValue: record.producerRate },
|
||||
]"
|
||||
/>
|
||||
<a-select default-value="MB" v-model="producerRateUnit" style="width: 100px">
|
||||
<a-select
|
||||
default-value="MB"
|
||||
v-model="producerRateUnit"
|
||||
style="width: 100px"
|
||||
>
|
||||
<a-select-option value="MB"> MB/s</a-select-option>
|
||||
<a-select-option value="KB"> KB/s</a-select-option>
|
||||
<a-select-option value="Byte"> Byte/s</a-select-option>
|
||||
@@ -61,13 +60,18 @@
|
||||
</a-form-item>
|
||||
<a-form-item label="消费速率">
|
||||
<a-input-number
|
||||
:min="1"
|
||||
:max="102400000"
|
||||
v-decorator="[
|
||||
'consumerRate', { initialValue: record.consumerRate }
|
||||
:min="1"
|
||||
:max="102400000"
|
||||
v-decorator="[
|
||||
'consumerRate',
|
||||
{ initialValue: record.consumerRate },
|
||||
]"
|
||||
/>
|
||||
<a-select default-value="MB" v-model="consumerRateUnit" style="width: 100px">
|
||||
<a-select
|
||||
default-value="MB"
|
||||
v-model="consumerRateUnit"
|
||||
style="width: 100px"
|
||||
>
|
||||
<a-select-option value="MB"> MB/s</a-select-option>
|
||||
<a-select-option value="KB"> KB/s</a-select-option>
|
||||
<a-select-option value="Byte"> Byte/s</a-select-option>
|
||||
@@ -75,10 +79,11 @@
|
||||
</a-form-item>
|
||||
<a-form-item label="吞吐量">
|
||||
<a-input-number
|
||||
:min="1"
|
||||
:max="102400000"
|
||||
v-decorator="[
|
||||
'requestPercentage', { initialValue: record.requestPercentage }
|
||||
:min="1"
|
||||
:max="102400000"
|
||||
v-decorator="[
|
||||
'requestPercentage',
|
||||
{ initialValue: record.requestPercentage },
|
||||
]"
|
||||
/>
|
||||
</a-form-item>
|
||||
@@ -93,7 +98,7 @@
|
||||
|
||||
<script>
|
||||
import request from "@/utils/request";
|
||||
import {KafkaClientQuotaApi} from "@/utils/api";
|
||||
import { KafkaClientQuotaApi } from "@/utils/api";
|
||||
import notification from "ant-design-vue/es/notification";
|
||||
|
||||
export default {
|
||||
@@ -110,7 +115,7 @@ export default {
|
||||
record: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return {}
|
||||
return {};
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -119,7 +124,7 @@ export default {
|
||||
show: this.visible,
|
||||
data: [],
|
||||
loading: false,
|
||||
form: this.$form.createForm(this, {name: "coordinated"}),
|
||||
form: this.$form.createForm(this, { name: "coordinated" }),
|
||||
producerRateUnit: "MB",
|
||||
consumerRateUnit: "MB",
|
||||
showUser: false,
|
||||
@@ -139,16 +144,24 @@ export default {
|
||||
handleSubmit() {
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
const params = {type: this.type, deleteConfigs: []};
|
||||
const unitMap = {MB: 1024 * 1024, KB: 1024, Byte: 1};
|
||||
const params = { type: this.type, deleteConfigs: [] };
|
||||
const unitMap = { MB: 1024 * 1024, KB: 1024, Byte: 1 };
|
||||
if (values.consumerRate) {
|
||||
const num = typeof (values.consumerRate) == "string" && values.consumerRate.indexOf(" ") > 0 ? values.consumerRate.split(" ")[0] : values.consumerRate;
|
||||
const num =
|
||||
typeof values.consumerRate == "string" &&
|
||||
values.consumerRate.indexOf(" ") > 0
|
||||
? values.consumerRate.split(" ")[0]
|
||||
: values.consumerRate;
|
||||
params.consumerRate = num * unitMap[this.consumerRateUnit];
|
||||
} else {
|
||||
params.deleteConfigs.push("consumerRate");
|
||||
}
|
||||
if (values.producerRate) {
|
||||
const num = typeof (values.producerRate) == "string" && values.producerRate.indexOf(" ") > 0 ? values.producerRate.split(" ")[0] : values.producerRate;
|
||||
const num =
|
||||
typeof values.producerRate == "string" &&
|
||||
values.producerRate.indexOf(" ") > 0
|
||||
? values.producerRate.split(" ")[0]
|
||||
: values.producerRate;
|
||||
params.producerRate = num * unitMap[this.producerRateUnit];
|
||||
} else {
|
||||
params.deleteConfigs.push("producerRate");
|
||||
@@ -207,7 +220,7 @@ export default {
|
||||
this.loading = false;
|
||||
if (res.code == 0) {
|
||||
this.$message.success(res.msg);
|
||||
this.$emit("closeUpdateQuotaDialog", {refresh: true});
|
||||
this.$emit("closeUpdateQuotaDialog", { refresh: true });
|
||||
} else {
|
||||
notification.error({
|
||||
message: "error",
|
||||
@@ -220,7 +233,7 @@ export default {
|
||||
},
|
||||
handleCancel() {
|
||||
this.data = [];
|
||||
this.$emit("closeUpdateQuotaDialog", {refresh: false});
|
||||
this.$emit("closeUpdateQuotaDialog", { refresh: false });
|
||||
},
|
||||
init() {
|
||||
this.producerRateUnit = "MB";
|
||||
@@ -243,9 +256,7 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
|
||||
},
|
||||
created() {},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,28 +3,24 @@
|
||||
<a-spin :spinning="loading">
|
||||
<div id="search-offset-form-advanced-search">
|
||||
<a-form
|
||||
class="ant-advanced-search-form"
|
||||
:form="form"
|
||||
@submit="handleSearch"
|
||||
class="ant-advanced-search-form"
|
||||
:form="form"
|
||||
@submit="handleSearch"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="10">
|
||||
<a-form-item label="用户标识">
|
||||
<a-input
|
||||
v-decorator="[
|
||||
'user',
|
||||
]"
|
||||
placeholder="请输入用户标识,如:用户名!"
|
||||
v-decorator="['user']"
|
||||
placeholder="请输入用户标识,如:用户名!"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="10">
|
||||
<a-form-item label="客户端ID">
|
||||
<a-input
|
||||
v-decorator="[
|
||||
'client',
|
||||
]"
|
||||
placeholder="请输入客户端ID!"
|
||||
v-decorator="['client']"
|
||||
placeholder="请输入客户端ID!"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
@@ -37,27 +33,40 @@
|
||||
</a-form>
|
||||
</div>
|
||||
<div class="operation-row-button">
|
||||
<a-button type="primary" @click="openAddQuotaDialog"
|
||||
>新增配置
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="openAddQuotaDialog"
|
||||
v-action:quota:user-client:add
|
||||
>新增配置
|
||||
</a-button>
|
||||
</div>
|
||||
<QuotaList type="user&client-id" :columns="columns" :data="data" @refreshQuotaList="refresh"></QuotaList>
|
||||
<AddQuotaConfig type="user&client-id" :visible="showAddQuotaDialog" :showUser="true" :showClientId="true"
|
||||
@closeAddQuotaDialog="closeAddQuotaDialog"></AddQuotaConfig>
|
||||
<QuotaList
|
||||
type="user&client-id"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
@refreshQuotaList="refresh"
|
||||
></QuotaList>
|
||||
<AddQuotaConfig
|
||||
type="user&client-id"
|
||||
:visible="showAddQuotaDialog"
|
||||
:showUser="true"
|
||||
:showClientId="true"
|
||||
@closeAddQuotaDialog="closeAddQuotaDialog"
|
||||
></AddQuotaConfig>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from "@/utils/request";
|
||||
import {KafkaClientQuotaApi} from "@/utils/api";
|
||||
import { KafkaClientQuotaApi } from "@/utils/api";
|
||||
import notification from "ant-design-vue/lib/notification";
|
||||
import QuotaList from "@/views/quota/QuotaList.vue";
|
||||
import AddQuotaConfig from "@/views/quota/AddQuotaConfig.vue";
|
||||
|
||||
export default {
|
||||
name: "UserAndClientIDQuota",
|
||||
components: {QuotaList, AddQuotaConfig},
|
||||
components: { QuotaList, AddQuotaConfig },
|
||||
props: {
|
||||
topicList: {
|
||||
type: Array,
|
||||
@@ -66,7 +75,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
form: this.$form.createForm(this, {name: "user_client_id_quota"}),
|
||||
form: this.$form.createForm(this, { name: "user_client_id_quota" }),
|
||||
data: [],
|
||||
showAlterQuotaDialog: false,
|
||||
showAddQuotaDialog: false,
|
||||
@@ -75,15 +84,15 @@ export default {
|
||||
title: "用户标识",
|
||||
dataIndex: "user",
|
||||
key: "user",
|
||||
slots: {title: "user"},
|
||||
scopedSlots: {customRender: "user"},
|
||||
slots: { title: "user" },
|
||||
scopedSlots: { customRender: "user" },
|
||||
},
|
||||
{
|
||||
title: "客户端ID",
|
||||
dataIndex: "client",
|
||||
key: "client",
|
||||
slots: {title: "client"},
|
||||
scopedSlots: {customRender: "client"},
|
||||
slots: { title: "client" },
|
||||
scopedSlots: { customRender: "client" },
|
||||
},
|
||||
{
|
||||
title: "生产速率(带宽/秒)",
|
||||
@@ -108,7 +117,7 @@ export default {
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
this.loading = true;
|
||||
const params = {types: ["user", "client-id"], names: []};
|
||||
const params = { types: ["user", "client-id"], names: [] };
|
||||
if (values.user) {
|
||||
params.names.push(values.user.trim());
|
||||
}
|
||||
|
||||
@@ -3,18 +3,16 @@
|
||||
<a-spin :spinning="loading">
|
||||
<div id="search-offset-form-advanced-search">
|
||||
<a-form
|
||||
class="ant-advanced-search-form"
|
||||
:form="form"
|
||||
@submit="handleSearch"
|
||||
class="ant-advanced-search-form"
|
||||
:form="form"
|
||||
@submit="handleSearch"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="16">
|
||||
<a-form-item label="用户标识">
|
||||
<a-input
|
||||
v-decorator="[
|
||||
'user',
|
||||
]"
|
||||
placeholder="请输入用户标识,如:用户名!"
|
||||
v-decorator="['user']"
|
||||
placeholder="请输入用户标识,如:用户名!"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
@@ -27,26 +25,39 @@
|
||||
</a-form>
|
||||
</div>
|
||||
<div class="operation-row-button">
|
||||
<a-button type="primary" @click="openAddQuotaDialog"
|
||||
>新增配置
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="openAddQuotaDialog"
|
||||
v-action:quota:user:add
|
||||
>新增配置
|
||||
</a-button>
|
||||
</div>
|
||||
<QuotaList type="user" :columns="columns" :data="data" @refreshQuotaList="refresh"></QuotaList>
|
||||
<AddQuotaConfig type="user" :visible="showAddQuotaDialog" :showUser="true" @closeAddQuotaDialog="closeAddQuotaDialog"></AddQuotaConfig>
|
||||
<QuotaList
|
||||
type="user"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
@refreshQuotaList="refresh"
|
||||
></QuotaList>
|
||||
<AddQuotaConfig
|
||||
type="user"
|
||||
:visible="showAddQuotaDialog"
|
||||
:showUser="true"
|
||||
@closeAddQuotaDialog="closeAddQuotaDialog"
|
||||
></AddQuotaConfig>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from "@/utils/request";
|
||||
import {KafkaClientQuotaApi} from "@/utils/api";
|
||||
import { KafkaClientQuotaApi } from "@/utils/api";
|
||||
import notification from "ant-design-vue/lib/notification";
|
||||
import QuotaList from "@/views/quota/QuotaList.vue";
|
||||
import AddQuotaConfig from "@/views/quota/AddQuotaConfig.vue";
|
||||
|
||||
export default {
|
||||
name: "UserQuota",
|
||||
components: {QuotaList, AddQuotaConfig},
|
||||
components: { QuotaList, AddQuotaConfig },
|
||||
props: {
|
||||
topicList: {
|
||||
type: Array,
|
||||
@@ -55,7 +66,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
form: this.$form.createForm(this, {name: "user_quota"}),
|
||||
form: this.$form.createForm(this, { name: "user_quota" }),
|
||||
data: [],
|
||||
showAlterQuotaDialog: false,
|
||||
showAddQuotaDialog: false,
|
||||
@@ -65,8 +76,8 @@ export default {
|
||||
dataIndex: "user",
|
||||
key: "user",
|
||||
width: 300,
|
||||
slots: {title: "user"},
|
||||
scopedSlots: {customRender: "user"},
|
||||
slots: { title: "user" },
|
||||
scopedSlots: { customRender: "user" },
|
||||
},
|
||||
{
|
||||
title: "生产速率(带宽/秒)",
|
||||
@@ -91,7 +102,7 @@ export default {
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
this.loading = true;
|
||||
const params = {types: ["user"]};
|
||||
const params = { types: ["user"] };
|
||||
if (values.user) {
|
||||
params.names = [values.user.trim()];
|
||||
}
|
||||
|
||||
@@ -44,7 +44,11 @@
|
||||
cancel-text="取消"
|
||||
@confirm="electPreferredLeader(record)"
|
||||
>
|
||||
<a-button size="small" href="javascript:;" class="operation-btn"
|
||||
<a-button
|
||||
size="small"
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
v-action:topic:partition-detail:preferred
|
||||
>首选副本作为leader
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
|
||||
@@ -36,7 +36,13 @@
|
||||
|
||||
<a-col :span="8" :style="{ textAlign: 'right' }">
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit"> 刷新</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
v-action:topic:load
|
||||
>
|
||||
刷新</a-button
|
||||
>
|
||||
<!-- <a-button :style="{ marginLeft: '8px' }" @click="handleReset">-->
|
||||
<!-- 重置-->
|
||||
<!-- </a-button>-->
|
||||
@@ -46,7 +52,10 @@
|
||||
</a-form>
|
||||
</div>
|
||||
<div class="operation-row-button">
|
||||
<a-button type="primary" @click="openCreateTopicDialog"
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="openCreateTopicDialog"
|
||||
v-action:topic:add
|
||||
>新增</a-button
|
||||
>
|
||||
<a-popconfirm
|
||||
@@ -60,6 +69,7 @@
|
||||
class="btn-left"
|
||||
:disabled="!hasSelected"
|
||||
:loading="loading"
|
||||
v-action:topic:batch-del
|
||||
>
|
||||
批量删除
|
||||
</a-button>
|
||||
@@ -97,7 +107,11 @@
|
||||
cancel-text="取消"
|
||||
@confirm="deleteTopic(record.name)"
|
||||
>
|
||||
<a-button size="small" href="javascript:;" class="operation-btn"
|
||||
<a-button
|
||||
size="small"
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
v-action:topic:del
|
||||
>删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
@@ -106,6 +120,7 @@
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
@click="openPartitionInfoDialog(record.name)"
|
||||
v-action:topic:partition-detail
|
||||
>分区详情
|
||||
</a-button>
|
||||
<a-button
|
||||
@@ -113,6 +128,7 @@
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
@click="openAddPartitionDialog(record.name)"
|
||||
v-action:topic:partition-add
|
||||
>增加分区
|
||||
</a-button>
|
||||
<a-button
|
||||
@@ -120,6 +136,7 @@
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
@click="openConsumedDetailDialog(record.name)"
|
||||
v-action:topic:consumer-detail
|
||||
>消费详情
|
||||
</a-button>
|
||||
<a-button
|
||||
@@ -127,6 +144,7 @@
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
@click="openTopicConfigDialog(record.name)"
|
||||
v-action:topic:property-config
|
||||
>属性配置
|
||||
</a-button>
|
||||
<a-button
|
||||
@@ -134,6 +152,7 @@
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
@click="openUpdateReplicaDialog(record.name)"
|
||||
v-action:topic:replication-modify
|
||||
>变更副本
|
||||
</a-button>
|
||||
<a-button
|
||||
@@ -141,6 +160,7 @@
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
@click="openMessageStatsDialog(record.name)"
|
||||
v-action:topic:send-count
|
||||
>发送统计
|
||||
</a-button>
|
||||
<a-button
|
||||
@@ -148,6 +168,7 @@
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
@click="openThrottleDialog(record.name)"
|
||||
v-action:topic:replication-sync-throttle
|
||||
>限流
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
class="operation-btn"
|
||||
v-show="!record.readOnly"
|
||||
@click="openEditConfigDialog(record)"
|
||||
v-action:topic:property-config:edit
|
||||
>编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
@@ -44,7 +45,11 @@
|
||||
v-show="isDynamic(record.source)"
|
||||
@confirm="deleteTopicConfig(record)"
|
||||
>
|
||||
<a-button size="small" href="javascript:;" class="operation-btn"
|
||||
<a-button
|
||||
size="small"
|
||||
href="javascript:;"
|
||||
class="operation-btn"
|
||||
v-action:topic:property-config:del
|
||||
>删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
|
||||
143
ui/src/views/user/CreateUser.vue
Normal file
143
ui/src/views/user/CreateUser.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<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-select
|
||||
show-search
|
||||
option-filter-prop="children"
|
||||
v-decorator="[
|
||||
'roleIds',
|
||||
{ rules: [{ required: true, message: '请选择一个角色!' }] },
|
||||
]"
|
||||
placeholder="请选择一个角色"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="role in roles"
|
||||
:key="role.id"
|
||||
:value="role.id"
|
||||
>
|
||||
{{ role.roleName }}
|
||||
</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 { UserManageApi } from "@/utils/api";
|
||||
|
||||
export default {
|
||||
name: "CreateUser",
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: this.visible,
|
||||
data: [],
|
||||
loading: false,
|
||||
form: this.$form.createForm(this, { name: "coordinated" }),
|
||||
roles: [],
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
visible(v) {
|
||||
this.show = v;
|
||||
if (this.show) {
|
||||
this.getRoles();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
this.loading = true;
|
||||
request({
|
||||
url: UserManageApi.addOrUpdateUser.url,
|
||||
method: UserManageApi.addOrUpdateUser.method,
|
||||
data: values,
|
||||
}).then((res) => {
|
||||
this.loading = false;
|
||||
if (res.code == 0) {
|
||||
this.$message.success(res.msg);
|
||||
this.$emit("closeCreateUserDialog", {
|
||||
refresh: true,
|
||||
data: res.data,
|
||||
});
|
||||
} else {
|
||||
notification.error({
|
||||
message: "error",
|
||||
description: res.msg,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
getRoles() {
|
||||
this.loading = true;
|
||||
request({
|
||||
url: UserManageApi.getRole.url,
|
||||
method: UserManageApi.getRole.method,
|
||||
}).then((res) => {
|
||||
this.loading = false;
|
||||
if (res.code == 0) {
|
||||
this.roles = res.data;
|
||||
} else {
|
||||
notification.error({
|
||||
message: "error",
|
||||
description: res.msg,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
handleCancel() {
|
||||
this.data = [];
|
||||
this.$emit("closeCreateUserDialog", { refresh: false });
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.getRoles();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
80
ui/src/views/user/Permission.vue
Normal file
80
ui/src/views/user/Permission.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<a-spin :spinning="loading">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="data"
|
||||
:expanded-row-keys.sync="expandedRowKeys"
|
||||
>
|
||||
<div slot="type" slot-scope="text">
|
||||
<span v-if="text == 0" style="color: darkgreen">菜单</span
|
||||
><span v-else>按钮</span>
|
||||
</div>
|
||||
</a-table>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from "@/utils/request";
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "权限名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: "12%",
|
||||
slots: { title: "type" },
|
||||
scopedSlots: { customRender: "type" },
|
||||
},
|
||||
];
|
||||
|
||||
import { UserManageApi } from "@/utils/api";
|
||||
import notification from "ant-design-vue/lib/notification";
|
||||
|
||||
export default {
|
||||
name: "Permission",
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
data: [],
|
||||
columns,
|
||||
expandedRowKeys: [],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getPermissions() {
|
||||
this.loading = true;
|
||||
request({
|
||||
url: UserManageApi.getPermissions.url,
|
||||
method: UserManageApi.getPermissions.method,
|
||||
}).then((res) => {
|
||||
this.loading = false;
|
||||
if (res.code == 0) {
|
||||
this.data = res.data;
|
||||
} else {
|
||||
notification.error({
|
||||
message: "error",
|
||||
description: res.msg,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.getPermissions();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.editable-row-operations a {
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user