Files
browser/platform_web/pages/MaintenancePage.tsx
T
2026-07-11 14:56:10 +08:00

131 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Sparkles, WandSparkles } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { platformApiClient } from "../api/client";
import type { AuditEventResponse, RunEndpointResponse } from "../api/types";
import { EmptyState, ErrorState, LoadingState } from "../components/StateViews";
import { cx } from "../utils/classes";
type ModuleState<T> = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; data: T };
export function MaintenancePage() {
const [endpoints, setEndpoints] = useState<ModuleState<RunEndpointResponse[]>>({ status: "loading" });
const [events, setEvents] = useState<ModuleState<AuditEventResponse[]>>({ status: "loading" });
const refreshEndpoints = useCallback(async () => {
setEndpoints({ status: "loading" });
try {
const response = await platformApiClient.listRunEndpoints();
setEndpoints({ status: "ready", data: response.items });
} catch (error) {
setEndpoints({ status: "error", reason: error instanceof Error ? error.message : "加载失败" });
}
}, []);
const refreshEvents = useCallback(async () => {
setEvents({ status: "loading" });
try {
const response = await platformApiClient.listAuditEvents();
setEvents({ status: "ready", data: response.items });
} catch (error) {
setEvents({ status: "error", reason: error instanceof Error ? error.message : "加载失败" });
}
}, []);
useEffect(() => {
void refreshEndpoints();
void refreshEvents();
}, [refreshEndpoints, refreshEvents]);
return (
<div className="maintenance-page">
<header className="page-header">
<div>
<p className="page-kicker">平台管理员</p>
<h1 className="page-title">
<WandSparkles size={22} style={{ verticalAlign: "-3px" }} /> 系统维护
</h1>
</div>
<button
type="button"
className="icon-command"
onClick={() => {
void refreshEndpoints();
void refreshEvents();
}}
>
<Sparkles size={16} />
<span>刷新</span>
</button>
</header>
<section className="console-panel" aria-label="run endpoints">
<div className="panel-header">
<h2>运行节点</h2>
</div>
{endpoints.status === "loading" && <LoadingState label="正在加载运行节点…" compact />}
{endpoints.status === "error" && (
<ErrorState title="运行节点加载失败" reason={endpoints.reason} diagnosticId="maintenance-endpoints" onRetry={() => void refreshEndpoints()} compact />
)}
{endpoints.status === "ready" && endpoints.data.length === 0 && (
<EmptyState title="暂无运行节点" description="还没有运行端注册到平台。" actionLabel="刷新" onAction={() => void refreshEndpoints()} />
)}
{endpoints.status === "ready" && endpoints.data.length > 0 && (
<div className="resource-list">
{endpoints.data.map((endpoint) => (
<article key={endpoint.id} className="resource-list-item">
<div>
<strong>{endpoint.displayName}</strong>
<span className="provider-id">{endpoint.id}</span>
</div>
<span className={cx("status-pill", endpoint.status === "online" ? "status-active" : endpoint.status === "offline" ? "status-error" : "status-disabled")}>
{endpoint.status === "online" ? "在线" : endpoint.status === "offline" ? "离线" : endpoint.status}
</span>
<span>
任务 {endpoint.capacity.runningJobs}/{endpoint.capacity.maxJobs}(排队 {endpoint.capacity.queuedJobs}
</span>
<span>心跳 {new Date(endpoint.lastHeartbeatAt).toLocaleString()}</span>
</article>
))}
</div>
)}
</section>
<section className="console-panel" aria-label="audit events">
<div className="panel-header">
<h2>审计事件</h2>
</div>
{events.status === "loading" && <LoadingState label="正在加载审计事件…" compact />}
{events.status === "error" && (
<ErrorState title="审计事件加载失败" reason={events.reason} diagnosticId="maintenance-audit" onRetry={() => void refreshEvents()} compact />
)}
{events.status === "ready" && events.data.length === 0 && (
<EmptyState title="暂无审计事件" description="平台还没有记录任何审计事件。" actionLabel="刷新" onAction={() => void refreshEvents()} />
)}
{events.status === "ready" && events.data.length > 0 && (
<div className="operation-list">
{events.data.slice(0, 30).map((event) => (
<div key={event.id} className="operation-item">
<div className="operation-item-head">
<strong>{event.summary || `${event.action} ${event.resourceKind}`}</strong>
<span className={cx("status-pill", event.result === "success" ? "status-active" : "status-error")}>{event.result}</span>
</div>
<div className="operation-meta">
<span>
事件 <code>{event.id}</code>
</span>
<span>操作者 {event.actorId}</span>
<span>
资源 {event.resourceKind}/{event.resourceId}
</span>
<span>{new Date(event.createdAt).toLocaleString()}</span>
</div>
</div>
))}
</div>
)}
</section>
</div>
);
}