Skip to content

Commit

Permalink
update dashboard (#3)
Browse files Browse the repository at this point in the history
* update

* update
  • Loading branch information
qiweiii authored Oct 23, 2024
1 parent 68c236d commit a241544
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 99 deletions.
62 changes: 34 additions & 28 deletions components/state.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
"use client";

import { useState, useCallback } from "react";
import { useState, useEffect, useCallback } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { connectToNode, sendRequest, disconnectFromNode } from "@/lib/ws";
import { toast } from "sonner";
import { useEffect } from "react";

type KeyValuePair = {
key: string;
value: string;
blockHash: string;
stateRoot: string;
};

export default function State({ endpoint }: { endpoint: string }) {
const [keyInput, setKeyInput] = useState("");
const [keyValuePairs, setKeyValuePairs] = useState<KeyValuePair[]>([]);
const [hashInput, setHashInput] = useState("");
const [stateRoots, setStateRoots] = useState<KeyValuePair[]>([]);

useEffect(() => {
connectToNode(endpoint).catch((error: unknown) =>
Expand All @@ -30,52 +29,59 @@ export default function State({ endpoint }: { endpoint: string }) {
};
}, [endpoint]);

const fetchKeyValue = useCallback(async () => {
if (keyInput.length !== 66 || !keyInput.startsWith("0x")) {
toast.error("Key must be a valid hex string `0x{string}`");
const fetchState = useCallback(async () => {
if (hashInput.length !== 66 && hashInput.length !== 0) {
toast.error(
"Hash must be a valid 32 byte hex string `0x{string}` or empty for best block"
);
return;
}

try {
const result = await sendRequest(endpoint, "chain_getState", {
key: keyInput,
});
setKeyValuePairs((prev) => [
{ key: keyInput, value: result as string },
const method = "chain_getState";
const params = hashInput ? { hash: hashInput } : {};
const result = (await sendRequest(endpoint, method, params)) as {
[key: string]: string;
};
setStateRoots((prev) => [
{
blockHash: result?.blockHash,
stateRoot: result?.stateRoot,
},
...prev,
]);
setKeyInput("");
setHashInput("");
} catch (error: unknown) {
toast.error(
`Failed to fetch key value: ${
`Failed to fetch state root: ${
error instanceof Error ? error.message : "Unknown error"
}`
);
}
}, [endpoint, keyInput]);
}, [endpoint, hashInput]);

return (
<div className="w-full space-y-4">
<div>
<h3 className="text-lg font-semibold">Fetch Value</h3>
<h3 className="text-lg font-semibold">Fetch State</h3>
<div className="flex space-x-2">
<Input
type="text"
placeholder="0x... (enter a 32 byte key)"
value={keyInput}
onChange={(e) => setKeyInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && fetchKeyValue()}
placeholder="0x... (optional 32 byte block hash)"
value={hashInput}
onChange={(e) => setHashInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && fetchState()}
/>
<Button onClick={fetchKeyValue}>Fetch</Button>
<Button onClick={fetchState}>Fetch</Button>
</div>
</div>
{keyValuePairs.map((pair, index) => (
{stateRoots.map((pair, index) => (
<div key={index}>
<h3 className="text-md font-semibold">Key</h3>
<p className="font-mono break-all">{pair.key}</p>
<h3 className="text-md font-semibold mt-2">Value</h3>
<h3 className="text-md font-semibold">Block Hash</h3>
<p className="font-mono break-all">{pair.blockHash}</p>
<h3 className="text-md font-semibold mt-2">State Root</h3>
<p className="font-mono break-all">
{pair.value || "No value found"}
{pair.stateRoot || "No state root found"}
</p>
</div>
))}
Expand Down
188 changes: 125 additions & 63 deletions components/telemetry-dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState, useEffect, useCallback } from "react";
import { useState, useEffect, useCallback, useRef } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
Expand All @@ -17,10 +17,9 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Hash, Cpu, Blocks, Users, X } from "lucide-react";
import { Hash, Cpu, Blocks, Users, X, RefreshCw } from "lucide-react";
import { connectToNode, sendRequest, disconnectFromNode } from "@/lib/ws";
import { toast } from "sonner";
import { useRouter } from "next/navigation";

type NodeInfo = {
endpoint: string;
Expand All @@ -36,7 +35,94 @@ const STORAGE_KEY = "telemetry-endpoints";
export default function TelemetryDashboard() {
const [rpcInput, setRpcInput] = useState("");
const [nodeInfo, setNodeInfo] = useState<NodeInfo[]>([]);
const router = useRouter();
const intervalsRef = useRef<{ [key: string]: NodeJS.Timeout }>({});

const setNodeConnected = useCallback(
(endpoint: string, connected: boolean) => {
setNodeInfo((prev) =>
prev.map((node) =>
node.endpoint === endpoint ? { ...node, connected } : node
)
);
},
[]
);

const updateNodeInfo = useCallback(
async (endpoint: string) => {
try {
const data = await sendRequest(endpoint, "telemetry_getUpdate");
setNodeInfo((prev) => {
const index = prev.findIndex((node) => node.endpoint === endpoint);
if (index !== -1) {
const newNodeInfo = [...prev];
newNodeInfo[index] = {
...newNodeInfo[index],
endpoint,
...(data as Partial<NodeInfo>),
connected: true,
};
return newNodeInfo;
} else {
return [
...prev,
{ endpoint, ...(data as Partial<NodeInfo>), connected: true },
];
}
});
} catch (error: unknown) {
setNodeConnected(endpoint, false);
toast.error(
`Failed to update info for ${endpoint}: ${
error instanceof Error ? error.message : "Unknown error"
}`
);
}
},
[setNodeConnected]
);

const connectAndSubscribe = useCallback(
(endpoint: string) => {
const connect = async () => {
try {
await connectToNode(endpoint);
setNodeConnected(endpoint, true);
await updateNodeInfo(endpoint);
} catch (error: unknown) {
setNodeConnected(endpoint, false);
toast.error(
`Failed to connect to ${endpoint}: ${
error instanceof Error ? error.message : "Unknown error"
}`
);
}
};

// Initial connection attempt
connect();

// Set up interval for periodic updates and reconnection attempts
const interval = setInterval(async () => {
if (!nodeInfo.find((node) => node.endpoint === endpoint)?.connected) {
// If not connected, try to reconnect
await connect();
} else {
// If connected, update node info
await updateNodeInfo(endpoint);
}
}, 4000);

intervalsRef.current[endpoint] = interval;

return () => {
clearInterval(interval);
delete intervalsRef.current[endpoint];
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[setNodeConnected, updateNodeInfo]
);

useEffect(() => {
const loadSavedEndpoints = () => {
Expand All @@ -59,60 +145,12 @@ export default function TelemetryDashboard() {
loadSavedEndpoints();

return () => {
const currentIntervals = intervalsRef.current;
Object.values(currentIntervals).forEach(clearInterval);
nodeInfo.forEach((node) => disconnectFromNode(node.endpoint));
};
}, []);

const updateNodeInfo = useCallback(async (endpoint: string) => {
try {
const data = await sendRequest(endpoint, "telemetry_getUpdate");
setNodeInfo((prev) => {
const index = prev.findIndex((node) => node.endpoint === endpoint);
if (index !== -1) {
const newNodeInfo = [...prev];
newNodeInfo[index] = {
...newNodeInfo[index],
endpoint,
...(data as Partial<NodeInfo>),
connected: true,
};
return newNodeInfo;
} else {
return [
...prev,
{ endpoint, ...(data as Partial<NodeInfo>), connected: true },
];
}
});
} catch (error: unknown) {
toast.error(
`Failed to update info for ${endpoint}: ${
error instanceof Error ? error.message : "Unknown error"
}`
);
}
}, []);

const connectAndSubscribe = useCallback(
async (endpoint: string) => {
try {
await connectToNode(endpoint);
updateNodeInfo(endpoint);
setNodeInfo((prev) =>
prev.map((node) =>
node.endpoint === endpoint ? { ...node, connected: true } : node
)
);
} catch (error: unknown) {
toast.error(
`Failed to connect to ${endpoint}: ${
error instanceof Error ? error.message : "Unknown error"
}`
);
}
},
[updateNodeInfo]
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [connectAndSubscribe]);

const validateUrl = (url: string) => {
const pattern = /^(wss?:\/\/).+$/;
Expand Down Expand Up @@ -152,14 +190,28 @@ export default function TelemetryDashboard() {
(savedEndpoint: string) => savedEndpoint !== endpoint
);
localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedEndpoints));

if (intervalsRef.current[endpoint]) {
clearInterval(intervalsRef.current[endpoint]);
delete intervalsRef.current[endpoint];
}
}, []);

const handleRowClick = useCallback(
(endpoint: string) => {
router.push(`/node?endpoint=${encodeURIComponent(endpoint)}`);
},
[router]
);
const refreshAll = useCallback(() => {
// Disconnect from all endpoints
nodeInfo.forEach((node) => {
disconnectFromNode(node.endpoint);
if (intervalsRef.current[node.endpoint]) {
clearInterval(intervalsRef.current[node.endpoint]);
delete intervalsRef.current[node.endpoint];
}
});

// Reconnect to all endpoints
nodeInfo.forEach((node) => {
connectAndSubscribe(node.endpoint);
});
}, [nodeInfo, connectAndSubscribe]);

return (
<div className="w-full h-full flex flex-col gap-4">
Expand All @@ -174,6 +226,9 @@ export default function TelemetryDashboard() {
className="flex-grow"
/>
<Button onClick={addRpc}>Add RPC</Button>
<Button onClick={refreshAll}>
<RefreshCw className="h-4 w-4" />
</Button>
</div>
<div className="overflow-x-auto min-h-[300px]">
<Table>
Expand Down Expand Up @@ -218,7 +273,14 @@ export default function TelemetryDashboard() {
? ""
: "bg-red-100 dark:bg-red-500 text-black dark:text-white"
}`}
onClick={() => handleRowClick(node.endpoint)}
onClick={() =>
window.open(
`jam-dashboard/node/?endpoint=${encodeURIComponent(
node.endpoint
)}`,
"_blank"
)
}
>
<TableCell className="font-medium truncate max-w-[150px]">
{node.name || "-"}
Expand Down
Loading

0 comments on commit a241544

Please sign in to comment.