mirror of
https://github.com/hamster1963/nezha-dash.git
synced 2025-04-24 21:10:45 +08:00
commit
7cfc5a49cc
@ -10,24 +10,13 @@ export default function ServerListClient() {
|
|||||||
refreshInterval: 3000,
|
refreshInterval: 3000,
|
||||||
});
|
});
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
const sortedResult = data.result.sort((a: any, b: any) => a.id - b.id);
|
const sortedResult = data.result.sort((a, b) => a.id - b.id);
|
||||||
const timestamp = Date.now() / 1000;
|
const timestamp = Date.now() / 1000;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={"grid grid-cols-1 gap-2 md:grid-cols-2"}>
|
<section className={"grid grid-cols-1 gap-2 md:grid-cols-2"}>
|
||||||
{sortedResult.map((server: any) => (
|
{sortedResult.map((serverInfo) => (
|
||||||
<ServerCard
|
<ServerCard key={serverInfo.id} timestamp={timestamp} serverInfo={serverInfo} />
|
||||||
key={server.id}
|
|
||||||
id={server.id}
|
|
||||||
cpu={server.status.CPU}
|
|
||||||
name={server.name}
|
|
||||||
up={server.status.NetOutSpeed / 1024 / 1024}
|
|
||||||
down={server.status.NetInSpeed / 1024 / 1024}
|
|
||||||
status={timestamp - server.last_active > 300 ? "offline" : "online"}
|
|
||||||
uptime={server.status.Uptime / 86400}
|
|
||||||
mem={(server.status.MemUsed / server.host.MemTotal) * 100}
|
|
||||||
stg={(server.status.DiskUsed / server.host.DiskTotal) * 100}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
@ -1,62 +1,43 @@
|
|||||||
import React from "react";
|
import { NezhaAPISafe } from "@/app/types/nezha-api";
|
||||||
|
|
||||||
import ServerUsageBar from "@/components/ServerUsageBar";
|
import ServerUsageBar from "@/components/ServerUsageBar";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Popover,
|
||||||
TooltipContent,
|
PopoverContent,
|
||||||
TooltipProvider,
|
PopoverTrigger,
|
||||||
TooltipTrigger,
|
} from "@/components/ui/popover";
|
||||||
} from "@/components/ui/tooltip";
|
import { formatNezhaInfo } from "@/lib/utils";
|
||||||
|
import ServerCardPopover from "./ServerCardPopover";
|
||||||
type ServerCardProps = {
|
|
||||||
id: number;
|
|
||||||
status: string;
|
|
||||||
name: string;
|
|
||||||
uptime: number;
|
|
||||||
cpu: number;
|
|
||||||
mem: number;
|
|
||||||
stg: number;
|
|
||||||
up: number;
|
|
||||||
down: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ServerCard({
|
export default function ServerCard({
|
||||||
status,
|
timestamp,
|
||||||
name,
|
serverInfo,
|
||||||
uptime,
|
}: {
|
||||||
cpu,
|
timestamp: number;
|
||||||
mem,
|
serverInfo: NezhaAPISafe;
|
||||||
stg,
|
}) {
|
||||||
up,
|
const { name, online, cpu, up, down, mem, stg, ...props } = formatNezhaInfo(
|
||||||
down,
|
timestamp,
|
||||||
}: ServerCardProps) {
|
serverInfo,
|
||||||
return status === "online" ? (
|
);
|
||||||
|
|
||||||
|
return online === "online" ? (
|
||||||
<Card
|
<Card
|
||||||
className={
|
className={
|
||||||
"flex flex-col items-center justify-start gap-3 p-3 md:px-5 lg:flex-row"
|
"flex flex-col items-center justify-start gap-3 p-3 md:px-5 lg:flex-row"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TooltipProvider delayDuration={0}>
|
<Popover>
|
||||||
<Tooltip>
|
<PopoverTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<section className={"flex items-center justify-start gap-2 lg:w-28"}>
|
||||||
<section
|
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500"></span>
|
||||||
className={"flex items-center justify-start gap-2 lg:w-28"}
|
<p className="break-all text-sm font-bold tracking-tight">{name}</p>
|
||||||
>
|
</section>
|
||||||
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500"></span>
|
</PopoverTrigger>
|
||||||
<p className="break-all text-sm font-bold tracking-tight">
|
<PopoverContent side="top">
|
||||||
{name}
|
<ServerCardPopover status={props.status} host={props.host} />
|
||||||
</p>
|
</PopoverContent>
|
||||||
</section>
|
</Popover>
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<section>
|
|
||||||
<div>Hostname: {name}</div>
|
|
||||||
<div>Online: {uptime.toFixed(0)} Days</div>
|
|
||||||
</section>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
<section className={"grid grid-cols-5 items-center gap-3"}>
|
<section className={"grid grid-cols-5 items-center gap-3"}>
|
||||||
<div className={"flex flex-col"}>
|
<div className={"flex flex-col"}>
|
||||||
<p className="text-xs text-muted-foreground">CPU</p>
|
<p className="text-xs text-muted-foreground">CPU</p>
|
||||||
@ -89,19 +70,15 @@ export default function ServerCard({
|
|||||||
"flex flex-col items-center justify-start gap-3 p-3 md:px-5 lg:flex-row"
|
"flex flex-col items-center justify-start gap-3 p-3 md:px-5 lg:flex-row"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TooltipProvider delayDuration={0}>
|
<Popover>
|
||||||
<Tooltip>
|
<PopoverTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<section className={"flex items-center justify-start gap-2 lg:w-28"}>
|
||||||
<section
|
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500"></span>
|
||||||
className={"flex items-center justify-start gap-2 lg:w-28"}
|
<p className="text-sm font-bold tracking-tight">{name}</p>
|
||||||
>
|
</section>
|
||||||
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500"></span>
|
</PopoverTrigger>
|
||||||
<p className="text-sm font-bold tracking-tight">{name}</p>
|
<PopoverContent side="top">Offline</PopoverContent>
|
||||||
</section>
|
</Popover>
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Offline</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
68
components/ServerCardPopover.tsx
Normal file
68
components/ServerCardPopover.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { NezhaAPISafe } from "@/app/types/nezha-api";
|
||||||
|
import { cn, formatBytes } from "@/lib/utils";
|
||||||
|
|
||||||
|
export function ServerCardPopoverCard({
|
||||||
|
className,
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
title: string;
|
||||||
|
content?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={cn("mb-[6px] flex w-full flex-col", className)}>
|
||||||
|
<div className="text-sm font-semibold">{title}</div>
|
||||||
|
{children ? children : <div className="break-all text-sm">{content}</div>}
|
||||||
|
<div className="h-[0.5px] w-full bg-accent"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ServerCardPopover({
|
||||||
|
host,
|
||||||
|
status,
|
||||||
|
}: {
|
||||||
|
host: NezhaAPISafe["host"];
|
||||||
|
status: NezhaAPISafe["status"];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section className="max-w-[300px]">
|
||||||
|
<ServerCardPopoverCard
|
||||||
|
title="System"
|
||||||
|
content={`${host.Platform}-${host.PlatformVersion} [${host.Virtualization}: ${host.Arch}]`}
|
||||||
|
/>
|
||||||
|
<ServerCardPopoverCard
|
||||||
|
title="CPU"
|
||||||
|
content={`${host.CPU.map((item) => item).join(", ")}`}
|
||||||
|
/>
|
||||||
|
<ServerCardPopoverCard
|
||||||
|
title="Mem"
|
||||||
|
content={`${formatBytes(host.MemTotal)} / ${formatBytes(status.MemUsed)}`}
|
||||||
|
/>
|
||||||
|
<ServerCardPopoverCard
|
||||||
|
title="STG"
|
||||||
|
content={`${formatBytes(status.DiskUsed)} / ${formatBytes(host.DiskTotal)}`}
|
||||||
|
/>
|
||||||
|
<ServerCardPopoverCard
|
||||||
|
title="Swap"
|
||||||
|
content={`${formatBytes(status.SwapUsed)} / ${formatBytes(host.SwapTotal)}`}
|
||||||
|
/>
|
||||||
|
<ServerCardPopoverCard
|
||||||
|
title="Network"
|
||||||
|
content={`${formatBytes(status.NetInTransfer)} / ${formatBytes(status.NetOutTransfer)}`}
|
||||||
|
/>
|
||||||
|
<ServerCardPopoverCard
|
||||||
|
title="Load"
|
||||||
|
content={`${status.Load1.toFixed(2)} / ${status.Load5.toFixed(2)} / ${status.Load15.toFixed(2)}`}
|
||||||
|
/>
|
||||||
|
<ServerCardPopoverCard
|
||||||
|
className="mb-0"
|
||||||
|
title="Online"
|
||||||
|
content={`${(status.Uptime / 86400).toFixed(0)} Days`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
31
components/ui/popover.tsx
Normal file
31
components/ui/popover.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Popover = PopoverPrimitive.Root
|
||||||
|
|
||||||
|
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||||
|
|
||||||
|
const PopoverContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||||
|
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
))
|
||||||
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent }
|
13
lib/utils.ts
13
lib/utils.ts
@ -1,3 +1,4 @@
|
|||||||
|
import { NezhaAPISafe } from "@/app/types/nezha-api";
|
||||||
import { type ClassValue, clsx } from "clsx";
|
import { type ClassValue, clsx } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
@ -5,6 +6,18 @@ export function cn(...inputs: ClassValue[]) {
|
|||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatNezhaInfo(timestamp: number, serverInfo: NezhaAPISafe) {
|
||||||
|
return {
|
||||||
|
...serverInfo,
|
||||||
|
cpu: serverInfo.status.CPU,
|
||||||
|
up: serverInfo.status.NetOutSpeed / 1024 / 1024,
|
||||||
|
down: serverInfo.status.NetInSpeed / 1024 / 1024,
|
||||||
|
online: timestamp - serverInfo.last_active > 300 ? "offline" : "online",
|
||||||
|
mem: (serverInfo.status.MemUsed / serverInfo.host.MemTotal) * 100,
|
||||||
|
stg: (serverInfo.status.DiskUsed / serverInfo.host.DiskTotal) * 100,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function formatBytes(bytes: number, decimals: number = 2) {
|
export function formatBytes(bytes: number, decimals: number = 2) {
|
||||||
if (!+bytes) return "0 Bytes";
|
if (!+bytes) return "0 Bytes";
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
"@radix-ui/react-navigation-menu": "^1.1.4",
|
"@radix-ui/react-navigation-menu": "^1.1.4",
|
||||||
|
"@radix-ui/react-popover": "^1.1.1",
|
||||||
"@radix-ui/react-progress": "^1.0.3",
|
"@radix-ui/react-progress": "^1.0.3",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
Loading…
Reference in New Issue
Block a user