|
|
"use client";
|
|
|
|
|
|
import { useCallback, useEffect, useState } from "react";
|
|
|
import { AdminLayout } from "@/components/layout/admin-layout";
|
|
|
import { PageHeader } from "@/components/layout/page-header";
|
|
|
import { Card } from "@/components/ui/card";
|
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
|
import { ErrorBanner } from "@/components/ui/error-banner";
|
|
|
import { PrimaryButton, SecondaryButton } from "@/components/ui/primary-button";
|
|
|
import { InputField } from "@/components/ui/input-field";
|
|
|
import {
|
|
|
adminGetRpaStatus,
|
|
|
adminPauseWorker,
|
|
|
} from "@/lib/frontend/api-client";
|
|
|
import { useAuth } from "@/hooks/use-auth";
|
|
|
import type { DataPageStatus, RpaStatus } from "@/lib/frontend/types";
|
|
|
|
|
|
export default function RpaAdminPage() {
|
|
|
const { token } = useAuth();
|
|
|
const [status, setStatus] = useState<DataPageStatus>("loading");
|
|
|
const [data, setData] = useState<RpaStatus | null>(null);
|
|
|
const [pauseWorkerId, setPauseWorkerId] = useState("");
|
|
|
const [pauseMinutes, setPauseMinutes] = useState("10");
|
|
|
const [pausing, setPausing] = useState(false);
|
|
|
const [message, setMessage] = useState<string | null>(null);
|
|
|
|
|
|
const load = useCallback(async () => {
|
|
|
if (!token) return;
|
|
|
setStatus("loading");
|
|
|
const res = await adminGetRpaStatus("", token);
|
|
|
if (res.code !== 0) {
|
|
|
setStatus("error");
|
|
|
return;
|
|
|
}
|
|
|
setData(res.data);
|
|
|
if (res.data.workers[0] && !pauseWorkerId) {
|
|
|
setPauseWorkerId(res.data.workers[0].worker_id);
|
|
|
}
|
|
|
setStatus("success");
|
|
|
}, [token, pauseWorkerId]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
void load();
|
|
|
const id = setInterval(() => void load(), 10_000);
|
|
|
return () => clearInterval(id);
|
|
|
}, [load]);
|
|
|
|
|
|
const handlePause = async () => {
|
|
|
if (!token || !pauseWorkerId) return;
|
|
|
setPausing(true);
|
|
|
setMessage(null);
|
|
|
const res = await adminPauseWorker(
|
|
|
"",
|
|
|
token,
|
|
|
pauseWorkerId,
|
|
|
Number(pauseMinutes),
|
|
|
);
|
|
|
setPausing(false);
|
|
|
if (res.code !== 0) {
|
|
|
setMessage(res.message);
|
|
|
return;
|
|
|
}
|
|
|
setMessage("Worker 已暂停");
|
|
|
void load();
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
<AdminLayout>
|
|
|
<PageHeader
|
|
|
title="RPA 状态"
|
|
|
subtitle="Worker 在线状态、队列深度与熔断"
|
|
|
/>
|
|
|
|
|
|
{status === "loading" && <Skeleton className="h-40 w-full" />}
|
|
|
{status === "error" && (
|
|
|
<ErrorBanner
|
|
|
action={<SecondaryButton onClick={() => void load()}>重试</SecondaryButton>}
|
|
|
>
|
|
|
加载 RPA 状态失败
|
|
|
</ErrorBanner>
|
|
|
)}
|
|
|
|
|
|
{status === "success" && data && (
|
|
|
<div className="space-y-6">
|
|
|
<div className="grid gap-4 sm:grid-cols-3">
|
|
|
<Card>
|
|
|
<p className="text-sm text-text-secondary">队列深度</p>
|
|
|
<p className="mt-1 text-2xl font-semibold num">{data.queue_depth}</p>
|
|
|
</Card>
|
|
|
<Card>
|
|
|
<p className="text-sm text-text-secondary">24h RPA 成功率</p>
|
|
|
<p className="mt-1 text-2xl font-semibold num">
|
|
|
{data.success_rate_24h}%
|
|
|
</p>
|
|
|
</Card>
|
|
|
<Card>
|
|
|
<p className="text-sm text-text-secondary">熔断状态</p>
|
|
|
<p
|
|
|
className={`mt-1 text-2xl font-semibold ${
|
|
|
data.circuit_open ? "text-error" : "text-success"
|
|
|
}`}
|
|
|
>
|
|
|
{data.circuit_open ? "已熔断" : "正常"}
|
|
|
</p>
|
|
|
</Card>
|
|
|
</div>
|
|
|
|
|
|
<Card>
|
|
|
<h3 className="mb-4 font-semibold">Worker 列表</h3>
|
|
|
{data.workers.length === 0 ? (
|
|
|
<p className="text-sm text-text-secondary">暂无 Worker 心跳</p>
|
|
|
) : (
|
|
|
<ul className="space-y-3">
|
|
|
{data.workers.map((w) => (
|
|
|
<li
|
|
|
key={w.worker_id}
|
|
|
className="flex flex-wrap items-center justify-between gap-2 rounded-md border border-border px-4 py-3 text-sm"
|
|
|
>
|
|
|
<span className="font-mono">{w.worker_id}</span>
|
|
|
<span className={w.online ? "text-success" : "text-text-disabled"}>
|
|
|
{w.online ? "在线" : "离线"}
|
|
|
</span>
|
|
|
{w.paused_seconds > 0 && (
|
|
|
<span className="text-warning">
|
|
|
暂停中(剩余 {w.paused_seconds}s)
|
|
|
</span>
|
|
|
)}
|
|
|
</li>
|
|
|
))}
|
|
|
</ul>
|
|
|
)}
|
|
|
</Card>
|
|
|
|
|
|
<Card>
|
|
|
<h3 className="mb-4 font-semibold">暂停 Worker</h3>
|
|
|
<div className="grid gap-4 sm:grid-cols-3">
|
|
|
<InputField
|
|
|
label="Worker ID"
|
|
|
name="worker_id"
|
|
|
value={pauseWorkerId}
|
|
|
onChange={(e) => setPauseWorkerId(e.target.value)}
|
|
|
/>
|
|
|
<InputField
|
|
|
label="暂停时长(分钟)"
|
|
|
name="duration"
|
|
|
type="number"
|
|
|
value={pauseMinutes}
|
|
|
onChange={(e) => setPauseMinutes(e.target.value)}
|
|
|
/>
|
|
|
<div className="flex items-end">
|
|
|
<PrimaryButton loading={pausing} onClick={() => void handlePause()}>
|
|
|
暂停 Worker
|
|
|
</PrimaryButton>
|
|
|
</div>
|
|
|
</div>
|
|
|
{message && (
|
|
|
<p className="mt-3 text-sm text-text-secondary">{message}</p>
|
|
|
)}
|
|
|
</Card>
|
|
|
</div>
|
|
|
)}
|
|
|
</AdminLayout>
|
|
|
);
|
|
|
}
|