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
8 changes: 7 additions & 1 deletion admin-dashboard/app/admin/chains/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import Link from "next/link";
import { auth } from "@/auth";
import { ChainRegistryManager } from "@/components/chains/ChainRegistryManager";
import type { ChainRecord } from "@/components/chains/ChainRegistryManager";
import { HorizonLatencyGrid } from "@/components/dashboard/HorizonLatencyGrid";
import { getHorizonLatencyGridData } from "@/lib/horizon-monitor";

async function fetchChains(): Promise<ChainRecord[]> {
const serverUrl = process.env.FLUID_SERVER_URL?.trim().replace(/\/$/, "");
Expand All @@ -27,7 +29,10 @@ async function fetchChains(): Promise<ChainRecord[]> {

export default async function AdminChainsPage() {
const session = await auth();
const chains = await fetchChains();
const [chains, horizonHealth] = await Promise.all([
fetchChains(),
getHorizonLatencyGridData(),
]);

return (
<main className="min-h-screen bg-slate-100">
Expand Down Expand Up @@ -68,6 +73,7 @@ export default async function AdminChainsPage() {

{/* ── Content ── */}
<div className="mx-auto max-w-7xl space-y-6 px-4 py-6 sm:px-6 lg:px-8">
<HorizonLatencyGrid endpoints={horizonHealth} />
<ChainRegistryManager chains={chains} />
</div>
</main>
Expand Down
13 changes: 13 additions & 0 deletions admin-dashboard/app/admin/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"use client";

import { ErrorBoundaryScreen } from "@/components/feedback/ErrorBoundaryScreen";

export default function AdminError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return <ErrorBoundaryScreen error={error} reset={reset} scope="the admin dashboard" />;
}
13 changes: 13 additions & 0 deletions admin-dashboard/app/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"use client";

import { ErrorBoundaryScreen } from "@/components/feedback/ErrorBoundaryScreen";

export default function RootError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return <ErrorBoundaryScreen error={error} reset={reset} scope="the Fluid app" />;
}
44 changes: 24 additions & 20 deletions admin-dashboard/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";
import Image from "next/image";
import { NotificationBell } from "./dashboard/NotificationBell";
import { HelpCenter } from "./HelpCenter";
import { ThemeSwitcher } from "./theme/ThemeSwitcher";
import Link from "next/link";
import { usePathname } from "next/navigation";
import Image from "next/image";
import { useTranslations } from "next-intl";
import { NotificationBell } from "./dashboard/NotificationBell";
import { HelpCenter } from "./HelpCenter";
import { ThemeSwitcher } from "./theme/ThemeSwitcher";
import { LanguageSwitcher } from "@/i18n/LanguageSwitcher";

export function Navbar() {
const pathname = usePathname();
const t = useTranslations("navigation");
const isAdmin = Boolean(pathname?.startsWith("/admin"));
const badge =
pathname === "/" ? "Developer portal" : isAdmin ? "Admin" : null;
Expand All @@ -35,25 +38,26 @@ export function Navbar() {
href="/plugins"
className="hidden text-sm font-medium text-muted-foreground transition-colors hover:text-foreground sm:inline-block"
>
Plugins
{t("plugins")}
</Link>
<Link
href="/sdk"
className="hidden text-sm font-medium text-muted-foreground transition-colors hover:text-foreground sm:inline-block"
>
SDKs
{t("sdk")}
</Link>
<Link
href="/changelog"
className="hidden text-sm font-medium text-muted-foreground transition-colors hover:text-foreground sm:inline-block"
>
Changelog
</Link>
<ThemeSwitcher />
<HelpCenter />
{isAdmin && <NotificationBell />}
</nav>
</div>
</header>
<Link
href="/changelog"
className="hidden text-sm font-medium text-muted-foreground transition-colors hover:text-foreground sm:inline-block"
>
Changelog
</Link>
<ThemeSwitcher />
<LanguageSwitcher />
<HelpCenter />
{isAdmin && <NotificationBell />}
</nav>
</div>
</header>
);
}
80 changes: 80 additions & 0 deletions admin-dashboard/components/dashboard/HorizonLatencyGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { HorizonEndpointHealth } from "@/lib/horizon-monitor";

function statusTone(status: HorizonEndpointHealth["syncStatus"]) {
switch (status) {
case "synced":
return "border-emerald-200 bg-emerald-50 text-emerald-700";
case "degraded":
return "border-amber-200 bg-amber-50 text-amber-700";
default:
return "border-slate-200 bg-slate-100 text-slate-600";
}
}

function statusLabel(status: HorizonEndpointHealth["syncStatus"]) {
switch (status) {
case "synced":
return "Synced";
case "degraded":
return "Lagging";
default:
return "Offline";
}
}

function formatLatency(latencyMs: number | null) {
return latencyMs === null ? "—" : `${latencyMs} ms`;
}

export function HorizonLatencyGrid({ endpoints }: { endpoints: HorizonEndpointHealth[] }) {
return (
<section className="rounded-3xl border border-slate-200 bg-white p-5 shadow-sm">
<div className="flex flex-col gap-2 border-b border-slate-100 pb-4">
<p className="text-xs font-semibold uppercase tracking-[0.25em] text-sky-600">
Horizon Health
</p>
<h2 className="text-xl font-semibold text-slate-950">Horizon Node Live Latency Grid</h2>
<p className="text-sm text-slate-600">
Active Horizon URLs with ping latency and current synchronization status.
</p>
</div>

{endpoints.length > 0 ? (
<div className="mt-5 grid gap-4 lg:grid-cols-3">
{endpoints.map((endpoint) => (
<article key={endpoint.url} className="rounded-2xl border border-slate-200 bg-slate-50 p-4">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-sm font-semibold text-slate-950">{endpoint.label}</p>
<p className="mt-1 break-all text-xs text-slate-500">{endpoint.url}</p>
</div>
<span className={`rounded-full border px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] ${statusTone(endpoint.syncStatus)}`}>
{statusLabel(endpoint.syncStatus)}
</span>
</div>

<dl className="mt-4 grid gap-3 text-sm text-slate-600 sm:grid-cols-2">
<div className="rounded-xl bg-white px-3 py-2 shadow-sm">
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">Latency</dt>
<dd className="mt-1 font-semibold text-slate-950">{formatLatency(endpoint.latencyMs)}</dd>
</div>
<div className="rounded-xl bg-white px-3 py-2 shadow-sm">
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">Ping</dt>
<dd className="mt-1 font-semibold text-slate-950">{endpoint.online ? "Reachable" : "Unreachable"}</dd>
</div>
</dl>

<p className="mt-3 text-xs text-slate-500">
Last checked {new Date(endpoint.lastCheckedAt).toLocaleTimeString()}
</p>
</article>
))}
</div>
) : (
<div className="mt-5 rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-5 py-8 text-sm text-slate-500">
No Horizon endpoints are available.
</div>
)}
</section>
);
}
70 changes: 70 additions & 0 deletions admin-dashboard/components/feedback/ErrorBoundaryScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"use client";

import { useState } from "react";
import { AlertTriangle, RotateCcw } from "lucide-react";

interface ErrorBoundaryScreenProps {
error: Error & { digest?: string };
reset: () => void;
scope?: string;
}

export function ErrorBoundaryScreen({ error, reset, scope = "the page" }: ErrorBoundaryScreenProps) {
const [showDetails, setShowDetails] = useState(false);

return (
<main className="flex min-h-screen items-center justify-center bg-slate-950 px-4 py-10 text-white">
<section className="w-full max-w-2xl rounded-[2rem] border border-white/10 bg-white/5 p-6 shadow-2xl shadow-black/20 backdrop-blur">
<div className="flex items-start gap-4">
<div className="rounded-2xl border border-rose-400/30 bg-rose-500/10 p-3 text-rose-200">
<AlertTriangle className="h-6 w-6" aria-hidden="true" />
</div>

<div className="min-w-0 flex-1">
<p className="text-xs font-semibold uppercase tracking-[0.3em] text-rose-200/80">
Application error
</p>
<h1 className="mt-2 text-3xl font-semibold tracking-tight text-white">
Something went wrong in {scope}.
</h1>
<p className="mt-3 text-sm leading-6 text-slate-300">
The dashboard hit an unexpected React error. Retry the route after the issue is cleared,
or review the details below for debugging.
</p>
</div>
</div>

<div className="mt-6 flex flex-wrap gap-3">
<button
type="button"
onClick={reset}
className="inline-flex min-h-11 items-center gap-2 rounded-full bg-white px-5 text-sm font-semibold text-slate-950 transition hover:bg-slate-100"
>
<RotateCcw className="h-4 w-4" aria-hidden="true" />
Retry
</button>
<button
type="button"
onClick={() => setShowDetails((current) => !current)}
className="inline-flex min-h-11 items-center rounded-full border border-white/15 px-5 text-sm font-semibold text-white transition hover:bg-white/10"
>
{showDetails ? "Hide details" : "Show details"}
</button>
</div>

{showDetails ? (
<div className="mt-6 rounded-2xl border border-white/10 bg-slate-900/80 p-4 text-sm text-slate-200">
<p className="font-semibold text-white">Error details</p>
<p className="mt-2 break-words text-slate-300">{error.message}</p>
{error.digest ? <p className="mt-2 text-xs text-slate-400">Digest: {error.digest}</p> : null}
{error.stack ? (
<pre className="mt-4 overflow-x-auto whitespace-pre-wrap rounded-xl bg-black/30 p-3 text-xs leading-5 text-slate-200">
{error.stack}
</pre>
) : null}
</div>
) : null}
</section>
</main>
);
}
28 changes: 15 additions & 13 deletions admin-dashboard/components/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,25 @@ import { SessionTimeoutWarning } from "@/components/dashboard/SessionTimeoutWarn
import { RESOLVED_THEMES, THEME_STORAGE_KEY } from "@/lib/theme";
import { SessionProvider } from "next-auth/react";
import { ThemeProvider } from "next-themes";
import { I18nProvider } from "@/i18n/provider";
import { DashboardIntlProvider } from "@/i18n/provider";

export function Providers({ children }: { children: React.ReactNode }) {
return (
<SessionProvider>
<ThemeProvider
attribute="data-theme"
defaultTheme="system"
disableTransitionOnChange
enableSystem
storageKey={THEME_STORAGE_KEY}
themes={[...RESOLVED_THEMES]}
>
{children}
<AiSupportWidget />
<SessionTimeoutWarning />
</ThemeProvider>
<DashboardIntlProvider>
<ThemeProvider
attribute="data-theme"
defaultTheme="system"
disableTransitionOnChange
enableSystem
storageKey={THEME_STORAGE_KEY}
themes={[...RESOLVED_THEMES]}
>
{children}
<AiSupportWidget />
<SessionTimeoutWarning />
</ThemeProvider>
</DashboardIntlProvider>
</SessionProvider>
);
}
Loading
Loading