feat: detail info

This commit is contained in:
hamster1963 2024-10-19 02:09:04 +08:00
parent dbe67ab205
commit b83219bbf2
6 changed files with 402 additions and 216 deletions

View File

@ -4,10 +4,10 @@ import AnimatedCircularProgressBar from "@/components/ui/animated-circular-progr
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import useSWR from "swr"; import useSWR from "swr";
import { NezhaAPISafe } from "../../types/nezha-api"; import { NezhaAPISafe } from "../../types/nezha-api";
import { formatRelativeTime, formatTime, nezhaFetcher } from "@/lib/utils"; import { formatNezhaInfo, formatRelativeTime, formatTime, nezhaFetcher } from "@/lib/utils";
import getEnv from "@/lib/env-entry"; import getEnv from "@/lib/env-entry";
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; import { Area, AreaChart, CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@ -16,6 +16,18 @@ type cpuChartData = {
cpu: number; cpu: number;
} }
type memChartData = {
timeStamp: string;
mem: number;
swap: number;
}
type networkChartData = {
timeStamp: string;
upload: number;
download: number;
}
export default function ServerDetailChartClient({ export default function ServerDetailChartClient({
server_id, server_id,
}: { }: {
@ -29,22 +41,6 @@ export default function ServerDetailChartClient({
}, },
); );
const [chartData, setChartData] = useState([] as cpuChartData[]);
useEffect(() => {
if (data) {
const timestamp = Date.now().toString();
const newData = [
...chartData,
{ timeStamp: timestamp, cpu: data.status.CPU },
];
if (newData.length > 30) {
newData.shift();
}
setChartData(newData);
}
}, [data]);
if (error) { if (error) {
return ( return (
<> <>
@ -60,6 +56,33 @@ export default function ServerDetailChartClient({
} }
if (!data) return null; if (!data) return null;
return (
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3">
<CpuChart data={data} />
<MemChart data={data} />
<NetworkChart data={data} />
</section>
)
}
function CpuChart({ data }: { data: NezhaAPISafe }) {
const [cpuChartData, setCpuChartData] = useState([] as cpuChartData[]);
const { cpu } = formatNezhaInfo(data);
useEffect(() => {
if (data) {
const timestamp = Date.now().toString();
const newData = [
...cpuChartData,
{ timeStamp: timestamp, cpu: cpu },
];
if (newData.length > 30) {
newData.shift();
}
setCpuChartData(newData);
}
}, [data]);
const chartConfig = { const chartConfig = {
cpu: { cpu: {
@ -68,73 +91,303 @@ export default function ServerDetailChartClient({
} satisfies ChartConfig } satisfies ChartConfig
return ( return (
<section className="grid md:grid-cols-3 grid-cols-1"> <Card className=" rounded-sm">
<Card className=" rounded-sm"> <CardContent className="px-6 py-3">
<CardContent className="px-6 py-3 h-[217px]"> <section className="flex flex-col gap-1">
<section className="flex flex-col gap-1"> <div className="flex items-center justify-between">
<div className="flex items-center justify-between"> <p className="text-md font-medium">
<p className="text-sm font-medium md:text-base"> CPU
CPU </p>
<section className="flex items-center gap-2">
<p className="text-xs text-end w-10 font-medium">
{cpu.toFixed(0)}%
</p> </p>
<section className="flex items-center gap-2"> <AnimatedCircularProgressBar
<p className="text-xs text-end w-10 font-medium"> className="size-3 text-[0px]"
{data?.status.CPU.toFixed(0)}% max={100}
</p> min={0}
<AnimatedCircularProgressBar value={cpu}
className="size-3 text-[0px]" primaryColor="hsl(var(--chart-1))"
max={100} />
min={0} </section>
value={data?.status.CPU} </div>
/> <ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
</section> <AreaChart
</div> accessibilityLayer
data={cpuChartData}
margin={{
top: 12,
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="timeStamp"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={200}
interval="preserveStartEnd"
tickFormatter={(value) => formatRelativeTime(value)}
/>
<YAxis
tickLine={false}
axisLine={false}
mirror={true}
tickMargin={-15}
domain={[0, 100]}
tickFormatter={(value) => `${value}%`}
/>
<Area
isAnimationActive={false}
dataKey="cpu"
type="step"
fill="hsl(var(--chart-1))"
fillOpacity={0.3}
stroke="hsl(var(--chart-1))"
/>
</AreaChart>
</ChartContainer>
</section>
</CardContent>
</Card>
)
}
<ChartContainer config={chartConfig}> function MemChart({ data }: { data: NezhaAPISafe }) {
<AreaChart const [memChartData, setMemChartData] = useState([] as memChartData[]);
accessibilityLayer
data={chartData} const { mem, swap } = formatNezhaInfo(data);
margin={{
top: 12, useEffect(() => {
left: 12, if (data) {
right: 12, const timestamp = Date.now().toString();
}} const newData = [
> ...memChartData,
<CartesianGrid vertical={false} /> { timeStamp: timestamp, mem: mem, swap: swap },
<XAxis ];
dataKey="timeStamp" if (newData.length > 30) {
tickLine={false} newData.shift();
axisLine={false} }
ticks={[chartData[0]?.timeStamp, chartData[chartData.length - 1]?.timeStamp]} setMemChartData(newData);
tickFormatter={(value) => formatRelativeTime(value)} }
/> }, [data]);
<YAxis
tickLine={false} const chartConfig = {
axisLine={false} mem: {
mirror={true} label: "Mem",
tickMargin={-15} },
domain={[0, 100]} swap: {
tickFormatter={(value) => `${value}%`} label: "Swap",
/> },
{/* <ChartTooltip } satisfies ChartConfig
isAnimationActive={false}
cursor={false} return (
content={<ChartTooltipContent indicator={"line"} labelFormatter={(_, payload) => { <Card className=" rounded-sm">
return formatTime(Number(payload[0].payload.timeStamp)); <CardContent className="px-6 py-3">
}} />} <section className="flex flex-col gap-1">
/> */} <div className="flex items-center">
<Area <section className="flex items-center gap-4">
isAnimationActive={false} <div className="flex flex-col">
dataKey="cpu" <p className=" text-xs text-muted-foreground">Mem</p>
type="step" <div className="flex items-center gap-2">
fill="hsl(var(--chart-1))" <AnimatedCircularProgressBar
fillOpacity={0.3} className="size-3 text-[0px]"
stroke="hsl(var(--chart-1))" max={100}
/> min={0}
</AreaChart> value={mem}
</ChartContainer> primaryColor="hsl(var(--chart-1))"
</section> />
</CardContent> <p className="text-xs font-medium">
</Card> {mem.toFixed(0)}%
</section> </p>
</div>
</div>
<div className="flex flex-col">
<p className=" text-xs text-muted-foreground">Swap</p>
<div className="flex items-center gap-2">
<AnimatedCircularProgressBar
className="size-3 text-[0px]"
max={100}
min={0}
value={swap}
primaryColor="hsl(var(--chart-4))"
/>
<p className="text-xs font-medium">
{swap.toFixed(0)}%
</p>
</div>
</div>
</section>
</div>
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
<AreaChart
accessibilityLayer
data={memChartData}
margin={{
top: 12,
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="timeStamp"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={200}
interval="preserveStartEnd"
tickFormatter={(value) => formatRelativeTime(value)}
/>
<YAxis
tickLine={false}
axisLine={false}
mirror={true}
tickMargin={-15}
domain={[0, 100]}
tickFormatter={(value) => `${value}%`}
/>
<Area
isAnimationActive={false}
dataKey="mem"
type="step"
fill="hsl(var(--chart-1))"
fillOpacity={0.3}
stroke="hsl(var(--chart-1))"
/>
<Area
isAnimationActive={false}
dataKey="swap"
type="step"
fill="hsl(var(--chart-4))"
fillOpacity={0.3}
stroke="hsl(var(--chart-4))"
/>
</AreaChart>
</ChartContainer>
</section>
</CardContent>
</Card>
)
}
function NetworkChart({ data }: { data: NezhaAPISafe }) {
const [networkChartData, setNetworkChartData] = useState([] as networkChartData[]);
const { up, down } = formatNezhaInfo(data);
useEffect(() => {
if (data) {
const timestamp = Date.now().toString();
const newData = [
...networkChartData,
{ timeStamp: timestamp, upload: up, download: down },
];
if (newData.length > 30) {
newData.shift();
}
setNetworkChartData(newData);
}
}, [data]);
let maxDownload = Math.max(...networkChartData.map((item) => item.download));
maxDownload = Math.ceil(maxDownload);
if (maxDownload < 1) {
maxDownload = 1;
}
const chartConfig = {
upload: {
label: "Upload",
},
download: {
label: "Download",
},
} satisfies ChartConfig
return (
<Card className=" rounded-sm">
<CardContent className="px-6 py-3">
<section className="flex flex-col gap-1">
<div className="flex items-center">
<section className="flex items-center gap-4">
<div className="flex flex-col w-20">
<p className="text-xs text-muted-foreground">Upload</p>
<div className="flex items-center gap-1">
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-1))]"></span>
<p className="text-xs font-medium">
{up.toFixed(2)} M/s
</p>
</div>
</div>
<div className="flex flex-col w-20">
<p className=" text-xs text-muted-foreground">Download</p>
<div className="flex items-center gap-1">
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-4))]"></span>
<p className="text-xs font-medium">
{down.toFixed(2)} M/s
</p>
</div>
</div>
</section>
</div>
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
<LineChart
accessibilityLayer
data={networkChartData}
margin={{
top: 12,
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="timeStamp"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={200}
interval="preserveStartEnd"
tickFormatter={(value) => formatRelativeTime(value)}
/>
<YAxis
tickLine={false}
axisLine={false}
mirror={true}
tickMargin={-15}
type="number"
minTickGap={50}
interval="preserveStartEnd"
domain={[1, maxDownload]}
tickFormatter={(value) => `${value.toFixed(0)}M/s`}
/>
<Line
isAnimationActive={false}
dataKey="upload"
type="linear"
stroke="hsl(var(--chart-1))"
strokeWidth={1}
dot={false}
/>
<Line
isAnimationActive={false}
dataKey="download"
type="linear"
stroke="hsl(var(--chart-4))"
strokeWidth={1}
dot={false}
/>
</LineChart>
</ChartContainer>
</section>
</CardContent>
</Card>
) )
} }

View File

@ -6,7 +6,7 @@ import AnimatedCircularProgressBar from "@/components/ui/animated-circular-progr
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import getEnv from "@/lib/env-entry"; import getEnv from "@/lib/env-entry";
import { cn, nezhaFetcher } from "@/lib/utils"; import { cn, formatBytes, nezhaFetcher } from "@/lib/utils";
import { useLocale } from "next-intl"; import { useLocale } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import useSWR from "swr"; import useSWR from "swr";
@ -51,10 +51,10 @@ export default function ServerDetailClient({
{data?.name} {data?.name}
</div> </div>
<section className="flex flex-wrap gap-2 mt-2"> <section className="flex flex-wrap gap-2 mt-2">
<Card className="rounded-[10px]"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex items-center gap-2"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs font-semibold">Status</p> <p className="text-xs text-muted-foreground">Status</p>
<Badge <Badge
className={cn( className={cn(
"text-xs rounded-[6px] w-fit px-1 py-0 dark:text-white", "text-xs rounded-[6px] w-fit px-1 py-0 dark:text-white",
@ -69,160 +69,85 @@ export default function ServerDetailClient({
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
<Card className="rounded-[10px]"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex items-center gap-2"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs font-semibold">Uptime</p> <p className="text-xs text-muted-foreground">Uptime</p>
<Badge <div className="text-xs">
className="text-xs rounded-[6px] w-fit px-1 py-0"
variant="secondary"
>
{" "} {" "}
{(data?.status.Uptime / 86400).toFixed(0)} Days{" "} {(data?.status.Uptime / 86400).toFixed(0)} Days{" "}
</Badge> </div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
<Card className="rounded-[10px]"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex items-center gap-2"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs font-semibold">Arch</p> <p className="text-xs text-muted-foreground">Version</p>
<Badge <div className="text-xs">
className="text-xs rounded-[6px] w-fit px-1 py-0"
variant="secondary"
>
{" "}
{data?.host.Arch || "Unknown"}{" "}
</Badge>
</section>
</CardContent>
</Card>
<Card className="rounded-[10px]">
<CardContent className="px-1.5 py-1">
<section className="flex items-center gap-2">
<p className="text-xs font-semibold">Version</p>
<Badge
className="text-xs rounded-[6px] w-fit px-1 py-0"
variant="secondary"
>
{" "}
{data?.host.Version || "Unknown"}{" "} {data?.host.Version || "Unknown"}{" "}
</Badge> </div>
</section>
</CardContent>
</Card>
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">Arch</p>
<div className="text-xs">
{data?.host.Arch || "Unknown"}{" "}
</div>
</section>
</CardContent>
</Card>
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">Mem</p>
<div className="text-xs">
{formatBytes(data?.host.MemTotal)}
</div>
</section>
</CardContent>
</Card>
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">Disk</p>
<div className="text-xs">
{formatBytes(data?.host.DiskTotal)}
</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
</section> </section>
<section className="flex flex-wrap gap-2 mt-2"> <section className="flex flex-wrap gap-2 mt-1">
<Card className="rounded-[10px] flex flex-col justify-center"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex items-center gap-2"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs font-semibold">System</p> <p className="text-xs text-muted-foreground">System</p>
{data?.host.Platform ? ( {data?.host.Platform ? (
<div className="text-xs w-fit font-medium"> <div className="text-xs">
{" "} {" "}
{data?.host.Platform || "Unknown"} -{" "} {data?.host.Platform || "Unknown"} -{" "}
{data?.host.PlatformVersion}{" "} {data?.host.PlatformVersion}{" "}
</div> </div>) : <div className="text-xs">Unknown</div>}
) : (
<div className="text-xs w-fit font-medium"> Unknown </div>
)}
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
<Card className="rounded-[10px] flex flex-col justify-center"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex items-center gap-2"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs font-semibold">CPU</p> <p className="text-xs text-muted-foreground">CPU</p>
{data?.host.CPU ? ( {data?.host.CPU ? (
<div className="text-xs w-fit font-medium"> <div className="text-xs">
{" "} {" "}
{data?.host.CPU || "Unknown"} {data?.host.CPU}
</div> </div>) : <div className="text-xs">Unknown</div>}
) : (
<div className="text-xs w-fit font-medium"> Unknown </div>
)}
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
</section> </section>
{/* <section className="flex flex-wrap gap-2 mt-1">
<Card className="rounded-[10px]">
<CardContent className="px-1.5 py-1">
<section className="flex items-center gap-2">
<p className="text-xs font-semibold">CPU</p>
<p className="text-xs text-end w-10 font-medium">
{data?.status.CPU.toFixed(0)}%
</p>
<AnimatedCircularProgressBar
className="size-3 text-[0px]"
max={100}
min={0}
value={data?.status.CPU}
/>
</section>
</CardContent>
</Card>
<Card className="rounded-[10px]">
<CardContent className="px-1.5 py-1">
<section className="flex items-center gap-2">
<p className="text-xs font-semibold">Mem</p>
<p className="text-xs w-10 text-end font-medium">
{((data?.status.MemUsed / data?.host.MemTotal) * 100).toFixed(
0,
)}
%
</p>
<AnimatedCircularProgressBar
className="size-3 text-[0px]"
max={100}
min={0}
value={(data?.status.MemUsed / data?.host.MemTotal) * 100}
/>
</section>
</CardContent>
</Card>
<Card className="rounded-[10px]">
<CardContent className="px-1.5 py-1">
<section className="flex items-center gap-2">
<p className="text-xs font-semibold">Swap</p>
<p className="text-xs w-10 text-end font-medium">
{data?.status.SwapUsed
? (
(data?.status.SwapUsed / data?.host.SwapTotal) *
100
).toFixed(0)
: 0}
%
</p>
<AnimatedCircularProgressBar
className="size-3 text-[0px]"
max={100}
min={0}
value={(data?.status.SwapUsed / data?.host.SwapTotal) * 100}
/>
</section>
</CardContent>
</Card>
<Card className="rounded-[10px]">
<CardContent className="px-1.5 py-1">
<section className="flex items-center gap-2">
<p className="text-xs font-semibold">Disk</p>
<p className="text-xs w-10 text-end font-medium">
{((data?.status.DiskUsed / data?.host.DiskTotal) * 100).toFixed(
0,
)}
%
</p>
<AnimatedCircularProgressBar
className="size-3 text-[0px]"
max={100}
min={0}
value={(data?.status.DiskUsed / data?.host.DiskTotal) * 100}
/>
</section>
</CardContent>
</Card>
</section> */}
</div> </div>
); );
} }

View File

@ -1,9 +1,11 @@
import ServerDetailClient from "@/app/[locale]/(main)/ClientComponents/ServerDetailClient"; import ServerDetailClient from "@/app/[locale]/(main)/ClientComponents/ServerDetailClient";
import ServerDetailChartClient from "@/app/[locale]/(main)/ClientComponents/ServerDetailChartClient"; import ServerDetailChartClient from "@/app/[locale]/(main)/ClientComponents/ServerDetailChartClient";
import { Separator } from "@/components/ui/separator";
export default function Page({ params }: { params: { id: string } }) { export default function Page({ params }: { params: { id: string } }) {
return <div className="mx-auto grid w-full max-w-5xl gap-2"> return <div className="mx-auto grid w-full max-w-5xl gap-2">
<ServerDetailClient server_id={Number(params.id)} /> <ServerDetailClient server_id={Number(params.id)} />
<Separator className="mt-1 mb-2" />
<ServerDetailChartClient server_id={Number(params.id)} /> <ServerDetailChartClient server_id={Number(params.id)} />
</div> </div>
; ;

BIN
bun.lockb

Binary file not shown.

View File

@ -5,12 +5,14 @@ interface Props {
value: number; value: number;
min: number; min: number;
className?: string; className?: string;
primaryColor?: string;
} }
export default function AnimatedCircularProgressBar({ export default function AnimatedCircularProgressBar({
max = 100, max = 100,
min = 0, min = 0,
value = 0, value = 0,
primaryColor,
className, className,
}: Props) { }: Props) {
const circumference = 2 * Math.PI * 45; const circumference = 2 * Math.PI * 45;
@ -74,9 +76,12 @@ export default function AnimatedCircularProgressBar({
strokeDashoffset="0" strokeDashoffset="0"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
className="opacity-100 stroke-current" className={cn("opacity-100 stroke-current", {
"stroke-[var(--stroke-primary-color)]": primaryColor,
})}
style={ style={
{ {
"--stroke-primary-color": primaryColor,
"--stroke-percent": currentPercent, "--stroke-percent": currentPercent,
strokeDasharray: strokeDasharray:
"calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)", "calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",

View File

@ -14,6 +14,7 @@ export function formatNezhaInfo(serverInfo: NezhaAPISafe) {
down: serverInfo.status.NetInSpeed / 1024 / 1024, down: serverInfo.status.NetInSpeed / 1024 / 1024,
online: serverInfo.online_status, online: serverInfo.online_status,
mem: (serverInfo.status.MemUsed / serverInfo.host.MemTotal) * 100, mem: (serverInfo.status.MemUsed / serverInfo.host.MemTotal) * 100,
swap: (serverInfo.status.SwapUsed / serverInfo.host.SwapTotal) * 100,
stg: (serverInfo.status.DiskUsed / serverInfo.host.DiskTotal) * 100, stg: (serverInfo.status.DiskUsed / serverInfo.host.DiskTotal) * 100,
country_code: serverInfo.host.CountryCode, country_code: serverInfo.host.CountryCode,
}; };