first commit
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user