From 82eb9ca6d6c5433421b5fdf069f2ee23ada926d8 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Tue, 16 Dec 2025 17:55:13 +0800 Subject: [PATCH] =?UTF-8?q?1.33=20=E6=97=A5=E5=BF=97=E9=80=9A=E7=94=A8?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deploy/entity/TeamApplication.java | 7 + .../deploy/query/TeamApplicationQuery.java | 9 + .../strategy/log/DockerLogStreamStrategy.java | 65 ++---- .../strategy/log/ServerLogStreamStrategy.java | 65 ++---- .../framework/enums/SSHTargetType.java | 5 + .../AbstractSSHWebSocketHandler.java | 45 ++-- .../ssh/websocket/SSHSessionManager.java | 42 ++++ .../framework/utils/EntityPathResolver.java | 39 ++++ .../AbstractLogStreamWebSocketHandler.java | 54 ++++- .../Dashboard/components/LogViewerDialog.tsx | 198 ++++++++++-------- 10 files changed, 321 insertions(+), 208 deletions(-) diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/TeamApplication.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/TeamApplication.java index adee627e..611785a4 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/TeamApplication.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/TeamApplication.java @@ -32,6 +32,13 @@ public class TeamApplication extends Entity { @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 */ diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/query/TeamApplicationQuery.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/query/TeamApplicationQuery.java index 4af9a149..a53d3376 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/query/TeamApplicationQuery.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/query/TeamApplicationQuery.java @@ -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; } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/strategy/log/DockerLogStreamStrategy.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/strategy/log/DockerLogStreamStrategy.java index 3782dde0..876bf2cf 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/strategy/log/DockerLogStreamStrategy.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/strategy/log/DockerLogStreamStrategy.java @@ -33,16 +33,6 @@ public class DockerLogStreamStrategy implements ILogStreamStrategy { @Resource private SSHCommandServiceFactory sshCommandServiceFactory; - /** - * SSH连接存储:sessionId → SSHClient - */ - private final Map sshClients = new ConcurrentHashMap<>(); - - /** - * SSH会话存储:sessionId → Session - */ - private final Map 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块处理,这里只需要日志记录 } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/strategy/log/ServerLogStreamStrategy.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/strategy/log/ServerLogStreamStrategy.java index 0bbc6685..c14150a9 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/strategy/log/ServerLogStreamStrategy.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/strategy/log/ServerLogStreamStrategy.java @@ -33,16 +33,6 @@ public class ServerLogStreamStrategy implements ILogStreamStrategy { @Resource private SSHCommandServiceFactory sshCommandServiceFactory; - /** - * SSH连接存储:sessionId → SSHClient - */ - private final Map sshClients = new ConcurrentHashMap<>(); - - /** - * SSH会话存储:sessionId → Session - */ - private final Map 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块处理,这里只需要日志记录 } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/SSHTargetType.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/SSHTargetType.java index e0a93217..68085abc 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/SSHTargetType.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/SSHTargetType.java @@ -19,6 +19,11 @@ public enum SSHTargetType { */ CONTAINER, + /** + * 日志流(用于日志查看功能的SSH连接) + */ + LOG_STREAM, + /** * 其他自定义类型 */ diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/websocket/AbstractSSHWebSocketHandler.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/websocket/AbstractSSHWebSocketHandler.java index e9974728..f4a56c90 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/websocket/AbstractSSHWebSocketHandler.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/websocket/AbstractSSHWebSocketHandler.java @@ -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处理 } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/websocket/SSHSessionManager.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/websocket/SSHSessionManager.java index 533d11cf..4da53323 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/websocket/SSHSessionManager.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/ssh/websocket/SSHSessionManager.java @@ -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; // 注册成功 + } + /** * 获取会话信息 * diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/utils/EntityPathResolver.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/utils/EntityPathResolver.java index 18c5c1cf..3a80bdae 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/utils/EntityPathResolver.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/utils/EntityPathResolver.java @@ -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; + } + } } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/websocket/log/AbstractLogStreamWebSocketHandler.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/websocket/log/AbstractLogStreamWebSocketHandler.java index 6a274235..645990c7 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/websocket/log/AbstractLogStreamWebSocketHandler.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/websocket/log/AbstractLogStreamWebSocketHandler.java @@ -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 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); diff --git a/frontend/src/pages/Dashboard/components/LogViewerDialog.tsx b/frontend/src/pages/Dashboard/components/LogViewerDialog.tsx index 31f1933c..50971335 100644 --- a/frontend/src/pages/Dashboard/components/LogViewerDialog.tsx +++ b/frontend/src/pages/Dashboard/components/LogViewerDialog.tsx @@ -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 = ({ return ( - - + +
+ {/* 左侧:标题信息 */}
{app.applicationName} - 日志查看 -
-
+ | {runtimeConfig.label} + {environment.environmentName} - {environment.environmentName}
- - -
- {/* 控制面板 */} -
-
-
- - setLines(Number(e.target.value))} - min={10} - max={1000} - className="h-8 mt-1" - /> -
+ {/* 右侧:控制元素 */} +
+ setLines(Number(e.target.value))} + min={10} + max={1000} + placeholder="行数" + className="h-8 w-20" + /> {app.runtimeType === 'K8S' && ( -
- + <> {loadingPods ? ( -
+
) : podNames.length > 0 ? ( ) : ( setPodName(e.target.value)} placeholder="无可用Pod" - className="h-8 mt-1" + className="h-8 w-48" disabled /> )} -
+ )} -
+ +
+ {/* 启动/恢复/重试按钮 */} + {(status === LogStreamStatus.DISCONNECTED || + status === LogStreamStatus.CONNECTED || + status === LogStreamStatus.ERROR) && ( + + + + + + {status === LogStreamStatus.ERROR ? '重试' : '启动'} + + + )} -
- {/* 启动/恢复/重试按钮 */} - {(status === LogStreamStatus.DISCONNECTED || - status === LogStreamStatus.CONNECTED || - status === LogStreamStatus.ERROR) && ( - - )} + {/* 连接中按钮 */} + {status === LogStreamStatus.CONNECTING && ( + + + + + 连接中 + + )} - {/* 连接中按钮 */} - {status === LogStreamStatus.CONNECTING && ( - - )} + {/* 恢复按钮(暂停状态) */} + {status === LogStreamStatus.PAUSED && ( + + + + + 恢复 + + )} - {/* 恢复按钮(暂停状态) */} - {status === LogStreamStatus.PAUSED && ( - - )} + {/* 暂停按钮(流式传输中) */} + {status === LogStreamStatus.STREAMING && ( + + + + + 暂停 + + )} - {/* 暂停按钮(流式传输中) */} - {status === LogStreamStatus.STREAMING && ( - - )} + {/* 停止按钮 */} + {(status === LogStreamStatus.CONNECTING || + status === LogStreamStatus.STREAMING || + status === LogStreamStatus.PAUSED) && ( + + + + + 停止 + + )} - {/* 停止按钮 */} - {(status === LogStreamStatus.CONNECTING || - status === LogStreamStatus.STREAMING || - status === LogStreamStatus.PAUSED) && ( - - )} - - {/* 清空按钮 */} - -
+ {/* 清空按钮 */} + + + + + 清空 + +
+
+ + +
{/* 日志显示区域 */}