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
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>
|
|
);
|
|
}
|