27 Commits

Author SHA1 Message Date
许晓东
fe759aaf74 集群字段长度增加到1024,修复漏洞(CVE-2023-20860):spring升级到5.3.26 2023-04-11 12:25:24 +08:00
许晓东
1e6a7bb269 polish 文档. 2023-02-10 20:41:15 +08:00
许晓东
fb440ae153 polish 文档. 2023-02-10 20:28:23 +08:00
许晓东
07c6714fd9 替换淘宝镜像,限制客户端ID太长显示宽度. 2023-02-09 22:23:34 +08:00
许晓东
5e9304efc2 增加限流说明,fixed acl 开启授权提示. 2023-02-07 22:24:37 +08:00
许晓东
fc7f05cf4e 限流,支持用户and客户端ID同时存在. 2023-02-06 22:11:56 +08:00
许晓东
5a87e9cad8 限流,支持用户. 2023-02-05 23:08:14 +08:00
许晓东
608f7cdc47 限流,支持客户端ID修改、删除. 2023-02-05 22:01:37 +08:00
许晓东
ee6defe5d2 限流,支持客户端ID查询. 2023-02-04 21:28:51 +08:00
许晓东
56621e0b8c 新增客户端限流菜单页. 2023-01-30 21:40:11 +08:00
许晓东
832b20a83e 客户端限流查询接口. 2023-01-09 22:11:50 +08:00
许晓东
4dbadee0d4 客户端限流 service. 2023-01-03 22:01:43 +08:00
许晓东
7d76632f08 客户端限流console 2023-01-03 21:28:11 +08:00
许晓东
d5102f626c 客户端限流console. 2023-01-02 21:28:47 +08:00
许晓东
daf77290da 客户端限流console. 2023-01-02 21:16:22 +08:00
许晓东
4df20f9ca5 contact jpg 2022-12-09 09:21:10 +08:00
许晓东
b465ba78b8 update contact 2022-11-01 19:05:40 +08:00
许晓东
9a69bad93a wechat contact. 2022-10-16 13:32:41 +08:00
许晓东
3785e9aaca wechat contact. 2022-10-16 13:31:35 +08:00
许晓东
d502da1b39 update weixin contact 2022-09-20 20:07:06 +08:00
许晓东
d6282cb902 认证授权分离页面未开启ACL时,显示效果. 2022-08-28 22:48:21 +08:00
许晓东
ca4dc2ebc9 Polish READM. 2022-08-28 22:04:31 +08:00
许晓东
50775994b5 Polish READM. 2022-08-28 22:02:21 +08:00
许晓东
3bd14a35d6 ACL的认证和授权管理功能分离. 2022-08-28 21:39:08 +08:00
许晓东
4c3fe5230c update contact jpg. 2022-08-04 09:12:39 +08:00
许晓东
57d549635b 恢复interceptory路径. 2022-07-27 18:19:50 +08:00
许晓东
923b89b6bd 更新下载地址. 2022-07-24 17:33:22 +08:00
45 changed files with 3277 additions and 620 deletions

View File

@@ -8,7 +8,9 @@
## 集群迁移支持说明
当前主分支及日后版本不再提供消息同步、集群迁移的解决方案,如有需要,查看:[集群迁移说明](./document/datasync/集群迁移.md)
## ACL说明
acl配置说明如果kafka集群启用了ACL但是控制台没看到Acl菜单可以查看[Acl配置启用说明](./document/acl/Acl.md)
最新代码运行即可看到acl菜单将权限管理和认证的用户管理SASL_SCRAM)进行了分离。分离之后支持只开启SASL_SCRAM认证的时候未开启鉴权用户变更操作。或者使用其它认证机制下的权限管理操作可视化的权限管理但是可视化的认证用户管理目前只支持Scram。
v1.0.6版本之前如果kafka集群启用了ACL但是控制台没看到Acl菜单可以查看[Acl配置启用说明](./document/acl/Acl.md)
## 功能支持
* 多集群支持
* 集群信息
@@ -16,15 +18,18 @@ acl配置说明如果kafka集群启用了ACL但是控制台没看到Acl菜
* 消费组管理
* 消息管理
* ACL
* 客户端限流
* 运维
功能明细看这个脑图:
![功能特性](./document/img/功能特性.png)
## 安装包下载
点击下载(v1.0.4版本)[kafka-console-ui.zip](https://github.com/xxd763795151/kafka-console-ui/releases/download/v1.0.4/kafka-console-ui.zip)
点击下载(v1.0.6版本)[kafka-console-ui.zip](https://github.com/xxd763795151/kafka-console-ui/releases/download/v1.0.6/kafka-console-ui.zip)
如果安装包下载的比较慢,可以查看下面的源码打包说明,把代码下载下来,快速打包不过最新main分支代码刚升级了kafka版本到3.2.0,还没有充分测试,如果需要稳定版本,可以下载 1.0.4-release分支代码
如果安装包下载的比较慢,可以查看下面的源码打包说明,把代码下载下来,本地快速打包.
github下载慢也可以试试从gitee下载点击下载[gitee来源kafka-console-ui.zip](https://gitee.com/xiaodong_xu/kafka-console-ui/releases/download/v1.0.6/kafka-console-ui.zip)
## 快速使用
### Windows
@@ -84,9 +89,10 @@ sh bin/shutdown.sh
## 联系方式
+ 微信群
<img src="./document/contact/weixin_contact.jpg" width="40%"/>
[//]: # (<img src="https://github.com/xxd763795151/kafka-console-ui/blob/main/document/contact/weixin_contact.jpeg" width="40%"/>)
[//]: # (<img src="https://github.com/xxd763795151/kafka-console-ui/blob/main/document/contact/weixin_contact.jpg" width="40%"/>)
+ 若联系方式失效, 请联系加一下微信, 说明意图
- xxd763795151

View File

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

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 178 KiB

View File

@@ -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

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 439 KiB

View File

@@ -10,7 +10,7 @@
</parent>
<groupId>com.xuxd</groupId>
<artifactId>kafka-console-ui</artifactId>
<version>1.0.5</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>
@@ -219,7 +220,8 @@
<goal>npm</goal>
</goals>
<configuration>
<arguments>install --registry=https://registry.npmjs.org/</arguments>
<!-- <arguments>install &#45;&#45;registry=https://registry.npmjs.org/</arguments>-->
<arguments>install --registry=https://registry.npm.taobao.org</arguments>
</configuration>
</execution>
<execution>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package com.xuxd.kafka.console.service.interceptor;
package com.xuxd.kafka.console.interceptor;
import com.xuxd.kafka.console.beans.ResponseData;
import javax.servlet.http.HttpServletRequest;

View File

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

View File

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

View File

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

View File

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

View File

@@ -73,6 +73,10 @@ public class ClusterServiceImpl implements ClusterService {
}
@Override public ResponseData updateClusterInfo(ClusterInfoDO infoDO) {
if (infoDO.getProperties() == null) {
// null 的话不更新这个是bug设置为空字符串解决
infoDO.setProperties("");
}
clusterInfoMapper.updateById(infoDO);
return ResponseData.create().success();
}

View File

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

View File

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

View File

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

160
ui/package-lock.json generated
View File

@@ -1820,6 +1820,63 @@
"integrity": "sha1-/q7SVZc9LndVW4PbwIhRpsY1IPo=",
"dev": true
},
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"optional": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"optional": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"optional": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"optional": true
},
"loader-utils": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
"dev": true,
"optional": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"ssri": {
"version": "8.0.1",
"resolved": "https://registry.nlark.com/ssri/download/ssri-8.0.1.tgz",
@@ -1828,6 +1885,28 @@
"requires": {
"minipass": "^3.1.1"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"optional": true,
"requires": {
"has-flag": "^4.0.0"
}
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.8.3",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz",
"integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==",
"dev": true,
"optional": true,
"requires": {
"chalk": "^4.1.0",
"hash-sum": "^2.0.0",
"loader-utils": "^2.0.0"
}
}
}
},
@@ -12097,87 +12176,6 @@
}
}
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.8.3",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz",
"integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==",
"dev": true,
"optional": true,
"requires": {
"chalk": "^4.1.0",
"hash-sum": "^2.0.0",
"loader-utils": "^2.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"optional": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"optional": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"optional": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"optional": true
},
"loader-utils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
"dev": true,
"optional": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"optional": true,
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"vue-ref": {
"version": "2.0.0",
"resolved": "https://registry.npm.taobao.org/vue-ref/download/vue-ref-2.0.0.tgz",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -50,25 +50,33 @@
>新增</a-button
>
<a-popconfirm
title="删除这些Topic?"
ok-text="确认"
cancel-text="取消"
@confirm="deleteTopics(selectedRowKeys)"
title="删除这些Topic?"
ok-text="确认"
cancel-text="取消"
@confirm="deleteTopics(selectedRowKeys)"
>
<a-button type="danger" class="btn-left" :disabled="!hasSelected" :loading="loading">
<a-button
type="danger"
class="btn-left"
:disabled="!hasSelected"
:loading="loading"
>
批量删除
</a-button>
</a-popconfirm>
<span style="margin-left: 8px">
<template v-if="hasSelected">
{{ `已选择 ${selectedRowKeys.length} 个Topic` }}
</template>
</span>
<template v-if="hasSelected">
{{ `已选择 ${selectedRowKeys.length} 个Topic` }}
</template>
</span>
</div>
<a-table
:columns="columns"
:data-source="filteredData"
:row-selection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }"
:row-selection="{
selectedRowKeys: selectedRowKeys,
onChange: onSelectChange,
}"
bordered
row-key="name"
>
@@ -282,7 +290,7 @@ export default {
request({
url: KafkaTopicApi.deleteTopic.url,
method: KafkaTopicApi.deleteTopic.method,
data: topics
data: topics,
}).then((res) => {
if (res.code == 0) {
this.$message.success(res.msg);
@@ -297,7 +305,7 @@ export default {
});
},
deleteTopic(topic) {
this.deleteTopics([topic])
this.deleteTopics([topic]);
},
onTopicUpdate(input) {
this.filterTopic = input.target.value;