4 Commits

Author SHA1 Message Date
yinuo
8942760a5a basic user auth 2022-05-06 10:36:00 +08:00
Xiaodong Xu
b1feaad9f7 Merge pull request #14 from dongyinuo/feature/dongyinuo/add/contact
Feature/dongyinuo/add/contact
2022-04-29 17:27:19 +08:00
yinuo
4d372f8374 添加联系方式 2022-04-29 17:22:44 +08:00
yinuo
4b2c544c0d 添加联系方式 2022-04-29 17:21:32 +08:00
60 changed files with 2273 additions and 13977 deletions

View File

@@ -70,3 +70,11 @@ sh bin/shutdown.sh
## 本地开发
如果需要本地开发,开发环境配置查看:[本地开发](./document/develop/开发配置.md)
## 联系方式
+ 微信群
<img src="https://github.com/dongyinuo/kafka-console-ui/blob/feature/dongyinuo/add/contact/document/contact/weixin_contact.jpeg" width="40%"/>
+ 若联系方式失效, 请联系加一下微信, 说明意图
- xxd763795151
- wxid_7jy2ezljvebt12

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

View File

@@ -25,8 +25,14 @@
<maven.assembly.plugin.version>3.0.0</maven.assembly.plugin.version>
<mybatis-plus-boot-starter.version>3.4.2</mybatis-plus-boot-starter.version>
<scala.version>2.13.6</scala.version>
<jwt.version>0.9.0</jwt.version>
</properties>
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

13125
ui/package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

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

View File

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

BIN
ui/src/assets/bg.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

3
ui/src/utils/role.js Normal file
View File

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

87
ui/src/utils/validate.js Normal file
View File

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

View File

@@ -1,157 +1,160 @@
<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>
<div>
<Header/>
<div class="content">
<a-spin :spinning="loading">
<div class="acl">
<div id="components-form-acl-advanced-search">
<a-form
class="ant-advanced-search-form"
:form="form"
@submit="handleSearch"
>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item :label="`用户名`">
<a-input
placeholder="username"
class="input-w"
v-decorator="['username']"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="`topic`">
<a-input
placeholder="topic"
class="input-w"
v-decorator="['topic']"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="`消费组`">
<a-input
placeholder="groupId"
class="input-w"
v-decorator="['groupId']"
/>
</a-form-item>
</a-col>
<a-col :span="24" :style="{ textAlign: 'right' }">
<a-button type="primary" html-type="submit"> 搜索</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleReset">
重置
</a-button>
</a-col>
</a-row>
</a-form>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="updateUser">新增/更新用户</a-button>
<UpdateUser
:visible="showUpdateUser"
@updateUserDialogData="closeUpdateUserDialog"
></UpdateUser>
</div>
<a-table :columns="columns" :data-source="data" bordered>
<div slot="username" slot-scope="username">
<a-col :span="24" :style="{ textAlign: 'right' }">
<a-button type="primary" html-type="submit"> 搜索</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleReset">
重置
</a-button>
</a-col>
</a-row>
</a-form>
</div>
<div class="operation-row-button">
<a-button type="primary" @click="updateUser">新增/更新用户</a-button>
<UpdateUser
:visible="showUpdateUser"
@updateUserDialogData="closeUpdateUserDialog"
></UpdateUser>
</div>
<a-table :columns="columns" :data-source="data" bordered>
<div slot="username" slot-scope="username">
<span>{{ username }}</span
><a-button
size="small"
shape="round"
type="dashed"
style="float: right"
@click="onUserDetail(username)"
>详情</a-button
size="small"
shape="round"
type="dashed"
style="float: right"
@click="onUserDetail(username)"
>详情</a-button
>
</div>
</div>
<div slot="topicList" slot-scope="topicList, record">
<a
href="#"
v-for="t in topicList"
:key="t"
@click="onTopicDetail(t, record.username)"
<div slot="topicList" slot-scope="topicList, record">
<a
href="#"
v-for="t in topicList"
:key="t"
@click="onTopicDetail(t, record.username)"
><div style="border-bottom: 1px solid #e5e1e1">{{ t }}</div>
</a>
</div>
</a>
</div>
<div slot="groupList" slot-scope="groupList, record">
<a
href="#"
v-for="t in groupList"
:key="t"
@click="onGroupDetail(t, record.username)"
<div slot="groupList" slot-scope="groupList, record">
<a
href="#"
v-for="t in groupList"
:key="t"
@click="onGroupDetail(t, record.username)"
><div style="border-bottom: 1px solid #e5e1e1">{{ t }}</div>
</a>
</div>
</a>
</div>
<div
slot="operation"
slot-scope="record"
v-show="!record.user || record.user.role != 'admin'"
>
<a-popconfirm
:title="'删除用户: ' + record.username + '及相关权限?'"
ok-text="确认"
cancel-text="取消"
@confirm="onDeleteUser(record)"
<div
slot="operation"
slot-scope="record"
v-show="!record.user || record.user.role != 'admin'"
>
<a-button size="small" href="javascript:;" class="operation-btn"
>删除</a-button
<a-popconfirm
:title="'删除用户: ' + record.username + '及相关权限?'"
ok-text="确认"
cancel-text="取消"
@confirm="onDeleteUser(record)"
>
</a-popconfirm>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onManageProducerAuth(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>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onManageConsumerAuth(record)"
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onManageConsumerAuth(record)"
>管理消费权限
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onAddAuth(record)"
</a-button>
<a-button
size="small"
href="javascript:;"
class="operation-btn"
@click="onAddAuth(record)"
>增加权限
</a-button>
</div>
</a-table>
<UserDetail
:visible="openUserDetailDialog"
:username="selectDetail.username"
@userDetailDialog="closeUserDetailDialog"
></UserDetail>
<AclDetail
:visible="openAclDetailDialog"
:selectDetail="selectDetail"
@aclDetailDialog="closeAclDetailDialog"
></AclDetail>
<ManageProducerAuth
:visible="openManageProducerAuthDialog"
:record="selectRow"
@manageProducerAuthDialog="closeManageProducerAuthDialog"
></ManageProducerAuth>
<ManageConsumerAuth
:visible="openManageConsumerAuthDialog"
:record="selectRow"
@manageConsumerAuthDialog="closeManageConsumerAuthDialog"
></ManageConsumerAuth>
<AddAuth
:visible="openAddAuthDialog"
:record="selectRow"
@addAuthDialog="closeAddAuthDialog"
></AddAuth>
</div>
</a-spin>
</a-button>
</div>
</a-table>
<UserDetail
:visible="openUserDetailDialog"
:username="selectDetail.username"
@userDetailDialog="closeUserDetailDialog"
></UserDetail>
<AclDetail
:visible="openAclDetailDialog"
:selectDetail="selectDetail"
@aclDetailDialog="closeAclDetailDialog"
></AclDetail>
<ManageProducerAuth
:visible="openManageProducerAuthDialog"
:record="selectRow"
@manageProducerAuthDialog="closeManageProducerAuthDialog"
></ManageProducerAuth>
<ManageConsumerAuth
:visible="openManageConsumerAuthDialog"
:record="selectRow"
@manageConsumerAuthDialog="closeManageConsumerAuthDialog"
></ManageConsumerAuth>
<AddAuth
:visible="openAddAuthDialog"
:record="selectRow"
@addAuthDialog="closeAddAuthDialog"
></AddAuth>
</div>
</a-spin>
</div>
</div>
</template>
@@ -165,7 +168,7 @@ import ManageConsumerAuth from "@/views/acl/ManageConsumerAuth";
import AddAuth from "@/views/acl/AddAuth";
import AclDetail from "@/views/acl/AclDetail";
import UserDetail from "@/views/acl/UserDetail";
import Header from "@/components/Header"
export default {
name: "Acl",
components: {
@@ -175,6 +178,7 @@ export default {
AddAuth,
AclDetail,
UserDetail,
Header
},
data() {
return {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,21 @@
<template>
<div class="content">
<a-spin :spinning="loading">
<a-tabs default-active-key="1" size="large" tabPosition="top">
<a-tab-pane key="1" tab="根据时间查询消息">
<SearchByTime :topic-list="topicList"></SearchByTime>
</a-tab-pane>
<a-tab-pane key="2" tab="根据偏移查询消息">
<SearchByOffset :topic-list="topicList"></SearchByOffset>
</a-tab-pane>
<a-tab-pane key="3" tab="在线发送">
<SendMessage :topic-list="topicList"></SendMessage>
</a-tab-pane>
</a-tabs>
</a-spin>
<div>
<Header/>
<div class="content">
<a-spin :spinning="loading">
<a-tabs default-active-key="1" size="large" tabPosition="top">
<a-tab-pane key="1" tab="根据时间查询消息">
<SearchByTime :topic-list="topicList"></SearchByTime>
</a-tab-pane>
<a-tab-pane key="2" tab="根据偏移查询消息">
<SearchByOffset :topic-list="topicList"></SearchByOffset>
</a-tab-pane>
<a-tab-pane key="3" tab="在线发送">
<SendMessage :topic-list="topicList"></SendMessage>
</a-tab-pane>
</a-tabs>
</a-spin>
</div>
</div>
</template>
@@ -23,9 +26,10 @@ import request from "@/utils/request";
import { KafkaTopicApi } from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
import SendMessage from "@/views/message/SendMessage";
import Header from "@/components/Header"
export default {
name: "Message",
components: { SearchByTime, SearchByOffset, SendMessage },
components: { SearchByTime, SearchByOffset, SendMessage, Header },
data() {
return {
loading: false,

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,123 @@
<template>
<a-modal
title="重置密码"
:visible="show"
:width="800"
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
@cancel="handleCancel"
>
<div>
<a-spin :spinning="loading">
<a-form
:form="form"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 12 }"
@submit="handleSubmit"
>
<a-form-item label="账号">
<a-input
:disabled="true"
v-decorator="['username', { initialValue: username }]"
placeholder="username"
/>
</a-form-item>
<a-form-item label="密码">
<a-input
v-decorator="[
'password',
{ rules: [{ required: true, message: '请输入密码!' }] },
]"
placeholder="请输入密码"
/>
</a-form-item>
<a-form-item :wrapper-col="{ span: 12, offset: 5 }">
<a-button type="primary" html-type="submit"> 提交 </a-button>
</a-form-item>
</a-form>
</a-spin>
</div>
</a-modal>
</template>
<script>
import request from "@/utils/request";
import { DevOpsUserAPi } from "@/utils/api";
import notification from "ant-design-vue/es/notification";
export default {
name: "UpdatePassword",
props: {
username: {
type: String,
default: "",
},
visible: {
type: Boolean,
default: false,
},
},
data() {
return {
show: this.visible,
data: [],
loading: false,
form: this.$form.createForm(this, { name: "coordinated" }),
};
},
watch: {
visible(v) {
this.show = v;
},
},
methods: {
handleSubmit(e) {
e.preventDefault();
this.form.validateFields((err, values) => {
if (!err) {
if (values.assignment) {
const assignment = {};
values.assignment.split("\n").forEach((e) => {
const c = e.split("=");
if (c.length > 1) {
let k = c[0];
let v = c[1];
let arr = v.split(",");
if (arr.length > 0) {
assignment[k] = arr;
}
}
});
values.assignment = assignment;
}
this.loading = true;
request({
url: DevOpsUserAPi.updateUser.url,
method: DevOpsUserAPi.updateUser.method,
data: values,
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.$message.success(res.msg);
this.$emit("closeResetPasswordDialog", { refresh: true });
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
}
});
},
handleCancel() {
this.data = [];
this.$emit("closeResetPasswordDialog", { refresh: false });
},
},
};
</script>
<style scoped></style>

227
ui/src/views/user/index.vue Normal file
View File

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