Files

80 lines
3.5 KiB
TypeScript

import { AlertTriangle, CheckCircle2, ChevronDown, ChevronUp, Clock3, ListChecks } from "lucide-react";
import { useMemo, useState } from "react";
import type { OperationRecord } from "../contracts/workspace";
import { projectOperationForTray } from "../contracts/operationsConsole";
import { cx } from "../utils/classes";
interface OperationsTrayProps {
operations: OperationRecord[];
}
export function OperationsTray({ operations }: OperationsTrayProps) {
const [open, setOpen] = useState(false);
const items = useMemo(() => operations.slice(0, 6).map(projectOperationForTray), [operations]);
const pendingCount = operations.filter((operation) => operation.status === "pending").length;
const failedCount = operations.filter((operation) => operation.status === "failed").length;
return (
<section className={cx("operations-tray", open && "operations-tray-open")} aria-label="当前会话操作">
<button
type="button"
className="operations-tray-trigger"
aria-expanded={open}
aria-controls="session-operations-panel"
onClick={() => setOpen((current) => !current)}
>
<span className="operations-tray-glyph" aria-hidden="true">
<ListChecks size={17} />
</span>
<span className="operations-tray-copy">
<strong>会话操作</strong>
<span>{pendingCount > 0 ? `${pendingCount} 项处理中` : failedCount > 0 ? `${failedCount} 项失败` : `${operations.length} 条记录`}</span>
</span>
{open ? <ChevronDown size={15} aria-hidden="true" /> : <ChevronUp size={15} aria-hidden="true" />}
</button>
{open && (
<div id="session-operations-panel" className="operations-tray-panel" role="region" aria-live="polite">
<div className="operations-tray-heading">
<strong>当前浏览器会话</strong>
<span>持久任务记录以 Platform 页面为准</span>
</div>
{items.length === 0 ? (
<p className="operations-tray-empty">本会话尚未提交资源变更。</p>
) : (
<ol className="operations-tray-list">
{items.map((item) => (
<li key={item.id} className={cx("operations-tray-item", `operations-tray-item-${item.status}`)}>
<span className="operations-tray-status" aria-hidden="true">
{item.status === "pending" ? <Clock3 size={14} /> : item.status === "succeeded" ? <CheckCircle2 size={14} /> : <AlertTriangle size={14} />}
</span>
<span className="operations-tray-item-copy">
<strong>{item.intent}</strong>
<span>{item.targetLabel}</span>
<span>{item.status === "failed" ? item.errorReason : item.message ?? operationStatusLabel(item.status)}</span>
{item.diagnosticId && <code>{item.diagnosticId}</code>}
</span>
<time dateTime={item.updatedAt}>{formatOperationTime(item.updatedAt)}</time>
</li>
))}
</ol>
)}
</div>
)}
</section>
);
}
function operationStatusLabel(status: OperationRecord["status"]): string {
if (status === "pending") {
return "等待 Platform 响应";
}
return status === "succeeded" ? "Platform 已确认" : "Platform 请求失败";
}
function formatOperationTime(value: string): string {
const time = new Date(value);
return Number.isNaN(time.getTime()) ? "时间未知" : time.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
}