feat(quota): sync ?provider= URL param with provider filter for bookmarkable deep links (#4395)
This commit is contained in:
1 parent
92c7bdd5bc
commit
65826d95f2
2 files changed
+77
-1
No files matched your search
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
||||||
|
import { useSearchParams, useRouter, usePathname } from "next/navigation";
|
||||||
import ProviderIcon from "@/shared/components/ProviderIcon";
|
import ProviderIcon from "@/shared/components/ProviderIcon";
|
||||||
import QuotaTable from "./QuotaTable";
|
import QuotaTable from "./QuotaTable";
|
||||||
import Toggle from "@/shared/components/Toggle";
|
import Toggle from "@/shared/components/Toggle";
|
||||||
@@ -152,6 +153,9 @@ function formatTimeRemaining(value) {
|
|||||||
|
|
||||||
export default function ProviderLimits() {
|
export default function ProviderLimits() {
|
||||||
const { copied, copy } = useCopyToClipboard();
|
const { copied, copy } = useCopyToClipboard();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
const [connections, setConnections] = useState([]);
|
const [connections, setConnections] = useState([]);
|
||||||
const [quotaData, setQuotaData] = useState({});
|
const [quotaData, setQuotaData] = useState({});
|
||||||
const [loading, setLoading] = useState({});
|
const [loading, setLoading] = useState({});
|
||||||
@@ -171,7 +175,26 @@ export default function ProviderLimits() {
|
|||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
const [selectedConnection, setSelectedConnection] = useState(null);
|
const [selectedConnection, setSelectedConnection] = useState(null);
|
||||||
const [proxyPools, setProxyPools] = useState([]);
|
const [proxyPools, setProxyPools] = useState([]);
|
||||||
const [providerFilter, setProviderFilter] = useState("all");
|
// Initialize providerFilter from URL ?provider= param so the page
|
||||||
|
// can be bookmarked / deep-linked to a specific provider (#4217).
|
||||||
|
const [providerFilter, _setProviderFilter] = useState(
|
||||||
|
() => searchParams?.get("provider") || "all"
|
||||||
|
);
|
||||||
|
// Wrapper: keeps URL in sync with the selected provider so the view can be
|
||||||
|
// bookmarked. Replaces the URL without adding to browser history.
|
||||||
|
const setProviderFilter = useCallback((value) => {
|
||||||
|
_setProviderFilter(value);
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams(searchParams?.toString() || "");
|
||||||
|
if (value === "all") {
|
||||||
|
params.delete("provider");
|
||||||
|
} else {
|
||||||
|
params.set("provider", value);
|
||||||
|
}
|
||||||
|
const newUrl = params.toString() ? `${pathname}?${params.toString()}` : pathname;
|
||||||
|
router.replace(newUrl, { scroll: false });
|
||||||
|
} catch { /* non-fatal: URL sync is best-effort */ }
|
||||||
|
}, [pathname, router, searchParams]);
|
||||||
const [providerOptions, setProviderOptions] = useState([]);
|
const [providerOptions, setProviderOptions] = useState([]);
|
||||||
const [accountFilter, setAccountFilter] = useState("all");
|
const [accountFilter, setAccountFilter] = useState("all");
|
||||||
const [quotaSortMode, setQuotaSortMode] = useState("default");
|
const [quotaSortMode, setQuotaSortMode] = useState("default");
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Tests for #4217 — Quota Tracker URL ?provider= parameter support.
|
||||||
|
*
|
||||||
|
* Before this fix, visiting /dashboard/quota?provider=codex ignored the query
|
||||||
|
* parameter and always defaulted to "All providers". The dropdown selection
|
||||||
|
* updated only local state and did not update the URL.
|
||||||
|
*
|
||||||
|
* Fix: ProviderLimits initializes providerFilter from useSearchParams and
|
||||||
|
* wraps setProviderFilter to call router.replace when the filter changes.
|
||||||
|
*
|
||||||
|
* Because ProviderLimits is a JSX file we cannot import it in Vitest without
|
||||||
|
* a full Next.js setup. We verify the source text instead.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const src = fs.readFileSync(
|
||||||
|
path.resolve("../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js"),
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("ProviderLimits — URL ?provider= sync (#4217)", () => {
|
||||||
|
it("imports useSearchParams from next/navigation", () => {
|
||||||
|
expect(src).toContain("useSearchParams");
|
||||||
|
expect(src).toContain("next/navigation");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("imports useRouter from next/navigation", () => {
|
||||||
|
expect(src).toContain("useRouter");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("imports usePathname from next/navigation", () => {
|
||||||
|
expect(src).toContain("usePathname");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("initializes providerFilter from searchParams.get('provider')", () => {
|
||||||
|
expect(src).toMatch(/searchParams.*get.*provider/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls router.replace when provider filter changes", () => {
|
||||||
|
expect(src).toContain("router.replace");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes ?provider from URL when 'all' is selected", () => {
|
||||||
|
expect(src).toContain("params.delete");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets ?provider in URL when a specific provider is selected", () => {
|
||||||
|
expect(src).toContain('params.set("provider"');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user