diff --git a/src/app/(dashboard)/[workspaceId]/(main)/deployments/ethereum/execution-clients/[nodeName]/components/execution-client-node-stats.tsx b/src/app/(dashboard)/[workspaceId]/(main)/deployments/ethereum/execution-clients/[nodeName]/components/execution-client-node-stats.tsx index 026ca143..e95f81d5 100644 --- a/src/app/(dashboard)/[workspaceId]/(main)/deployments/ethereum/execution-clients/[nodeName]/components/execution-client-node-stats.tsx +++ b/src/app/(dashboard)/[workspaceId]/(main)/deployments/ethereum/execution-clients/[nodeName]/components/execution-client-node-stats.tsx @@ -99,7 +99,7 @@ export const ExecutionClientNodeStats: React.FC< - + {!("error" in data) && ( <> {!data.peersCount ? ( @@ -118,7 +118,7 @@ export const ExecutionClientNodeStats: React.FC< Peers - + {!("error" in data) && data.peersCount} diff --git a/src/app/(dashboard)/[workspaceId]/(main)/deployments/near/[nodeName]/components/danger-zone-tab.tsx b/src/app/(dashboard)/[workspaceId]/(main)/deployments/near/[nodeName]/components/danger-zone-tab.tsx new file mode 100644 index 00000000..5ae8be0c --- /dev/null +++ b/src/app/(dashboard)/[workspaceId]/(main)/deployments/near/[nodeName]/components/danger-zone-tab.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { useState } from "react"; +import { useParams, useRouter } from "next/navigation"; +import qs from "query-string"; + +import { AlertModal } from "@/components/modals/alert-modal"; +import { DeleteNodeForm } from "@/components/delete-node-form"; +import { useToast } from "@/components/ui/use-toast"; +import { TabsFooter } from "@/components/ui/tabs"; +import { Button } from "@/components/ui/button"; +import { NEARNode } from "@/types"; +import { client } from "@/lib/client-instance"; + +interface DangerZoneTabProps { + node: NEARNode; +} + +export const DangerZoneTab: React.FC = ({ node }) => { + const params = useParams(); + const router = useRouter(); + const { toast } = useToast(); + + const [open, setOpen] = useState(false); + + async function onDeleteNEARNode() { + const url = qs.stringifyUrl({ + url: `/near/nodes/${node.name}`, + query: { workspace_id: params.workspaceId }, + }); + await client.delete(url); + router.push(`/${params.workspaceId}/deployments/near`); + router.refresh(); + toast({ + title: "Near node has been deleted", + description: `${node.name} node has been deleted successfully.`, + }); + setOpen(false); + } + + return ( + <> +
+

+ By deleting this node, all connected apps will lose access to the + Blockchain Network. +

+

+ Node attached volume that persists Blockchain data will not be + removed, you need to delete it yourself. +

+

+ Are you sure you want to delete this node? +

+ + + +
+ + setOpen(false)} + title="Delete NEAR Node" + description={`This action cann't be undone. This will permnantly delete (${node.name}) NEAR Node.`} + > + + + + ); +}; diff --git a/src/app/(dashboard)/[workspaceId]/(main)/deployments/near/[nodeName]/components/near-node-stats.tsx b/src/app/(dashboard)/[workspaceId]/(main)/deployments/near/[nodeName]/components/near-node-stats.tsx new file mode 100644 index 00000000..7f645825 --- /dev/null +++ b/src/app/(dashboard)/[workspaceId]/(main)/deployments/near/[nodeName]/components/near-node-stats.tsx @@ -0,0 +1,150 @@ +"use client"; + +import useSWRSubscription from "swr/subscription"; +import type { SWRSubscription } from "swr/subscription"; +import { cx } from "class-variance-authority"; + +import { Skeleton } from "@/components/ui/skeleton"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { getWsBaseURL } from "@/lib/utils"; +import { AlertCircle, AlertTriangle, RefreshCw } from "lucide-react"; +import { BitcoinStats, NEARStats, StatsError } from "@/types"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +interface NEARNodeStatsProps { + nodeName: string; + token: string; + workspaceId: string; +} + +const WS_URL = getWsBaseURL(); + +export const NEARNodeStats: React.FC = ({ + nodeName, + token, + workspaceId, +}) => { + const subscription: SWRSubscription< + string, + NEARStats | StatsError, + string + > = (key, { next }) => { + const socket = new WebSocket(key); + socket.onmessage = (event: MessageEvent) => { + const stats = JSON.parse(event.data); + next(null, stats); + }; + socket.onerror = () => next("Connection Error (Can not access node stats)"); + + return () => socket.close(); + }; + const { data, error } = useSWRSubscription( + `${WS_URL}/near/nodes/${nodeName}/stats?authorization=Bearer ${token}&workspace_id=${workspaceId}`, + subscription + ); + + if (error) { + return ( + + + {error} + + + ); + } + + if (!data) + return ( + <> +
+ +
+
+ +
+ + ); + + return ( +
+
+ + + + Blocks + + + + + + + If block number doesn't change, it means node is not + syncing or syncing headers + + + + + + + {!("error" in data) && ( + <> + {data.syncing ? ( + + ) : ( + + )} + + {new Intl.NumberFormat("en-US").format( + +data.latestBlockHeight + )} + + + )} + + + + + + Peers + + + + + + Active peers / Max peers + + + + + + {!("error" in data) && ( + <> + {data.activePeersCount} / {data.maxPeersCount} + + )} + + +
+ {"error" in data && typeof data.error === "string" && ( +
+
+ )} +
+ ); +}; diff --git a/src/app/(dashboard)/[workspaceId]/(main)/deployments/near/[nodeName]/components/networking-tab.tsx b/src/app/(dashboard)/[workspaceId]/(main)/deployments/near/[nodeName]/components/networking-tab.tsx new file mode 100644 index 00000000..c22b39c5 --- /dev/null +++ b/src/app/(dashboard)/[workspaceId]/(main)/deployments/near/[nodeName]/components/networking-tab.tsx @@ -0,0 +1,258 @@ +"use client"; + +import * as z from "zod"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +import { isAxiosError } from "axios"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; + +import { client } from "@/lib/client-instance"; +import { NEARNode, Secret } from "@/types"; +import { Roles, SecretType } from "@/enums"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Button } from "@/components/ui/button"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { TabsFooter } from "@/components/ui/tabs"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { Input } from "@/components/ui/input"; + +interface NetWorkingTabProps { + node: NEARNode; + role: Roles; + secrets: Secret[]; +} + +const schema = z.object({ + nodePrivateKeySecretName: z.string().optional().default(""), + minPeers: z.coerce + .number({ invalid_type_error: "Minimum Peers is number" }) + .min(1, "Minimum Peers is greater than 0") + .optional() + .default(5), + p2pPort: z.coerce + .number({ invalid_type_error: "P2P Port is number" }) + .min(1, "P2P Port is between 1 and 65535") + .max(65535, "P2P Port is between 1 and 65535") + .optional() + .default(24567), + bootnodes: z + .string() + .transform((value) => + value ? value.split("\n").filter((value) => !!value) : [] + ) + .optional(), +}); + +type Schema = z.input; + +export const NetworkingTab: React.FC = ({ + node, + role, + secrets, +}) => { + const params = useParams(); + const { nodePrivateKeySecretName, minPeers, p2pPort, bootnodes } = node; + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + nodePrivateKeySecretName, + minPeers, + p2pPort, + bootnodes: bootnodes?.join("\n"), + }, + }); + + const { + formState: { + isSubmitted, + isSubmitting, + isValid, + isDirty, + isSubmitSuccessful, + errors, + }, + reset, + setError, + } = form; + + const onSubmit = async (values: Schema) => { + try { + const { data } = await client.put( + `/near/nodes/${node.name}`, + values + ); + const { nodePrivateKeySecretName, minPeers, p2pPort, bootnodes } = data; + reset({ + nodePrivateKeySecretName, + minPeers, + p2pPort, + bootnodes: bootnodes?.join("\n"), + }); + } catch (error) { + if (isAxiosError(error)) { + const { response } = error; + + setError("root", { + type: response?.status.toString(), + message: "Something went wrong.", + }); + } + } + }; + + return ( +
+ + ( + + Node Private Key +
+ + {field.value && ( + + )} +
+ +
+ )} + /> + + ( + + Minimum Peers + + + + + + )} + /> + + ( + + P2P Port + + + + + + )} + /> + + ( + + Boot Nodes + +