Fix run log ingest and terminal console layout
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Pause, Play, RotateCw, Send, Sparkles, Terminal, Trash2, X } from "lucide-react";
|
||||
import { ListChecks, Pause, Play, RotateCw, Send, Sparkles, Terminal, Trash2, X } from "lucide-react";
|
||||
import { type FormEvent, type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { platformApiClient } from "../api/client";
|
||||
@@ -13,6 +13,12 @@ type TerminalLine = { id: string; tone: "input" | "info" | "success" | "error";
|
||||
|
||||
const liveLogPollMs = 2000;
|
||||
const maxLogEntries = 500;
|
||||
const terminalQuickCommands = [
|
||||
{ label: "查询玩家", command: "ListPlayers" },
|
||||
{ label: "服务器状态", command: "ServerInfo" },
|
||||
{ label: "设为中午", command: "SetTime 12" },
|
||||
{ label: "保存世界", command: "SaveWorld" }
|
||||
];
|
||||
|
||||
interface LiveOperationDrawerProps {
|
||||
open: boolean;
|
||||
@@ -20,9 +26,12 @@ interface LiveOperationDrawerProps {
|
||||
description?: string;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
backdropClassName?: string;
|
||||
panelClassName?: string;
|
||||
bodyClassName?: string;
|
||||
}
|
||||
|
||||
function LiveOperationDrawer({ open, title, description, onClose, children }: LiveOperationDrawerProps) {
|
||||
function LiveOperationDrawer({ open, title, description, onClose, children, backdropClassName, panelClassName, bodyClassName }: LiveOperationDrawerProps) {
|
||||
useEffect(() => {
|
||||
if (!open) return undefined;
|
||||
const previous = document.body.style.overflow;
|
||||
@@ -39,8 +48,8 @@ function LiveOperationDrawer({ open, title, description, onClose, children }: Li
|
||||
|
||||
if (!open) return null;
|
||||
return (
|
||||
<div className="drawer-backdrop" role="presentation" onClick={onClose}>
|
||||
<aside className="drawer-panel live-operation-drawer" role="dialog" aria-modal="true" aria-label={title} onClick={(event) => event.stopPropagation()}>
|
||||
<div className={cx("drawer-backdrop", backdropClassName)} role="presentation" onClick={onClose}>
|
||||
<aside className={cx("drawer-panel live-operation-drawer", panelClassName)} role="dialog" aria-modal="true" aria-label={title} onClick={(event) => event.stopPropagation()}>
|
||||
<div className="panel-header">
|
||||
<div>
|
||||
<h2>{title}</h2>
|
||||
@@ -48,7 +57,7 @@ function LiveOperationDrawer({ open, title, description, onClose, children }: Li
|
||||
</div>
|
||||
<button type="button" className="theme-upload drawer-close" aria-label={`关闭${title}`} onClick={onClose}><X size={14} /><span>关闭</span></button>
|
||||
</div>
|
||||
{children}
|
||||
<div className={cx("live-operation-content", bodyClassName)}>{children}</div>
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
@@ -200,21 +209,31 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, plu
|
||||
}
|
||||
|
||||
return (
|
||||
<LiveOperationDrawer open={open} title="管理终端" description={`${serverName} · 平台授权的一次性命令调度`} onClose={onClose}>
|
||||
<div className="terminal-output" role="log" aria-live="polite">
|
||||
{lines.map((line) => <div key={line.id} className={`terminal-line terminal-line-${line.tone}`}><time>{line.at}</time><span>{line.text}</span></div>)}
|
||||
</div>
|
||||
{result && <ResultBadge status={result.status} label={result.label} />}
|
||||
{!supportsCommands && <EmptyState icon={<Terminal size={24} />} title="终端不可用" description="当前插件没有声明平台可调度的即时命令能力,因此不会开放浏览器直连 shell。" />}
|
||||
{supportsCommands && (
|
||||
<form className="terminal-command-form" onSubmit={(event) => void submitCommand(event)}>
|
||||
<label>
|
||||
SCUM 管理命令
|
||||
<input value={command} disabled={!canManage || pending} placeholder={canManage ? "输入单行命令,回车提交" : "当前账号没有运行操作权限"} onChange={(event) => setCommand(event.target.value)} />
|
||||
</label>
|
||||
<button type="submit" className="primary-command" disabled={!canManage || pending || !command.trim()}>{pending ? <Sparkles size={15} /> : <Send size={15} />}<span>{pending ? "提交中…" : "发送"}</span></button>
|
||||
</form>
|
||||
)}
|
||||
<LiveOperationDrawer open={open} title="管理终端" description={`${serverName} · 平台授权的一次性命令调度`} onClose={onClose} backdropClassName="terminal-drawer-backdrop" panelClassName="management-terminal-drawer" bodyClassName="management-terminal-body">
|
||||
<section className="terminal-output-panel" aria-label="terminal output">
|
||||
<div className="terminal-output" role="log" aria-live="polite">
|
||||
{lines.map((line) => <div key={line.id} className={`terminal-line terminal-line-${line.tone}`}><time>{line.at}</time><span>{line.text}</span></div>)}
|
||||
</div>
|
||||
</section>
|
||||
<section className="terminal-command-dock" aria-label="terminal command controls">
|
||||
<div className="terminal-command-dock-header">
|
||||
<span><ListChecks size={14} />快捷指令</span>
|
||||
{result && <ResultBadge status={result.status} label={result.label} />}
|
||||
</div>
|
||||
<div className="terminal-quick-command-list">
|
||||
{terminalQuickCommands.map((item) => <button key={item.command} type="button" className="terminal-quick-command" disabled={!supportsCommands || !canManage || pending} onClick={() => setCommand(item.command)}><strong>{item.label}</strong><span>{item.command}</span></button>)}
|
||||
</div>
|
||||
{!supportsCommands && <EmptyState icon={<Terminal size={24} />} title="终端不可用" description="当前插件没有声明平台可调度的即时命令能力,因此不会开放浏览器直连 shell。" />}
|
||||
{supportsCommands && (
|
||||
<form className="terminal-command-form" onSubmit={(event) => void submitCommand(event)}>
|
||||
<label>
|
||||
SCUM 管理命令
|
||||
<input value={command} disabled={!canManage || pending} placeholder={canManage ? "输入单行命令,回车提交" : "当前账号没有运行操作权限"} onChange={(event) => setCommand(event.target.value)} />
|
||||
</label>
|
||||
<button type="submit" className="primary-command" disabled={!canManage || pending || !command.trim()}>{pending ? <Sparkles size={15} /> : <Send size={15} />}<span>{pending ? "提交中…" : "发送"}</span></button>
|
||||
</form>
|
||||
)}
|
||||
</section>
|
||||
</LiveOperationDrawer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ServersPage } from "./ServersPage";
|
||||
import { UsersPage } from "./UsersPage";
|
||||
import runtimeTaskProgressSource from "../components/RuntimeTaskProgress.tsx?raw";
|
||||
import serverDeploymentWorkflowSource from "../components/ServerDeploymentWorkflow.tsx?raw";
|
||||
import serverLiveOperationsSource from "../components/ServerLiveOperations.tsx?raw";
|
||||
import serversPageSource from "./ServersPage.tsx?raw";
|
||||
import serverDetailPageSource from "./ServerDetailPage.tsx?raw";
|
||||
import type { PageComponentProps } from "../contracts/page";
|
||||
@@ -289,6 +290,13 @@ describe("first-party console pages", () => {
|
||||
expect(html).toContain("操作历史");
|
||||
});
|
||||
|
||||
it("renders management terminal as a large command console with quick commands", () => {
|
||||
expect(serverLiveOperationsSource).toContain("management-terminal-drawer");
|
||||
expect(serverLiveOperationsSource).toContain("terminal-command-dock");
|
||||
expect(serverLiveOperationsSource).toContain("terminalQuickCommands");
|
||||
expect(serverLiveOperationsSource).toContain("setCommand(item.command)");
|
||||
});
|
||||
|
||||
it("renders plugin catalog bridge readiness", () => {
|
||||
const html = renderToStaticMarkup(<PluginsPage />);
|
||||
|
||||
|
||||
@@ -165,6 +165,15 @@ describe("platform web shared theme CSS", () => {
|
||||
expect(css).toContain("max-height:calc(100dvh-20px)");
|
||||
});
|
||||
|
||||
it("keeps the management terminal opaque and large", () => {
|
||||
const css = compact(readThemeCss());
|
||||
|
||||
expect(css).toContain(".management-terminal-drawer{width:100%;max-width:none;height:90dvh;max-height:90dvh");
|
||||
expect(css).toContain("background:#05090f;backdrop-filter:none");
|
||||
expect(css).toContain(".management-terminal-body{height:100%;grid-template-rows:minmax(0,1fr)auto");
|
||||
expect(css).toContain(".terminal-quick-command-list{display:grid;grid-template-columns:repeat(4,minmax(0,1fr))");
|
||||
});
|
||||
|
||||
it("keeps deployment workflow fields compact when a mode has one input", () => {
|
||||
const css = compact(readThemeCss());
|
||||
|
||||
|
||||
@@ -486,16 +486,32 @@ to{transform:translate(-50%,-50%) rotate(calc(var(--construct-drift) + 360deg))}
|
||||
.drawer-backdrop{position:fixed;inset:0;z-index:40;background:rgba(61,36,65,.34);backdrop-filter:blur(6px);display:flex;justify-content:flex-end;overflow-y:auto;overscroll-behavior:contain}
|
||||
.drawer-panel{width:min(440px,100%);height:100%;max-height:100dvh;display:grid;align-content:start;gap:14px;padding:20px;background:var(--glass-wash),var(--glass-tint),var(--surface-raised);backdrop-filter:blur(14px) saturate(0.28);border-left:1px solid var(--line);box-shadow:inset 1px 0 0 var(--crystal-rim),-18px 0 42px var(--glass-shadow);position:relative;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain}
|
||||
.live-operation-drawer{width:min(720px,100%)}
|
||||
.live-operation-content{display:grid;gap:12px;min-height:0}
|
||||
.live-operation-toolbar .icon-command{min-height:36px}
|
||||
.live-log-list{max-height:min(58dvh,560px)}
|
||||
.live-log-line{cursor:default}
|
||||
.terminal-drawer-backdrop{align-items:flex-end;justify-content:center;padding:5dvh 0 0;background:rgba(2,6,10,.76);backdrop-filter:none}
|
||||
.management-terminal-drawer{width:100%;max-width:none;height:90dvh;max-height:90dvh;align-content:stretch;grid-template-rows:auto minmax(0,1fr);padding:16px 18px;border:1px solid var(--line-strong);border-bottom:0;border-radius:8px 8px 0 0;background:#05090f;backdrop-filter:none;box-shadow:0 -18px 48px rgba(0,0,0,.56),inset 0 1px 0 color-mix(in srgb,var(--rim-light) 24%,transparent)}
|
||||
.management-terminal-drawer>.panel-header{top:-16px;margin:-16px -18px 0;padding:14px 18px 12px;background:#07101a;-webkit-backdrop-filter:none;backdrop-filter:none}
|
||||
.management-terminal-body{height:100%;grid-template-rows:minmax(0,1fr) auto;gap:12px}
|
||||
.terminal-output-panel{display:grid;min-height:0}
|
||||
.terminal-output{display:grid;gap:6px;min-height:260px;max-height:min(54dvh,520px);overflow:auto;padding:12px;border-radius:8px;background:radial-gradient(circle at 92% 0,rgba(255,255,255,.1),transparent 36%),var(--code-surface);font-family:var(--font-mono);font-size:12.5px;color:var(--code-ink);box-shadow:inset 0 1px 0 rgba(255,255,255,.16),0 14px 32px rgba(255,255,255,.12)}
|
||||
.management-terminal-body .terminal-output{height:100%;min-height:0;max-height:none}
|
||||
.terminal-line{display:grid;grid-template-columns:72px minmax(0,1fr);gap:10px;align-items:start}
|
||||
.terminal-line time{color:var(--code-muted)}
|
||||
.terminal-line span{overflow-wrap:anywhere}
|
||||
.terminal-line-input span{color:var(--accent)}
|
||||
.terminal-line-success span{color:var(--teal)}
|
||||
.terminal-line-error span{color:var(--danger)}
|
||||
.terminal-command-dock{display:grid;gap:10px;padding:12px;border:1px solid var(--line-strong);border-radius:8px;background:#08111b;box-shadow:inset 0 1px 0 rgba(255,255,255,.12),0 14px 32px rgba(0,0,0,.28)}
|
||||
.terminal-command-dock-header{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
||||
.terminal-command-dock-header>span{display:inline-flex;align-items:center;gap:6px;color:var(--ink);font-weight:800}
|
||||
.terminal-quick-command-list{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
|
||||
.terminal-quick-command{display:grid;gap:4px;min-height:52px;padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:#0b1722;color:var(--ink-soft);cursor:pointer;text-align:left}
|
||||
.terminal-quick-command:hover,.terminal-quick-command:focus-visible{border-color:var(--accent);outline:0;color:var(--ink)}
|
||||
.terminal-quick-command:disabled{cursor:not-allowed;opacity:.56}
|
||||
.terminal-quick-command strong{font-size:12px;color:var(--ink)}
|
||||
.terminal-quick-command span{font-family:var(--font-mono);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.terminal-command-form{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;align-items:end}
|
||||
.terminal-command-form label{display:grid;gap:6px;color:var(--ink-soft);font-size:12px}
|
||||
.terminal-command-form input{min-height:38px;border:1px solid var(--line-strong);border-radius:8px;padding:0 10px;background:var(--surface-solid);color:var(--ink);font:inherit}
|
||||
@@ -695,6 +711,8 @@ to{transform:translate(-50%,-50%) rotate(calc(var(--construct-drift) + 360deg))}
|
||||
.server-detail-title-row .action-strip>*{flex:1 1 auto}
|
||||
.provider-table,.resource-table{min-width:680px}
|
||||
.log-line{grid-template-columns:minmax(0,1fr);gap:2px}
|
||||
.terminal-quick-command-list{grid-template-columns:repeat(2,minmax(0,1fr))}
|
||||
.management-terminal-drawer{height:92dvh;max-height:92dvh}
|
||||
.ai-provider-toolbar{align-items:stretch}
|
||||
.icon-command,.segmented-button{flex:1 1 auto}
|
||||
.provider-form{order:-1}
|
||||
|
||||
Reference in New Issue
Block a user