From bd2cf82e0acbddd9e5b9e33f360d16e52790d8f3 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 28 May 2026 08:24:49 -0300 Subject: [PATCH] feat(quota-plans): add /dashboard/costs/quota-share/plans page (B/F9) --- .../plans/ProviderPlanConfigClient.tsx | 389 ++++++++++++++++++ .../costs/quota-share/plans/page.tsx | 7 + 2 files changed, 396 insertions(+) create mode 100644 src/app/(dashboard)/dashboard/costs/quota-share/plans/ProviderPlanConfigClient.tsx create mode 100644 src/app/(dashboard)/dashboard/costs/quota-share/plans/page.tsx diff --git a/src/app/(dashboard)/dashboard/costs/quota-share/plans/ProviderPlanConfigClient.tsx b/src/app/(dashboard)/dashboard/costs/quota-share/plans/ProviderPlanConfigClient.tsx new file mode 100644 index 0000000000..25e1051880 --- /dev/null +++ b/src/app/(dashboard)/dashboard/costs/quota-share/plans/ProviderPlanConfigClient.tsx @@ -0,0 +1,389 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { useTranslations } from "next-intl"; +import { Button } from "@/shared/components"; +import ProviderIcon from "@/shared/components/ProviderIcon"; +import { knownProviders, getKnownPlan } from "@/lib/quota/planRegistry"; +import type { QuotaDimension, QuotaUnit, QuotaWindow } from "@/lib/quota/dimensions"; + +// ──────────────────────────────────────────────────────────────────────────── +// Types +// ──────────────────────────────────────────────────────────────────────────── + +interface Connection { + id: string; + provider: string; + name?: string; + displayName?: string; + email?: string; +} + +interface ProviderPlanOverride { + connectionId: string; + provider: string; + dimensions: QuotaDimension[]; + source: "auto" | "manual"; +} + +// ──────────────────────────────────────────────────────────────────────────── +// Constants +// ──────────────────────────────────────────────────────────────────────────── + +const UNIT_OPTIONS: QuotaUnit[] = ["percent", "requests", "tokens", "usd"]; +const WINDOW_OPTIONS: QuotaWindow[] = ["5h", "hourly", "daily", "weekly", "monthly"]; + +// ──────────────────────────────────────────────────────────────────────────── +// Component +// ──────────────────────────────────────────────────────────────────────────── + +export default function ProviderPlanConfigClient() { + const t = useTranslations("quotaPlans"); + + const [connections, setConnections] = useState([]); + const [selectedConnectionId, setSelectedConnectionId] = useState(""); + const [overrides, setOverrides] = useState>({}); + const [editDimensions, setEditDimensions] = useState([]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [reverting, setReverting] = useState(false); + const [error, setError] = useState(null); + const [successMsg, setSuccessMsg] = useState(null); + + // ── Load connections and existing overrides ─────────────────────────────── + + useEffect(() => { + setLoading(true); + Promise.all([ + fetch("/api/providers/client") + .then((r) => (r.ok ? r.json() : null)) + .catch(() => null), + fetch("/api/quota/plans") + .then((r) => (r.ok ? r.json() : null)) + .catch(() => null), + ]) + .then(([connsData, plansData]) => { + const conns: Connection[] = Array.isArray(connsData?.connections) + ? connsData.connections + : []; + setConnections(conns); + + if (Array.isArray(plansData)) { + const map: Record = {}; + for (const p of plansData as ProviderPlanOverride[]) { + if (p.connectionId) map[p.connectionId] = p; + } + setOverrides(map); + } + }) + .catch(() => { + setError("Failed to load data"); + }) + .finally(() => setLoading(false)); + }, []); + + // ── Derived: selected connection and plan info ──────────────────────────── + + const selectedConn = connections.find((c) => c.id === selectedConnectionId); + const selectedProvider = selectedConn?.provider || ""; + + const existingOverride = selectedConnectionId ? overrides[selectedConnectionId] : undefined; + const catalogPlan = selectedProvider ? getKnownPlan(selectedProvider) : null; + + const detectedSource = existingOverride?.source || (catalogPlan ? "auto" : null); + + const connLabel = (c: Connection) => + `${c.provider} / ${c.name || c.email || c.displayName || c.id.slice(0, 12)}`; + + // ── When connection changes, populate edit dimensions ───────────────────── + + useEffect(() => { + if (!selectedConnectionId) { + setEditDimensions([]); + return; + } + // Priority: manual override > catalog + if (existingOverride && existingOverride.source === "manual") { + setEditDimensions(existingOverride.dimensions); + } else if (catalogPlan) { + setEditDimensions([...catalogPlan.dimensions]); + } else { + setEditDimensions([]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedConnectionId]); + + // ── Dimension editors ───────────────────────────────────────────────────── + + const addDimension = () => { + setEditDimensions((prev) => [...prev, { unit: "percent", window: "daily", limit: 100 }]); + }; + + const removeDimension = (i: number) => { + setEditDimensions((prev) => prev.filter((_, idx) => idx !== i)); + }; + + const updateDimension = (i: number, patch: Partial) => { + setEditDimensions((prev) => prev.map((d, idx) => (idx === i ? { ...d, ...patch } : d))); + }; + + // ── Save override ───────────────────────────────────────────────────────── + + const handleSaveOverride = useCallback(async () => { + if (!selectedConnectionId) return; + setSaving(true); + setError(null); + setSuccessMsg(null); + try { + const res = await fetch(`/api/quota/plans/${selectedConnectionId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ dimensions: editDimensions }), + }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + // Refresh overrides + const data = (await res.json()) as ProviderPlanOverride; + setOverrides((prev) => ({ ...prev, [selectedConnectionId]: data })); + setSuccessMsg(t("saveOverrideButton") + " — saved"); + } catch (err) { + setError(err instanceof Error ? err.message : "Save failed"); + } finally { + setSaving(false); + } + }, [selectedConnectionId, editDimensions, t]); + + // ── Revert to catalog ───────────────────────────────────────────────────── + + const handleRevertToCatalog = useCallback(async () => { + if (!selectedConnectionId) return; + setReverting(true); + setError(null); + setSuccessMsg(null); + try { + const res = await fetch(`/api/quota/plans/${selectedConnectionId}`, { + method: "DELETE", + }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + setOverrides((prev) => { + const next = { ...prev }; + delete next[selectedConnectionId]; + return next; + }); + // Reset edit dims to catalog + if (catalogPlan) setEditDimensions([...catalogPlan.dimensions]); + else setEditDimensions([]); + setSuccessMsg(t("revertToCatalogButton") + " — reverted"); + } catch (err) { + setError(err instanceof Error ? err.message : "Revert failed"); + } finally { + setReverting(false); + } + }, [selectedConnectionId, catalogPlan, t]); + + // ── Render ──────────────────────────────────────────────────────────────── + + return ( +
+ {/* Header */} +
+

+ fact_check + {t("title")} +

+

{t("description")}

+
+ + {loading ? ( +
Loading…
+ ) : ( +
+ {/* Left: connection selector */} +
+
+ + +
+ + {/* Catalog known plans */} +
+
+ {t("catalogTitle")} +
+

{t("catalogDescription")}

+
+ {knownProviders().map((prov) => { + const plan = getKnownPlan(prov); + if (!plan) return null; + return ( +
+
+ +
+
+
{prov}
+ {plan.dimensions.map((d, i) => ( +
+ {d.unit}/{d.window}: {d.limit} +
+ ))} +
+
+ ); + })} +
+
+
+ + {/* Right: plan config */} + {selectedConnectionId ? ( +
+ {/* Status badge */} +
+ {selectedProvider && ( +
+ +
+ )} + {connLabel(selectedConn!)} + {detectedSource === "auto" && ( + + {t("detectedPlanLabel")} (auto) + + )} + {detectedSource === "manual" && ( + + {t("manualPlanLabel")} + + )} + {!detectedSource && ( + + {t("unconfiguredLabel")} + + )} +
+ + {/* Dimensions editor */} +
+
+ + {t("dimensionLabel")} + + +
+ + {editDimensions.length === 0 && ( +
+ {t("unconfiguredLabel")} — {t("addDimension")} +
+ )} + +
+ {editDimensions.map((dim, i) => ( +
+ + + updateDimension(i, { limit: Number(e.target.value) })} + placeholder={t("limitLabel")} + className="px-2 py-1.5 rounded border border-border bg-bg-base text-xs tabular-nums text-right" + /> + +
+ ))} +
+
+ + {/* Error / success */} + {error && ( +

{error}

+ )} + {successMsg && ( +

+ {successMsg} +

+ )} + + {/* Actions */} +
+ + {existingOverride && existingOverride.source === "manual" && ( + + )} +
+
+ ) : ( +
+ {t("unknownProviderNotice")} +
+ )} +
+ )} +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/costs/quota-share/plans/page.tsx b/src/app/(dashboard)/dashboard/costs/quota-share/plans/page.tsx new file mode 100644 index 0000000000..97c8c3c1cc --- /dev/null +++ b/src/app/(dashboard)/dashboard/costs/quota-share/plans/page.tsx @@ -0,0 +1,7 @@ +import ProviderPlanConfigClient from "./ProviderPlanConfigClient"; + +export const dynamic = "force-dynamic"; + +export default function PlansPage() { + return ; +}