|
|
"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 { SecondaryButton } from "@/components/ui/primary-button";
|
|
|
import { adminGetQueueStats } from "@/lib/frontend/api-client";
|
|
|
import { useAuth } from "@/hooks/use-auth";
|
|
|
import type { DataPageStatus, QueueStats } from "@/lib/frontend/types";
|
|
|
|
|
|
const BULL_BOARD_PATH = "/api/admin/queues";
|
|
|
|
|
|
export default function QueuesAdminPage() {
|
|
|
const { token } = useAuth();
|
|
|
const [status, setStatus] = useState<DataPageStatus>("loading");
|
|
|
const [stats, setStats] = useState<QueueStats | null>(null);
|
|
|
const [boardReady, setBoardReady] = useState(false);
|
|
|
|
|
|
const load = useCallback(async () => {
|
|
|
if (!token) return;
|
|
|
setStatus("loading");
|
|
|
const res = await adminGetQueueStats("", token);
|
|
|
if (res.code !== 0) {
|
|
|
setStatus("error");
|
|
|
return;
|
|
|
}
|
|
|
setStats(res.data);
|
|
|
setStatus("success");
|
|
|
}, [token]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
void load();
|
|
|
const id = setInterval(() => void load(), 10_000);
|
|
|
return () => clearInterval(id);
|
|
|
}, [load]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (!token) {
|
|
|
setBoardReady(false);
|
|
|
return;
|
|
|
}
|
|
|
const encoded = encodeURIComponent(token);
|
|
|
document.cookie = `bull_board_auth=${encoded}; path=${BULL_BOARD_PATH}; max-age=3600; SameSite=Lax`;
|
|
|
setBoardReady(true);
|
|
|
return () => {
|
|
|
document.cookie = `bull_board_auth=; path=${BULL_BOARD_PATH}; max-age=0`;
|
|
|
setBoardReady(false);
|
|
|
};
|
|
|
}, [token]);
|
|
|
|
|
|
return (
|
|
|
<AdminLayout>
|
|
|
<PageHeader
|
|
|
title="队列监控"
|
|
|
subtitle="quote-rpa 队列深度、失败任务与延迟任务(Bull Board)"
|
|
|
/>
|
|
|
|
|
|
{status === "loading" && <Skeleton className="mb-4 h-24 w-full" />}
|
|
|
{status === "error" && (
|
|
|
<ErrorBanner
|
|
|
action={<SecondaryButton onClick={() => void load()}>重试</SecondaryButton>}
|
|
|
>
|
|
|
加载队列统计失败
|
|
|
</ErrorBanner>
|
|
|
)}
|
|
|
|
|
|
{status === "success" && stats && (
|
|
|
<div className="mb-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-5">
|
|
|
<Card>
|
|
|
<p className="text-sm text-text-secondary">队列深度</p>
|
|
|
<p className="mt-1 text-2xl font-semibold num">{stats.depth}</p>
|
|
|
</Card>
|
|
|
<Card>
|
|
|
<p className="text-sm text-text-secondary">等待中</p>
|
|
|
<p className="mt-1 text-2xl font-semibold num">{stats.waiting}</p>
|
|
|
</Card>
|
|
|
<Card>
|
|
|
<p className="text-sm text-text-secondary">执行中</p>
|
|
|
<p className="mt-1 text-2xl font-semibold num">{stats.active}</p>
|
|
|
</Card>
|
|
|
<Card>
|
|
|
<p className="text-sm text-text-secondary">延迟</p>
|
|
|
<p
|
|
|
className={`mt-1 text-2xl font-semibold num ${
|
|
|
stats.delayed > 0 ? "text-warning" : ""
|
|
|
}`}
|
|
|
>
|
|
|
{stats.delayed}
|
|
|
</p>
|
|
|
</Card>
|
|
|
<Card>
|
|
|
<p className="text-sm text-text-secondary">失败</p>
|
|
|
<p
|
|
|
className={`mt-1 text-2xl font-semibold num ${
|
|
|
stats.failed > 0 ? "text-error" : ""
|
|
|
}`}
|
|
|
>
|
|
|
{stats.failed}
|
|
|
</p>
|
|
|
</Card>
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
<Card className="overflow-hidden p-0">
|
|
|
{!boardReady && <Skeleton className="h-[70vh] w-full rounded-none" />}
|
|
|
{boardReady && (
|
|
|
<iframe
|
|
|
title="Bull Board 队列面板"
|
|
|
src={BULL_BOARD_PATH}
|
|
|
className="h-[70vh] w-full border-0 bg-white"
|
|
|
/>
|
|
|
)}
|
|
|
</Card>
|
|
|
</AdminLayout>
|
|
|
);
|
|
|
}
|