From 5c3545b0453398c08a62160aae5a8ea2520d256d Mon Sep 17 00:00:00 2001 From: Randi <55005611+rdself@users.noreply.github.com> Date: Mon, 8 Jun 2026 22:36:26 -0400 Subject: [PATCH] Add Endpoint Token Saver visibility setting (#3461) Integrated into release/v3.8.17 --- .../dashboard/endpoint/EndpointPageClient.tsx | 5 +- .../__tests__/EndpointPageClient.test.tsx | 73 ++++++++++++++++++- .../settings/components/AppearanceTab.tsx | 31 +++++++- .../components/proxy/GlobalConfigTab.tsx | 33 +++++---- src/i18n/messages/ar.json | 12 +++ src/i18n/messages/az.json | 12 +++ src/i18n/messages/bg.json | 12 +++ src/i18n/messages/bn.json | 12 +++ src/i18n/messages/cs.json | 12 +++ src/i18n/messages/da.json | 12 +++ src/i18n/messages/de.json | 12 +++ src/i18n/messages/en.json | 17 ++++- src/i18n/messages/es.json | 12 +++ src/i18n/messages/fa.json | 12 +++ src/i18n/messages/fi.json | 12 +++ src/i18n/messages/fr.json | 12 +++ src/i18n/messages/gu.json | 12 +++ src/i18n/messages/he.json | 12 +++ src/i18n/messages/hi.json | 12 +++ src/i18n/messages/hu.json | 12 +++ src/i18n/messages/id.json | 12 +++ src/i18n/messages/in.json | 12 +++ src/i18n/messages/it.json | 12 +++ src/i18n/messages/ja.json | 12 +++ src/i18n/messages/ko.json | 12 +++ src/i18n/messages/mr.json | 12 +++ src/i18n/messages/ms.json | 12 +++ src/i18n/messages/nl.json | 12 +++ src/i18n/messages/no.json | 12 +++ src/i18n/messages/phi.json | 12 +++ src/i18n/messages/pl.json | 12 +++ src/i18n/messages/pt-BR.json | 28 +++++-- src/i18n/messages/pt.json | 12 +++ src/i18n/messages/ro.json | 12 +++ src/i18n/messages/ru.json | 28 +++++-- src/i18n/messages/sk.json | 12 +++ src/i18n/messages/sv.json | 12 +++ src/i18n/messages/sw.json | 12 +++ src/i18n/messages/ta.json | 12 +++ src/i18n/messages/te.json | 12 +++ src/i18n/messages/th.json | 12 +++ src/i18n/messages/tr.json | 12 +++ src/i18n/messages/uk-UA.json | 12 +++ src/i18n/messages/ur.json | 12 +++ src/i18n/messages/vi.json | 12 +++ src/i18n/messages/zh-CN.json | 28 +++++-- src/shared/constants/homeWidgets.ts | 3 +- src/shared/validation/schemas.ts | 1 + src/shared/validation/settingsSchemas.ts | 1 + src/types/settings.ts | 1 + ...appearance-widget-settings-schema.test.ts} | 10 ++- 51 files changed, 669 insertions(+), 46 deletions(-) rename tests/unit/{home-page-pin-settings-schema.test.ts => appearance-widget-settings-schema.test.ts} (81%) diff --git a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx index 4feb02b230..2f12bc7c61 100644 --- a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx +++ b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx @@ -7,6 +7,7 @@ import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import { useDisplayBaseUrl } from "@/shared/hooks"; import { AI_PROVIDERS, getProviderByAlias } from "@/shared/constants/providers"; import { getProviderDisplayName } from "@/lib/display/names"; +import { SHOW_TOKEN_SAVER_ON_ENDPOINT_KEY } from "@/shared/constants/homeWidgets"; import { useTranslations } from "next-intl"; import A2ADashboardPage from "./components/A2ADashboard"; import McpDashboardPage from "./components/MCPDashboard"; @@ -186,6 +187,7 @@ export default function APIPageClient({ machineId }: Readonly(null); const [ngrokToken, setNgrokToken] = useState(""); const [showNgrokTunnel, setShowNgrokTunnel] = useState(true); + const [showTokenSaverOnEndpoint, setShowTokenSaverOnEndpoint] = useState(true); const [expandedTunnel, setExpandedTunnel] = useState(null); const [lanUrls, setLanUrls] = useState([]); const [tailscaleIpUrl, setTailscaleIpUrl] = useState(null); @@ -496,6 +498,7 @@ export default function APIPageClient({ machineId }: Readonly - + {showTokenSaverOnEndpoint && }
diff --git a/src/app/(dashboard)/dashboard/endpoint/__tests__/EndpointPageClient.test.tsx b/src/app/(dashboard)/dashboard/endpoint/__tests__/EndpointPageClient.test.tsx index acb0ccd5f5..91308a88df 100644 --- a/src/app/(dashboard)/dashboard/endpoint/__tests__/EndpointPageClient.test.tsx +++ b/src/app/(dashboard)/dashboard/endpoint/__tests__/EndpointPageClient.test.tsx @@ -33,6 +33,23 @@ function getRequestPath(input: RequestInfo | URL) { const cleanupCallbacks: Array<() => void> = []; +function installLocalStorageStub() { + const store = new Map(); + + vi.stubGlobal("localStorage", { + getItem: vi.fn((key: string) => store.get(key) ?? null), + setItem: vi.fn((key: string, value: string) => { + store.set(key, value); + }), + removeItem: vi.fn((key: string) => { + store.delete(key); + }), + clear: vi.fn(() => { + store.clear(); + }), + }); +} + async function waitForText(text: string) { const startedAt = Date.now(); while (!document.body.textContent?.includes(text)) { @@ -119,10 +136,14 @@ vi.mock("next-intl", () => ({ "endpoint.categoryMedia": "Media APIs", "endpoint.categoryUtility": "Utility APIs", "endpoint.vscodeAliasTitle": "VS Code Token Alias", - "endpoint.vscodeAliasDescriptionReady": "Ready-to-paste compatibility URLs using the /api/v1/vscode/{token}/... endpoint.", - "endpoint.vscodeAliasDescriptionError": "Showing placeholder URLs because CLI keys could not be loaded in this session.", - "endpoint.vscodeAliasDescriptionLoading": "Loading CLI keys. Placeholder URLs are shown until a key is available.", - "endpoint.vscodeAliasDescriptionPlaceholder": "Showing placeholder URLs. Create or activate an API key in CLI Tools to replace {token}.", + "endpoint.vscodeAliasDescriptionReady": + "Ready-to-paste compatibility URLs using the /api/v1/vscode/{token}/... endpoint.", + "endpoint.vscodeAliasDescriptionError": + "Showing placeholder URLs because CLI keys could not be loaded in this session.", + "endpoint.vscodeAliasDescriptionLoading": + "Loading CLI keys. Placeholder URLs are shown until a key is available.", + "endpoint.vscodeAliasDescriptionPlaceholder": + "Showing placeholder URLs. Create or activate an API key in CLI Tools to replace {token}.", "endpoint.vscodeAliasManage": "CLI Tools", "endpoint.vscodeAliasBaseLabel": "VS Code base", "endpoint.vscodeAliasModelsLabel": "VS Code models", @@ -157,6 +178,7 @@ describe("EndpointPageClient", () => { globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } ).IS_REACT_ACT_ENVIRONMENT = true; vi.stubGlobal("fetch", fetchMock); + installLocalStorageStub(); }); afterEach(() => { @@ -286,4 +308,47 @@ describe("EndpointPageClient", () => { expect(requestPaths.length).toBeGreaterThan(0); expect(requestPaths.every((path) => path === "/api/settings")).toBe(true); }); + + it("hides the Token Saver card when endpoint visibility is disabled", async () => { + fetchMock.mockImplementation((input: RequestInfo | URL) => { + const path = getRequestPath(input); + if (path === "/api/settings") { + return Promise.resolve( + jsonResponse({ + cloudEnabled: false, + cloudConfigured: false, + hideEndpointCloudflaredTunnel: true, + hideEndpointTailscaleFunnel: true, + hideEndpointNgrokTunnel: true, + showTokenSaverOnEndpoint: false, + }) + ); + } + if (path === "/v1/models") { + return Promise.resolve(jsonResponse({ data: [] })); + } + if (path === "/api/mcp/status") { + return Promise.resolve(jsonResponse({ online: false })); + } + if (path === "/api/a2a/status") { + return Promise.resolve(jsonResponse({ status: "ok", tasks: { activeStreams: 0 } })); + } + if (path === "/api/search/providers") { + return Promise.resolve(jsonResponse({ providers: [] })); + } + if (path === "/api/cli-tools/keys") { + return Promise.resolve(jsonResponse({ keys: [] })); + } + throw new Error(`Unexpected request: ${path}`); + }); + + renderEndpointPage(); + + await waitForText("Endpoint"); + await waitForText("0 models across"); + + const requestPaths = fetchMock.mock.calls.map(([input]) => getRequestPath(input)); + expect(document.body.textContent).not.toContain("Token Saver"); + expect(requestPaths).not.toContain("/api/settings/compression"); + }); }); diff --git a/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx b/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx index 5c41be21b8..f099c6333d 100644 --- a/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx @@ -13,7 +13,10 @@ import { normalizeComboConfigMode, type ComboConfigMode, } from "@/shared/constants/comboConfigMode"; -import { PIN_PROVIDER_QUOTA_TO_HOME_KEY } from "@/shared/constants/homeWidgets"; +import { + PIN_PROVIDER_QUOTA_TO_HOME_KEY, + SHOW_TOKEN_SAVER_ON_ENDPOINT_KEY, +} from "@/shared/constants/homeWidgets"; export default function AppearanceTab() { const { theme, setTheme, isDark } = useTheme(); @@ -38,8 +41,11 @@ export default function AppearanceTab() { const pinProviderQuotaToHome = settings.pinProviderQuotaToHome === true; const showQuickStartOnHome = settings.showQuickStartOnHome !== false; const showProviderTopologyOnHome = settings.showProviderTopologyOnHome !== false; + const showTokenSaverOnEndpoint = settings[SHOW_TOKEN_SAVER_ON_ENDPOINT_KEY] !== false; const autoRefreshProviderQuota = settings.autoRefreshProviderQuota === true; - const autoRefreshProviderQuotaInterval = Number.isFinite(settings.autoRefreshProviderQuotaInterval) + const autoRefreshProviderQuotaInterval = Number.isFinite( + settings.autoRefreshProviderQuotaInterval + ) ? Number(settings.autoRefreshProviderQuotaInterval) : 180; const comboConfigMode = normalizeComboConfigMode(settings[COMBO_CONFIG_MODE_SETTING_KEY]); @@ -254,6 +260,27 @@ export default function AppearanceTab() { disabled={loading} />
+ +
+
+

+ {getSettingsLabel("endpointTokenSaver", "Token Saver")} +

+

+ {getSettingsLabel( + "endpointTokenSaverDesc", + "Show the Token Saver panel on the Endpoint page." + )} +

+
+ { + await updateSetting(SHOW_TOKEN_SAVER_ON_ENDPOINT_KEY, checked); + }} + disabled={loading} + /> +
diff --git a/src/app/(dashboard)/dashboard/settings/components/proxy/GlobalConfigTab.tsx b/src/app/(dashboard)/dashboard/settings/components/proxy/GlobalConfigTab.tsx index a588dce7cb..7197cccad9 100644 --- a/src/app/(dashboard)/dashboard/settings/components/proxy/GlobalConfigTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/proxy/GlobalConfigTab.tsx @@ -147,7 +147,10 @@ export default function GlobalConfigTab() {
-