31 Commits
v1.0.9 ... main

Author SHA1 Message Date
许晓东
f0369c6246 优化linux启停脚本 2025-08-06 20:21:02 +08:00
许晓东
0027f55184 windows启动脚本支持任意路径,增加PowerShell启动脚本. 2025-07-30 20:57:56 +08:00
许晓东
745aa6e9bc 升级v1.0.13版本 2025-07-01 20:34:22 +08:00
许晓东
1e86aa4569 增加消息转发功能. 2025-06-16 20:28:26 +08:00
许晓东
780d05bc06 增加消息转发接口. 2025-06-11 20:12:09 +08:00
许晓东
a755ffb010 消息详情对话框点击模态框的遮罩层关闭模态框. 2025-05-27 20:52:37 +08:00
许晓东
39544327ee 补充TopicConsole一个注释. 2025-05-20 21:07:08 +08:00
许晓东
9b7263241c broker配置mask close默认设置为true 2025-04-24 20:28:33 +08:00
许晓东
680630d372 Topic菜单消费详情支持点击刷新 2025-03-16 19:47:42 +08:00
许晓东
d554053e44 发布v1.0.12版本 2025-02-24 22:53:02 +08:00
许晓东
ba0a12fd5f 升级v1.0.12版本 2025-02-24 22:39:27 +08:00
许晓东
a927dd412e 消费组支持模糊过滤查询. 2025-02-22 20:56:00 +08:00
许晓东
a48812ed0e message提示框显示1秒,最多显示1条. 2025-01-12 22:02:49 +08:00
许晓东
dd2863e51d topic-发送统计: 增加刷新按钮 2024-12-16 20:50:54 +08:00
许晓东
89846018cc 发布1.0.11版本 2024-10-06 12:55:43 +08:00
许晓东
19cc635151 升级1.0.11版本 2024-10-06 11:30:08 +08:00
许晓东
a1206fb094 主页显示broker版本 2024-09-01 21:47:35 +08:00
许晓东
83c018f6a7 计算broker版本 2024-08-30 22:57:06 +08:00
许晓东
2729627a80 更新jetbrains 最新logo 2024-08-14 21:42:20 +08:00
许晓东
096792beb0 集群信息/topic:副本,配置模态框点击背景关闭#26 2024-07-07 21:10:00 +08:00
许晓东
fd495351bc 发布v1.0.10版本. 2024-06-16 21:06:54 +08:00
许晓东
ecbd9dda6a 开启集群数据权限,同一浏览器不同账号登录看到其它账号集群信息bug fixed. 2024-06-16 20:36:00 +08:00
许晓东
30707e84a7 brokerId不是从0开始,topic管理->变更副本失败fixed. 2024-06-11 20:00:23 +08:00
许晓东
f98cca8727 升级kafka到3.5.0版本. 2024-05-23 21:37:38 +08:00
许晓东
ea95697e88 长级kafka到3.5.0版本. 2024-05-23 21:35:34 +08:00
许晓东
e90268a56e issue #36, searchByTime maxNums 改为配置. 2024-03-03 21:46:45 +08:00
许晓东
fcc315782c 更新wechat. 2024-02-25 21:47:06 +08:00
许晓东
b79529f607 调整acl user用户名/密码长度. 2024-01-15 20:38:30 +08:00
许晓东
f49e2f7d0c 更新功能脑图 2024-01-06 20:48:50 +08:00
许晓东
4929953acd 登录按钮支持回车登录. 2023-12-23 21:14:38 +08:00
许晓东
8c946a96ba 发布1.0.9版本. 2023-12-06 20:41:37 +08:00
49 changed files with 893 additions and 65 deletions

View File

@@ -25,16 +25,16 @@ v1.0.6版本之前如果kafka集群启用了ACL但是控制台没看到Acl
![功能特性](./document/img/功能特性.png)
## 安装包下载
点击下载(v1.0.8版本)[kafka-console-ui.zip](https://github.com/xxd763795151/kafka-console-ui/releases/download/v1.0.8/kafka-console-ui.zip)
点击下载(v1.0.13版本)[kafka-console-ui.zip](https://github.com/xxd763795151/kafka-console-ui/releases/download/v1.0.13/kafka-console-ui-1.0.13.zip)
如果安装包下载的比较慢,可以查看下面的源码打包说明,把代码下载下来,本地快速打包.
github下载慢也可以试试从gitee下载点击下载[gitee来源kafka-console-ui.zip](https://gitee.com/xiaodong_xu/kafka-console-ui/releases/download/v1.0.8/kafka-console-ui.zip)
github下载慢也可以试试从gitee下载点击下载[gitee来源kafka-console-ui.zip](https://gitee.com/xiaodong_xu/kafka-console-ui/releases/download/v1.0.13/kafka-console-ui-1.0.13.zip)
## 快速使用
### Windows
1. 解压缩zip安装包
2. 进入bin目录必须在bin目录下双击执行`start.bat`启动
2. 进入bin目录, 双击执行`start.bat`启动; 如果使用PowerShell, 也可以选择运行`start.ps1`启动
3. 停止:直接关闭启动的命令行窗口即可
### Linux或Mac OS
@@ -68,7 +68,7 @@ sh bin/shutdown.sh
在新增集群的时候除了集群地址还可以输入集群的其它属性配置比如请求超时ACL配置等。如果开启了ACL切换到该集群的时候导航栏上便会出现ACL菜单支持进行相关操作目前是基于SASL_SCRAM认证授权管理支持的最完善其它的我也没验证过虽然是我开发的但是我也没具体全部验证这一块功能授权部分应该是通用的
## kafka版本
* 当前使用的kafka 3.2.0
* 当前使用的kafka 3.5.0
## 监控
仅提供运维管理功能监控、告警需要配合其它组件如有需要建议请查看https://blog.csdn.net/x763795151/article/details/119705372
@@ -99,7 +99,11 @@ auth:
## DockerCompose部署
感谢@wdkang123 同学分享的部署方式,如果有需要请查看[DockerCompose部署方式](./document/deploy/docker部署.md)
## 感谢支持
感谢jetbrains的开源支持如果有朋友愿意一起维护很欢迎提pr.
[![jetbrains](./document/img/jb_beam.svg "jetbrains")](https://jb.gg/OpenSourceSupport)
jetbrains官方地址: https://www.jetbrains.com/
## 联系方式
+ 微信群
@@ -107,6 +111,14 @@ auth:
[//]: # (<img src="https://github.com/xxd763795151/kafka-console-ui/blob/main/document/contact/weixin_contact.jpg" width="40%"/>)
+ 若联系方式失效, 请联系加一下微信, 说明意图
- xxd763795151
- wxid_7jy2ezljvebt12
抱歉,后面就不再提供新的联系方式加群了。
在很早之前,有个兄弟提了个建议,可以拉个群,大家可以一起交流,所以成立了一个群,当时我以为没有多少朋友愿意加入。
可是后来确实有不少朋友进群了这让我很惶恐其实这个平台我自己并不觉得有太多技术深度却有一些朋友愿意来捧场这个让我觉得很惭愧所以现在考虑了下如果真有使用问题可以留个issue。
另外,我自己也确实挺忙,对于这个项目的需求处理不够及时,实在是时间和精力上有限,所以有朋友希望新增的一些能力,拖了这么久我也没有下文,实在是抱歉。
对于一些不太耗时的功能,我还是可以积极处理的。
另外有些功能,是想要放到后面再加的,所以迟迟没有动手。

View File

@@ -23,6 +23,7 @@
<includes>
<include>*.sh</include>
<include>*.bat</include>
<include>*.ps1</include>
</includes>
<outputDirectory>bin</outputDirectory>
<fileMode>0755</fileMode>

View File

@@ -1,13 +1,61 @@
#!/bin/bash
PREFIX='./'
CMD=$0
if [[ $CMD == $PREFIX* ]]; then
CMD=${CMD:2}
# 获取脚本真实路径兼容Linux和macOS
if [ -L "$0" ]; then
# 处理符号链接
if command -v greadlink >/dev/null 2>&1; then
SCRIPT_PATH=$(greadlink -f "$0")
elif command -v readlink >/dev/null 2>&1; then
SCRIPT_PATH=$(readlink -f "$0")
else
# 使用perl作为备选
SCRIPT_PATH=$(perl -e 'use Cwd "abs_path"; print abs_path(shift)' "$0" 2>/dev/null)
fi
else
SCRIPT_PATH="$0"
fi
SCRIPT_DIR=$(dirname "`pwd`/$CMD")
PROJECT_DIR=`dirname "$SCRIPT_DIR"`
# 最终回退方案
if [ -z "$SCRIPT_PATH" ] || [ ! -f "$SCRIPT_PATH" ]; then
SCRIPT_PATH="$0"
fi
# 计算项目根目录
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
PROJECT_DIR=$(cd "$SCRIPT_DIR" && cd .. && pwd 2>/dev/null)
# 验证项目目录是否存在
if [ ! -d "$PROJECT_DIR" ]; then
echo "ERROR: Failed to determine project directory!" >&2
exit 1
fi
# 不要修改进程标记,作为进程属性关闭使用
PROCESS_FLAG="kafka-console-ui-process-flag:${PROJECT_DIR}"
pkill -f $PROCESS_FLAG
echo 'Stop Kafka-console-ui!'
# 停止前检查进程是否存在
if pgrep -f "$PROCESS_FLAG" >/dev/null; then
# 先尝试正常停止
pkill -f "$PROCESS_FLAG"
# 等待进程退出
TIMEOUT=10
while [ $TIMEOUT -gt 0 ]; do
if ! pgrep -f "$PROCESS_FLAG" >/dev/null; then
break
fi
sleep 1
TIMEOUT=$((TIMEOUT - 1))
done
# 检查是否仍有进程存在
if pgrep -f "$PROCESS_FLAG" >/dev/null; then
# 强制终止
pkill -9 -f "$PROCESS_FLAG"
echo "Stop Kafka-console-ui! (force killed)"
else
echo "Stop Kafka-console-ui! (gracefully stopped)"
fi
else
echo "Kafka-console-ui is not running."
fi

View File

@@ -1,8 +1,38 @@
@echo off
rem MAIN_CLASS=org.springframework.boot.loader.JarLauncher
rem JAVA_HOME=jre1.8.0_66
set JAVA_CMD=%JAVA_HOME%\bin\java
set JAVA_OPTS=-Xmx512m -Xms512m -Xmn256m -Xss256k -Dfile.encoding=utf-8
set CONFIG_FILE=../config/application.yml
set TARGET=../lib/kafka-console-ui.jar
set DATA_DIR=..
"%JAVA_CMD%" -jar %TARGET% --spring.config.location=%CONFIG_FILE% --data.dir=%DATA_DIR%
setlocal enabledelayedexpansion
set "BIN_DIR=%~dp0"
if not "%BIN_DIR:~-1%"=="\" set "BIN_DIR=%BIN_DIR%\"
set "BASE_DIR=%BIN_DIR%.."
for %%I in ("%BASE_DIR%") do set "BASE_DIR=%%~fI"
if defined JAVA_HOME (
set "JAVA_CMD=%JAVA_HOME%\bin\java"
) else (
echo ERROR: JAVA_HOME is not defined
exit /b 1
)
set "JAVA_OPTS=-Xmx512m -Xms512m -Xmn256m -Xss256k -Dfile.encoding=utf-8"
set "CONFIG_FILE=%BASE_DIR%\config\application.yml"
set "TARGET=%BASE_DIR%\lib\kafka-console-ui.jar"
set "DATA_DIR=%BASE_DIR%"
set "LOG_HOME=%BASE_DIR%"
if not exist "%TARGET%" (
echo ERROR: Jar file not found at [%TARGET%]
exit /b 1
)
if not exist "%CONFIG_FILE%" (
echo WARNING: Config file not found at [%CONFIG_FILE%]
)
"%JAVA_CMD%" %JAVA_OPTS% -jar "%TARGET%" --spring.config.location="%CONFIG_FILE%" --data.dir="%DATA_DIR%" --logging.home="%LOG_HOME%"
endlocal

41
bin/start.ps1 Normal file
View File

@@ -0,0 +1,41 @@
# PowerShell
# Set the script execution policy. If necessary, execute this command in PowerShell and then run the script.
# Set-ExecutionPolicy Bypass -Scope Process -Force
param()
$BIN_DIR = $PSScriptRoot
if (-not $BIN_DIR.EndsWith('\')) {
$BIN_DIR += '\'
}
$BASE_DIR = (Get-Item (Join-Path $BIN_DIR "..")).FullName
if (-not $env:JAVA_HOME) {
Write-Error "ERROR: JAVA_HOME is not defined"
exit 1
}
$JAVA_OPTS = "-Xmx512m -Xms512m -Xmn256m -Xss256k -Dfile.encoding=utf-8"
$CONFIG_FILE = Join-Path $BASE_DIR "config\application.yml"
$TARGET = Join-Path $BASE_DIR "lib\kafka-console-ui.jar"
$DATA_DIR = $BASE_DIR
$LOG_HOME = $BASE_DIR
if (-not (Test-Path $TARGET -PathType Leaf)) {
Write-Error "ERROR: Jar file not found at [$TARGET]"
exit 1
}
if (-not (Test-Path $CONFIG_FILE -PathType Leaf)) {
Write-Warning "WARNING: Config file not found at [$CONFIG_FILE]"
}
$javaCmd = Join-Path $env:JAVA_HOME "bin\java.exe"
& $javaCmd $JAVA_OPTS.Split() `
-jar $TARGET `
"--spring.config.location=$CONFIG_FILE" `
"--data.dir=$DATA_DIR" `
"--logging.home=$LOG_HOME"

View File

@@ -1,29 +1,55 @@
#!/bin/bash
# 设置jvm堆大小及栈大小栈大小最少设置为256K不要小于这个值比如设置为128太小了
# 设置jvm堆大小及栈大小
JAVA_MEM_OPTS="-Xmx512m -Xms512m -Xmn256m -Xss256k"
PREFIX='./'
CMD=$0
if [[ $CMD == $PREFIX* ]]; then
CMD=${CMD:2}
# 获取脚本真实路径兼容Linux和macOS
if [ -L "$0" ]; then
# 处理符号链接
if command -v greadlink >/dev/null 2>&1; then
# macOS使用greadlink需brew install coreutils
SCRIPT_PATH=$(greadlink -f "$0")
else
# Linux使用readlink
SCRIPT_PATH=$(readlink -f "$0")
fi
else
SCRIPT_PATH="$0"
fi
SCRIPT_DIR=$(dirname "`pwd`/$CMD")
PROJECT_DIR=`dirname "$SCRIPT_DIR"`
# 如果上述方法失败如macOS无greadlink使用替代方案
if [ -z "$SCRIPT_PATH" ] || [ ! -f "$SCRIPT_PATH" ]; then
# 使用perl跨平台解决方案
SCRIPT_PATH=$(perl -e 'use Cwd "abs_path"; print abs_path(shift)' "$0" 2>/dev/null)
fi
# 最终回退方案
if [ -z "$SCRIPT_PATH" ] || [ ! -f "$SCRIPT_PATH" ]; then
SCRIPT_PATH="$0"
fi
# 计算项目根目录
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
PROJECT_DIR=$(cd "$SCRIPT_DIR" && cd .. && pwd)
CONF_FILE="$PROJECT_DIR/config/application.yml"
TARGET="$PROJECT_DIR/lib/kafka-console-ui.jar"
#设置h2文件根目录
DATA_DIR=$PROJECT_DIR
# 设置h2文件根目录
DATA_DIR="$PROJECT_DIR"
# 日志目录,默认为当前工程目录下
# 这个是错误输出如果启动命令有误输出到这个文件应用日志不会输出到error.out应用日志输出到上面的kafka-console-ui.log中
ERROR_OUT="$PROJECT_DIR/error.out"
# 不要修改进程标记作为进程属性关闭使用如果要修改请把shutdown.sh里的该属性的值保持一致
# 日志目录
LOG_HOME="$PROJECT_DIR"
PROCESS_FLAG="kafka-console-ui-process-flag:${PROJECT_DIR}"
JAVA_OPTS="$JAVA_OPTS $JAVA_MEM_OPTS -Dfile.encoding=utf-8"
nohup java -jar $JAVA_OPTS $TARGET --spring.config.location="$CONF_FILE" --logging.home="$PROJECT_DIR" --data.dir=$DATA_DIR $PROCESS_FLAG 1>/dev/null 2>$ERROR_OUT &
# 启动应用
nohup java -jar $JAVA_OPTS "$TARGET" \
--spring.config.location="$CONF_FILE" \
--logging.home="$LOG_HOME" \
--data.dir="$DATA_DIR" \
"$PROCESS_FLAG" >/dev/null 2>"$ERROR_OUT" &
echo "Kafka-console-ui Started!"
echo "Kafka-console-ui Started! PID: $!"

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 127 KiB

View File

@@ -1 +1,13 @@
<svg height="180" viewBox="0 0 180 180" width="180" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="32.64" x2="82.77" y1="61.16" y2="85.54"><stop offset=".21" stop-color="#fe2857"/><stop offset="1" stop-color="#293896"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="17.38" x2="82.95" y1="69.86" y2="21.23"><stop offset="0" stop-color="#fe2857"/><stop offset=".01" stop-color="#fe2857"/><stop offset=".86" stop-color="#ff318c"/></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="74.17" x2="160.27" y1="21.58" y2="99.76"><stop offset=".02" stop-color="#ff318c"/><stop offset=".21" stop-color="#fe2857"/><stop offset=".86" stop-color="#fdb60d"/></linearGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="155.46" x2="55.07" y1="89.8" y2="158.9"><stop offset=".01" stop-color="#fdb60d"/><stop offset=".86" stop-color="#fcf84a"/></linearGradient><path d="m81.56 83.71-41.35-35a15 15 0 1 0 -14.47 25.7h.15l.39.12 52.16 15.89a3.53 3.53 0 0 0 1.18.21 3.73 3.73 0 0 0 1.93-6.91z" fill="url(#a)"/><path d="m89.85 25.93a10.89 10.89 0 0 0 -16.85-9.18l-50.5 30.66a15 15 0 1 0 17.9 24l45.27-36.89.36-.3a10.93 10.93 0 0 0 3.82-8.29z" fill="url(#b)"/><path d="m163.29 92-76.62-73.79a10.91 10.91 0 1 0 -14.81 16l.14.12 81.4 68.58a7.36 7.36 0 0 0 12.09-5.65 7.39 7.39 0 0 0 -2.2-5.26z" fill="url(#c)"/><path d="m165.5 97.29a7.35 7.35 0 0 0 -11.67-6l-92.71 45.3a15 15 0 1 0 15.48 25.59l85.73-58.84a7.35 7.35 0 0 0 3.17-6.05z" fill="url(#d)"/><path d="m60 60h60v60h-60z"/><g fill="#fff"><path d="m66.53 108.75h22.5v3.75h-22.5z"/><path d="m65.59 75.47 1.67-1.58a1.88 1.88 0 0 0 1.47.87c.64 0 1.06-.45 1.06-1.32v-5.92h2.58v5.94a3.44 3.44 0 0 1 -.92 2.63 3.52 3.52 0 0 1 -2.57 1 3.84 3.84 0 0 1 -3.29-1.62z"/><path d="m73.53 67.52h7.53v2.19h-5v1.43h4.49v2h-4.45v1.49h5v2.2h-7.6z"/><path d="m84.73 69.79h-2.8v-2.27h8.21v2.27h-2.81v7.09h-2.6z"/><path d="m66.63 80.58h4.42a3.47 3.47 0 0 1 2.55.83 2.09 2.09 0 0 1 .61 1.52 2.18 2.18 0 0 1 -1.45 2.09 2.27 2.27 0 0 1 1.86 2.29c0 1.69-1.31 2.69-3.55 2.69h-4.44zm5 2.89c0-.52-.42-.8-1.18-.8h-1.29v1.64h1.25c.78 0 1.24-.27 1.24-.81zm-.9 2.66h-1.57v1.73h1.62c.8 0 1.24-.31 1.24-.86-.02-.53-.4-.87-1.27-.87z"/><path d="m75.45 80.58h4.15a4.14 4.14 0 0 1 3.05 1 2.92 2.92 0 0 1 .83 2.18 3 3 0 0 1 -1.93 2.89l2.24 3.35h-3l-1.89-2.84h-.87v2.84h-2.6zm4 4.5c.87 0 1.4-.43 1.4-1.12 0-.75-.55-1.13-1.41-1.13h-1.39v2.27z"/><path d="m87.09 80.51h2.5l4 9.44h-2.79l-.67-1.69h-3.63l-.67 1.74h-2.71zm2.28 5.73-1.05-2.65-1.06 2.65z"/><path d="m94 80.55h2.6v9.37h-2.6z"/><path d="m97.56 80.55h2.44l3.37 5v-5h2.57v9.37h-2.27l-3.53-5.14v5.14h-2.58z"/><path d="m106.37 88.53 1.44-1.73a4.86 4.86 0 0 0 3 1.13c.71 0 1.08-.25 1.08-.65 0-.41-.3-.61-1.59-.91-2-.46-3.53-1-3.53-2.93 0-1.74 1.38-3 3.63-3a5.88 5.88 0 0 1 3.85 1.25l-1.25 1.78a4.56 4.56 0 0 0 -2.62-.92c-.63 0-.94.25-.94.6 0 .43.32.62 1.63.91 2.15.47 3.48 1.17 3.48 2.92 0 1.91-1.51 3-3.78 3a6.56 6.56 0 0 1 -4.4-1.45z"/></g><path d="m0 0h180v180h-180z" fill="none"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="298" height="64" fill="none" viewBox="0 0 298 64">
<defs>
<linearGradient id="a" x1=".850001" x2="62.62" y1="62.72" y2="1.81" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF9419"/>
<stop offset=".43" stop-color="#FF021D"/>
<stop offset=".99" stop-color="#E600FF"/>
</linearGradient>
</defs>
<path fill="#000" d="M86.4844 40.5858c0 .8464-.1792 1.5933-.5377 2.2505-.3585.6573-.8564 1.1651-1.5137 1.5236-.6572.3585-1.3941.5378-2.2406.5378H78v6.1044h5.0787c1.912 0 3.6248-.4282 5.1484-1.2846 1.5236-.8564 2.7186-2.0415 3.585-3.5452.8663-1.5037 1.3045-3.1966 1.3045-5.0886V21.0178h-6.6322v19.568Zm17.8556-1.8224h13.891v-5.6065H104.34v-6.3633h15.355v-5.7758H97.8766v29.9743h22.2464v-5.7757H104.34v-6.453Zm17.865-11.8005h8.882v24.0193h6.633V26.9629h8.842v-5.9451h-24.367v5.9551l.01-.01Zm47.022 9.0022c-.517-.2788-1.085-.4879-1.673-.6472.449-.1295.877-.2888 1.275-.488 1.096-.5676 1.962-1.3643 2.579-2.39.618-1.0257.936-2.2007.936-3.5351 0-1.5237-.418-2.8879-1.244-4.0929-.827-1.195-1.992-2.131-3.486-2.8082-1.494-.6672-3.206-1.0058-5.118-1.0058h-13.315v29.9743h13.574c2.011 0 3.804-.3485 5.387-1.0556 1.573-.707 2.798-1.6829 3.675-2.9476.866-1.2547 1.304-2.6887 1.304-4.302 0-1.4837-.338-2.8082-1.026-3.9833-.687-1.175-1.633-2.0812-2.858-2.7285l-.01.0099Zm-13.603-9.9184h5.886c.816 0 1.533.1494 2.161.4382.627.2888 1.115.707 1.464 1.2547.348.5378.527 1.1751.527 1.9021 0 .7269-.179 1.414-.527 1.9817-.349.5676-.837.9958-1.464 1.3045-.628.3087-1.345.4581-2.161.4581h-5.886v-7.3492.0099Zm10.138 18.134c-.378.5676-.916 1.0058-1.603 1.3145-.697.3087-1.484.4581-2.39.4581h-6.145v-7.6878h6.145c.886 0 1.673.1693 2.37.4979.687.3286 1.235.7867 1.613 1.3842.378.5975.578 1.2747.578 2.0414 0 .7668-.19 1.4241-.568 1.9917Zm29.596-5.3077c1.663-.7967 2.947-1.922 3.864-3.3659.916-1.444 1.374-3.117 1.374-5.0289 0-1.912-.448-3.5253-1.344-4.9592-.897-1.434-2.171-2.5394-3.814-3.3261-1.644-.7867-3.546-1.1751-5.717-1.1751h-13.124v29.9743h6.642V40.0779h4.322l6.084 10.9142h7.578l-6.851-11.7208c.339-.1195.677-.249.996-.3983h-.01Zm-2.151-6.1244c-.369.6274-.896 1.1154-1.583 1.444-.688.3386-1.494.5079-2.42.5079h-5.975v-8.2953h5.975c.926 0 1.732.1693 2.42.4979.687.3287 1.214.8166 1.583 1.434.368.6174.558 1.3544.558 2.1908 0 .8365-.19 1.5734-.558 2.2008v.0199Zm20.594-11.7308-10.706 29.9743h6.742l2.121-6.6122h11.114l2.27 6.6122h6.612L220.99 21.0178h-7.189Zm-.339 18.3431 3.445-10.5756.409-1.922.408 1.922 3.685 10.5756h-7.947Zm20.693 11.6312h6.851V21.0178h-6.851v29.9743Zm31.02-9.6993-12.896-20.275h-6.463v29.9743h6.055V30.7172l12.826 20.2749h6.533V21.0178h-6.055v20.275Zm31.528-3.3559c-.647-1.2448-1.564-2.2904-2.729-3.1369-1.165-.8464-2.509-1.4041-4.023-1.6929l-5.098-1.0456c-.797-.1892-1.434-.5178-1.902-.9958-.469-.478-.708-1.0755-.708-1.7825 0-.6473.17-1.205.518-1.683.339-.478.827-.8464 1.444-1.1153.618-.2689 1.335-.3983 2.151-.3983.817 0 1.554.1394 2.181.4182.627.2788 1.115.6672 1.464 1.1751s.528 1.0755.528 1.7228h6.642c-.04-1.7427-.528-3.2863-1.444-4.6207-.916-1.3443-2.201-2.3899-3.834-3.1468-1.633-.7568-3.505-1.1352-5.597-1.1352-2.091 0-3.943.3884-5.566 1.1751-1.623.7867-2.898 1.8721-3.804 3.2663-.906 1.3941-1.364 2.9775-1.364 4.76 0 1.444.288 2.7485.876 3.9036.587 1.1652 1.414 2.1311 2.479 2.8979 1.076.7668 2.311 1.3045 3.725 1.6033l5.397 1.1153c.886.2091 1.584.5975 2.101 1.1551.518.5577.767 1.2448.767 2.0813 0 .6672-.189 1.2747-.567 1.8025-.379.5277-.907.936-1.584 1.2248-.677.2888-1.474.4282-2.39.4282-.916 0-1.782-.1593-2.529-.478-.747-.3186-1.325-.7767-1.733-1.3742-.418-.5875-.617-1.2747-.617-2.0414h-6.642c.029 1.8721.527 3.5152 1.513 4.9492.976 1.424 2.32 2.5394 4.033 3.336 1.713.7967 3.675 1.195 5.886 1.195 2.21 0 4.202-.4083 5.915-1.2249 1.723-.8165 3.057-1.9418 4.023-3.3758.966-1.434 1.444-3.0572 1.444-4.8696 0-1.4838-.329-2.848-.976-4.1028l.02.01Z"/>
<path fill="url(#a)" d="M20.34 3.66 3.66 20.34C1.32 22.68 0 25.86 0 29.18V59c0 2.76 2.24 5 5 5h29.82c3.32 0 6.49-1.32 8.84-3.66l16.68-16.68c2.34-2.34 3.66-5.52 3.66-8.84V5c0-2.76-2.24-5-5-5H29.18c-3.32 0-6.49 1.32-8.84 3.66Z"/>
<path fill="#000" d="M48 16H8v40h40V16Z"/>
<path fill="#fff" d="M30 47H13v4h17v-4Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 439 KiB

After

Width:  |  Height:  |  Size: 605 KiB

10
pom.xml
View File

@@ -10,7 +10,7 @@
</parent>
<groupId>com.xuxd</groupId>
<artifactId>kafka-console-ui</artifactId>
<version>1.0.9</version>
<version>1.0.13</version>
<name>kafka-console-ui</name>
<description>Kafka console manage ui</description>
<properties>
@@ -21,7 +21,7 @@
<ui.path>${project.basedir}/ui</ui.path>
<frontend-maven-plugin.version>1.11.0</frontend-maven-plugin.version>
<compiler.version>1.8</compiler.version>
<kafka.version>3.2.0</kafka.version>
<kafka.version>3.5.0</kafka.version>
<maven.assembly.plugin.version>3.0.0</maven.assembly.plugin.version>
<mybatis-plus-boot-starter.version>3.4.2</mybatis-plus-boot-starter.version>
<scala.version>2.13.6</scala.version>
@@ -90,6 +90,12 @@
</exclusions>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.apache.kafka</groupId>-->
<!-- <artifactId>kafka-tools</artifactId>-->
<!-- <version>${kafka.version}</version>-->
<!-- </dependency>-->
<dependency>
<groupId>com.typesafe.scala-logging</groupId>
<artifactId>scala-logging_2.13</artifactId>

View File

@@ -0,0 +1,30 @@
package com.xuxd.kafka.console.beans;
import lombok.Data;
/**
* 消息转发请求参数.
*
* @author: xuxd
* @since: 2025/6/5 16:52
**/
@Data
public class ForwardMessage {
private SendMessage message;
/**
* 是否还发到同一个分区.
*/
private boolean samePartition;
/**
* 目标集群id.
*/
private long targetClusterId;
/**
* 目标topic.
*/
private String targetTopic;
}

View File

@@ -33,4 +33,6 @@ public class QueryMessage {
private String headerKey;
private String headerValue;
private int filterNumber;
}

View File

@@ -37,6 +37,8 @@ public class QueryMessageDTO {
private String headerValue;
private int filterNumber;
public QueryMessage toQueryMessage() {
QueryMessage queryMessage = new QueryMessage();
queryMessage.setTopic(topic);
@@ -69,6 +71,7 @@ public class QueryMessageDTO {
if (StringUtils.isNotBlank(headerValue)) {
queryMessage.setHeaderValue(headerValue.trim());
}
queryMessage.setFilterNumber(filterNumber);
return queryMessage;
}

View File

@@ -21,4 +21,6 @@ public class BrokerApiVersionVO {
private int unSupportNums;
private List<String> versionInfo;
private String brokerVersion;
}

View File

@@ -33,4 +33,9 @@ public class AuthController {
public ResponseData login(@RequestBody LoginUserDTO userDTO) {
return authService.login(userDTO);
}
@GetMapping("/own/data/auth")
public boolean ownDataAuthority() {
return authService.ownDataAuthority();
}
}

View File

@@ -2,6 +2,7 @@ package com.xuxd.kafka.console.controller;
import com.xuxd.kafka.console.aspect.annotation.ControllerLog;
import com.xuxd.kafka.console.aspect.annotation.Permission;
import com.xuxd.kafka.console.beans.ForwardMessage;
import com.xuxd.kafka.console.beans.QueryMessage;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.SendMessage;
@@ -83,4 +84,11 @@ public class MessageController {
}
return messageService.sendStatisticsByTime(dto);
}
@Permission("message:forward")
@ControllerLog("消息转发")
@PostMapping("/forward")
public Object forward(@RequestBody ForwardMessage message) {
return messageService.forward(message);
}
}

View File

@@ -49,6 +49,10 @@ public class ContextSetFilter implements Filter {
String uri = request.getRequestURI();
if (!excludes.contains(uri)) {
String headerId = request.getHeader(Header.ID);
String specificId = request.getHeader(Header.SPECIFIC_ID);
if (StringUtils.isNotBlank(specificId)) {
headerId = specificId;
}
if (StringUtils.isBlank(headerId)) {
// ResponseData failed = ResponseData.create().failed("Cluster info is null.");
ResponseData failed = ResponseData.create().failed("没有集群信息,请先切换集群");
@@ -84,5 +88,6 @@ public class ContextSetFilter implements Filter {
interface Header {
String ID = "X-Cluster-Info-Id";
String NAME = "X-Cluster-Info-Name";
String SPECIFIC_ID = "X-Specific-Cluster-Info-Id";
}
}

View File

@@ -10,4 +10,6 @@ import com.xuxd.kafka.console.beans.dto.LoginUserDTO;
public interface AuthService {
ResponseData login(LoginUserDTO userDTO);
boolean ownDataAuthority();
}

View File

@@ -1,5 +1,6 @@
package com.xuxd.kafka.console.service;
import com.xuxd.kafka.console.beans.ForwardMessage;
import com.xuxd.kafka.console.beans.QueryMessage;
import com.xuxd.kafka.console.beans.dto.QuerySendStatisticsDTO;
import com.xuxd.kafka.console.beans.ResponseData;
@@ -32,4 +33,6 @@ public interface MessageService {
ResponseData delete(List<QueryMessage> messages);
ResponseData sendStatisticsByTime(QuerySendStatisticsDTO request);
ResponseData forward(ForwardMessage message);
}

View File

@@ -93,4 +93,16 @@ public class AuthServiceImpl implements AuthService {
return ResponseData.create().data(loginResult).success();
}
@Override
public boolean ownDataAuthority() {
if (!authConfig.isEnable()) {
return true;
}
if (!authConfig.isEnableClusterAuthority()) {
return true;
}
return false;
}
}

View File

@@ -14,6 +14,7 @@ import com.xuxd.kafka.console.dao.ClusterInfoMapper;
import com.xuxd.kafka.console.dao.ClusterRoleRelationMapper;
import com.xuxd.kafka.console.filter.CredentialsContext;
import com.xuxd.kafka.console.service.ClusterService;
import kafka.console.BrokerVersion;
import kafka.console.ClusterConsole;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
@@ -188,6 +189,9 @@ public class ClusterServiceImpl implements ClusterService {
versionInfo = versionInfo.substring(1, versionInfo.length() - 2);
vo.setVersionInfo(Arrays.asList(StringUtils.split(versionInfo, ",")));
list.add(vo);
// 推测broker版本
String vs = BrokerVersion.guessBrokerVersion(versions);
vo.setBrokerVersion(vs);
}));
Collections.sort(list, Comparator.comparingInt(BrokerApiVersionVO::getBrokerId));
return ResponseData.create().data(list).success();

View File

@@ -1,14 +1,15 @@
package com.xuxd.kafka.console.service.impl;
import com.xuxd.kafka.console.beans.MessageFilter;
import com.xuxd.kafka.console.beans.QueryMessage;
import com.xuxd.kafka.console.beans.ResponseData;
import com.xuxd.kafka.console.beans.SendMessage;
import com.xuxd.kafka.console.beans.*;
import com.xuxd.kafka.console.beans.dos.ClusterInfoDO;
import com.xuxd.kafka.console.beans.dto.QuerySendStatisticsDTO;
import com.xuxd.kafka.console.beans.enums.FilterType;
import com.xuxd.kafka.console.beans.vo.ConsumerRecordVO;
import com.xuxd.kafka.console.beans.vo.MessageDetailVO;
import com.xuxd.kafka.console.beans.vo.QuerySendStatisticsVO;
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.service.ConsumerService;
import com.xuxd.kafka.console.service.MessageService;
import kafka.console.ConsumerConsole;
@@ -52,6 +53,9 @@ public class MessageServiceImpl implements MessageService, ApplicationContextAwa
@Autowired
private ConsumerConsole consumerConsole;
@Autowired
private ClusterInfoMapper clusterInfoMapper;
private ApplicationContext applicationContext;
private Map<String, Deserializer> deserializerDict = new HashMap<>();
@@ -70,7 +74,7 @@ public class MessageServiceImpl implements MessageService, ApplicationContextAwa
@Override
public ResponseData searchByTime(QueryMessage queryMessage) {
int maxNums = 5000;
int maxNums = queryMessage.getFilterNumber() <= 0 ? 5000 : queryMessage.getFilterNumber();
Object searchContent = null;
String headerKey = null;
@@ -304,6 +308,47 @@ public class MessageServiceImpl implements MessageService, ApplicationContextAwa
return ResponseData.create().data(vo).success();
}
@Override
public ResponseData forward(ForwardMessage message) {
ClusterInfoDO clusterInfoDO = clusterInfoMapper.selectById(message.getTargetClusterId());
if (clusterInfoDO == null) {
return ResponseData.create().failed("Target cluster not found.");
}
SendMessage sendMessage = message.getMessage();
// first, search message detail
Map<TopicPartition, Object> offsetTable = new HashMap<>();
TopicPartition topicPartition = new TopicPartition(sendMessage.getTopic(), sendMessage.getPartition());
offsetTable.put(topicPartition, sendMessage.getOffset());
Map<TopicPartition, ConsumerRecord<byte[], byte[]>> recordMap = messageConsole.searchBy(offsetTable);
ConsumerRecord<byte[], byte[]> consumerRecord = recordMap.get(topicPartition);
if (consumerRecord == null) {
return ResponseData.create().failed("Source message not found.");
}
String topic = message.getTargetTopic();
if (StringUtils.isEmpty(topic)) {
topic = sendMessage.getTopic();
}
// copy from consumer record.
ProducerRecord<byte[], byte[]> record = new ProducerRecord<>(topic,
message.isSamePartition() ? consumerRecord.partition() : null,
consumerRecord.key(),
consumerRecord.value(),
consumerRecord.headers());
ContextConfig config = ContextConfigHolder.CONTEXT_CONFIG.get();
config.setClusterInfoId(clusterInfoDO.getId());
config.setClusterName(clusterInfoDO.getClusterName());
config.setBootstrapServer(clusterInfoDO.getAddress());
// send.
Tuple2<Object, String> tuple2 = messageConsole.sendSync(record);
boolean success = (boolean) tuple2._1;
if (!success) {
return ResponseData.create().failed(tuple2._2);
}
return ResponseData.create().success();
}
private Map<TopicPartition, ConsumerRecord<byte[], byte[]>> searchRecordByOffset(QueryMessage queryMessage) {
Set<TopicPartition> partitions = getPartitions(queryMessage);

View File

@@ -15,7 +15,7 @@ kafka:
# 缓存连接,不缓存的情况下,每次请求建立连接. 即使每次请求建立连接其实也很快某些情况下开启ACL查询可能很慢可以设置连接缓存为true
# 或者想提高查询速度也可以设置下面连接缓存为true
# 缓存 admin client的连接
cache-admin-connection: false
cache-admin-connection: true
# 缓存 producer的连接
cache-producer-connection: false
# 缓存 consumer的连接

View File

@@ -43,6 +43,7 @@ insert into t_sys_permission(id, name,type,parent_id,permission) values(65,'在
insert into t_sys_permission(id, name,type,parent_id,permission) values(66,'消息详情',1,61,'message:detail');
insert into t_sys_permission(id, name,type,parent_id,permission) values(67,'重新发送',1,61,'message:resend');
insert into t_sys_permission(id, name,type,parent_id,permission) values(68,'发送统计',1,61,'message:send-statistics');
insert into t_sys_permission(id, name,type,parent_id,permission) values(69,'消息转发',1,61,'message:forward');
insert into t_sys_permission(id, name,type,parent_id,permission) values(80,'限流',0,null,'quota');
insert into t_sys_permission(id, name,type,parent_id,permission) values(81,'用户',1,80,'quota:user');
@@ -102,8 +103,8 @@ insert into t_sys_permission(id, name,type,parent_id,permission) values(171,'取
-- t_sys_permission end--
-- t_sys_role start--
insert into t_sys_role(id, role_name, description, permission_ids) VALUES (1,'超级管理员','超级管理员','12,13,14,22,23,24,25,26,27,28,29,30,34,35,31,32,33,42,43,44,45,46,47,48,49,50,62,63,64,65,66,67,68,81,82,83,84,85,86,87,88,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,141,142,143,144,145,146,147,148,149,150,151,152,153,161,162,163,164,165,166,167,168,169,171,170');
insert into t_sys_role(id, role_name, description, permission_ids) VALUES (2,'普通管理员','普通管理员,不能更改用户信息','12,13,14,22,23,24,25,26,27,28,29,30,34,35,31,32,33,42,43,44,45,46,47,48,49,50,62,63,64,65,66,67,68,81,82,83,84,85,86,87,88,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,141,146,149,150,161,162,163,164,165,166,167,168,169,171,170');
insert into t_sys_role(id, role_name, description, permission_ids) VALUES (1,'超级管理员','超级管理员','12,13,14,22,23,24,25,26,27,28,29,30,34,35,31,32,33,42,43,44,45,46,47,48,49,50,62,63,64,65,66,67,68,69,81,82,83,84,85,86,87,88,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,141,142,143,144,145,146,147,148,149,150,151,152,153,161,162,163,164,165,166,167,168,169,171,170');
insert into t_sys_role(id, role_name, description, permission_ids) VALUES (2,'普通管理员','普通管理员,不能更改用户信息','12,13,14,22,23,24,25,26,27,28,29,30,34,35,31,32,33,42,43,44,45,46,47,48,49,50,62,63,64,65,66,67,68,69,81,82,83,84,85,86,87,88,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,141,146,149,150,161,162,163,164,165,166,167,168,169,171,170');
-- insert into t_sys_role(id, role_name, description, permission_ids) VALUES (2,'访客','访客','12,13,22,26,29,32,44,45,50,62,63,81,83,85,141,146,149,150,161,163');
-- t_sys_role end--

View File

@@ -4,8 +4,8 @@
CREATE TABLE IF NOT EXISTS T_KAFKA_USER
(
ID IDENTITY NOT NULL COMMENT '主键ID',
USERNAME VARCHAR(50) NOT NULL DEFAULT '' COMMENT '用户名',
PASSWORD VARCHAR(50) NOT NULL DEFAULT '' COMMENT '密码',
USERNAME VARCHAR(128) NOT NULL DEFAULT '' COMMENT '用户名',
PASSWORD VARCHAR(128) NOT NULL DEFAULT '' COMMENT '密码',
UPDATE_TIME TIMESTAMP NOT NULL DEFAULT NOW() COMMENT '更新时间',
CLUSTER_INFO_ID BIGINT NOT NULL COMMENT '集群信息里的集群ID',
PRIMARY KEY (ID),

View File

@@ -21,7 +21,7 @@ import org.apache.kafka.common.utils.{KafkaThread, LogContext, Time}
import org.slf4j.{Logger, LoggerFactory}
import java.io.IOException
import java.util.Properties
import java.util.{Collections, Properties}
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.{ConcurrentLinkedQueue, TimeUnit}
import scala.jdk.CollectionConverters.{ListHasAsScala, MapHasAsJava, PropertiesHasAsScala, SetHasAsScala}
@@ -162,7 +162,7 @@ object BrokerApiVersion{
def listAllBrokerVersionInfo(): Map[Node, Try[NodeApiVersions]] =
findAllBrokers().map { broker =>
broker -> Try[NodeApiVersions](new NodeApiVersions(getApiVersions(broker)))
broker -> Try[NodeApiVersions](new NodeApiVersions(getApiVersions(broker), Collections.emptyList(), false))
}.toMap
def close(): Unit = {

View File

@@ -0,0 +1,101 @@
package kafka.console
import org.apache.kafka.clients.NodeApiVersions
import org.apache.kafka.common.message.ApiVersionsResponseData.ApiVersion
import java.util.Collections
import scala.jdk.CollectionConverters.SeqHasAsJava
import scala.reflect.runtime.universe._
import scala.reflect.runtime.{universe => ru}
/**
* broker version with api version.
*
* @author: xuxd
* @since: 2024/8/29 16:12
* */
class BrokerVersion(val apiVersion: Int, val brokerVersion: String)
object BrokerVersion {
val define: List[BrokerVersion] = List(
new BrokerVersion(33, "0.11"),
new BrokerVersion(37, "1.0"),
new BrokerVersion(42, "1.1"),
new BrokerVersion(43, "2.2"),
new BrokerVersion(44, "2.3"),
new BrokerVersion(47, "2.4~2.5"),
new BrokerVersion(49, "2.6"),
new BrokerVersion(57, "2.7"),
new BrokerVersion(64, "2.8"),
new BrokerVersion(67, "3.0~3.4"),
new BrokerVersion(68, "3.5~3.6"),
new BrokerVersion(74, "3.7"),
new BrokerVersion(75, "3.8")
)
def getDefineWithJavaList(): java.util.List[BrokerVersion] = {
define.toBuffer.asJava
}
def guessBrokerVersion(nodeVersion: NodeApiVersions): String = {
// if (nodeVersion.)
val unknown = "unknown";
var guessVersion = unknown
var maxApiKey: Short = -1
if (nodeVersion.toString().contains("UNKNOWN")) {
val unknownApis = getFieldValueByName(nodeVersion, "unknownApis")
unknownApis match {
case Some(unknownApis: java.util.List[ApiVersion]) => {
if (unknownApis.size > 0) {
maxApiKey = unknownApis.get(unknownApis.size() - 1).apiKey()
}
}
case _ => -1
}
}
if (maxApiKey < 0) {
val versions = new java.util.ArrayList[ApiVersion](nodeVersion.allSupportedApiVersions().values())
Collections.sort(versions, (o1: ApiVersion, o2: ApiVersion) => o2.apiKey() - o1.apiKey)
maxApiKey = versions.get(0).apiKey()
}
if (maxApiKey > 0) {
if (maxApiKey > define.last.apiVersion) {
guessVersion = "> " + define.last.brokerVersion
} else if (maxApiKey < define.head.apiVersion) {
guessVersion = "< " + define.head.brokerVersion
} else {
for (i <- define.indices) {
if (maxApiKey <= define(i).apiVersion && guessVersion == unknown) {
guessVersion = define(i).brokerVersion
}
}
}
}
guessVersion
}
def getFieldValueByName(obj: Object, fieldName: String): Option[Any] = {
val runtimeMirror = ru.runtimeMirror(obj.getClass.getClassLoader)
val instanceMirror = runtimeMirror.reflect(obj)
val typeOfObj = instanceMirror.symbol.toType
// 查找名为 fieldName 的字段
val fieldSymbol = typeOfObj.member(newTermName(fieldName)).asTerm
// 检查字段是否存在并且不是私有字段
if (fieldSymbol.isPrivate || fieldSymbol.isPrivateThis) {
// None // 如果字段是私有的,返回 None
val fieldMirror = runtimeMirror.reflect(obj).reflectField(fieldSymbol)
Some(fieldMirror.get)
} else {
// 反射获取字段值
val fieldMirror = instanceMirror.reflectField(fieldSymbol)
Some(fieldMirror.get)
}
}
}

View File

@@ -15,7 +15,7 @@ import scala.collection.{Map, Seq}
import scala.jdk.CollectionConverters.{CollectionHasAsScala, MapHasAsJava, MapHasAsScala, SeqHasAsJava, SetHasAsJava}
/**
* kafka-console-ui.
* kafka topic console.
*
* @author xuxd
* @date 2021-09-08 19:52:27
@@ -52,6 +52,12 @@ class TopicConsole(config: KafkaConfig) extends KafkaConsole(config: KafkaConfig
}).asInstanceOf[Set[String]]
}
/**
* get topic list by topic name list.
*
* @param topics topic name list.
* @return topic list.
*/
def getTopicList(topics: Set[String]): List[TopicDescription] = {
if (topics == null || topics.isEmpty) {
Collections.emptyList()

View File

@@ -17,3 +17,8 @@ new Vue({
store,
render: (h) => h(App),
}).$mount("#app");
Vue.prototype.$message.config({
duration: 1,
maxCount: 1,
});

View File

@@ -6,6 +6,7 @@ import {
setPermissions,
setToken,
setUsername,
deleteClusterInfo,
} from "@/utils/local-cache";
Vue.use(Vuex);
@@ -38,6 +39,9 @@ export default new Vuex.Store({
state.clusterInfo.enableSasl = enableSasl;
setClusterInfo(clusterInfo);
},
[CLUSTER.DELETE]() {
deleteClusterInfo();
},
[AUTH.ENABLE](state, enable) {
state.auth.enable = enable;
},

View File

@@ -1,5 +1,6 @@
export const CLUSTER = {
SWITCH: "switchCluster",
DELETE: "deleteClusterInfo",
};
export const AUTH = {

View File

@@ -300,6 +300,10 @@ export const KafkaMessageApi = {
url: "/message/send/statistics",
method: "post",
},
forward: {
url: "/message/forward",
method: "post",
},
};
export const KafkaClientQuotaApi = {
@@ -365,6 +369,10 @@ export const AuthApi = {
url: "/auth/login",
method: "post",
},
ownDataAuthority: {
url: "/auth/own/data/auth",
method: "get",
},
};
export const ClusterRoleRelationApi = {

View File

@@ -4,6 +4,10 @@ export function setClusterInfo(clusterInfo) {
localStorage.setItem(Cache.clusterInfo, JSON.stringify(clusterInfo));
}
export function deleteClusterInfo() {
localStorage.removeItem(Cache.clusterInfo);
}
export function getClusterInfo() {
const str = localStorage.getItem(Cache.clusterInfo);
return str ? JSON.parse(str) : undefined;

View File

@@ -6,6 +6,10 @@
<p></p>
<hr />
<h3>kafka API 版本兼容性</h3>
<div class="green">
broker版本说明:
该值并不保证broker实际版本一定是该值(大概是这个版本范围)broker使用不同的模式(如kraft)可能显示不同的值
</div>
<a-spin :spinning="apiVersionInfoLoading">
<a-table
:columns="columns"
@@ -104,6 +108,11 @@ const columns = [
dataIndex: "host",
key: "host",
},
{
title: "broker版本",
dataIndex: "brokerVersion",
key: "brokerVersion",
},
{
title: "支持的api数量",
dataIndex: "supportNums",
@@ -125,4 +134,7 @@ const columns = [
.card-style {
width: 100%;
}
.green {
color: green;
}
</style>

View File

@@ -17,7 +17,7 @@
<a-input
placeholder="username"
:allowClear="true"
:maxLength="30"
:maxLength="100"
v-decorator="[
'username',
{
@@ -30,7 +30,7 @@
<a-input
placeholder="password"
:allowClear="true"
:maxLength="30"
:maxLength="100"
v-decorator="[
'password',
{

View File

@@ -6,7 +6,7 @@
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
:maskClosable="true"
@cancel="handleCancel"
>
<div>

View File

@@ -6,7 +6,7 @@
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
:maskClosable="true"
@cancel="handleCancel"
>
<div>

View File

@@ -58,6 +58,22 @@
</a-form-item>
</a-col>
</a-row>
<hr class="hr" />
<a-row :gutter="24">
<a-col :span="24">
<a-form-item label="过滤消费组">
<a-input
placeholder="groupId 模糊过滤"
class="input-w"
v-decorator="['filterGroupId']"
@change="onFilterGroupIdUpdate"
/>
<span>
仅过滤当前已查出来的消费组如果要查询服务端最新消费组请点击查询按钮</span
>
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
<div class="operation-row-button">
@@ -70,7 +86,7 @@
</div>
<a-table
:columns="columns"
:data-source="data"
:data-source="filteredData"
bordered
row-key="groupId"
>
@@ -169,6 +185,7 @@ export default {
return {
queryParam: {},
data: [],
filteredData: [],
columns,
selectRow: {},
form: this.$form.createForm(this, {
@@ -186,6 +203,7 @@ export default {
showConsumerDetailDialog: false,
showAddSubscriptionDialog: false,
showOffsetPartitionDialog: false,
filterGroupId: "",
};
},
methods: {
@@ -209,6 +227,7 @@ export default {
this.loading = false;
if (res.code == 0) {
this.data = res.data.list;
this.filter();
} else {
notification.error({
message: "error",
@@ -268,6 +287,19 @@ export default {
closeOffsetPartitionDialog() {
this.showOffsetPartitionDialog = false;
},
onFilterGroupIdUpdate(input) {
this.filterGroupId = input.target.value;
this.filter();
},
filter() {
if (this.filterGroupId) {
this.filteredData = this.data.filter(
(e) => e.groupId.indexOf(this.filterGroupId) != -1
);
} else {
this.filteredData = this.data;
}
},
},
created() {
this.getConsumerGroupList();
@@ -372,4 +404,9 @@ const columns = [
.type-select {
width: 200px !important;
}
.hr {
height: 1px;
border: none;
border-top: 1px dashed #0066cc;
}
</style>

View File

@@ -9,6 +9,7 @@
<h3 class="login-title">登录kafka-console-ui</h3>
<a-form-item label="账号">
<a-input
@keyup.enter="handleSubmit"
style="width: 200px"
allowClear
v-decorator="[
@@ -20,6 +21,7 @@
</a-form-item>
<a-form-item label="密码">
<a-input-password
@keyup.enter="handleSubmit"
style="width: 200px"
v-decorator="[
'password',
@@ -28,7 +30,11 @@
/>
</a-form-item>
<a-form-item :wrapper-col="{ span: 16, offset: 5 }">
<a-button type="primary" @click="handleSubmit" :loading="loading"
<a-button
type="primary"
@click="handleSubmit"
:loading="loading"
@keyup.enter="handleSubmit"
>登录</a-button
>
</a-form-item>
@@ -40,7 +46,7 @@ import request from "@/utils/request";
import { AuthApi } from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
import { mapMutations } from "vuex";
import { AUTH } from "@/store/mutation-types";
import { AUTH, CLUSTER } from "@/store/mutation-types";
export default {
name: "Login",
@@ -82,8 +88,19 @@ export default {
setToken: AUTH.SET_TOKEN,
setUsername: AUTH.SET_USERNAME,
setPermissions: AUTH.SET_PERMISSIONS,
deleteClusterInfo: CLUSTER.DELETE,
}),
},
created() {
request({
url: AuthApi.ownDataAuthority.url,
method: AuthApi.ownDataAuthority.method,
}).then((res) => {
if (!res) {
this.deleteClusterInfo();
}
});
},
};
</script>

View File

@@ -0,0 +1,248 @@
<template>
<a-modal
title="转发消息"
:visible="show"
:width="600"
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="true"
@cancel="handleCancel"
>
<div>
<a-spin :spinning="loading">
<div>
<h4>选择集群</h4>
<hr />
<div class="message-detail" id="message-detail">
<a-form
:form="form"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 18 }"
@submit="handleSubmit"
>
<a-form-item label="集群">
<a-select
class="select-width"
@change="clusterChange"
v-decorator="[
'targetClusterId',
{
rules: [{ required: true, message: '请选择一个集群!' }],
},
]"
placeholder="请选择一个集群"
>
<a-select-option
v-for="v in clusterList"
:key="v.id"
:value="v.id"
>
{{ v.clusterName }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Topic">
<a-select
class="select-width"
show-search
option-filter-prop="children"
v-decorator="[
'targetTopic',
{
rules: [{ required: true, message: '请选择一个topic!' }],
},
]"
placeholder="请选择一个topic"
>
<a-select-option v-for="v in topicList" :key="v" :value="v">
{{ v }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="相同分区">
<a-radio-group
v-decorator="[
'samePartition',
{
initialValue: 'false',
rules: [{ required: true, message: '请选择!' }],
},
]"
>
<a-radio value="false"> 否</a-radio>
<a-radio value="true"> 是</a-radio>
</a-radio-group>
<span class="mar-left">和原消息保持同一个分区</span>
</a-form-item>
<a-form-item>
<div class="form-footer">
<a-button type="primary" html-type="submit"> 提交</a-button>
</div>
</a-form-item>
</a-form>
</div>
</div>
</a-spin>
</div>
</a-modal>
</template>
<script>
import request from "@/utils/request";
import { KafkaClusterApi, KafkaMessageApi, KafkaTopicApi } from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
import moment from "moment";
export default {
name: "ForwardMessage",
props: {
record: {},
visible: {
type: Boolean,
default: false,
},
},
data() {
return {
show: this.visible,
data: {},
loading: false,
showForwardDialog: false,
targetClusterId: -1,
clusterList: [],
partition: -1,
topicList: [],
form: this.$form.createForm(this, { name: "ForwardMessageForm" }),
};
},
watch: {
visible(v) {
this.show = v;
if (this.show) {
this.getClusterList();
}
},
},
methods: {
getClusterList() {
this.loading = true;
request({
url: KafkaClusterApi.getClusterInfoList.url,
method: KafkaClusterApi.getClusterInfoList.method,
}).then((res) => {
this.loading = false;
if (res.code != 0) {
notification.error({
message: "error",
description: res.msg,
});
} else {
this.clusterList = res.data;
this.targetClusterId = this.clusterList[0].id;
}
});
},
handleSubmit(e) {
e.preventDefault();
this.form.validateFields((err, values) => {
if (!err) {
const params = {
message: Object.assign({}, this.record),
};
this.forward({ ...params, ...values });
}
});
},
handleCancel() {
this.$emit("closeForwardDialog", { refresh: false });
},
formatTime(time) {
return time == -1 ? -1 : moment(time).format("YYYY-MM-DD HH:mm:ss:SSS");
},
clusterChange(e) {
this.getTopicNameList(e);
},
forward(params) {
this.loading = true;
request({
url: KafkaMessageApi.forward.url,
method: KafkaMessageApi.forward.method,
data: params,
}).then((res) => {
this.loading = false;
if (res.code != 0) {
notification.error({
message: "error",
description: res.msg,
});
} else {
this.$message.success(res.msg);
}
});
},
openForwardDialog() {
this.showForwardDialog = true;
},
closeForwardDialog() {
this.showForwardDialog = false;
},
getTopicNameList(clusterInfoId) {
this.loading = true;
request({
url: KafkaTopicApi.getTopicNameList.url,
method: KafkaTopicApi.getTopicNameList.method,
headers: {
"X-Specific-Cluster-Info-Id": clusterInfoId,
},
}).then((res) => {
this.loading = false;
if (res.code == 0) {
this.topicList = res.data;
} else {
notification.error({
message: "error",
description: res.msg,
});
}
});
},
},
};
</script>
<style scoped>
.m-info {
/*text-decoration: underline;*/
}
.title {
width: 15%;
display: inline-block;
text-align: right;
margin-right: 2%;
font-weight: bold;
}
.ant-spin-container #message-detail textarea {
max-width: 80% !important;
vertical-align: top !important;
}
.center {
text-align: center;
}
.mar-left {
margin-left: 1%;
}
.select-width {
width: 80%;
}
.form-footer {
text-align: center;
margin-top: 3%;
}
</style>

View File

@@ -24,7 +24,11 @@
<DeleteMessage :topic-list="topicList"></DeleteMessage>
</a-tab-pane>
<a-tab-pane key="5" tab="发送统计" v-if="isAuthorized('message:send-statistics')">
<a-tab-pane
key="5"
tab="发送统计"
v-if="isAuthorized('message:send-statistics')"
>
<SendStatistics :topic-list="topicList"></SendStatistics>
</a-tab-pane>
</a-tabs>

View File

@@ -6,7 +6,7 @@
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
:maskClosable="true"
@cancel="handleCancel"
>
<div>
@@ -120,8 +120,22 @@
重新发送
</a-button>
</a-popconfirm>
<a-button
type="dashed"
class="mar-left"
icon="plus"
v-action:message:forward
@click="openForwardDialog()"
>
转发消息
</a-button>
</div>
</a-spin>
<ForwardMessage
:visible="showForwardDialog"
:record="data"
@closeForwardDialog="closeForwardDialog"
></ForwardMessage>
</div>
</a-modal>
</template>
@@ -131,9 +145,11 @@ import request from "@/utils/request";
import { KafkaMessageApi } from "@/utils/api";
import notification from "ant-design-vue/lib/notification";
import moment from "moment";
import ForwardMessage from "@/views/message/ForwardMessage.vue";
export default {
name: "MessageDetail",
components: { ForwardMessage },
props: {
record: {},
visible: {
@@ -151,6 +167,7 @@ export default {
valueDeserializer: "String",
consumerDetail: [],
columns,
showForwardDialog: false,
};
},
watch: {
@@ -232,6 +249,12 @@ export default {
}
});
},
openForwardDialog() {
this.showForwardDialog = true;
},
closeForwardDialog() {
this.showForwardDialog = false;
},
},
};
const columns = [
@@ -264,4 +287,7 @@ const columns = [
max-width: 80% !important;
vertical-align: top !important;
}
.mar-left {
margin-left: 1%;
}
</style>

View File

@@ -60,6 +60,33 @@
</a-col>
</a-row>
<hr class="hr" />
<a-row :gutter="24">
<a-col :span="24">
<a-form-item label="最大检索数">
<a-input-number
v-decorator="[
'filterNumber',
{
initialValue: 5000,
rules: [
{
required: true,
message: '输入消息数!',
},
],
},
]"
:min="1"
:max="100000"
/>
<span
>条
注意这里允许最多检索10万条但是不建议将该值设置过大这意味着一次查询要在内存里缓存这么多的数据可能导致内存溢出并且更大的消息量会导致更长的检索时间</span
>
</a-form-item>
</a-col>
</a-row>
<hr class="hr" />
<a-row :gutter="24">
<a-col :span="5">
<a-form-item label="消息过滤">

View File

@@ -6,7 +6,7 @@
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
:maskClosable="true"
@cancel="handleCancel"
>
<div>

View File

@@ -14,6 +14,15 @@
<div v-for="(v, k) in data" :key="k">
<strong>消费组: </strong><span class="color-font">{{ k }}</span
><strong> | 积压: </strong><span class="color-font">{{ v.lag }}</span>
<a-button
type="primary"
icon="reload"
size="small"
style="float: right"
@click="getConsumerDetail"
>
刷新
</a-button>
<hr />
<a-table
:columns="columns"

View File

@@ -6,7 +6,7 @@
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
:maskClosable="true"
@cancel="handleCancel"
>
<div>

View File

@@ -11,7 +11,18 @@
>
<div>
<a-spin :spinning="loading">
<h4>今天发送消息数{{ today.total }}</h4>
<h4>
今天发送消息数{{ today.total
}}<a-button
type="primary"
icon="reload"
size="small"
style="float: right"
@click="sendStatus"
>
刷新
</a-button>
</h4>
<a-table
:columns="columns"
:data-source="today.detail"

View File

@@ -6,7 +6,7 @@
:mask="false"
:destroyOnClose="true"
:footer="null"
:maskClosable="false"
:maskClosable="true"
@cancel="handleCancel"
>
<div>

View File

@@ -5,7 +5,7 @@
:width="1200"
:mask="false"
:destroyOnClose="true"
:maskClosable="false"
:maskClosable="true"
@cancel="handleCancel"
okText="确认"
cancelText="取消"
@@ -91,6 +91,7 @@ export default {
loading: false,
form: this.$form.createForm(this, { name: "coordinated" }),
brokerSize: 0,
brokerIdList: [],
replicaNums: 0,
defaultReplicaNums: 0,
};
@@ -136,6 +137,8 @@ export default {
method: KafkaClusterApi.getClusterInfo.method,
}).then((res) => {
this.brokerSize = res.data.nodes.length;
this.brokerIdList = res.data.nodes.map((o) => o.id);
this.brokerIdList.sort((a, b) => a - b);
});
},
handleCancel() {
@@ -149,9 +152,16 @@ export default {
if (this.data.partitions.length > 0) {
this.data.partitions.forEach((p) => {
if (value > p.replicas.length) {
let min = this.brokerIdList[0];
let max = this.brokerIdList[this.brokerSize - 1] + 1;
let num = p.replicas[p.replicas.length - 1];
for (let i = p.replicas.length; i < value; i++) {
p.replicas.push(++num % this.brokerSize);
++num;
if (num < max) {
p.replicas.push(num);
} else {
p.replicas.push((num % max) + min);
}
}
}
if (value < p.replicas.length) {