Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 81 additions & 42 deletions src/app/(dashboard)/dashboard/HomePageClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 = {
Expand Down Expand Up @@ -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`);
Expand Down Expand Up @@ -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">
Expand Down Expand Up @@ -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>
</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>
{/* 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>
<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 && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
Expand Down Expand Up @@ -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>
Expand Down
7 changes: 7 additions & 0 deletions src/i18n/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions src/shared/validation/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
3 changes: 3 additions & 0 deletions src/types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down