You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

144 lines
4.2 KiB

"use client";
import { 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 { METRICS_THRESHOLDS } from "@/lib/frontend/constants";
import { adminGetMetrics } from "@/lib/frontend/api-client";
import { useAuth } from "@/hooks/use-auth";
import type { DataPageStatus, MetricsDashboard } from "@/lib/frontend/types";
function MetricCard({
label,
value,
suffix,
threshold,
higherIsBetter = true,
}: {
label: string;
value: number;
suffix?: string;
threshold?: number;
higherIsBetter?: boolean;
}) {
const below =
threshold !== undefined &&
(higherIsBetter ? value < threshold : value > threshold);
return (
<Card>
<p className="text-sm text-text-secondary">{label}</p>
<p
className={`mt-1 text-2xl font-semibold num ${
below ? "text-warning" : "text-text-primary"
}`}
>
{value}
{suffix}
</p>
</Card>
);
}
export default function MetricsDashboardPage() {
const { token } = useAuth();
const [status, setStatus] = useState<DataPageStatus>("loading");
const [metrics, setMetrics] = useState<MetricsDashboard | null>(null);
const load = async () => {
if (!token) return;
setStatus("loading");
const res = await adminGetMetrics("", token);
if (res.code !== 0) {
setStatus("error");
return;
}
setMetrics(res.data);
setStatus("success");
};
useEffect(() => {
void load();
}, [token]);
return (
<AdminLayout>
<PageHeader title="指标看板" subtitle="GD-4 三率与系统运行指标" />
{status === "loading" && (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-24 w-full" />
))}
</div>
)}
{status === "error" && (
<ErrorBanner
action={<SecondaryButton onClick={() => void load()}></SecondaryButton>}
>
</ErrorBanner>
)}
{status === "success" && metrics && (
<div className="space-y-6">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<MetricCard
label="API 成功率"
value={metrics.api_success_rate}
suffix="%"
threshold={METRICS_THRESHOLDS.api_success_rate}
/>
<MetricCard
label="RPA 成功率"
value={metrics.rpa_success_rate}
suffix="%"
threshold={METRICS_THRESHOLDS.rpa_success_rate}
/>
<MetricCard
label="实时报价率"
value={metrics.realtime_rate}
suffix="%"
threshold={METRICS_THRESHOLDS.realtime_rate}
/>
<MetricCard
label="缓存命中率"
value={metrics.cache_hit_rate}
suffix="%"
threshold={METRICS_THRESHOLDS.cache_hit_rate}
/>
</div>
<div className="grid gap-4 sm:grid-cols-3">
<Card>
<p className="text-sm text-text-secondary">P95 </p>
<p className="mt-1 text-2xl font-semibold num">
{metrics.p95_latency_ms} ms
</p>
</Card>
<Card>
<p className="text-sm text-text-secondary"></p>
<p className="mt-1 text-2xl font-semibold num">
{metrics.queue_depth}
</p>
</Card>
<Card>
<p className="text-sm text-text-secondary"></p>
<p
className={`mt-1 text-2xl font-semibold num ${
metrics.open_alerts_count > 0 ? "text-warning" : ""
}`}
>
{metrics.open_alerts_count}
</p>
</Card>
</div>
</div>
)}
</AdminLayout>
);
}