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
26 changes: 19 additions & 7 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Suspense } from "react";
import type { Metadata } from "next";
import localFont from "next/font/local";
import Link from "next/link";
import Image from "next/image";
import { ThemeProvider } from "@/components/theme-provider";
import { ModeToggle } from "@/components/mode-toggle";
import { Toaster } from "@/components/ui/sonner";
import Link from "next/link";
import Image from "next/image";
import "./globals.css";
import { DynamicBreadcrumb } from "@/components/dynamic-breadcrumb";
import { Loader } from "lucide-react";
import "./globals.css";

const geistSans = localFont({
src: "./fonts/GeistVF.woff",
Expand All @@ -20,8 +22,8 @@ const geistMono = localFont({
});

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "JAM Dashboard",
description: "UI for monitoring, testing and interacting with JAM Nodes.",
};

export default function RootLayout({
Expand Down Expand Up @@ -57,9 +59,19 @@ export default function RootLayout({
<DynamicBreadcrumb />
</div>

<main>{children}</main>
<main>
<Suspense
fallback={
<div className="flex justify-center items-center h-64">
<Loader className="w-12 h-12 animate-spin text-gray-600" />
</div>
}
>
{children}
</Suspense>
</main>

<Toaster />
<Toaster richColors />

<footer className="border-t">
<div className="container flex items-center justify-center h-14">
Expand Down
1 change: 0 additions & 1 deletion components/dynamic-breadcrumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export function DynamicBreadcrumb() {
</BreadcrumbLink>
</BreadcrumbItem>
{pathSegments.map((segment, index) => {
console.log(segment);
const href = `/${pathSegments.slice(0, index + 1).join("/")}`;
const isLast = index === pathSegments.length - 1;
const label = routeLabels[segment] || segment;
Expand Down
45 changes: 34 additions & 11 deletions components/telemetry-dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type NodeInfo = {
chainHead?: number | null;
blockHash?: string | null;
peerCount?: number | null;
connected: boolean;
};

const STORAGE_KEY = "telemetry-endpoints";
Expand All @@ -41,8 +42,15 @@ export default function TelemetryDashboard() {
const loadSavedEndpoints = () => {
const savedEndpoints = localStorage.getItem(STORAGE_KEY);
if (savedEndpoints) {
const endpoints = JSON.parse(savedEndpoints);
endpoints.forEach((endpoint: string) => {
const endpoints: string[] = JSON.parse(savedEndpoints);
setNodeInfo(
endpoints.map((endpoint) => ({
endpoint,
connected: false,
}))
);

endpoints.forEach((endpoint) => {
connectAndSubscribe(endpoint);
});
}
Expand All @@ -53,7 +61,6 @@ export default function TelemetryDashboard() {
return () => {
nodeInfo.forEach((node) => disconnectFromNode(node.endpoint));
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const updateNodeInfo = useCallback(async (endpoint: string) => {
Expand All @@ -63,10 +70,18 @@ export default function TelemetryDashboard() {
const index = prev.findIndex((node) => node.endpoint === endpoint);
if (index !== -1) {
const newNodeInfo = [...prev];
newNodeInfo[index] = { endpoint, ...(data as Partial<NodeInfo>) };
newNodeInfo[index] = {
...newNodeInfo[index],
endpoint,
...(data as Partial<NodeInfo>),
connected: true,
};
return newNodeInfo;
} else {
return [...prev, { endpoint, ...(data as Partial<NodeInfo>) }];
return [
...prev,
{ endpoint, ...(data as Partial<NodeInfo>), connected: true },
];
}
});
} catch (error: unknown) {
Expand All @@ -78,14 +93,16 @@ export default function TelemetryDashboard() {
}
}, []);

// TODO: use real ws subscription if available
const connectAndSubscribe = useCallback(
async (endpoint: string) => {
try {
await connectToNode(endpoint);
updateNodeInfo(endpoint);
const interval = setInterval(() => updateNodeInfo(endpoint), 3000);
return () => clearInterval(interval);
setNodeInfo((prev) =>
prev.map((node) =>
node.endpoint === endpoint ? { ...node, connected: true } : node
)
);
} catch (error: unknown) {
toast.error(
`Failed to connect to ${endpoint}: ${
Expand All @@ -109,10 +126,13 @@ export default function TelemetryDashboard() {
}

if (rpcInput && !nodeInfo.some((node) => node.endpoint === rpcInput)) {
setNodeInfo((prev) => [
...prev,
{ endpoint: rpcInput, connected: false },
]); // Add as disconnected
connectAndSubscribe(rpcInput);
setRpcInput("");

// Save to localStorage
const savedEndpoints = JSON.parse(
localStorage.getItem(STORAGE_KEY) || "[]"
);
Expand All @@ -125,7 +145,6 @@ export default function TelemetryDashboard() {
disconnectFromNode(endpoint);
setNodeInfo((prev) => prev.filter((node) => node.endpoint !== endpoint));

// Remove from localStorage
const savedEndpoints = JSON.parse(
localStorage.getItem(STORAGE_KEY) || "[]"
);
Expand Down Expand Up @@ -194,7 +213,11 @@ export default function TelemetryDashboard() {
nodeInfo.map((node, index) => (
<TableRow
key={index}
className="cursor-pointer hover:bg-muted/50"
className={`cursor-pointer hover:bg-muted/50 ${
node.connected
? ""
: "bg-red-100 dark:bg-red-500 text-black dark:text-white"
}`}
onClick={() => handleRowClick(node.endpoint)}
>
<TableCell className="font-medium truncate max-w-[150px]">
Expand Down
2 changes: 1 addition & 1 deletion lib/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export function connectToNode(url: string): Promise<void> {
};
ws.onerror = (error) => {
console.error(`WebSocket error for ${url}:`, error);
reject(error);
reject(new Error(`WebSocket error`));
};

ws.onclose = () => {
Expand Down