1.33 日志通用查询
This commit is contained in:
parent
5a7970da36
commit
82eb9ca6d6
@ -32,6 +32,13 @@ public class TeamApplication extends Entity<Long> {
|
||||
@Column(name = "application_id", nullable = false)
|
||||
private Long applicationId;
|
||||
|
||||
/**
|
||||
* 应用关联(用于查询,不参与保存和更新)
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "application_id", insertable = false, updatable = false)
|
||||
private Application application;
|
||||
|
||||
/**
|
||||
* 环境ID
|
||||
*/
|
||||
|
||||
@ -2,6 +2,7 @@ package com.qqchen.deploy.backend.deploy.query;
|
||||
|
||||
import com.qqchen.deploy.backend.deploy.enums.BuildTypeEnum;
|
||||
import com.qqchen.deploy.backend.framework.annotation.QueryField;
|
||||
import com.qqchen.deploy.backend.framework.enums.QueryType;
|
||||
import com.qqchen.deploy.backend.framework.query.BaseQuery;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
@ -33,5 +34,13 @@ public class TeamApplicationQuery extends BaseQuery {
|
||||
@QueryField(field = "buildType")
|
||||
@Schema(description = "构建类型")
|
||||
private BuildTypeEnum buildType;
|
||||
|
||||
@QueryField(field = "application.appName", type = QueryType.LIKE)
|
||||
@Schema(description = "应用名称(模糊查询)")
|
||||
private String applicationName;
|
||||
|
||||
@QueryField(field = "application.appCode", type = QueryType.LIKE)
|
||||
@Schema(description = "应用编码(模糊查询)")
|
||||
private String applicationCode;
|
||||
}
|
||||
|
||||
|
||||
@ -33,16 +33,6 @@ public class DockerLogStreamStrategy implements ILogStreamStrategy {
|
||||
@Resource
|
||||
private SSHCommandServiceFactory sshCommandServiceFactory;
|
||||
|
||||
/**
|
||||
* SSH连接存储:sessionId → SSHClient
|
||||
*/
|
||||
private final Map<String, SSHClient> sshClients = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* SSH会话存储:sessionId → Session
|
||||
*/
|
||||
private final Map<String, Session> sshSessions = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public RuntimeTypeEnum supportedType() {
|
||||
return RuntimeTypeEnum.DOCKER;
|
||||
@ -73,9 +63,6 @@ public class DockerLogStreamStrategy implements ILogStreamStrategy {
|
||||
target.getPassphrase()
|
||||
);
|
||||
|
||||
// 保存SSH连接,用于后续清理
|
||||
sshClients.put(sessionId, sshClient);
|
||||
|
||||
// 2. 构建docker logs命令
|
||||
String command = String.format("docker logs -f %s --tail %d",
|
||||
target.getName(), target.getLines());
|
||||
@ -84,7 +71,6 @@ public class DockerLogStreamStrategy implements ILogStreamStrategy {
|
||||
|
||||
// 3. 执行命令
|
||||
sshSession = sshClient.startSession();
|
||||
sshSessions.put(sessionId, sshSession);
|
||||
|
||||
Session.Command cmd = sshSession.exec(command);
|
||||
|
||||
@ -111,41 +97,30 @@ public class DockerLogStreamStrategy implements ILogStreamStrategy {
|
||||
log.error("Docker日志流异常: sessionId={}", sessionId, e);
|
||||
throw e;
|
||||
} finally {
|
||||
// 清理资源(正常结束时)
|
||||
cleanupResources(sessionId, sshSession, sshClient);
|
||||
// 清理SSH资源
|
||||
if (sshSession != null) {
|
||||
try {
|
||||
sshSession.close();
|
||||
log.debug("SSH Session已关闭: sessionId={}", sessionId);
|
||||
} catch (Exception e) {
|
||||
log.error("关闭SSH Session失败: sessionId={}", sessionId, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (sshClient != null) {
|
||||
try {
|
||||
sshClient.disconnect();
|
||||
log.debug("SSH Client已断开: sessionId={}", sessionId);
|
||||
} catch (Exception e) {
|
||||
log.error("断开SSH Client失败: sessionId={}", sessionId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(String sessionId) {
|
||||
log.info("停止Docker日志流并清理资源: sessionId={}", sessionId);
|
||||
|
||||
Session sshSession = sshSessions.remove(sessionId);
|
||||
SSHClient sshClient = sshClients.remove(sessionId);
|
||||
|
||||
cleanupResources(sessionId, sshSession, sshClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理SSH资源
|
||||
*/
|
||||
private void cleanupResources(String sessionId, Session sshSession, SSHClient sshClient) {
|
||||
if (sshSession != null) {
|
||||
try {
|
||||
sshSession.close();
|
||||
log.debug("SSH Session已关闭: sessionId={}", sessionId);
|
||||
} catch (Exception e) {
|
||||
log.error("关闭SSH Session失败: sessionId={}", sessionId, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (sshClient != null) {
|
||||
try {
|
||||
sshClient.disconnect();
|
||||
log.debug("SSH Client已断开: sessionId={}", sessionId);
|
||||
} catch (Exception e) {
|
||||
log.error("断开SSH Client失败: sessionId={}", sessionId, e);
|
||||
}
|
||||
}
|
||||
log.info("停止Docker日志流: sessionId={}", sessionId);
|
||||
// SSH连接的清理由finally块处理,这里只需要日志记录
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,16 +33,6 @@ public class ServerLogStreamStrategy implements ILogStreamStrategy {
|
||||
@Resource
|
||||
private SSHCommandServiceFactory sshCommandServiceFactory;
|
||||
|
||||
/**
|
||||
* SSH连接存储:sessionId → SSHClient
|
||||
*/
|
||||
private final Map<String, SSHClient> sshClients = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* SSH会话存储:sessionId → Session
|
||||
*/
|
||||
private final Map<String, Session> sshSessions = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public RuntimeTypeEnum supportedType() {
|
||||
return RuntimeTypeEnum.SERVER;
|
||||
@ -73,9 +63,6 @@ public class ServerLogStreamStrategy implements ILogStreamStrategy {
|
||||
target.getPassphrase()
|
||||
);
|
||||
|
||||
// 保存SSH连接,用于后续清理
|
||||
sshClients.put(sessionId, sshClient);
|
||||
|
||||
// 2. 构建tail命令
|
||||
String command = String.format("tail -f %s -n %d",
|
||||
target.getLogFilePath(), target.getLines());
|
||||
@ -84,7 +71,6 @@ public class ServerLogStreamStrategy implements ILogStreamStrategy {
|
||||
|
||||
// 3. 执行命令
|
||||
sshSession = sshClient.startSession();
|
||||
sshSessions.put(sessionId, sshSession);
|
||||
|
||||
Session.Command cmd = sshSession.exec(command);
|
||||
|
||||
@ -111,41 +97,30 @@ public class ServerLogStreamStrategy implements ILogStreamStrategy {
|
||||
log.error("Server日志流异常: sessionId={}", sessionId, e);
|
||||
throw e;
|
||||
} finally {
|
||||
// 清理资源(正常结束时)
|
||||
cleanupResources(sessionId, sshSession, sshClient);
|
||||
// 清理SSH资源
|
||||
if (sshSession != null) {
|
||||
try {
|
||||
sshSession.close();
|
||||
log.debug("SSH Session已关闭: sessionId={}", sessionId);
|
||||
} catch (Exception e) {
|
||||
log.error("关闭SSH Session失败: sessionId={}", sessionId, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (sshClient != null) {
|
||||
try {
|
||||
sshClient.disconnect();
|
||||
log.debug("SSH Client已断开: sessionId={}", sessionId);
|
||||
} catch (Exception e) {
|
||||
log.error("断开SSH Client失败: sessionId={}", sessionId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(String sessionId) {
|
||||
log.info("停止Server日志流并清理资源: sessionId={}", sessionId);
|
||||
|
||||
Session sshSession = sshSessions.remove(sessionId);
|
||||
SSHClient sshClient = sshClients.remove(sessionId);
|
||||
|
||||
cleanupResources(sessionId, sshSession, sshClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理SSH资源
|
||||
*/
|
||||
private void cleanupResources(String sessionId, Session sshSession, SSHClient sshClient) {
|
||||
if (sshSession != null) {
|
||||
try {
|
||||
sshSession.close();
|
||||
log.debug("SSH Session已关闭: sessionId={}", sessionId);
|
||||
} catch (Exception e) {
|
||||
log.error("关闭SSH Session失败: sessionId={}", sessionId, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (sshClient != null) {
|
||||
try {
|
||||
sshClient.disconnect();
|
||||
log.debug("SSH Client已断开: sessionId={}", sessionId);
|
||||
} catch (Exception e) {
|
||||
log.error("断开SSH Client失败: sessionId={}", sessionId, e);
|
||||
}
|
||||
}
|
||||
log.info("停止Server日志流: sessionId={}", sessionId);
|
||||
// SSH连接的清理由finally块处理,这里只需要日志记录
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,11 @@ public enum SSHTargetType {
|
||||
*/
|
||||
CONTAINER,
|
||||
|
||||
/**
|
||||
* 日志流(用于日志查看功能的SSH连接)
|
||||
*/
|
||||
LOG_STREAM,
|
||||
|
||||
/**
|
||||
* 其他自定义类型
|
||||
*/
|
||||
|
||||
@ -212,31 +212,28 @@ public abstract class AbstractSSHWebSocketHandler extends TextWebSocketHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. 提前注册会话(解决并发竞态问题)
|
||||
sessionManager.registerSession(sessionId, userId, target.getTargetType(), target.getMetadata());
|
||||
boolean sessionRegistered = true;
|
||||
// 5. 尝试注册会话(原子操作,包含配额检查)
|
||||
boolean registered = sessionManager.tryRegisterSession(
|
||||
sessionId, userId, target.getTargetType(), target.getMetadata(), getMaxSessions());
|
||||
|
||||
if (!registered) {
|
||||
long currentCount = sessionManager.countUserTotalSessions(userId);
|
||||
log.warn("用户SSH连接数超过限制: userId={}, current={}, max={}",
|
||||
userId, currentCount, getMaxSessions());
|
||||
sendError(session, "SSH连接数已达上限(" + getMaxSessions() + "个),请关闭其他连接后重试");
|
||||
session.close(CloseStatus.POLICY_VIOLATION);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 6. 检查并发连接数(注册后立即检查)
|
||||
long activeCount = sessionManager.countActiveSessions(userId, target.getTargetType(), target.getMetadata());
|
||||
if (activeCount > getMaxSessions()) {
|
||||
log.warn("用户连接数超过限制: userId={}, target={}:{}, current={}, max={}",
|
||||
userId, target.getTargetType(), target.getMetadata(), activeCount, getMaxSessions());
|
||||
sendError(session, "连接数超过限制(最多" + getMaxSessions() + "个)");
|
||||
sessionManager.removeSession(sessionId);
|
||||
sessionRegistered = false;
|
||||
session.close(CloseStatus.POLICY_VIOLATION);
|
||||
return;
|
||||
}
|
||||
|
||||
// 7. 发送连接中状态
|
||||
// 6. 发送连接中状态
|
||||
sendStatus(session, SSHStatusEnum.CONNECTING);
|
||||
|
||||
// 8. 建立SSH连接
|
||||
// 7. 建立SSH连接
|
||||
SSHClient sshClient = createSSHConnection(target);
|
||||
sshClients.put(sessionId, sshClient);
|
||||
|
||||
// 9. 打开Shell通道并分配PTY(伪终端)
|
||||
// 8. 打开Shell通道并分配PTY(伪终端)
|
||||
Session sshSession = sshClient.startSession();
|
||||
|
||||
// ⚠️ 关键:分配PTY,启用交互式Shell、回显、提示符
|
||||
@ -249,7 +246,7 @@ public abstract class AbstractSSHWebSocketHandler extends TextWebSocketHandler {
|
||||
webSocketSessions.put(sessionId, session);
|
||||
sshShells.put(sessionId, shell);
|
||||
|
||||
// 10. ⚠️ 优化:先启动输出监听线程,确保不错过任何SSH输出
|
||||
// 9. ⚠️ 优化:先启动输出监听线程,确保不错过任何SSH输出
|
||||
// 直接传递session和shell,sessionId从session.attributes中获取
|
||||
Future<?> stdoutTask = asyncTaskExecutor.submit(() -> readSSHOutput(session, shell));
|
||||
outputTasks.put(sessionId, stdoutTask);
|
||||
@ -258,12 +255,12 @@ public abstract class AbstractSSHWebSocketHandler extends TextWebSocketHandler {
|
||||
Future<?> stderrTask = asyncTaskExecutor.submit(() -> readSSHError(session, shell));
|
||||
outputTasks.put(sessionId + "_stderr", stderrTask);
|
||||
|
||||
// 11. 发送连接成功状态
|
||||
// 10. 发送连接成功状态
|
||||
sendStatus(session, SSHStatusEnum.CONNECTED);
|
||||
log.info("SSH连接成功: sessionId={}, userId={}, target={}:{}",
|
||||
sessionId, userId, target.getTargetType(), target.getMetadata());
|
||||
|
||||
// 12. ⚠️ 异步创建审计日志,不阻塞主线程
|
||||
// 11. ⚠️ 异步创建审计日志,不阻塞主线程
|
||||
// 使用CompletableFuture异步执行,避免数据库操作延迟影响SSH输出接收
|
||||
java.util.concurrent.CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
@ -274,10 +271,8 @@ public abstract class AbstractSSHWebSocketHandler extends TextWebSocketHandler {
|
||||
});
|
||||
|
||||
} catch (Exception ex) {
|
||||
// 如果会话已注册,需要清理
|
||||
if (sessionRegistered) {
|
||||
sessionManager.removeSession(sessionId);
|
||||
}
|
||||
// 如果连接失败,需要清理已注册的会话
|
||||
sessionManager.removeSession(sessionId);
|
||||
throw ex; // 重新抛出,由外层catch处理
|
||||
}
|
||||
|
||||
|
||||
@ -72,6 +72,48 @@ public class SSHSessionManager {
|
||||
.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计用户的总SSH连接数(所有目标)
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 用户的总活跃会话数
|
||||
*/
|
||||
public long countUserTotalSessions(Long userId) {
|
||||
return sessions.values().stream()
|
||||
.filter(s -> s.getUserId().equals(userId))
|
||||
.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试注册会话(原子操作,包含配额检查)
|
||||
*
|
||||
* @param sessionId WebSocket会话ID
|
||||
* @param userId 用户ID
|
||||
* @param targetType 目标类型
|
||||
* @param targetId 目标ID
|
||||
* @param maxSessions 最大连接数
|
||||
* @return 是否注册成功(false表示超过配额)
|
||||
*/
|
||||
public synchronized boolean tryRegisterSession(String sessionId, Long userId,
|
||||
SSHTargetType targetType, Object targetId,
|
||||
int maxSessions) {
|
||||
// 1. 统计用户当前总连接数
|
||||
long currentCount = countUserTotalSessions(userId);
|
||||
|
||||
// 2. 检查是否超过限制
|
||||
if (currentCount >= maxSessions) {
|
||||
log.warn("用户SSH连接数已达上限: userId={}, current={}, max={}",
|
||||
userId, currentCount, maxSessions);
|
||||
return false; // 超过配额,拒绝注册
|
||||
}
|
||||
|
||||
// 3. 注册会话
|
||||
registerSession(sessionId, userId, targetType, targetId);
|
||||
log.debug("会话注册成功: sessionId={}, userId={}, current={}/{}",
|
||||
sessionId, userId, currentCount + 1, maxSessions);
|
||||
return true; // 注册成功
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话信息
|
||||
*
|
||||
|
||||
@ -11,6 +11,11 @@ public class EntityPathResolver {
|
||||
private static final Logger log = LoggerFactory.getLogger(EntityPathResolver.class);
|
||||
|
||||
public static Path<?> getPath(EntityPath<?> entityPath, String fieldName) {
|
||||
// 支持点号分隔的关联路径(如 "application.appName")
|
||||
if (fieldName.contains(".")) {
|
||||
return getNestedPath(entityPath, fieldName);
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 首先尝试直接获取字段
|
||||
try {
|
||||
@ -53,4 +58,38 @@ public class EntityPathResolver {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析嵌套路径(支持点号分隔,如 "application.appName")
|
||||
*/
|
||||
private static Path<?> getNestedPath(EntityPath<?> entityPath, String fieldName) {
|
||||
String[] parts = fieldName.split("\\.", 2);
|
||||
if (parts.length != 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String firstField = parts[0];
|
||||
String remainingPath = parts[1];
|
||||
|
||||
try {
|
||||
// 获取第一级字段(如 "application")
|
||||
Path<?> firstPath = getPath(entityPath, firstField);
|
||||
if (firstPath == null) {
|
||||
log.debug("First level field {} not found", firstField);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 如果第一级字段不是 EntityPath,无法继续解析
|
||||
if (!(firstPath instanceof EntityPath)) {
|
||||
log.debug("Field {} is not an EntityPath, cannot resolve nested path", firstField);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 递归解析剩余路径
|
||||
return getPath((EntityPath<?>) firstPath, remainingPath);
|
||||
} catch (Exception e) {
|
||||
log.error("Error resolving nested path {}: {}", fieldName, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,18 @@
|
||||
package com.qqchen.deploy.backend.framework.websocket.log;
|
||||
|
||||
import com.qqchen.deploy.backend.deploy.enums.RuntimeTypeEnum;
|
||||
import com.qqchen.deploy.backend.framework.enums.LogControlAction;
|
||||
import com.qqchen.deploy.backend.framework.enums.LogMessageType;
|
||||
import com.qqchen.deploy.backend.framework.enums.LogStatusEnum;
|
||||
import com.qqchen.deploy.backend.framework.enums.SSHTargetType;
|
||||
import com.qqchen.deploy.backend.framework.ssh.websocket.SSHSessionManager;
|
||||
import com.qqchen.deploy.backend.framework.utils.JsonUtils;
|
||||
import com.qqchen.deploy.backend.framework.websocket.log.request.LogControlRequest;
|
||||
import com.qqchen.deploy.backend.framework.websocket.log.request.LogStreamRequest;
|
||||
import com.qqchen.deploy.backend.framework.websocket.log.response.LogErrorResponse;
|
||||
import com.qqchen.deploy.backend.framework.websocket.log.response.LogLineResponse;
|
||||
import com.qqchen.deploy.backend.framework.websocket.log.response.LogStatusResponse;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
@ -64,6 +68,17 @@ public abstract class AbstractLogStreamWebSocketHandler extends TextWebSocketHan
|
||||
*/
|
||||
protected final Map<String, ILogStreamStrategy> sessionStrategies = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* SSH会话管理器(用于配额管理)
|
||||
*/
|
||||
@Resource
|
||||
private SSHSessionManager sshSessionManager;
|
||||
|
||||
/**
|
||||
* 最大SSH连接数(与SSH终端共享配额)
|
||||
*/
|
||||
private static final int MAX_SSH_SESSIONS = 5;
|
||||
|
||||
// ========== 子类必须实现的抽象方法 ==========
|
||||
|
||||
/**
|
||||
@ -194,7 +209,28 @@ public abstract class AbstractLogStreamWebSocketHandler extends TextWebSocketHan
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. 发送流式传输状态
|
||||
// 5. 对于需要SSH的运行时类型,检查配额
|
||||
if (target.getRuntimeType() == RuntimeTypeEnum.SERVER
|
||||
|| target.getRuntimeType() == RuntimeTypeEnum.DOCKER) {
|
||||
|
||||
// 尝试注册会话(包含配额检查)
|
||||
boolean registered = sshSessionManager.tryRegisterSession(
|
||||
sessionId, userId,
|
||||
SSHTargetType.LOG_STREAM,
|
||||
target.getName(), MAX_SSH_SESSIONS);
|
||||
|
||||
if (!registered) {
|
||||
long currentCount = sshSessionManager.countUserTotalSessions(userId);
|
||||
log.warn("用户SSH连接数超过限制: userId={}, current={}, max={}",
|
||||
userId, currentCount, MAX_SSH_SESSIONS);
|
||||
sendError(session, "SSH连接数已达上限(" + MAX_SSH_SESSIONS + "个),请关闭其他连接后重试");
|
||||
return; // 不启动日志流
|
||||
}
|
||||
|
||||
log.debug("日志流SSH会话注册成功: sessionId={}, userId={}", sessionId, userId);
|
||||
}
|
||||
|
||||
// 6. 发送流式传输状态
|
||||
sendStatus(session, LogStatusEnum.STREAMING);
|
||||
|
||||
// 6. 创建暂停标志
|
||||
@ -331,20 +367,28 @@ public abstract class AbstractLogStreamWebSocketHandler extends TextWebSocketHan
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 取消日志流任务
|
||||
// 2. 从SSH会话管理器移除(释放配额)
|
||||
try {
|
||||
sshSessionManager.removeSession(sessionId);
|
||||
log.debug("SSH会话已从配额管理器移除: sessionId={}", sessionId);
|
||||
} catch (Exception e) {
|
||||
log.error("从SSH会话管理器移除失败: sessionId={}", sessionId, e);
|
||||
}
|
||||
|
||||
// 3. 取消日志流任务
|
||||
Future<?> task = streamTasks.remove(sessionId);
|
||||
if (task != null && !task.isDone()) {
|
||||
log.debug("取消日志流任务: sessionId={}", sessionId);
|
||||
task.cancel(true);
|
||||
}
|
||||
|
||||
// 3. 移除WebSocketSession
|
||||
// 4. 移除WebSocketSession
|
||||
webSocketSessions.remove(sessionId);
|
||||
|
||||
// 4. 移除暂停标志
|
||||
// 5. 移除暂停标志
|
||||
pausedFlags.remove(sessionId);
|
||||
|
||||
// 5. 移除target信息
|
||||
// 6. 移除target信息
|
||||
sessionTargets.remove(sessionId);
|
||||
|
||||
log.info("日志流会话资源清理完成: sessionId={}", sessionId);
|
||||
|
||||
@ -9,6 +9,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@ -178,50 +179,42 @@ export const LogViewerDialog: React.FC<LogViewerDialogProps> = ({
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-6xl h-[85vh] flex flex-col p-0 overflow-hidden">
|
||||
<DialogHeader className="px-6 pt-6 pb-4 border-b flex-shrink-0">
|
||||
<DialogTitle className="flex items-center justify-between">
|
||||
<DialogHeader className="pl-6 pr-16 pt-6 pb-4 border-b flex-shrink-0">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
{/* 左侧:标题信息 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<ScrollText className="h-5 w-5" />
|
||||
<span className="text-lg font-semibold">{app.applicationName} - 日志查看</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm font-normal">
|
||||
<span className="text-muted-foreground">|</span>
|
||||
<Badge variant="outline" className={runtimeConfig.bg}>
|
||||
<RuntimeIcon className={`h-3 w-3 mr-1 ${runtimeConfig.color}`} />
|
||||
{runtimeConfig.label}
|
||||
</Badge>
|
||||
<span className="text-sm font-normal text-muted-foreground">{environment.environmentName}</span>
|
||||
<Circle className={`h-2 w-2 ${statusIndicator.color} fill-current`} />
|
||||
<span className="text-muted-foreground">{environment.environmentName}</span>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* 控制面板 */}
|
||||
<div className="px-6 py-3 border-b flex-shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-24">
|
||||
<Label htmlFor="lines" className="text-xs">行数</Label>
|
||||
<Input
|
||||
id="lines"
|
||||
type="number"
|
||||
value={lines}
|
||||
onChange={(e) => setLines(Number(e.target.value))}
|
||||
min={10}
|
||||
max={1000}
|
||||
className="h-8 mt-1"
|
||||
/>
|
||||
</div>
|
||||
{/* 右侧:控制元素 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
value={lines}
|
||||
onChange={(e) => setLines(Number(e.target.value))}
|
||||
min={10}
|
||||
max={1000}
|
||||
placeholder="行数"
|
||||
className="h-8 w-20"
|
||||
/>
|
||||
|
||||
{app.runtimeType === 'K8S' && (
|
||||
<div className="w-64">
|
||||
<Label htmlFor="podName" className="text-xs">Pod</Label>
|
||||
<>
|
||||
{loadingPods ? (
|
||||
<div className="h-8 flex items-center justify-center border rounded-md mt-1">
|
||||
<div className="h-8 w-48 flex items-center justify-center border rounded-md">
|
||||
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : podNames.length > 0 ? (
|
||||
<Select value={podName} onValueChange={setPodName}>
|
||||
<SelectTrigger className="h-8 mt-1">
|
||||
<SelectTrigger className="h-8 w-48">
|
||||
<SelectValue placeholder="选择Pod" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@ -234,82 +227,111 @@ export const LogViewerDialog: React.FC<LogViewerDialogProps> = ({
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
id="podName"
|
||||
value={podName}
|
||||
onChange={(e) => setPodName(e.target.value)}
|
||||
placeholder="无可用Pod"
|
||||
className="h-8 mt-1"
|
||||
className="h-8 w-48"
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex-1" />
|
||||
<TooltipProvider>
|
||||
<div className="flex gap-1">
|
||||
{/* 启动/恢复/重试按钮 */}
|
||||
{(status === LogStreamStatus.DISCONNECTED ||
|
||||
status === LogStreamStatus.CONNECTED ||
|
||||
status === LogStreamStatus.ERROR) && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleRestart}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Play className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{status === LogStreamStatus.ERROR ? '重试' : '启动'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 items-end">
|
||||
{/* 启动/恢复/重试按钮 */}
|
||||
{(status === LogStreamStatus.DISCONNECTED ||
|
||||
status === LogStreamStatus.CONNECTED ||
|
||||
status === LogStreamStatus.ERROR) && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleRestart}
|
||||
disabled={false}
|
||||
>
|
||||
<Play className="h-3 w-3 mr-1" />
|
||||
{status === LogStreamStatus.ERROR ? '重试' : '启动'}
|
||||
</Button>
|
||||
)}
|
||||
{/* 连接中按钮 */}
|
||||
{status === LogStreamStatus.CONNECTING && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="sm" variant="outline" disabled className="h-8 w-8 p-0">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>连接中</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* 连接中按钮 */}
|
||||
{status === LogStreamStatus.CONNECTING && (
|
||||
<Button size="sm" variant="outline" disabled>
|
||||
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
|
||||
连接中
|
||||
</Button>
|
||||
)}
|
||||
{/* 恢复按钮(暂停状态) */}
|
||||
{status === LogStreamStatus.PAUSED && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="sm" variant="outline" onClick={resume} className="h-8 w-8 p-0">
|
||||
<Play className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>恢复</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* 恢复按钮(暂停状态) */}
|
||||
{status === LogStreamStatus.PAUSED && (
|
||||
<Button size="sm" variant="outline" onClick={resume}>
|
||||
<Play className="h-3 w-3 mr-1" />
|
||||
恢复
|
||||
</Button>
|
||||
)}
|
||||
{/* 暂停按钮(流式传输中) */}
|
||||
{status === LogStreamStatus.STREAMING && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="sm" variant="outline" onClick={pause} className="h-8 w-8 p-0">
|
||||
<Pause className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>暂停</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* 暂停按钮(流式传输中) */}
|
||||
{status === LogStreamStatus.STREAMING && (
|
||||
<Button size="sm" variant="outline" onClick={pause}>
|
||||
<Pause className="h-3 w-3 mr-1" />
|
||||
暂停
|
||||
</Button>
|
||||
)}
|
||||
{/* 停止按钮 */}
|
||||
{(status === LogStreamStatus.CONNECTING ||
|
||||
status === LogStreamStatus.STREAMING ||
|
||||
status === LogStreamStatus.PAUSED) && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="sm" variant="outline" onClick={stop} className="h-8 w-8 p-0">
|
||||
<Square className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>停止</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* 停止按钮 */}
|
||||
{(status === LogStreamStatus.CONNECTING ||
|
||||
status === LogStreamStatus.STREAMING ||
|
||||
status === LogStreamStatus.PAUSED) && (
|
||||
<Button size="sm" variant="outline" onClick={stop}>
|
||||
<Square className="h-3 w-3 mr-1" />
|
||||
停止
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* 清空按钮 */}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={clearLogs}
|
||||
disabled={status === LogStreamStatus.CONNECTING}
|
||||
>
|
||||
<Trash2 className="h-3 w-3 mr-1" />
|
||||
清空
|
||||
</Button>
|
||||
</div>
|
||||
{/* 清空按钮 */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={clearLogs}
|
||||
disabled={status === LogStreamStatus.CONNECTING}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>清空</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
|
||||
{/* 日志显示区域 */}
|
||||
<div className="flex-1 overflow-hidden px-6 py-4">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user