mirror of
https://github.com/woodchen-ink/nezha-dash-v1.git
synced 2025-07-18 09:31:55 +08:00
feat: init
This commit is contained in:
parent
e5682aacbd
commit
65902d5385
2
.github/workflows/Build.yml
vendored
2
.github/workflows/Build.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
- name: Build static export
|
||||
run: |
|
||||
bun run build --base=/dashboard
|
||||
bun run build
|
||||
|
||||
- name: Compress dist folder
|
||||
run: zip -r dist.zip dist
|
||||
|
@ -11,12 +11,7 @@ const App: React.FC = () => {
|
||||
<main className="flex min-h-[calc(100vh-calc(var(--spacing)*16))] flex-1 flex-col gap-4 bg-background p-4 md:p-10 md:pt-8">
|
||||
<Header />
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<Server />
|
||||
}
|
||||
/>
|
||||
<Route path="/" element={<Server />} />
|
||||
</Routes>
|
||||
<Footer />
|
||||
</main>
|
||||
|
@ -8,13 +8,10 @@ import { DateTime } from "luxon";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
function Header() {
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-5xl">
|
||||
<section className="flex items-center justify-between">
|
||||
<section
|
||||
className="flex items-center text-base font-medium"
|
||||
>
|
||||
<section className="flex items-center text-base font-medium">
|
||||
<div className="mr-1 flex flex-row items-center justify-start">
|
||||
<img
|
||||
width={40}
|
||||
@ -74,9 +71,7 @@ function Overview() {
|
||||
<section className={"mt-10 flex flex-col md:mt-16"}>
|
||||
<p className="text-base font-semibold">👋 Overview</p>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<p className="text-sm font-medium opacity-50">
|
||||
where the time is
|
||||
</p>
|
||||
<p className="text-sm font-medium opacity-50">where the time is</p>
|
||||
{mouted ? (
|
||||
<p className="text-sm font-medium">{timeString}</p>
|
||||
) : (
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { Theme } from "@/components/ThemeProvider";
|
||||
import { useTheme } from "../hooks/use-theme";
|
||||
@ -40,21 +39,18 @@ export function ModeToggle() {
|
||||
onSelect={(e) => handleSelect(e, "light")}
|
||||
>
|
||||
Light
|
||||
{theme === "light" && <CheckCircleIcon className="size-4" />}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className={cn({ "gap-3 bg-muted": theme === "dark" })}
|
||||
onSelect={(e) => handleSelect(e, "dark")}
|
||||
>
|
||||
Dark
|
||||
{theme === "dark" && <CheckCircleIcon className="size-4" />}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className={cn({ "gap-3 bg-muted": theme === "system" })}
|
||||
onSelect={(e) => handleSelect(e, "system")}
|
||||
>
|
||||
System
|
||||
{theme === "system" && <CheckCircleIcon className="size-4" />}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
import * as React from "react";
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
@ -9,7 +9,7 @@ const Separator = React.forwardRef<
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
ref,
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
@ -18,12 +18,12 @@ const Separator = React.forwardRef<
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
),
|
||||
);
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
|
||||
export { Separator }
|
||||
export { Separator };
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
@ -9,7 +9,7 @@ function Skeleton({
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
export { Skeleton };
|
||||
|
@ -1,89 +1,89 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
|
||||
export interface WebSocketHook {
|
||||
socket: WebSocket | null
|
||||
connected: boolean
|
||||
onlineCount: number
|
||||
message: string | null
|
||||
sendMessage: (msg: string) => void
|
||||
socket: WebSocket | null;
|
||||
connected: boolean;
|
||||
onlineCount: number;
|
||||
message: string | null;
|
||||
sendMessage: (msg: string) => void;
|
||||
}
|
||||
|
||||
export default function useWebSocket(url: string): WebSocketHook {
|
||||
const [socket, setSocket] = useState<WebSocket | null>(null)
|
||||
const [message, setMessage] = useState<string | null>(null)
|
||||
const [connected, setConnected] = useState<boolean>(false)
|
||||
const [onlineCount, setOnlineCount] = useState<number>(0)
|
||||
const socketRef = useRef<WebSocket | null>(null)
|
||||
const reconnectAttempts = useRef<number>(0)
|
||||
const reconnectTimeout = useRef<NodeJS.Timeout | null>(null)
|
||||
const isUnmounted = useRef<boolean>(false)
|
||||
const [socket, setSocket] = useState<WebSocket | null>(null);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [connected, setConnected] = useState<boolean>(false);
|
||||
const [onlineCount, setOnlineCount] = useState<number>(0);
|
||||
const socketRef = useRef<WebSocket | null>(null);
|
||||
const reconnectAttempts = useRef<number>(0);
|
||||
const reconnectTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const isUnmounted = useRef<boolean>(false);
|
||||
|
||||
const connect = useCallback(() => {
|
||||
if (isUnmounted.current) return
|
||||
if (isUnmounted.current) return;
|
||||
|
||||
const ws = new WebSocket(url)
|
||||
setSocket(ws)
|
||||
socketRef.current = ws
|
||||
const ws = new WebSocket(url);
|
||||
setSocket(ws);
|
||||
socketRef.current = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
setConnected(true)
|
||||
reconnectAttempts.current = 0
|
||||
}
|
||||
setConnected(true);
|
||||
reconnectAttempts.current = 0;
|
||||
};
|
||||
|
||||
ws.onmessage = (event: MessageEvent) => {
|
||||
setMessage(event.data)
|
||||
const msgJson = JSON.parse(event.data)
|
||||
if (msgJson.type === 'live') {
|
||||
setOnlineCount(msgJson.data.count)
|
||||
}
|
||||
setMessage(event.data);
|
||||
const msgJson = JSON.parse(event.data);
|
||||
if (msgJson.type === "live") {
|
||||
setOnlineCount(msgJson.data.count);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket Error:', error)
|
||||
}
|
||||
console.error("WebSocket Error:", error);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
setConnected(false)
|
||||
setConnected(false);
|
||||
if (!isUnmounted.current) {
|
||||
// Attempt to reconnect
|
||||
if (reconnectAttempts.current < 5) {
|
||||
const timeout = Math.pow(2, reconnectAttempts.current) * 1000 // Exponential backoff
|
||||
reconnectAttempts.current += 1
|
||||
const timeout = Math.pow(2, reconnectAttempts.current) * 1000; // Exponential backoff
|
||||
reconnectAttempts.current += 1;
|
||||
reconnectTimeout.current = setTimeout(() => {
|
||||
connect()
|
||||
}, timeout)
|
||||
connect();
|
||||
}, timeout);
|
||||
} else {
|
||||
console.warn('Max reconnect attempts reached.')
|
||||
console.warn("Max reconnect attempts reached.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [url])
|
||||
};
|
||||
}, [url]);
|
||||
|
||||
useEffect(() => {
|
||||
connect()
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
isUnmounted.current = true
|
||||
isUnmounted.current = true;
|
||||
if (socketRef.current) {
|
||||
socketRef.current.close()
|
||||
socketRef.current.close();
|
||||
}
|
||||
if (reconnectTimeout.current) {
|
||||
clearTimeout(reconnectTimeout.current)
|
||||
clearTimeout(reconnectTimeout.current);
|
||||
}
|
||||
}
|
||||
}, [connect])
|
||||
};
|
||||
}, [connect]);
|
||||
|
||||
// Function to send messages
|
||||
const sendMessage = useCallback((msg: string) => {
|
||||
if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) {
|
||||
socketRef.current.send(msg)
|
||||
socketRef.current.send(msg);
|
||||
} else {
|
||||
console.warn(
|
||||
'WebSocket is not open. Ready state:',
|
||||
socketRef.current?.readyState
|
||||
)
|
||||
"WebSocket is not open. Ready state:",
|
||||
socketRef.current?.readyState,
|
||||
);
|
||||
}
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
return { socket, message, sendMessage, connected, onlineCount }
|
||||
return { socket, message, sendMessage, connected, onlineCount };
|
||||
}
|
||||
|
@ -1,26 +1,25 @@
|
||||
import { createContext, useContext, ReactNode } from 'react'
|
||||
import useWebSocket, { WebSocketHook } from './useWebsocket'
|
||||
|
||||
import { createContext, useContext, ReactNode } from "react";
|
||||
import useWebSocket, { WebSocketHook } from "./useWebsocket";
|
||||
|
||||
interface WebSocketProviderProps {
|
||||
children: ReactNode
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const WebSocketContext = createContext<WebSocketHook | undefined>(undefined)
|
||||
const WebSocketContext = createContext<WebSocketHook | undefined>(undefined);
|
||||
|
||||
export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
|
||||
const ws = useWebSocket('wss://dev-next.buycoffee.top:4433/api/v1/ws/server')
|
||||
const ws = useWebSocket("wss://dev-next.buycoffee.top:4433/api/v1/ws/server");
|
||||
return (
|
||||
<WebSocketContext.Provider value={ws}>{children}</WebSocketContext.Provider>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const useWebSocketContext = (): WebSocketHook => {
|
||||
const context = useContext(WebSocketContext)
|
||||
const context = useContext(WebSocketContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useWebSocketContext must be used within a WebSocketProvider'
|
||||
)
|
||||
}
|
||||
return context
|
||||
"useWebSocketContext must be used within a WebSocketProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
39
src/types/nezha-api.ts
Normal file
39
src/types/nezha-api.ts
Normal file
@ -0,0 +1,39 @@
|
||||
export interface NezhaAPI {
|
||||
id: number;
|
||||
name: string;
|
||||
host: NezhaAPIHost;
|
||||
status: NezhaAPIStatus;
|
||||
}
|
||||
|
||||
export interface NezhaAPIHost {
|
||||
Platform: string;
|
||||
PlatformVersion: string;
|
||||
CPU: string[];
|
||||
MemTotal: number;
|
||||
DiskTotal: number;
|
||||
SwapTotal: number;
|
||||
Arch: string;
|
||||
BootTime: number;
|
||||
CountryCode: string;
|
||||
Version: string;
|
||||
}
|
||||
|
||||
export interface NezhaAPIStatus {
|
||||
CPU: number;
|
||||
MemUsed: number;
|
||||
SwapUsed: number;
|
||||
DiskUsed: number;
|
||||
NetInTransfer: number;
|
||||
NetOutTransfer: number;
|
||||
NetInSpeed: number;
|
||||
NetOutSpeed: number;
|
||||
Uptime: number;
|
||||
Load1: number;
|
||||
Load5: number;
|
||||
Load15: number;
|
||||
TcpConnCount: number;
|
||||
UdpConnCount: number;
|
||||
ProcessCount: number;
|
||||
Temperatures: number;
|
||||
GPU: number;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user