feat(home): Add Home page customization options for experienced users (#2531)

Integrated into release/v3.8.2
This commit is contained in:
Apostol Apostolov
2026-05-22 05:15:48 +03:00
committed by GitHub
parent 363ecc4024
commit c2d4d6755c
5 changed files with 175 additions and 42 deletions

View File

@@ -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<UpdateStep[]>([]);
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) {
</div>
)}
{/* Quick Start */}
<Card>
<div className="flex flex-col gap-5">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold">{t("quickStart")}</h2>
<p className="text-sm text-text-muted">{t("quickStartDesc")}</p>
{/* Pinned Provider Quota Limits (compact, no filters) */}
{pinProviderQuotaToHome && (
<Suspense fallback={<CardSkeleton />}>
<ProviderLimits showFilters={false} />
</Suspense>
)}
{/* Quick Start (controlled by Appearance setting, default on) */}
{showQuickStartOnHome && (
<Card>
<div className="flex flex-col gap-5">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold">{t("quickStart")}</h2>
<p className="text-sm text-text-muted">{t("quickStartDesc")}</p>
</div>
<Link
href="/docs"
className="hidden sm:inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border border-border text-text-muted hover:text-text-main hover:bg-bg-subtle transition-colors"
>
<span className="material-symbols-outlined text-[14px]">menu_book</span>
{t("fullDocs")}
</Link>
</div>
<Link
href="/docs"
className="hidden sm:inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border border-border text-text-muted hover:text-text-main hover:bg-bg-subtle transition-colors"
>
<span className="material-symbols-outlined text-[14px]">menu_book</span>
{t("fullDocs")}
</Link>
</div>
<ol className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
<li className="rounded-lg border border-border bg-bg-subtle p-4 flex gap-3">
@@ -807,34 +843,37 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
</ol>
</div>
</Card>
)}
{/* Provider Topology */}
<Card>
<div className="flex items-center justify-between mb-3">
<div>
<h2 className="text-base font-semibold">{t("providerTopology")}</h2>
<p className="text-xs text-text-muted">
Connected providers routing through OmniRoute in real time
</p>
{/* Provider Topology (controlled by Appearance setting, default on) */}
{showProviderTopologyOnHome && (
<Card>
<div className="flex items-center justify-between mb-3">
<div>
<h2 className="text-base font-semibold">{t("providerTopology")}</h2>
<p className="text-xs text-text-muted">
Connected providers routing through OmniRoute in real time
</p>
</div>
<div className="flex items-center gap-3 text-[11px] text-text-muted">
<span className="flex items-center gap-1.5">
<span className="size-2 rounded-full bg-green-500" /> Active
</span>
<span className="flex items-center gap-1.5">
<span className="size-2 rounded-full bg-amber-500" /> Recent
</span>
<span className="flex items-center gap-1.5">
<span className="size-2 rounded-full bg-red-500" /> Error
</span>
</div>
</div>
<div className="flex items-center gap-3 text-[11px] text-text-muted">
<span className="flex items-center gap-1.5">
<span className="size-2 rounded-full bg-green-500" /> Active
</span>
<span className="flex items-center gap-1.5">
<span className="size-2 rounded-full bg-amber-500" /> Recent
</span>
<span className="flex items-center gap-1.5">
<span className="size-2 rounded-full bg-red-500" /> Error
</span>
</div>
</div>
<ProviderTopology
providers={providerStats
.filter((p) => p.total > 0)
.map((p) => ({ id: p.id, provider: p.id, name: p.provider.name }))}
/>
</Card>
<ProviderTopology
providers={providerStats
.filter((p) => p.total > 0)
.map((p) => ({ id: p.id, provider: p.id, name: p.provider.name }))}
/>
</Card>
)}
{/* Provider Models Modal */}
{selectedProvider && (

View File

@@ -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() {
</div>
</div>
{/* Pin Information to Home Page section */}
<div className="pt-4 border-t border-border">
<div className="mb-3">
<p className="font-medium">
{getSettingsLabel("pinProviderQuotaToHome", "Pin Information to Home Page")}
</p>
<p className="text-sm text-text-muted">
Choose which sections to pin to the top of the Home page.
</p>
</div>
{/* Pinned options as a rounded table/list */}
<div className="rounded-lg border border-border bg-surface/40 overflow-hidden">
<div className="divide-y divide-border/70">
{/* Provider Quota Limits */}
<div className="flex items-start justify-between px-4 py-3">
<div>
<p className="font-medium">
{getSettingsLabel("providerQuotaLimits", "Provider Quota Limits")}
</p>
<p className="text-sm text-text-muted">
{getSettingsLabel(
"providerQuotaLimitsDesc",
"Pin the Provider Quota status container (with Refresh All button) to the top of the Home page."
)}
</p>
</div>
<Toggle
checked={pinProviderQuotaToHome}
onChange={() => updateSetting(PIN_PROVIDER_QUOTA_TO_HOME_KEY, !pinProviderQuotaToHome)}
disabled={loading}
/>
</div>
{/* Quick Start */}
<div className="flex items-start justify-between px-4 py-3">
<div>
<p className="font-medium">
{getSettingsLabel("quickStart", "Quick Start")}
</p>
<p className="text-sm text-text-muted">
{getSettingsLabel(
"quickStartDesc",
"Show the Quick Start panel on the Home page."
)}
</p>
</div>
<Toggle
checked={showQuickStartOnHome}
onChange={() => updateSetting("showQuickStartOnHome", !showQuickStartOnHome)}
disabled={loading}
/>
</div>
{/* Provider Topology */}
<div className="flex items-start justify-between px-4 py-3">
<div>
<p className="font-medium">
{getSettingsLabel("providerTopology", "Provider Topology")}
</p>
<p className="text-sm text-text-muted">
{getSettingsLabel(
"providerTopologyDesc",
"Show the Provider Topology on the Home page."
)}
</p>
</div>
<Toggle
checked={showProviderTopologyOnHome}
onChange={() => updateSetting("showProviderTopologyOnHome", !showProviderTopologyOnHome)}
disabled={loading}
/>
</div>
</div>
</div>
</div>
<div className="pt-4 border-t border-border">
<div className="mb-3">
<p className="font-medium">{t("sidebarVisibilityToggle")}</p>

View File

@@ -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",

View File

@@ -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(),

View File

@@ -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