From 2300db6cc5d452bee9e1b8e583be876b4b6666ad Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sat, 30 May 2026 22:15:27 -0300 Subject: [PATCH] feat(quota): retire standalone Plans screen, unified into pool wizard (Phase C2) Remove plans/ route and sidebar entry; PoolWizard Step 2 now covers plan-dimensions inline. --- .../plans/ProviderPlanConfigClient.tsx | 389 ------------------ .../costs/quota-share/plans/page.tsx | 7 - src/shared/constants/sidebarVisibility.ts | 8 - tests/unit/quota-plans-route-retired.test.ts | 51 +++ tests/unit/sidebar-costs-quota-plans.test.ts | 36 +- 5 files changed, 64 insertions(+), 427 deletions(-) delete mode 100644 src/app/(dashboard)/dashboard/costs/quota-share/plans/ProviderPlanConfigClient.tsx delete mode 100644 src/app/(dashboard)/dashboard/costs/quota-share/plans/page.tsx create mode 100644 tests/unit/quota-plans-route-retired.test.ts diff --git a/src/app/(dashboard)/dashboard/costs/quota-share/plans/ProviderPlanConfigClient.tsx b/src/app/(dashboard)/dashboard/costs/quota-share/plans/ProviderPlanConfigClient.tsx deleted file mode 100644 index 25e1051880..0000000000 --- a/src/app/(dashboard)/dashboard/costs/quota-share/plans/ProviderPlanConfigClient.tsx +++ /dev/null @@ -1,389 +0,0 @@ -"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 deleted file mode 100644 index 97c8c3c1cc..0000000000 --- a/src/app/(dashboard)/dashboard/costs/quota-share/plans/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import ProviderPlanConfigClient from "./ProviderPlanConfigClient"; - -export const dynamic = "force-dynamic"; - -export default function PlansPage() { - return ; -} diff --git a/src/shared/constants/sidebarVisibility.ts b/src/shared/constants/sidebarVisibility.ts index 57b09909ac..613d3805a8 100644 --- a/src/shared/constants/sidebarVisibility.ts +++ b/src/shared/constants/sidebarVisibility.ts @@ -47,7 +47,6 @@ export const HIDEABLE_SIDEBAR_ITEM_IDS = [ "costs-pricing", "costs-budget", "costs-quota-share", - "costs-quota-plans", // Monitoring > Audit "audit", "audit-mcp", @@ -462,13 +461,6 @@ const COSTS_ITEMS: readonly SidebarItemDefinition[] = [ subtitleKey: "costsQuotaShareSubtitle", icon: "pie_chart", }, - { - id: "costs-quota-plans", - href: "/dashboard/costs/quota-share/plans", - i18nKey: "costsQuotaPlans", - subtitleKey: "costsQuotaPlansSubtitle", - icon: "fact_check", - }, ]; const AUDIT_GROUP: SidebarItemGroup = { diff --git a/tests/unit/quota-plans-route-retired.test.ts b/tests/unit/quota-plans-route-retired.test.ts new file mode 100644 index 0000000000..b9d762ea47 --- /dev/null +++ b/tests/unit/quota-plans-route-retired.test.ts @@ -0,0 +1,51 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const sidebarVisibility = await import("../../src/shared/constants/sidebarVisibility.ts"); + +test('sidebarVisibility.ts no longer contains "costs-quota-plans"', () => { + assert.ok( + !(sidebarVisibility.HIDEABLE_SIDEBAR_ITEM_IDS as readonly string[]).includes("costs-quota-plans"), + '"costs-quota-plans" must have been removed from HIDEABLE_SIDEBAR_ITEM_IDS (Plans screen retired)' + ); +}); + +test("retired Plans route file no longer exists", () => { + const routePath = path.resolve( + __dirname, + "../../src/app/(dashboard)/dashboard/costs/quota-share/plans/page.tsx" + ); + assert.strictEqual( + fs.existsSync(routePath), + false, + `Route file should not exist (Plans screen retired): ${routePath}` + ); +}); + +test("retired ProviderPlanConfigClient file no longer exists", () => { + const clientPath = path.resolve( + __dirname, + "../../src/app/(dashboard)/dashboard/costs/quota-share/plans/ProviderPlanConfigClient.tsx" + ); + assert.strictEqual( + fs.existsSync(clientPath), + false, + `ProviderPlanConfigClient should not exist (Plans screen retired): ${clientPath}` + ); +}); + +test("costs section no longer includes costs-quota-plans nav item", () => { + const section = sidebarVisibility.SIDEBAR_SECTIONS.find((s) => s.id === "costs"); + assert.ok(section, 'expected "costs" section to exist'); + const items = sidebarVisibility.getSectionItems(section); + const ids = items.map((i) => i.id); + assert.ok( + !ids.includes("costs-quota-plans"), + '"costs-quota-plans" nav item must be absent from the costs section (Plans screen retired)' + ); +}); diff --git a/tests/unit/sidebar-costs-quota-plans.test.ts b/tests/unit/sidebar-costs-quota-plans.test.ts index 7ac5609c4d..c245ba5660 100644 --- a/tests/unit/sidebar-costs-quota-plans.test.ts +++ b/tests/unit/sidebar-costs-quota-plans.test.ts @@ -1,3 +1,8 @@ +/** + * Phase C2 — Plans screen retired (unified into PoolWizard Step 2). + * These tests verify the sidebar entry is gone and the costs section + * still has the correct remaining items. + */ import test from "node:test"; import assert from "node:assert/strict"; @@ -9,36 +14,21 @@ function sectionItems(sectionId: string) { return sidebarVisibility.getSectionItems(section); } -test("HIDEABLE_SIDEBAR_ITEM_IDS contains costs-quota-plans", () => { +test("HIDEABLE_SIDEBAR_ITEM_IDS does NOT contain costs-quota-plans (retired)", () => { assert.ok( - (sidebarVisibility.HIDEABLE_SIDEBAR_ITEM_IDS as readonly string[]).includes("costs-quota-plans"), - "costs-quota-plans must be in HIDEABLE_SIDEBAR_ITEM_IDS" + !(sidebarVisibility.HIDEABLE_SIDEBAR_ITEM_IDS as readonly string[]).includes("costs-quota-plans"), + "costs-quota-plans must have been removed from HIDEABLE_SIDEBAR_ITEM_IDS" ); }); -test("HIDEABLE_SIDEBAR_ITEM_IDS: costs-quota-plans appears after costs-quota-share", () => { - const ids = sidebarVisibility.HIDEABLE_SIDEBAR_ITEM_IDS as readonly string[]; - const qsIdx = ids.indexOf("costs-quota-share"); - const qpIdx = ids.indexOf("costs-quota-plans"); - assert.ok(qsIdx !== -1, "costs-quota-share must exist"); - assert.ok(qpIdx !== -1, "costs-quota-plans must exist"); - assert.ok(qpIdx > qsIdx, "costs-quota-plans must come after costs-quota-share"); -}); - -test("costs section has 5 items including costs-quota-plans at end", () => { +test("costs section does not include costs-quota-plans item (retired)", () => { const items = sectionItems("costs"); const ids = items.map((i) => i.id); - assert.ok(ids.includes("costs-quota-plans"), "costs section must include costs-quota-plans"); - assert.strictEqual(ids[ids.length - 1], "costs-quota-plans", "costs-quota-plans must be last"); - assert.strictEqual(ids.length, 5, "costs section must have exactly 5 items"); + assert.ok(!ids.includes("costs-quota-plans"), "costs section must NOT include costs-quota-plans"); }); -test("costs-quota-plans has correct href and icon", () => { +test("costs section still contains costs-quota-share", () => { const items = sectionItems("costs"); - const item = items.find((i) => i.id === "costs-quota-plans"); - assert.ok(item, "costs-quota-plans item must exist"); - assert.strictEqual(item.href, "/dashboard/costs/quota-share/plans"); - assert.strictEqual(item.icon, "fact_check"); - assert.strictEqual(item.i18nKey, "costsQuotaPlans"); - assert.strictEqual(item.subtitleKey, "costsQuotaPlansSubtitle"); + const ids = items.map((i) => i.id); + assert.ok(ids.includes("costs-quota-share"), "costs section must still include costs-quota-share"); });