diff --git a/src/app/(dashboard)/dashboard/HomePageClient.tsx b/src/app/(dashboard)/dashboard/HomePageClient.tsx index e419e83b67..55fb234a7d 100644 --- a/src/app/(dashboard)/dashboard/HomePageClient.tsx +++ b/src/app/(dashboard)/dashboard/HomePageClient.tsx @@ -2,7 +2,7 @@ import { useTranslations } from "next-intl"; -import { useState, useEffect, useMemo, useCallback, useRef } from "react"; +import { useState, useEffect, useMemo, useCallback, useRef, Suspense } from "react"; import dynamic from "next/dynamic"; import Link from "next/link"; import { useRouter } from "next/navigation"; @@ -13,6 +13,7 @@ import { useNotificationStore } from "@/store/notificationStore"; import { copyToClipboard } from "@/shared/utils/clipboard"; const ProviderTopology = dynamic(() => import("../home/ProviderTopology"), { ssr: false }); +const ProviderLimits = dynamic(() => import("./usage/components/ProviderLimits"), { ssr: false }); import type { NewsAnnouncement } from "@/shared/utils/releaseNotes"; type UpdateStep = { @@ -95,6 +96,33 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { const [updateSteps, setUpdateSteps] = useState([]); const [updatePhase, setUpdatePhase] = useState<"idle" | "running" | "done" | "failed">("idle"); + // Appearance settings for home page pinning + const [pinProviderQuotaToHome, setPinProviderQuotaToHome] = useState(false); + const [showQuickStartOnHome, setShowQuickStartOnHome] = useState(true); // default on + const [showProviderTopologyOnHome, setShowProviderTopologyOnHome] = useState(true); // default on + + useEffect(() => { + // Fetch the pin settings (lightweight) + fetch("/api/settings") + .then((r) => (r.ok ? r.json() : {})) + .then((data) => { + if (data) { + if (typeof data.pinProviderQuotaToHome === "boolean") { + setPinProviderQuotaToHome(data.pinProviderQuotaToHome); + } + if (typeof data.showQuickStartOnHome === "boolean") { + setShowQuickStartOnHome(data.showQuickStartOnHome); + } + if (typeof data.showProviderTopologyOnHome === "boolean") { + setShowProviderTopologyOnHome(data.showProviderTopologyOnHome); + } + } + }) + .catch(() => { + /* ignore — defaults stay */ + }); + }, []); + useEffect(() => { if (typeof window !== "undefined") { setBaseUrl(`${window.location.origin}/v1`); @@ -721,22 +749,30 @@ export default function HomePageClient({ machineId }: HomePageClientProps) { )} - {/* Quick Start */} - -
-
-
-

{t("quickStart")}

-

{t("quickStartDesc")}

+ {/* Pinned Provider Quota Limits (compact, no filters) */} + {pinProviderQuotaToHome && ( + }> + + + )} + + {/* Quick Start (controlled by Appearance setting, default on) */} + {showQuickStartOnHome && ( + +
+
+
+

{t("quickStart")}

+

{t("quickStartDesc")}

+
+ + menu_book + {t("fullDocs")} +
- - menu_book - {t("fullDocs")} - -
  1. @@ -807,34 +843,37 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
+ )} - {/* Provider Topology */} - -
-
-

{t("providerTopology")}

-

- Connected providers routing through OmniRoute in real time -

+ {/* Provider Topology (controlled by Appearance setting, default on) */} + {showProviderTopologyOnHome && ( + +
+
+

{t("providerTopology")}

+

+ Connected providers routing through OmniRoute in real time +

+
+
+ + Active + + + Recent + + + Error + +
-
- - Active - - - Recent - - - Error - -
-
- p.total > 0) - .map((p) => ({ id: p.id, provider: p.id, name: p.provider.name }))} - /> - + p.total > 0) + .map((p) => ({ id: p.id, provider: p.id, name: p.provider.name }))} + /> + + )} {/* Provider Models Modal */} {selectedProvider && ( diff --git a/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx b/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx index 2b38ddbf79..1784b764d9 100644 --- a/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx @@ -19,6 +19,7 @@ import { normalizeHiddenSidebarItems, type HideableSidebarItemId, } from "@/shared/constants/sidebarVisibility"; +import { PIN_PROVIDER_QUOTA_TO_HOME_KEY } from "@/shared/constants/homeWidgets"; export default function AppearanceTab() { const { theme, setTheme, isDark } = useTheme(); @@ -40,6 +41,9 @@ export default function AppearanceTab() { const showCloudflaredTunnel = settings.hideEndpointCloudflaredTunnel !== true; const showTailscaleFunnel = settings.hideEndpointTailscaleFunnel !== true; const showNgrokTunnel = settings.hideEndpointNgrokTunnel !== true; + const pinProviderQuotaToHome = settings[PIN_PROVIDER_QUOTA_TO_HOME_KEY] === true; + const showQuickStartOnHome = settings.showQuickStartOnHome !== false; // default on + const showProviderTopologyOnHome = settings.showProviderTopologyOnHome !== false; // default on const getSettingsLabel = (key: string, fallback: string) => typeof t.has === "function" && t.has(key) ? t(key) : fallback; @@ -385,6 +389,83 @@ export default function AppearanceTab() {
+ {/* Pin Information to Home Page section */} +
+
+

+ {getSettingsLabel("pinProviderQuotaToHome", "Pin Information to Home Page")} +

+

+ Choose which sections to pin to the top of the Home page. +

+
+ + {/* Pinned options as a rounded table/list */} +
+
+ {/* Provider Quota Limits */} +
+
+

+ {getSettingsLabel("providerQuotaLimits", "Provider Quota Limits")} +

+

+ {getSettingsLabel( + "providerQuotaLimitsDesc", + "Pin the Provider Quota status container (with Refresh All button) to the top of the Home page." + )} +

+
+ updateSetting(PIN_PROVIDER_QUOTA_TO_HOME_KEY, !pinProviderQuotaToHome)} + disabled={loading} + /> +
+ + {/* Quick Start */} +
+
+

+ {getSettingsLabel("quickStart", "Quick Start")} +

+

+ {getSettingsLabel( + "quickStartDesc", + "Show the Quick Start panel on the Home page." + )} +

+
+ updateSetting("showQuickStartOnHome", !showQuickStartOnHome)} + disabled={loading} + /> +
+ + {/* Provider Topology */} +
+
+

+ {getSettingsLabel("providerTopology", "Provider Topology")} +

+

+ {getSettingsLabel( + "providerTopologyDesc", + "Show the Provider Topology on the Home page." + )} +

+
+ updateSetting("showProviderTopologyOnHome", !showProviderTopologyOnHome)} + disabled={loading} + /> +
+
+
+
+

{t("sidebarVisibilityToggle")}

diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 0537135b24..396cab6879 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -3886,6 +3886,13 @@ "systemTheme": "System Theme", "debugToggle": "Enable Debug Mode", "sidebarVisibilityToggle": "Show Sidebar Items", + "pinProviderQuotaToHome": "Pin Information to Home Page", + "providerQuotaLimits": "Provider Quota Limits", + "providerQuotaLimitsDesc": "Pin the Provider Quota status container (with Refresh All button) to the top of the Home page.", + "quickStart": "Quick Start", + "quickStartDesc": "Show the Quick Start panel on the Home page.", + "providerTopology": "Provider Topology", + "providerTopologyDesc": "Show the Provider Topology on the Home page.", "enableCache": "Enable Cache", "cacheTTL": "Cache TTL", "maxCacheSize": "Max Cache Size", diff --git a/src/shared/validation/schemas.ts b/src/shared/validation/schemas.ts index 11ea9c2be3..216f8e16b9 100644 --- a/src/shared/validation/schemas.ts +++ b/src/shared/validation/schemas.ts @@ -595,6 +595,9 @@ export const updateSettingsSchema = z.object({ hideEndpointCloudflaredTunnel: z.boolean().optional(), hideEndpointTailscaleFunnel: z.boolean().optional(), hideEndpointNgrokTunnel: z.boolean().optional(), + pinProviderQuotaToHome: z.boolean().optional(), + showQuickStartOnHome: z.boolean().optional(), + showProviderTopologyOnHome: z.boolean().optional(), bruteForceProtection: z.boolean().optional(), hiddenSidebarItems: z.array(z.enum(HIDEABLE_SIDEBAR_ITEM_IDS)).optional(), comboConfigMode: z.enum(COMBO_CONFIG_MODES).optional(), diff --git a/src/types/settings.ts b/src/types/settings.ts index 2559944868..3c70c4fcc0 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -24,6 +24,9 @@ export interface Settings { hideEndpointCloudflaredTunnel?: boolean; hideEndpointTailscaleFunnel?: boolean; hideEndpointNgrokTunnel?: boolean; + pinProviderQuotaToHome?: boolean; + showQuickStartOnHome?: boolean; + showProviderTopologyOnHome?: boolean; hiddenSidebarItems?: HideableSidebarItemId[]; resilienceSettings?: ResilienceSettings; // LOCAL_ONLY manage-scope bypass policy (DB-stored, hot-reloaded by