This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git

commit 0a3405c6efa4bc0642176405885a921690e71f41
Author: Yang Luo <[email protected]>
AuthorDate: Fri Sep 4 12:46:09 2026 +0800

    feat(nav): give the sidebar a second level
---
 web/src/App.tsx                           |  2 +-
 web/src/components/shared/app-sidebar.tsx | 48 ++++++++++++-------
 web/src/components/shared/page-header.tsx | 16 ++++++-
 web/src/locales/en/data.json              |  4 +-
 web/src/locales/zh/data.json              |  4 +-
 web/src/nav.ts                            | 80 +++++++++++++++++++++++++++----
 web/src/pages/AgentConfigsPage.tsx        | 13 ++++-
 web/src/pages/AuthenticityPage.tsx        | 13 ++++-
 web/src/pages/SettingPage.tsx             | 35 ++++++++++----
 web/src/pages/UsagePage.tsx               | 18 +++++--
 10 files changed, 188 insertions(+), 45 deletions(-)

diff --git a/web/src/App.tsx b/web/src/App.tsx
index 4d32a24..80e442a 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -69,7 +69,7 @@ export default function App() {
   const isDesktop = useIsDesktop();
   const location = useLocation();
 
-  const selectedKey = selectedKeyOf(location.pathname);
+  const selectedKey = selectedKeyOf(location.pathname, location.search, 
location.hash);
   const wasCollapsedRef = React.useRef(false);
   const [openKeys, setOpenKeys] = React.useState<string[]>(() => {
     if (localStorage.getItem(collapsedKey) === "true") {
diff --git a/web/src/components/shared/app-sidebar.tsx 
b/web/src/components/shared/app-sidebar.tsx
index 556028f..80554c4 100644
--- a/web/src/components/shared/app-sidebar.tsx
+++ b/web/src/components/shared/app-sidebar.tsx
@@ -23,7 +23,9 @@ import {navGroups, type NavGroup} from "@/nav";
 import {SimpleTooltip} from "@/components/ui/tooltip";
 
 const OPEN_KEYS_STORAGE = "siderMenuOpenKeys";
-const DEFAULT_OPEN_KEYS = navGroups.filter(group => group.children).map(group 
=> group.key);
+// Nothing is expanded until the reader is inside a group: nine sections of
+// Settings unfolded on every page would bury the rest of the rail.
+const DEFAULT_OPEN_KEYS: string[] = [];
 
 export function readSavedOpenKeys(): string[] {
   try {
@@ -199,7 +201,7 @@ export function AppSidebar({
               return (
                 <NavLink
                   key={group.key}
-                  to={group.children[0].path}
+                  to={group.path ?? group.children[0].path}
                   active={hasActiveChild}
                   collapsed
                   icon={group.icon}
@@ -208,22 +210,36 @@ export function AppSidebar({
               );
             }
 
+            const header = cn(
+              "flex items-center gap-2.5 text-sm transition-colors",
+              hasActiveChild ? "text-sidebar-foreground font-medium" : 
"text-sidebar-foreground/70",
+              "hover:text-sidebar-foreground",
+            );
+
             return (
               <div key={group.key}>
-                <button
-                  type="button"
-                  onClick={() => toggleGroup(group.key)}
-                  className={cn(
-                    "flex w-full items-center gap-2.5 rounded-md px-2.5 py-2 
text-sm transition-colors",
-                    hasActiveChild ? "text-sidebar-foreground font-medium" : 
"text-sidebar-foreground/70",
-                    "hover:bg-sidebar-accent/60 hover:text-sidebar-foreground",
-                  )}
-                  aria-expanded={isOpen}
-                >
-                  {group.icon ? <group.icon className="size-4 shrink-0" /> : 
null}
-                  <span className="flex-1 truncate 
text-left">{groupLabel}</span>
-                  <ChevronDown className={cn("size-3.5 shrink-0 
transition-transform", isOpen && "rotate-180")} />
-                </button>
+                {/* The label goes to the page, the chevron only unfolds it —
+                    a group is a destination as well as a heading. */}
+                <div className="hover:bg-sidebar-accent/60 flex items-center 
rounded-md pr-1 transition-colors">
+                  <Link to={group.path ?? group.children[0].path} 
className={cn(header, "min-w-0 flex-1 px-2.5 py-2")}>
+                    {group.icon ? <group.icon className="size-4 shrink-0" /> : 
null}
+                    <span className="truncate text-left">{groupLabel}</span>
+                  </Link>
+                  <button
+                    type="button"
+                    onClick={event => {
+                      // The rail closes itself on any link tap while it is a
+                      // drawer, and unfolding a group is not leaving it.
+                      event.stopPropagation();
+                      toggleGroup(group.key);
+                    }}
+                    className={cn(header, "rounded-md p-1.5")}
+                    aria-expanded={isOpen}
+                    aria-label={groupLabel}
+                  >
+                    <ChevronDown className={cn("size-3.5 shrink-0 
transition-transform", isOpen && "rotate-180")} />
+                  </button>
+                </div>
                 {isOpen ? (
                   <div className="mt-0.5 space-y-0.5">
                     {group.children.map(child => (
diff --git a/web/src/components/shared/page-header.tsx 
b/web/src/components/shared/page-header.tsx
index d7bbdbe..b4a5ebe 100644
--- a/web/src/components/shared/page-header.tsx
+++ b/web/src/components/shared/page-header.tsx
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 import * as React from "react";
+import {useLocation} from "react-router-dom";
 import {ChevronDown} from "lucide-react";
 
 import {cn} from "@/lib/utils";
@@ -49,6 +50,7 @@ export function PageHeader({
  * long form reads as a few short sections rather than one tall column.
  */
 export function Section({
+  id,
   title,
   description,
   columns = 3,
@@ -56,6 +58,8 @@ export function Section({
   collapsible = false,
   children,
 }: {
+  /** Names the section for the rail, which links to it by hash. */
+  id?: string;
   title?: React.ReactNode;
   description?: React.ReactNode;
   columns?: 1 | 2 | 3;
@@ -65,9 +69,19 @@ export function Section({
   children: React.ReactNode;
 }) {
   const [open, setOpen] = React.useState(!collapsible);
+  const {hash} = useLocation();
+  const targeted = id !== undefined && hash === `#${id}`;
+
+  // Arriving at a folded section by link has to unfold it, or the jump lands 
on
+  // a title with nothing under it.
+  React.useEffect(() => {
+    if (targeted) {
+      setOpen(true);
+    }
+  }, [targeted]);
 
   return (
-    <Card className={cn("gap-4 py-5", className)}>
+    <Card id={id} className={cn("scroll-mt-20 gap-4 py-5", className)}>
       {title || description ? (
         <CardHeader className="px-5">
           {title ? (
diff --git a/web/src/locales/en/data.json b/web/src/locales/en/data.json
index 9bc08d4..15aa73e 100644
--- a/web/src/locales/en/data.json
+++ b/web/src/locales/en/data.json
@@ -1189,6 +1189,7 @@
   },
   "usage": {
     "Add a price": "Add a price",
+    "Agent spend": "Agent spend",
     "Agents description": "What the agents on this machine spent, read from 
the transcripts they write themselves. This is the only account of a request 
that never went through Gateway.",
     "Automatic sync": "Automatic sync",
     "Automatic sync description": "Read the models.dev catalogue on a schedule 
and reprice the models this machine has been seen running. A price you edited 
by hand is left alone.",
@@ -1237,6 +1238,7 @@
     "Read from a catalogue of {count} models": "Read from a models.dev 
catalogue of {count} models.",
     "Recording is off detail": "llmRecordMode is \"off\", so nothing relayed 
is being counted. The agents' own transcripts are counted either way.",
     "Relayed description": "What Gateway relayed on behalf of the agents. This 
is the only account that knows which provider answered, and whether it failed.",
+    "Relayed spend": "Relayed spend",
     "Search the catalogue": "Search by model name",
     "Set their prices": "Set their prices",
     "Source": "Source",
@@ -1249,8 +1251,6 @@
     "Trend": "Trend",
     "Updated": "Updated:",
     "Usage": "Usage",
-    "What Gateway relayed": "What Gateway relayed",
-    "What the agents spent": "What the agents spent",
     "models.dev": "models.dev",
     "{count} providers list it": "{count} providers list it"
   }
diff --git a/web/src/locales/zh/data.json b/web/src/locales/zh/data.json
index 04a2a8e..0feaaba 100644
--- a/web/src/locales/zh/data.json
+++ b/web/src/locales/zh/data.json
@@ -1261,6 +1261,7 @@
   },
   "usage": {
     "Add a price": "新增价格",
+    "Agent spend": "Agent 账单",
     "Agents description": "这台机器上的 agent 花掉的量,读自它们自己写的会话记录。没有经过 Gateway 
的请求,只有这一份账。",
     "Automatic sync": "自动同步",
     "Automatic sync description": "定期读取 models.dev 
目录,为本机跑过的模型重新定价。你手改过的价格不会被动。",
@@ -1309,6 +1310,7 @@
     "Read from a catalogue of {count} models": "读自 models.dev 的 {count} 
个模型目录。",
     "Recording is off detail": "llmRecordMode 是 \"off\",转发的请求没有被统计。Agent 
自己的会话记录不受影响,照常统计。",
     "Relayed description": "Gateway 代为转发的量。只有这一份账知道是哪个 provider 应答的,以及是否失败。",
+    "Relayed spend": "转发账单",
     "Search the catalogue": "按模型名搜索",
     "Set their prices": "去设置价格",
     "Source": "来源",
@@ -1321,8 +1323,6 @@
     "Trend": "趋势",
     "Updated": "已更新:",
     "Usage": "用量",
-    "What Gateway relayed": "Gateway 转发的账",
-    "What the agents spent": "Agent 自己的账",
     "models.dev": "models.dev",
     "{count} providers list it": "{count} 家收录"
   }
diff --git a/web/src/nav.ts b/web/src/nav.ts
index c13fed3..b26f738 100644
--- a/web/src/nav.ts
+++ b/web/src/nav.ts
@@ -59,6 +59,8 @@ export interface NavGroup {
 export const navGroups: NavGroup[] = [
   // The setup pages come first — agents, the models behind them, and the gates
   // they pass through — then what agents ran and what it cost, settings last.
+  // A group's children are places inside its page, so the rail can land on the
+  // right tab or section instead of only on the page's top.
   {key: "/", label: "agent:Agents", icon: Bot, path: "/"},
   // The table of installations is the home screen's own advanced view, linked
   // from it rather than competing with it in the rail.
@@ -70,8 +72,23 @@ export const navGroups: NavGroup[] = [
     icon: Blocks,
     path: "/agent-configs",
     adminOnly: true,
+    children: [
+      {key: "/agent-configs?tab=skill", label: "agentConfig:Skills", path: 
"/agent-configs?tab=skill"},
+      {key: "/agent-configs?tab=mcp", label: "agentConfig:MCP servers", path: 
"/agent-configs?tab=mcp"},
+      {key: "/agent-configs?tab=prompt", label: "agentConfig:Prompts", path: 
"/agent-configs?tab=prompt"},
+    ],
+  },
+  {
+    key: "/authenticity",
+    label: "audit:Authenticity",
+    icon: ShieldCheck,
+    path: "/authenticity",
+    adminOnly: true,
+    children: [
+      {key: "/authenticity?tab=report", label: "audit:Report", path: 
"/authenticity?tab=report"},
+      {key: "/authenticity?tab=cases", label: "audit:Test cases", path: 
"/authenticity?tab=cases"},
+    ],
   },
-  {key: "/authenticity", label: "audit:Authenticity", icon: ShieldCheck, path: 
"/authenticity", adminOnly: true},
   {key: "/permissions", label: "agent:Permissions", icon: ShieldHalf, path: 
"/permissions", adminOnly: true},
   {
     key: "/agent-sessions",
@@ -82,9 +99,37 @@ export const navGroups: NavGroup[] = [
   },
   {key: "/agent-records", label: "agent:Agent Records", icon: FileSearch, 
path: "/agent-records", adminOnly: true},
   {key: "/llm-records", label: "llm:LLM Records", icon: Logs, path: 
"/llm-records", adminOnly: true},
-  {key: "/usage", label: "usage:Usage", icon: ChartColumn, path: "/usage", 
adminOnly: true},
+  {
+    key: "/usage",
+    label: "usage:Usage",
+    icon: ChartColumn,
+    path: "/usage",
+    adminOnly: true,
+    children: [
+      {key: "/usage?tab=agents", label: "usage:Agent spend", path: 
"/usage?tab=agents"},
+      {key: "/usage?tab=relayed", label: "usage:Relayed spend", path: 
"/usage?tab=relayed"},
+    ],
+  },
   {key: "/pricing", label: "usage:Model pricing", icon: CircleDollarSign, 
path: "/pricing", adminOnly: true},
-  {key: "/settings", label: "setting:Settings", icon: Settings, path: 
"/settings", adminOnly: true},
+  {
+    key: "/settings",
+    label: "setting:Settings",
+    icon: Settings,
+    path: "/settings",
+    adminOnly: true,
+    // One long page, so the children are its sections rather than tabs.
+    children: [
+      {key: "/settings#llm-records", label: "setting:LLM records", path: 
"/settings#llm-records"},
+      {key: "/settings#probes", label: "setting:Channel probes", path: 
"/settings#probes"},
+      {key: "/settings#agents", label: "setting:Agents", path: 
"/settings#agents"},
+      {key: "/settings#signin", label: "setting:Sign-in", path: 
"/settings#signin"},
+      {key: "/settings#security", label: "setting:Security", path: 
"/settings#security"},
+      {key: "/settings#network", label: "setting:Network", path: 
"/settings#network"},
+      {key: "/settings#backups", label: "setting:Backups", path: 
"/settings#backups"},
+      {key: "/settings#cloud-sync", label: "setting:Cloud sync", path: 
"/settings#cloud-sync"},
+      {key: "/settings#import-export", label: "setting:Import and export", 
path: "/settings#import-export"},
+    ],
+  },
 ];
 
 /** All leaf entries, flattened, for lookups by first path segment. */
@@ -92,16 +137,35 @@ export const navLeaves: NavLeaf[] = 
navGroups.flatMap(group =>
   group.children ? group.children : [{...group, path: group.path ?? group.key} 
as NavLeaf],
 );
 
-export function findLeaf(segmentKey: string) {
-  return navLeaves.find(leaf => leaf.key === segmentKey);
+export function findLeaf(segmentKey: string): NavLeaf | undefined {
+  const leaf = navLeaves.find(entry => entry.key === segmentKey);
+  if (leaf) {
+    return leaf;
+  }
+  // A grouped page contributes its children to the leaves, not itself, so the
+  // breadcrumb still has to be able to name the page they live on.
+  const group = navGroups.find(entry => entry.key === segmentKey);
+  return group ? {key: group.key, label: group.label, path: group.path ?? 
group.key} : undefined;
 }
 
 export function findGroupOf(segmentKey: string) {
   return navGroups.find(group => group.children?.some(child => child.key === 
segmentKey));
 }
 
-/** The key the sidebar treats as selected: the first path segment, or "/" at 
home. */
-export function selectedKeyOf(pathname: string) {
+/**
+ * The key the sidebar treats as selected. A page with children is selected at
+ * one of them: the tab named by `?tab=`, the section named by the hash, or the
+ * first child, which is what the page shows when neither is set.
+ */
+export function selectedKeyOf(pathname: string, search = "", hash = "") {
   const firstSegment = pathname.split("/").filter(Boolean)[0];
-  return firstSegment === undefined ? "/" : `/${firstSegment}`;
+  const base = firstSegment === undefined ? "/" : `/${firstSegment}`;
+  const group = navGroups.find(entry => entry.key === base);
+  if (!group?.children?.length) {
+    return base;
+  }
+  const tab = new URLSearchParams(search).get("tab");
+  const suffix = hash || (tab ? `?tab=${tab}` : "");
+  const match = group.children.find(child => child.key === `${base}${suffix}`);
+  return (match ?? group.children[0]).key;
 }
diff --git a/web/src/pages/AgentConfigsPage.tsx 
b/web/src/pages/AgentConfigsPage.tsx
index afbaa11..2930979 100644
--- a/web/src/pages/AgentConfigsPage.tsx
+++ b/web/src/pages/AgentConfigsPage.tsx
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 import * as React from "react";
+import {useSearchParams} from "react-router-dom";
 import {
   AlertTriangle,
   ArrowDownToLine,
@@ -160,7 +161,17 @@ export default function AgentConfigsPage({account}: 
{account: Account}) {
   const isAdmin = Setting.isAdminUser(account);
   const {inventories, loading, error, scanned, refresh} = useAgentConfigs();
 
-  const [kind, setKind] = React.useState<AgentConfigKind>("skill");
+  // The tab lives in the URL so the rail can link straight to one of them.
+  const [searchParams, setSearchParams] = useSearchParams();
+  const kind = (["skill", "mcp", "prompt"] as const).find(one => one === 
searchParams.get("tab")) ?? "skill";
+  const setKind = (next: AgentConfigKind) =>
+    setSearchParams(
+      previous => {
+        previous.set("tab", next);
+        return previous;
+      },
+      {replace: true},
+    );
   const [sourceKey, setSourceKey] = React.useState("");
   const [selected, setSelected] = React.useState<string[]>([]);
   const [detail, setDetail] = React.useState<DetailTarget | null>(null);
diff --git a/web/src/pages/AuthenticityPage.tsx 
b/web/src/pages/AuthenticityPage.tsx
index c64ff1c..1c6b637 100644
--- a/web/src/pages/AuthenticityPage.tsx
+++ b/web/src/pages/AuthenticityPage.tsx
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 import * as React from "react";
+import {useSearchParams} from "react-router-dom";
 import i18next from "i18next";
 
 import * as ProviderBackend from "@/backend/ProviderBackend";
@@ -35,7 +36,17 @@ type AuthenticityTab = "report" | "cases";
  */
 export default function AuthenticityPage({account}: {account: Account}) {
   const isAdmin = Setting.isAdminUser(account);
-  const [tab, setTab] = React.useState<AuthenticityTab>("report");
+  // The tab lives in the URL so the rail can link straight to one of them.
+  const [searchParams, setSearchParams] = useSearchParams();
+  const tab: AuthenticityTab = searchParams.get("tab") === "cases" ? "cases" : 
"report";
+  const setTab = (next: AuthenticityTab) =>
+    setSearchParams(
+      previous => {
+        previous.set("tab", next);
+        return previous;
+      },
+      {replace: true},
+    );
   const [providers, setProviders] = React.useState<Provider[]>([]);
 
   React.useEffect(() => {
diff --git a/web/src/pages/SettingPage.tsx b/web/src/pages/SettingPage.tsx
index 50ad9e0..7b36215 100644
--- a/web/src/pages/SettingPage.tsx
+++ b/web/src/pages/SettingPage.tsx
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 import * as React from "react";
+import {useLocation} from "react-router-dom";
 import i18next from "i18next";
 
 import * as SettingBackend from "@/backend/SettingBackend";
@@ -61,6 +62,17 @@ export default function SettingPage({account}: {account: 
Account}) {
 
   React.useEffect(load, [load]);
 
+  // The rail links to a section by hash, and the browser cannot honour that on
+  // its own while the page is still the loading placeholder.
+  const {hash} = useLocation();
+  const loaded = setting !== null;
+  React.useEffect(() => {
+    if (!loaded || !hash) {
+      return;
+    }
+    document.getElementById(hash.slice(1))?.scrollIntoView({block: "start"});
+  }, [hash, loaded]);
+
   if (!isAdmin) {
     return <UnauthorizedResult />;
   }
@@ -137,7 +149,7 @@ export default function SettingPage({account}: {account: 
Account}) {
         }
       />
 
-      <Section columns={2} title={i18next.t("setting:LLM records")} 
description={i18next.t("setting:LLM records description")}>
+      <Section id="llm-records" columns={2} title={i18next.t("setting:LLM 
records")} description={i18next.t("setting:LLM records description")}>
         <Field label={i18next.t("setting:Record mode")}>
           <SimpleSelect
             value={setting.llmRecordMode}
@@ -157,6 +169,7 @@ export default function SettingPage({account}: {account: 
Account}) {
       </Section>
 
       <Section
+        id="probes"
         columns={2}
         title={i18next.t("setting:Channel probes")}
         description={i18next.t("setting:Channel probes description")}
@@ -181,13 +194,13 @@ export default function SettingPage({account}: {account: 
Account}) {
         )}
       </Section>
 
-      <Section columns={2} title={i18next.t("setting:Agents")}>
+      <Section id="agents" columns={2} title={i18next.t("setting:Agents")}>
         {textField("agentPatchStateDir", i18next.t("setting:Agent state dir"), 
i18next.t("setting:Agent state dir hint"))}
         {numberField("agentRecordCapacity", i18next.t("setting:Agent record 
capacity"), i18next.t("setting:Agent record capacity hint"), 1)}
         {numberField("agentMonitorPollSeconds", i18next.t("setting:Agent poll 
seconds"), undefined, 1)}
       </Section>
 
-      <Section columns={2} collapsible title={i18next.t("setting:Sign-in")} 
description={i18next.t("setting:Sign-in description")}>
+      <Section id="signin" columns={2} collapsible 
title={i18next.t("setting:Sign-in")} description={i18next.t("setting:Sign-in 
description")}>
         {textField("casdoorEndpoint", i18next.t("setting:Casdoor endpoint"))}
         {textField("clientId", i18next.t("setting:Client ID"))}
         {secretField("clientSecret", i18next.t("setting:Client secret"))}
@@ -195,20 +208,26 @@ export default function SettingPage({account}: {account: 
Account}) {
         {textField("casdoorApplication", i18next.t("setting:Casdoor 
application"))}
       </Section>
 
-      <Section columns={2} title={i18next.t("setting:Security")}>
+      <Section id="security" columns={2} title={i18next.t("setting:Security")}>
         {secretField("apiKeyEncryptionKey", i18next.t("setting:API key 
encryption key"), i18next.t("setting:API key encryption key hint"))}
         {textField("relayToken", i18next.t("setting:Relay token"), 
i18next.t("setting:Relay token hint"))}
       </Section>
 
-      <Section columns={2} collapsible title={i18next.t("setting:Network")}>
+      <Section id="network" columns={2} collapsible 
title={i18next.t("setting:Network")}>
         {textField("httpProxy", i18next.t("setting:Outbound SOCKS5 proxy"), 
i18next.t("setting:Outbound SOCKS5 proxy hint"))}
       </Section>
 
-      <BackupPanel onSettingChanged={load} reloadToken={synced} />
+      <div id="backups" className="scroll-mt-20">
+        <BackupPanel onSettingChanged={load} reloadToken={synced} />
+      </div>
 
-      <CloudSyncPanel onSynced={() => setSynced(count => count + 1)} />
+      <div id="cloud-sync" className="scroll-mt-20">
+        <CloudSyncPanel onSynced={() => setSynced(count => count + 1)} />
+      </div>
 
-      <ImportExportPanel onSettingChanged={load} />
+      <div id="import-export" className="scroll-mt-20">
+        <ImportExportPanel onSettingChanged={load} />
+      </div>
     </PageContainer>
   );
 }
diff --git a/web/src/pages/UsagePage.tsx b/web/src/pages/UsagePage.tsx
index b38aa96..56f80e6 100644
--- a/web/src/pages/UsagePage.tsx
+++ b/web/src/pages/UsagePage.tsx
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import * as React from "react";
-import {Link} from "react-router-dom";
+import {Link, useSearchParams} from "react-router-dom";
 import {Logs, RefreshCw} from "lucide-react";
 import i18next from "i18next";
 
@@ -84,7 +84,9 @@ function rangeLabel(days: number) {
 
 export default function UsagePage({account}: {account: Account}) {
   const isAdmin = Setting.isAdminUser(account);
-  const [source, setSource] = React.useState<UsageSource>("agents");
+  // The tab lives in the URL so the rail can link straight to one of them.
+  const [searchParams, setSearchParams] = useSearchParams();
+  const source: UsageSource = searchParams.get("tab") === "relayed" ? 
"relayed" : "agents";
   const [days, setDays] = React.useState(7);
   const [agent, setAgent] = React.useState("");
   const [refreshMs, setRefreshMs] = React.useState(30000);
@@ -247,7 +249,13 @@ export default function UsagePage({account}: {account: 
Account}) {
   }
 
   const changeSource = (next: UsageSource) => {
-    setSource(next);
+    setSearchParams(
+      previous => {
+        previous.set("tab", next);
+        return previous;
+      },
+      {replace: true},
+    );
     // Each source offers the windows its own granularity can draw, so a window
     // the new one does not have falls back to its default.
     if (!RANGES[next].includes(days)) {
@@ -308,8 +316,8 @@ export default function UsagePage({account}: {account: 
Account}) {
 
       <Tabs value={source} onValueChange={value => changeSource(value as 
UsageSource)}>
         <TabsList>
-          <TabsTrigger value="agents">{i18next.t("usage:What the agents 
spent")}</TabsTrigger>
-          <TabsTrigger value="relayed">{i18next.t("usage:What Gateway 
relayed")}</TabsTrigger>
+          <TabsTrigger value="agents">{i18next.t("usage:Agent 
spend")}</TabsTrigger>
+          <TabsTrigger value="relayed">{i18next.t("usage:Relayed 
spend")}</TabsTrigger>
         </TabsList>
       </Tabs>
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to