mirror of
https://github.com/woodchen-ink/nezha-dash-v1.git
synced 2025-07-18 17:41:56 +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
|
- name: Build static export
|
||||||
run: |
|
run: |
|
||||||
bun run build --base=/dashboard
|
bun run build
|
||||||
|
|
||||||
- name: Compress dist folder
|
- name: Compress dist folder
|
||||||
run: zip -r dist.zip dist
|
run: zip -r dist.zip dist
|
||||||
|
@ -8,15 +8,10 @@ const App: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<Router basename={import.meta.env.BASE_URL}>
|
<Router basename={import.meta.env.BASE_URL}>
|
||||||
<div className="flex min-h-screen w-full flex-col">
|
<div className="flex min-h-screen w-full flex-col">
|
||||||
<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">
|
<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 />
|
<Header />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route path="/" element={<Server />} />
|
||||||
path="/"
|
|
||||||
element={
|
|
||||||
<Server />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Routes>
|
</Routes>
|
||||||
<Footer />
|
<Footer />
|
||||||
</main>
|
</main>
|
||||||
|
@ -8,13 +8,10 @@ import { DateTime } from "luxon";
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
function Header() {
|
function Header() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-5xl">
|
<div className="mx-auto w-full max-w-5xl">
|
||||||
<section className="flex items-center justify-between">
|
<section className="flex items-center justify-between">
|
||||||
<section
|
<section className="flex items-center text-base font-medium">
|
||||||
className="flex items-center text-base font-medium"
|
|
||||||
>
|
|
||||||
<div className="mr-1 flex flex-row items-center justify-start">
|
<div className="mr-1 flex flex-row items-center justify-start">
|
||||||
<img
|
<img
|
||||||
width={40}
|
width={40}
|
||||||
@ -45,7 +42,7 @@ function Header() {
|
|||||||
|
|
||||||
// https://github.com/streamich/react-use/blob/master/src/useInterval.ts
|
// https://github.com/streamich/react-use/blob/master/src/useInterval.ts
|
||||||
const useInterval = (callback: () => void, delay: number | null) => {
|
const useInterval = (callback: () => void, delay: number | null) => {
|
||||||
const savedCallback = useRef<() => void>(() => { });
|
const savedCallback = useRef<() => void>(() => {});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
savedCallback.current = callback;
|
savedCallback.current = callback;
|
||||||
});
|
});
|
||||||
@ -74,9 +71,7 @@ function Overview() {
|
|||||||
<section className={"mt-10 flex flex-col md:mt-16"}>
|
<section className={"mt-10 flex flex-col md:mt-16"}>
|
||||||
<p className="text-base font-semibold">👋 Overview</p>
|
<p className="text-base font-semibold">👋 Overview</p>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<p className="text-sm font-medium opacity-50">
|
<p className="text-sm font-medium opacity-50">where the time is</p>
|
||||||
where the time is
|
|
||||||
</p>
|
|
||||||
{mouted ? (
|
{mouted ? (
|
||||||
<p className="text-sm font-medium">{timeString}</p>
|
<p className="text-sm font-medium">{timeString}</p>
|
||||||
) : (
|
) : (
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
|
||||||
import { Moon, Sun } from "lucide-react";
|
import { Moon, Sun } from "lucide-react";
|
||||||
import { Theme } from "@/components/ThemeProvider";
|
import { Theme } from "@/components/ThemeProvider";
|
||||||
import { useTheme } from "../hooks/use-theme";
|
import { useTheme } from "../hooks/use-theme";
|
||||||
@ -40,21 +39,18 @@ export function ModeToggle() {
|
|||||||
onSelect={(e) => handleSelect(e, "light")}
|
onSelect={(e) => handleSelect(e, "light")}
|
||||||
>
|
>
|
||||||
Light
|
Light
|
||||||
{theme === "light" && <CheckCircleIcon className="size-4" />}
|
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={cn({ "gap-3 bg-muted": theme === "dark" })}
|
className={cn({ "gap-3 bg-muted": theme === "dark" })}
|
||||||
onSelect={(e) => handleSelect(e, "dark")}
|
onSelect={(e) => handleSelect(e, "dark")}
|
||||||
>
|
>
|
||||||
Dark
|
Dark
|
||||||
{theme === "dark" && <CheckCircleIcon className="size-4" />}
|
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={cn({ "gap-3 bg-muted": theme === "system" })}
|
className={cn({ "gap-3 bg-muted": theme === "system" })}
|
||||||
onSelect={(e) => handleSelect(e, "system")}
|
onSelect={(e) => handleSelect(e, "system")}
|
||||||
>
|
>
|
||||||
System
|
System
|
||||||
{theme === "system" && <CheckCircleIcon className="size-4" />}
|
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Separator = React.forwardRef<
|
const Separator = React.forwardRef<
|
||||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||||
@ -9,7 +9,7 @@ const Separator = React.forwardRef<
|
|||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||||
ref
|
ref,
|
||||||
) => (
|
) => (
|
||||||
<SeparatorPrimitive.Root
|
<SeparatorPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -18,12 +18,12 @@ const Separator = React.forwardRef<
|
|||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 bg-border",
|
"shrink-0 bg-border",
|
||||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...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({
|
function Skeleton({
|
||||||
className,
|
className,
|
||||||
@ -9,7 +9,7 @@ function Skeleton({
|
|||||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||||
{...props}
|
{...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 {
|
export interface WebSocketHook {
|
||||||
socket: WebSocket | null
|
socket: WebSocket | null;
|
||||||
connected: boolean
|
connected: boolean;
|
||||||
onlineCount: number
|
onlineCount: number;
|
||||||
message: string | null
|
message: string | null;
|
||||||
sendMessage: (msg: string) => void
|
sendMessage: (msg: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function useWebSocket(url: string): WebSocketHook {
|
export default function useWebSocket(url: string): WebSocketHook {
|
||||||
const [socket, setSocket] = useState<WebSocket | null>(null)
|
const [socket, setSocket] = useState<WebSocket | null>(null);
|
||||||
const [message, setMessage] = useState<string | null>(null)
|
const [message, setMessage] = useState<string | null>(null);
|
||||||
const [connected, setConnected] = useState<boolean>(false)
|
const [connected, setConnected] = useState<boolean>(false);
|
||||||
const [onlineCount, setOnlineCount] = useState<number>(0)
|
const [onlineCount, setOnlineCount] = useState<number>(0);
|
||||||
const socketRef = useRef<WebSocket | null>(null)
|
const socketRef = useRef<WebSocket | null>(null);
|
||||||
const reconnectAttempts = useRef<number>(0)
|
const reconnectAttempts = useRef<number>(0);
|
||||||
const reconnectTimeout = useRef<NodeJS.Timeout | null>(null)
|
const reconnectTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||||
const isUnmounted = useRef<boolean>(false)
|
const isUnmounted = useRef<boolean>(false);
|
||||||
|
|
||||||
const connect = useCallback(() => {
|
const connect = useCallback(() => {
|
||||||
if (isUnmounted.current) return
|
if (isUnmounted.current) return;
|
||||||
|
|
||||||
const ws = new WebSocket(url)
|
const ws = new WebSocket(url);
|
||||||
setSocket(ws)
|
setSocket(ws);
|
||||||
socketRef.current = ws
|
socketRef.current = ws;
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
setConnected(true)
|
setConnected(true);
|
||||||
reconnectAttempts.current = 0
|
reconnectAttempts.current = 0;
|
||||||
}
|
};
|
||||||
|
|
||||||
ws.onmessage = (event: MessageEvent) => {
|
ws.onmessage = (event: MessageEvent) => {
|
||||||
setMessage(event.data)
|
setMessage(event.data);
|
||||||
const msgJson = JSON.parse(event.data)
|
const msgJson = JSON.parse(event.data);
|
||||||
if (msgJson.type === 'live') {
|
if (msgJson.type === "live") {
|
||||||
setOnlineCount(msgJson.data.count)
|
setOnlineCount(msgJson.data.count);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
ws.onerror = (error) => {
|
||||||
console.error('WebSocket Error:', error)
|
console.error("WebSocket Error:", error);
|
||||||
}
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
setConnected(false)
|
setConnected(false);
|
||||||
if (!isUnmounted.current) {
|
if (!isUnmounted.current) {
|
||||||
// Attempt to reconnect
|
// Attempt to reconnect
|
||||||
if (reconnectAttempts.current < 5) {
|
if (reconnectAttempts.current < 5) {
|
||||||
const timeout = Math.pow(2, reconnectAttempts.current) * 1000 // Exponential backoff
|
const timeout = Math.pow(2, reconnectAttempts.current) * 1000; // Exponential backoff
|
||||||
reconnectAttempts.current += 1
|
reconnectAttempts.current += 1;
|
||||||
reconnectTimeout.current = setTimeout(() => {
|
reconnectTimeout.current = setTimeout(() => {
|
||||||
connect()
|
connect();
|
||||||
}, timeout)
|
}, timeout);
|
||||||
} else {
|
} else {
|
||||||
console.warn('Max reconnect attempts reached.')
|
console.warn("Max reconnect attempts reached.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}, [url])
|
}, [url]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
connect()
|
connect();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
isUnmounted.current = true
|
isUnmounted.current = true;
|
||||||
if (socketRef.current) {
|
if (socketRef.current) {
|
||||||
socketRef.current.close()
|
socketRef.current.close();
|
||||||
}
|
}
|
||||||
if (reconnectTimeout.current) {
|
if (reconnectTimeout.current) {
|
||||||
clearTimeout(reconnectTimeout.current)
|
clearTimeout(reconnectTimeout.current);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}, [connect])
|
}, [connect]);
|
||||||
|
|
||||||
// Function to send messages
|
// Function to send messages
|
||||||
const sendMessage = useCallback((msg: string) => {
|
const sendMessage = useCallback((msg: string) => {
|
||||||
if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) {
|
if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) {
|
||||||
socketRef.current.send(msg)
|
socketRef.current.send(msg);
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(
|
||||||
'WebSocket is not open. Ready state:',
|
"WebSocket is not open. Ready state:",
|
||||||
socketRef.current?.readyState
|
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 { createContext, useContext, ReactNode } from "react";
|
||||||
import useWebSocket, { WebSocketHook } from './useWebsocket'
|
import useWebSocket, { WebSocketHook } from "./useWebsocket";
|
||||||
|
|
||||||
|
|
||||||
interface WebSocketProviderProps {
|
interface WebSocketProviderProps {
|
||||||
children: ReactNode
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WebSocketContext = createContext<WebSocketHook | undefined>(undefined)
|
const WebSocketContext = createContext<WebSocketHook | undefined>(undefined);
|
||||||
|
|
||||||
export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
|
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 (
|
return (
|
||||||
<WebSocketContext.Provider value={ws}>{children}</WebSocketContext.Provider>
|
<WebSocketContext.Provider value={ws}>{children}</WebSocketContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useWebSocketContext = (): WebSocketHook => {
|
export const useWebSocketContext = (): WebSocketHook => {
|
||||||
const context = useContext(WebSocketContext)
|
const context = useContext(WebSocketContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'useWebSocketContext must be used within a WebSocketProvider'
|
"useWebSocketContext must be used within a WebSocketProvider",
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
return context
|
return context;
|
||||||
}
|
};
|
||||||
|
@ -12,14 +12,14 @@ const queryClient = new QueryClient();
|
|||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||||
<WebSocketProvider >
|
<WebSocketProvider>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<App />
|
<App />
|
||||||
<Toaster richColors position="top-right" />
|
<Toaster richColors position="top-right" />
|
||||||
<ReactQueryDevtools />
|
<ReactQueryDevtools />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</WebSocketProvider>
|
</WebSocketProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
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