feat: 重构运行日志界面
This commit is contained in:
parent
4fb0f64d4f
commit
9cb4eeab96
134
src/renderer/src/components/RunLogView.tsx
Normal file
134
src/renderer/src/components/RunLogView.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { ArrowDown, Check, Copy, Download } from "lucide-react";
|
||||
import type { RunLog } from "../store/runStore";
|
||||
|
||||
type LogFilter = "all" | "info" | "warn" | "error";
|
||||
|
||||
const LEVEL_LABEL: Record<LogFilter, string> = {
|
||||
all: "全部",
|
||||
info: "信息",
|
||||
warn: "警告",
|
||||
error: "错误"
|
||||
};
|
||||
|
||||
interface RunLogViewProps {
|
||||
logs: RunLog[];
|
||||
nameOf: (nodeId?: string) => string;
|
||||
fileName?: string;
|
||||
emptyText?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function RunLogView({ logs, nameOf, fileName = "rpa-run-logs.log", emptyText = "运行日志会显示在这里…", className }: RunLogViewProps) {
|
||||
const [filter, setFilter] = useState<LogFilter>("all");
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [pinned, setPinned] = useState(true);
|
||||
const consoleRef = useRef<HTMLDivElement>(null);
|
||||
const pinnedRef = useRef(true);
|
||||
|
||||
const counts = useMemo(() => ({
|
||||
all: logs.length,
|
||||
info: logs.filter((log) => log.level === "info").length,
|
||||
warn: logs.filter((log) => log.level === "warn").length,
|
||||
error: logs.filter((log) => log.level === "error").length
|
||||
}), [logs]);
|
||||
|
||||
const visibleLogs = useMemo(() => (
|
||||
filter === "all" ? logs : logs.filter((log) => log.level === filter)
|
||||
), [filter, logs]);
|
||||
|
||||
const logText = useMemo(() => (
|
||||
logs.map((log) => `[${log.time}] [${log.level.toUpperCase()}] ${nameOf(log.nodeId)} - ${log.message}`).join("\n")
|
||||
), [logs, nameOf]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const element = consoleRef.current;
|
||||
if (element && pinnedRef.current) element.scrollTop = element.scrollHeight;
|
||||
}, [visibleLogs]);
|
||||
|
||||
const trackPin = () => {
|
||||
const element = consoleRef.current;
|
||||
if (!element) return;
|
||||
const nextPinned = element.scrollHeight - element.scrollTop - element.clientHeight < 48;
|
||||
if (nextPinned !== pinnedRef.current) {
|
||||
pinnedRef.current = nextPinned;
|
||||
setPinned(nextPinned);
|
||||
}
|
||||
};
|
||||
|
||||
const scrollToLatest = () => {
|
||||
pinnedRef.current = true;
|
||||
const element = consoleRef.current;
|
||||
if (element) element.scrollTo({ top: element.scrollHeight, behavior: "smooth" });
|
||||
};
|
||||
|
||||
const copyLogs = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(logText);
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 1600);
|
||||
} catch {
|
||||
// Clipboard access can be blocked by system policy; download remains available.
|
||||
}
|
||||
};
|
||||
|
||||
const downloadLogs = () => {
|
||||
const blob = new Blob([logText], { type: "text/plain;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`run-log-view ${className || ""}`}>
|
||||
<div className="run-log-toolbar">
|
||||
<div className="run-log-filters" role="group" aria-label="日志级别过滤">
|
||||
{(Object.keys(LEVEL_LABEL) as LogFilter[]).map((level) => (
|
||||
<button
|
||||
type="button"
|
||||
key={level}
|
||||
className={`run-log-filter ${level}${filter === level ? " active" : ""}`}
|
||||
onClick={() => setFilter(level)}
|
||||
aria-pressed={filter === level}
|
||||
>
|
||||
{LEVEL_LABEL[level]}
|
||||
<small>{counts[level]}</small>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<span className="spacer" />
|
||||
<button type="button" className="run-log-action" onClick={() => void copyLogs()} disabled={!logs.length}>
|
||||
{copied ? <Check className="icon" /> : <Copy className="icon" />}
|
||||
{copied ? "已复制" : "复制"}
|
||||
</button>
|
||||
<button type="button" className="run-log-action" onClick={downloadLogs} disabled={!logs.length}>
|
||||
<Download className="icon" />
|
||||
导出
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="run-log-console-wrap">
|
||||
<div className="run-log-console" ref={consoleRef} onScroll={trackPin} role="log" aria-live="polite">
|
||||
{!visibleLogs.length && <div className="run-log-empty">{logs.length ? "当前级别暂无日志。" : emptyText}</div>}
|
||||
{visibleLogs.map((log) => (
|
||||
<div className={`run-log-row ${log.level}`} key={log.id}>
|
||||
<span className="run-log-time">{log.time}</span>
|
||||
<span className="run-log-level">{LEVEL_LABEL[log.level]}</span>
|
||||
<span className="run-log-node" title={log.nodeId ? nameOf(log.nodeId) : "流程"}>{log.nodeId ? nameOf(log.nodeId) : "流程"}</span>
|
||||
<span className="run-log-message">{log.message || "—"}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{!pinned && visibleLogs.length > 0 && (
|
||||
<button type="button" className="run-log-latest" onClick={scrollToLatest}>
|
||||
<ArrowDown className="icon" />
|
||||
回到最新
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
import { useMemo } from "react";
|
||||
import { CircleAlert, CircleCheck, History, Loader2, Play, RotateCcw, Square, Terminal, Timer } from "lucide-react";
|
||||
import { CircleAlert, CircleCheck, History, Loader2, Play, RotateCcw, Square, Terminal } from "lucide-react";
|
||||
import { useEditorStore } from "../store/editorStore";
|
||||
import { useRunStore } from "../store/runStore";
|
||||
import { RunLogView } from "../components/RunLogView";
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
running: "运行中", succeeded: "成功", failed: "失败", skipped: "已跳过", cancelled: "已取消", idle: "待运行"
|
||||
@ -26,7 +27,7 @@ export function RunsPage({ onBack }: { onBack: () => void }) {
|
||||
const component = catalog?.components.find((item) => item.id === node.componentId);
|
||||
map.set(node.id, node.label || component?.name || node.componentId);
|
||||
}
|
||||
return (id: string) => map.get(id) || id;
|
||||
return (id?: string) => (id ? map.get(id) || id : "流程");
|
||||
}, [catalog, flow]);
|
||||
|
||||
const entries = Object.entries(nodes);
|
||||
@ -52,6 +53,7 @@ export function RunsPage({ onBack }: { onBack: () => void }) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="run-main">
|
||||
<section className="panel run-detail">
|
||||
<div className="panel-head"><Play className="icon" /><strong>节点进度</strong><span className="spacer" /><span className={`status-pill ${statusClass}`}>{STATUS_LABEL[effectiveStatus]}</span></div>
|
||||
<div className="run-detail-body">
|
||||
@ -76,11 +78,9 @@ export function RunsPage({ onBack }: { onBack: () => void }) {
|
||||
|
||||
<section className="panel run-log-panel">
|
||||
<div className="panel-head"><Terminal className="icon" /><strong>运行日志</strong><span className="spacer" /><span className="count-pill">{logs.length}</span></div>
|
||||
<div className="run-page-logs">
|
||||
{!logs.length && <div className="empty"><Timer className="icon" /><strong>暂无日志</strong><span>节点执行、重试和异常信息会实时显示。</span></div>}
|
||||
{logs.map((log) => <div className={`run-page-log ${log.level}`} key={log.id}><span>{log.time}</span><strong>{log.nodeId ? nameOf(log.nodeId) : "流程"}</strong><p>{log.message}</p></div>)}
|
||||
</div>
|
||||
<RunLogView logs={logs} nameOf={nameOf} fileName={`${runId || "run"}.log`} emptyText="节点执行、重试和异常信息会实时显示。" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { useMemo } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { CircleAlert, CircleCheck, Loader2, Square, Timer, X } from "lucide-react";
|
||||
import { useEditorStore } from "../store/editorStore";
|
||||
import { useRunStore } from "../store/runStore";
|
||||
import { RunLogView } from "../components/RunLogView";
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
running: "运行中",
|
||||
@ -13,6 +14,7 @@ const STATUS_LABEL: Record<string, string> = {
|
||||
};
|
||||
|
||||
export function RunDrawer() {
|
||||
const [view, setView] = useState<"nodes" | "logs">("nodes");
|
||||
const running = useRunStore((state) => state.running);
|
||||
const stopping = useRunStore((state) => state.stopping);
|
||||
const nodes = useRunStore((state) => state.nodes);
|
||||
@ -55,6 +57,24 @@ export function RunDrawer() {
|
||||
已完成 {finished.length}/{Object.keys(nodes).length}
|
||||
</span>
|
||||
<span className="spacer" />
|
||||
<div className="run-drawer-tabs" role="tablist" aria-label="运行详情视图">
|
||||
<button
|
||||
type="button"
|
||||
className={`run-drawer-tab${view === "nodes" ? " active" : ""}`}
|
||||
onClick={() => setView("nodes")}
|
||||
aria-pressed={view === "nodes"}
|
||||
>
|
||||
节点进度
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`run-drawer-tab${view === "logs" ? " active" : ""}`}
|
||||
onClick={() => setView("logs")}
|
||||
aria-pressed={view === "logs"}
|
||||
>
|
||||
运行日志
|
||||
</button>
|
||||
</div>
|
||||
{running ? (
|
||||
<button type="button" className="btn" onClick={() => void stop()} disabled={stopping}>
|
||||
<Square className="icon" /> 停止
|
||||
@ -77,26 +97,20 @@ export function RunDrawer() {
|
||||
)}
|
||||
|
||||
<div className="run-drawer-body">
|
||||
<div className="run-node-list">
|
||||
{Object.entries(nodes).length === 0 && <div className="field-hint">等待节点启动…</div>}
|
||||
{Object.entries(nodes).map(([nodeId, value]) => (
|
||||
<div className={`run-node-item ${value.status}`} key={nodeId}>
|
||||
<span className="run-node-name">{nameOf(nodeId)}</span>
|
||||
<span className="run-node-status">{STATUS_LABEL[value.status] || value.status}</span>
|
||||
<span className="run-node-time">{value.durationMs == null ? "" : `${value.durationMs}ms`}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="run-logs">
|
||||
{logs.length === 0 && <div className="field-hint">运行日志会显示在这里…</div>}
|
||||
{logs.map((log) => (
|
||||
<div className={`run-log ${log.level}`} key={log.id}>
|
||||
<span className="run-log-time">{log.time}</span>
|
||||
<span className="run-log-node">{nameOf(log.nodeId)}</span>
|
||||
<span className="run-log-message">{log.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{view === "nodes" ? (
|
||||
<div className="run-node-list">
|
||||
{Object.entries(nodes).length === 0 && <div className="field-hint">等待节点启动…</div>}
|
||||
{Object.entries(nodes).map(([nodeId, value]) => (
|
||||
<div className={`run-node-item ${value.status}`} key={nodeId}>
|
||||
<span className="run-node-name">{nameOf(nodeId)}</span>
|
||||
<span className="run-node-status">{STATUS_LABEL[value.status] || value.status}</span>
|
||||
<span className="run-node-time">{value.durationMs == null ? "" : `${value.durationMs}ms`}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<RunLogView logs={logs} nameOf={nameOf} emptyText="运行日志会显示在这里…" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -575,6 +575,7 @@ textarea:focus-visible {
|
||||
|
||||
/* ---------- 画布 ---------- */
|
||||
.canvas-panel {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
@ -1282,11 +1283,19 @@ textarea:focus-visible {
|
||||
.runs {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 326px minmax(0, 1fr) 286px;
|
||||
grid-template-columns: 324px minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.run-main {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 1fr) minmax(270px, 44%);
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
height: 52px;
|
||||
flex: none;
|
||||
@ -1453,6 +1462,38 @@ textarea:focus-visible {
|
||||
|
||||
.run-page-actions .btn { justify-content: center; }
|
||||
|
||||
.run-error {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 9px;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--danger-bg);
|
||||
border-radius: 10px;
|
||||
background: var(--danger-bg);
|
||||
}
|
||||
|
||||
.run-error .icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex: none;
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.run-error strong {
|
||||
display: block;
|
||||
color: var(--danger-deep);
|
||||
font-size: 11.5px;
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
.run-error span {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
color: var(--danger-deep);
|
||||
font-size: 10.5px;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
.run-node-table {
|
||||
margin-top: 10px;
|
||||
border: 1px solid var(--line);
|
||||
@ -1484,13 +1525,277 @@ textarea:focus-visible {
|
||||
.run-node-main small { overflow: hidden; color: var(--danger); font-size: 9.5px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.run-node-duration { color: var(--muted); font-size: 10px; text-align: right; font-variant-numeric: tabular-nums; }
|
||||
|
||||
.run-page-logs { min-height: 0; flex: 1; overflow: auto; padding: 8px; }
|
||||
.run-page-log { padding: 8px; border-bottom: 1px solid var(--line); font-size: 10px; line-height: 15px; }
|
||||
.run-page-log > span { color: var(--faint); font-family: var(--mono); }
|
||||
.run-page-log > strong { display: block; margin-top: 2px; color: var(--ink-2); }
|
||||
.run-page-log > p { margin-top: 2px; color: var(--muted); overflow-wrap: anywhere; }
|
||||
.run-page-log.warn > p { color: var(--warn-deep); }
|
||||
.run-page-log.error > p { color: var(--danger); }
|
||||
/* ---------- 运行日志组件 ---------- */
|
||||
.run-log-view {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.run-log-toolbar {
|
||||
min-height: 46px;
|
||||
padding: 7px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.run-log-filters {
|
||||
padding: 2px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
border-radius: 9px;
|
||||
background: var(--soft);
|
||||
}
|
||||
|
||||
.run-log-filter {
|
||||
height: 26px;
|
||||
padding: 0 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
border-radius: 7px;
|
||||
color: var(--muted);
|
||||
font-size: 10.5px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.run-log-filter:hover { color: var(--ink); }
|
||||
.run-log-filter.active {
|
||||
color: var(--blue-dark);
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px rgba(35, 55, 72, 0.1);
|
||||
}
|
||||
|
||||
.run-log-filter small {
|
||||
min-width: 17px;
|
||||
padding: 0 4px;
|
||||
border-radius: 8px;
|
||||
background: rgba(78, 90, 107, 0.08);
|
||||
color: var(--muted);
|
||||
font-size: 9px;
|
||||
line-height: 15px;
|
||||
text-align: center;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.run-log-action {
|
||||
height: 28px;
|
||||
padding: 0 9px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
color: var(--ink-2);
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.run-log-action .icon { width: 13px; height: 13px; }
|
||||
.run-log-action:hover:not(:disabled) { color: var(--blue-dark); border-color: rgba(34, 100, 229, 0.22); background: #fff; }
|
||||
.run-log-action:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
|
||||
.run-log-console-wrap {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.run-log-console {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
border: 1px solid #243352;
|
||||
border-radius: 10px;
|
||||
background: #0f172a;
|
||||
color: #dce6f7;
|
||||
font-family: var(--mono);
|
||||
font-size: 11.5px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.run-log-row {
|
||||
min-width: 640px;
|
||||
padding: 7px 10px;
|
||||
display: grid;
|
||||
grid-template-columns: 74px 48px minmax(130px, 190px) minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
border-bottom: 1px solid rgba(148, 163, 184, 0.13);
|
||||
}
|
||||
|
||||
.run-log-row:hover { background: rgba(148, 163, 184, 0.09); }
|
||||
.run-log-time { color: #8ea3c2; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||
.run-log-level { color: #a8b8d4; font-size: 10px; font-weight: 750; }
|
||||
.run-log-node {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #b9c8e2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.run-log-message {
|
||||
min-width: 0;
|
||||
color: #dce6f7;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.run-log-row.warn .run-log-level,
|
||||
.run-log-row.warn .run-log-message { color: #facc6b; }
|
||||
.run-log-row.error .run-log-level,
|
||||
.run-log-row.error .run-log-message { color: #ff8080; }
|
||||
.run-log-row.error { background: rgba(248, 113, 113, 0.08); }
|
||||
|
||||
.run-log-empty {
|
||||
min-height: 160px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #8ea3c2;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.run-log-latest {
|
||||
position: absolute;
|
||||
right: 22px;
|
||||
bottom: 22px;
|
||||
height: 30px;
|
||||
padding: 0 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
border: 1px solid #35486e;
|
||||
border-radius: 15px;
|
||||
background: #1e293b;
|
||||
color: #dce6f7;
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 8px 20px rgba(2, 6, 23, 0.32);
|
||||
}
|
||||
|
||||
.run-log-latest .icon { width: 13px; height: 13px; }
|
||||
|
||||
/* ---------- 编排运行抽屉 ---------- */
|
||||
.run-drawer {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
right: 14px;
|
||||
bottom: 14px;
|
||||
max-height: min(420px, 54%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.82);
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
box-shadow: 0 16px 44px rgba(15, 23, 42, 0.18);
|
||||
z-index: 36;
|
||||
}
|
||||
|
||||
.run-drawer-head {
|
||||
min-height: 48px;
|
||||
padding: 8px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
.run-drawer-meta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.run-drawer-meta .icon { width: 13px; height: 13px; }
|
||||
|
||||
.run-drawer-tabs {
|
||||
padding: 2px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
border-radius: 9px;
|
||||
background: var(--soft);
|
||||
}
|
||||
|
||||
.run-drawer-tab {
|
||||
height: 26px;
|
||||
padding: 0 9px;
|
||||
border-radius: 7px;
|
||||
color: var(--muted);
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.run-drawer-tab:hover { color: var(--ink); }
|
||||
.run-drawer-tab.active {
|
||||
color: var(--blue-dark);
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px rgba(35, 55, 72, 0.1);
|
||||
}
|
||||
|
||||
.run-drawer-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.run-node-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 7px;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.run-node-item {
|
||||
min-height: 38px;
|
||||
padding: 6px 7px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
gap: 2px 7px;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--line);
|
||||
border-left: 3px solid var(--faint);
|
||||
}
|
||||
|
||||
.run-node-item:last-child { border-bottom: 0; }
|
||||
.run-node-item.running { border-left-color: var(--blue); }
|
||||
.run-node-item.succeeded { border-left-color: var(--success); }
|
||||
.run-node-item.failed { border-left-color: var(--danger); }
|
||||
.run-node-item.skipped,
|
||||
.run-node-item.cancelled { border-left-color: var(--warn); }
|
||||
|
||||
.run-node-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--ink);
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.run-node-status { color: var(--muted); font-size: 10px; }
|
||||
.run-node-time { color: var(--faint); font-size: 9.5px; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||
|
||||
.run-logs {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* ---------- 运行前检查 ---------- */
|
||||
.preflight-backdrop {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user