ai_api / src /app /(dashboard) /dashboard /analytics /ProviderUtilizationTab.tsx
Yogesh
initial deploy
cd8bd0a
Raw
History Blame Contribute Delete
17.1 kB
"use client";
import { useTranslations } from "next-intl";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useProviderNodeMap, resolveProviderName } from "@/lib/display/useProviderNodeMap";
import {
CartesianGrid,
Legend,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import Card from "@/shared/components/Card";
import ProviderIcon from "@/shared/components/ProviderIcon";
import TimeRangeSelector from "@/shared/components/analytics/TimeRangeSelector";
import type {
ProviderUtilizationPoint,
ProviderUtilizationResponse,
UtilizationTimeRange,
} from "@/shared/types/utilization";
const RANGE_LABELS: Record<UtilizationTimeRange, string> = {
"1h": "Last hour",
"24h": "Last 24 hours",
"7d": "Last 7 days",
"30d": "Last 30 days",
};
const PROVIDER_COLORS = [
"var(--color-primary)",
"var(--color-accent)",
"var(--color-success)",
"var(--color-warning)",
"var(--color-error)",
"var(--color-text-muted)",
];
function formatTimestamp(value: string, range: UtilizationTimeRange) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return value;
}
if (range === "1h" || range === "24h") {
return new Intl.DateTimeFormat(undefined, {
hour: "2-digit",
minute: "2-digit",
}).format(date);
}
return new Intl.DateTimeFormat(undefined, {
month: "short",
day: "numeric",
}).format(date);
}
function formatTooltipTimestamp(value: string, range: UtilizationTimeRange) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return value;
}
return new Intl.DateTimeFormat(undefined, {
month: "short",
day: "numeric",
hour: range === "1h" || range === "24h" ? "2-digit" : undefined,
minute: range === "1h" || range === "24h" ? "2-digit" : undefined,
}).format(date);
}
function formatPercent(value: number) {
return `${Math.round(value)}%`;
}
function getLatestPoints(points: ProviderUtilizationPoint[]) {
const latestByProvider = new Map<string, ProviderUtilizationPoint>();
for (const point of points) {
const current = latestByProvider.get(point.provider);
if (!current || new Date(point.timestamp).getTime() > new Date(current.timestamp).getTime()) {
latestByProvider.set(point.provider, point);
}
}
return Array.from(latestByProvider.values()).sort((a, b) => b.remainingPct - a.remainingPct);
}
export default function ProviderUtilizationTab() {
const t = useTranslations("analytics");
const nodeMap = useProviderNodeMap();
const [range, setRange] = useState<UtilizationTimeRange>("24h");
const [aggregateBy, setAggregateBy] = useState<"provider" | "connection">("provider");
const [data, setData] = useState<ProviderUtilizationResponse | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchUtilization = useCallback(
async (
selectedRange: UtilizationTimeRange,
selectedAggregate: "provider" | "connection",
signal?: AbortSignal
) => {
setLoading(true);
try {
const response = await fetch(
`/api/usage/utilization?range=${selectedRange}&aggregateBy=${selectedAggregate}`,
{
signal,
cache: "no-store",
}
);
if (!response.ok) {
throw new Error("Failed to fetch utilization data");
}
const json = (await response.json()) as ProviderUtilizationResponse;
setData(json);
setError(null);
} catch (fetchError) {
if (fetchError instanceof DOMException && fetchError.name === "AbortError") {
return;
}
setError(
fetchError instanceof Error ? fetchError.message : "Failed to fetch utilization data"
);
setData(null);
} finally {
if (!signal?.aborted) {
setLoading(false);
}
}
},
[]
);
useEffect(() => {
const controller = new AbortController();
fetchUtilization(range, aggregateBy, controller.signal);
return () => controller.abort();
}, [fetchUtilization, range, aggregateBy]);
const providerColors = useMemo(() => {
const colors = new Map<string, string>();
for (const [index, provider] of (data?.providers ?? []).entries()) {
colors.set(provider, PROVIDER_COLORS[index % PROVIDER_COLORS.length]);
}
return colors;
}, [data?.providers]);
const chartData = useMemo(() => {
if (!data?.data.length) {
return [];
}
const byTimestamp = new Map<string, Record<string, number | string>>();
for (const point of data.data) {
const entry = byTimestamp.get(point.timestamp) ?? {
timestamp: point.timestamp,
label: formatTimestamp(point.timestamp, data.timeRange),
};
entry[point.provider] = Number(point.remainingPct.toFixed(2));
byTimestamp.set(point.timestamp, entry);
}
return Array.from(byTimestamp.entries())
.sort(([left], [right]) => new Date(left).getTime() - new Date(right).getTime())
.map(([, value]) => value);
}, [data]);
const latestPoints = useMemo(() => getLatestPoints(data?.data ?? []), [data?.data]);
const hasData = Boolean(data?.data.length);
const [retrying, setRetrying] = useState(false);
const handleRetry = useCallback(() => {
setRetrying(true);
setError(null);
fetchUtilization(range, aggregateBy).finally(() => setRetrying(false));
}, [range, aggregateBy, fetchUtilization]);
return (
<div className="flex flex-col gap-6">
<Card
title={t("providerUtilizationTitle")}
subtitle={RANGE_LABELS[range]}
icon="monitoring"
action={
<div className="flex items-center gap-4">
<div className="flex rounded-lg border border-border/50 bg-black/5 p-1 dark:bg-white/5">
<button
onClick={() => setAggregateBy("provider")}
className={`flex items-center gap-2 rounded-md px-3 py-1.5 text-xs font-medium transition-colors ${
aggregateBy === "provider"
? "bg-surface text-text-main shadow-sm"
: "text-text-muted hover:text-text-main"
}`}
>
<span className="material-symbols-outlined text-[14px]">dns</span>
Global View
</button>
<button
onClick={() => setAggregateBy("connection")}
className={`flex items-center gap-2 rounded-md px-3 py-1.5 text-xs font-medium transition-colors ${
aggregateBy === "connection"
? "bg-surface text-text-main shadow-sm"
: "text-text-muted hover:text-text-main"
}`}
>
<span className="material-symbols-outlined text-[14px]">account_tree</span>
Account Split
</button>
</div>
<TimeRangeSelector value={range} onChange={setRange} />
</div>
}
className="overflow-hidden"
>
{loading && !hasData ? (
<div className="flex min-h-80 items-center justify-center text-sm text-text-muted">
<span className="material-symbols-outlined mr-2 animate-spin text-[18px]">
progress_activity
</span>
Loading utilization data…
</div>
) : error ? (
<div className="flex min-h-80 flex-col items-center justify-center gap-4 text-center">
<span className="material-symbols-outlined text-[32px] text-error">error</span>
<div className="flex flex-col gap-1">
<p className="text-sm font-medium text-text-main">
{t("providerUtilizationFailedToLoad")}
</p>
<p className="text-sm text-text-muted">{error}</p>
</div>
<button
type="button"
onClick={handleRetry}
disabled={retrying}
className="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-50 disabled:cursor-not-allowed"
>
{retrying ? (
<>
<span className="material-symbols-outlined animate-spin text-[18px]">
progress_activity
</span>
Retrying…
</>
) : (
<>
<span className="material-symbols-outlined text-[18px]">refresh</span>
Retry
</>
)}
</button>
</div>
) : !hasData ? (
<div className="flex min-h-80 flex-col items-center justify-center gap-4 text-center">
<span className="material-symbols-outlined text-[40px] text-text-muted/70">
timeline
</span>
<div className="flex flex-col gap-2">
<p className="text-sm font-medium text-text-main">{t("providerUtilizationNoData")}</p>
<p className="max-w-md text-sm text-text-muted">
Provider quota snapshots will appear here after utilization data is collected.
</p>
</div>
<div className="rounded-lg border border-black/5 bg-black/[0.02] p-4 dark:border-white/5 dark:bg-white/[0.02]">
<p className="text-xs font-medium text-text-main">
{t("providerUtilizationGettingStarted")}
</p>
<ul className="mt-2 text-left text-xs text-text-muted">
<li className="flex items-start gap-2">
<span className="material-symbols-outlined text-[14px] text-primary">
check_circle
</span>
<span>
Connect providers via OAuth or API keys in <strong>Providers</strong>
</span>
</li>
<li className="mt-1 flex items-start gap-2">
<span className="material-symbols-outlined text-[14px] text-primary">
check_circle
</span>
<span>
Enable quota tracking by using the provider in a combo or direct request
</span>
</li>
<li className="mt-1 flex items-start gap-2">
<span className="material-symbols-outlined text-[14px] text-primary">
check_circle
</span>
<span>Data will appear automatically as quota snapshots are collected</span>
</li>
</ul>
</div>
</div>
) : (
<div className="flex flex-col gap-5">
<div className="h-80 w-full rounded-xl border border-black/5 bg-black/[0.02] px-3 py-4 dark:border-white/5 dark:bg-white/[0.02]">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData} margin={{ top: 8, right: 16, bottom: 0, left: 0 }}>
<CartesianGrid
stroke="var(--color-border)"
strokeDasharray="3 3"
vertical={false}
/>
<XAxis
dataKey="timestamp"
tickFormatter={(value) => formatTimestamp(String(value), range)}
tick={{ fill: "var(--color-text-muted)", fontSize: 12 }}
axisLine={{ stroke: "var(--color-border)" }}
tickLine={{ stroke: "var(--color-border)" }}
minTickGap={24}
/>
<YAxis
domain={[0, 100]}
tickFormatter={formatPercent}
tick={{ fill: "var(--color-text-muted)", fontSize: 12 }}
axisLine={{ stroke: "var(--color-border)" }}
tickLine={{ stroke: "var(--color-border)" }}
width={44}
/>
<Tooltip
labelFormatter={(value) => formatTooltipTimestamp(String(value), range)}
formatter={(value: number, name: string) => [formatPercent(value), name]}
contentStyle={{
backgroundColor: "var(--color-surface)",
borderColor: "var(--color-border)",
borderRadius: 12,
color: "var(--color-text-main)",
boxShadow: "var(--shadow-soft)",
}}
itemStyle={{ color: "var(--color-text-main)" }}
labelStyle={{ color: "var(--color-text-main)", fontWeight: 600 }}
/>
<Legend />
{data?.providers.map((provider) => (
<Line
key={provider}
type="monotone"
dataKey={provider}
name={resolveProviderName(provider, nodeMap)}
stroke={providerColors.get(provider) ?? "var(--color-primary)"}
strokeWidth={2.5}
dot={false}
activeDot={{ r: 4, strokeWidth: 0 }}
connectNulls
/>
))}
</LineChart>
</ResponsiveContainer>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
{latestPoints.map((point) => {
const isLow = point.remainingPct <= 20;
return (
<Card.Section key={point.provider} className="flex h-full flex-col gap-4">
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-3">
<div className="flex h-11 w-11 items-center justify-center rounded-xl border border-black/5 bg-surface text-text-main dark:border-white/5">
<ProviderIcon providerId={point.provider} size={22} />
</div>
<div>
<p className="text-sm font-semibold text-text-main">
{resolveProviderName(point.provider, nodeMap)}
</p>
<p className="text-xs text-text-muted">
{t("providerUtilizationLatestSnapshot")}
</p>
</div>
</div>
<span
className={`inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium ${
point.isExhausted
? "bg-error/10 text-error"
: isLow
? "bg-warning/10 text-warning"
: "bg-success/10 text-success"
}`}
>
{point.isExhausted ? "Exhausted" : isLow ? "Low" : "Healthy"}
</span>
</div>
<div className="flex items-end justify-between gap-3">
<div>
<p className="text-3xl font-bold text-text-main">
{point.remainingPct.toFixed(point.remainingPct < 10 ? 1 : 0)}%
</p>
<p className="mt-1 text-xs text-text-muted">
{t("providerUtilizationRemainingCapacity")}
</p>
</div>
<div className="text-right text-xs text-text-muted">
<p>{formatTooltipTimestamp(point.timestamp, range)}</p>
<p className="mt-1 uppercase tracking-[0.14em]">{point.windowKey}</p>
</div>
</div>
<div className="flex flex-col gap-2">
<div className="h-2 overflow-hidden rounded-full bg-black/5 dark:bg-white/5">
<div
className={`h-full rounded-full transition-all ${
point.isExhausted ? "bg-error" : isLow ? "bg-warning" : "bg-primary"
}`}
style={{ width: `${Math.max(point.remainingPct, 0)}%` }}
/>
</div>
<div className="flex items-center justify-between text-xs text-text-muted">
<span>0%</span>
<span>Remaining quota</span>
<span>100%</span>
</div>
</div>
</Card.Section>
);
})}
</div>
</div>
)}
</Card>
</div>
);
}