feat: add detail charts

This commit is contained in:
hamster1963 2024-10-19 12:52:24 +08:00
parent b83219bbf2
commit aa07d49166
4 changed files with 799 additions and 492 deletions

View File

@ -1,32 +1,66 @@
"use client" "use client";
import AnimatedCircularProgressBar from "@/components/ui/animated-circular-progress-bar"; import AnimatedCircularProgressBar from "@/components/ui/animated-circular-progress-bar";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import useSWR from "swr"; import {
import { NezhaAPISafe } from "../../types/nezha-api"; ChartConfig,
import { formatNezhaInfo, formatRelativeTime, formatTime, nezhaFetcher } from "@/lib/utils"; ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import getEnv from "@/lib/env-entry"; import getEnv from "@/lib/env-entry";
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; import {
import { Area, AreaChart, CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"; formatNezhaInfo,
formatRelativeTime,
formatTime,
nezhaFetcher,
} from "@/lib/utils";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import {
Area,
AreaChart,
CartesianGrid,
Line,
LineChart,
XAxis,
YAxis,
} from "recharts";
import useSWR from "swr";
import { NezhaAPISafe } from "../../types/nezha-api";
type cpuChartData = { type cpuChartData = {
timeStamp: string; timeStamp: string;
cpu: number; cpu: number;
} };
type processChartData = {
timeStamp: string;
process: number;
};
type diskChartData = {
timeStamp: string;
disk: number;
};
type memChartData = { type memChartData = {
timeStamp: string; timeStamp: string;
mem: number; mem: number;
swap: number; swap: number;
} };
type networkChartData = { type networkChartData = {
timeStamp: string; timeStamp: string;
upload: number; upload: number;
download: number; download: number;
} };
type connectChartData = {
timeStamp: string;
tcp: number;
udp: number;
};
export default function ServerDetailChartClient({ export default function ServerDetailChartClient({
server_id, server_id,
@ -59,10 +93,13 @@ export default function ServerDetailChartClient({
return ( return (
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3"> <section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3">
<CpuChart data={data} /> <CpuChart data={data} />
<ProcessChart data={data} />
<DiskChart data={data} />
<MemChart data={data} /> <MemChart data={data} />
<NetworkChart data={data} /> <NetworkChart data={data} />
<ConnectChart data={data} />
</section> </section>
) );
} }
function CpuChart({ data }: { data: NezhaAPISafe }) { function CpuChart({ data }: { data: NezhaAPISafe }) {
@ -73,10 +110,7 @@ function CpuChart({ data }: { data: NezhaAPISafe }) {
useEffect(() => { useEffect(() => {
if (data) { if (data) {
const timestamp = Date.now().toString(); const timestamp = Date.now().toString();
const newData = [ const newData = [...cpuChartData, { timeStamp: timestamp, cpu: cpu }];
...cpuChartData,
{ timeStamp: timestamp, cpu: cpu },
];
if (newData.length > 30) { if (newData.length > 30) {
newData.shift(); newData.shift();
} }
@ -88,16 +122,14 @@ function CpuChart({ data }: { data: NezhaAPISafe }) {
cpu: { cpu: {
label: "CPU", label: "CPU",
}, },
} satisfies ChartConfig } satisfies ChartConfig;
return ( return (
<Card className=" rounded-sm"> <Card className=" rounded-sm">
<CardContent className="px-6 py-3"> <CardContent className="px-6 py-3">
<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-md font-medium">CPU</p>
CPU
</p>
<section className="flex items-center gap-2"> <section className="flex items-center gap-2">
<p className="text-xs text-end w-10 font-medium"> <p className="text-xs text-end w-10 font-medium">
{cpu.toFixed(0)}% {cpu.toFixed(0)}%
@ -111,7 +143,10 @@ function CpuChart({ data }: { data: NezhaAPISafe }) {
/> />
</section> </section>
</div> </div>
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full"> <ChartContainer
config={chartConfig}
className="aspect-auto h-[130px] w-full"
>
<AreaChart <AreaChart
accessibilityLayer accessibilityLayer
data={cpuChartData} data={cpuChartData}
@ -152,7 +187,89 @@ function CpuChart({ data }: { data: NezhaAPISafe }) {
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
) );
}
function ProcessChart({ data }: { data: NezhaAPISafe }) {
const [processChartData, setProcessChartData] = useState(
[] as processChartData[],
);
const { process } = formatNezhaInfo(data);
useEffect(() => {
if (data) {
const timestamp = Date.now().toString();
const newData = [
...processChartData,
{ timeStamp: timestamp, process: process },
];
if (newData.length > 30) {
newData.shift();
}
setProcessChartData(newData);
}
}, [data]);
const chartConfig = {
process: {
label: "Process",
},
} 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 justify-between">
<p className="text-md font-medium">Process</p>
<section className="flex items-center gap-2">
<p className="text-xs text-end w-10 font-medium">{process}</p>
</section>
</div>
<ChartContainer
config={chartConfig}
className="aspect-auto h-[130px] w-full"
>
<AreaChart
accessibilityLayer
data={processChartData}
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}
/>
<Area
isAnimationActive={false}
dataKey="process"
type="step"
fill="hsl(var(--chart-2))"
fillOpacity={0.3}
stroke="hsl(var(--chart-2))"
/>
</AreaChart>
</ChartContainer>
</section>
</CardContent>
</Card>
);
} }
function MemChart({ data }: { data: NezhaAPISafe }) { function MemChart({ data }: { data: NezhaAPISafe }) {
@ -181,7 +298,7 @@ function MemChart({ data }: { data: NezhaAPISafe }) {
swap: { swap: {
label: "Swap", label: "Swap",
}, },
} satisfies ChartConfig } satisfies ChartConfig;
return ( return (
<Card className=" rounded-sm"> <Card className=" rounded-sm">
@ -197,11 +314,9 @@ function MemChart({ data }: { data: NezhaAPISafe }) {
max={100} max={100}
min={0} min={0}
value={mem} value={mem}
primaryColor="hsl(var(--chart-1))" primaryColor="hsl(var(--chart-8))"
/> />
<p className="text-xs font-medium"> <p className="text-xs font-medium">{mem.toFixed(0)}%</p>
{mem.toFixed(0)}%
</p>
</div> </div>
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
@ -212,17 +327,17 @@ function MemChart({ data }: { data: NezhaAPISafe }) {
max={100} max={100}
min={0} min={0}
value={swap} value={swap}
primaryColor="hsl(var(--chart-4))" primaryColor="hsl(var(--chart-10))"
/> />
<p className="text-xs font-medium"> <p className="text-xs font-medium">{swap.toFixed(0)}%</p>
{swap.toFixed(0)}%
</p>
</div> </div>
</div> </div>
</section> </section>
</div> </div>
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full"> <ChartContainer
config={chartConfig}
className="aspect-auto h-[130px] w-full"
>
<AreaChart <AreaChart
accessibilityLayer accessibilityLayer
data={memChartData} data={memChartData}
@ -254,30 +369,118 @@ function MemChart({ data }: { data: NezhaAPISafe }) {
isAnimationActive={false} isAnimationActive={false}
dataKey="mem" dataKey="mem"
type="step" type="step"
fill="hsl(var(--chart-1))" fill="hsl(var(--chart-8))"
fillOpacity={0.3} fillOpacity={0.3}
stroke="hsl(var(--chart-1))" stroke="hsl(var(--chart-8))"
/> />
<Area <Area
isAnimationActive={false} isAnimationActive={false}
dataKey="swap" dataKey="swap"
type="step" type="step"
fill="hsl(var(--chart-4))" fill="hsl(var(--chart-10))"
fillOpacity={0.3} fillOpacity={0.3}
stroke="hsl(var(--chart-4))" stroke="hsl(var(--chart-10))"
/> />
</AreaChart> </AreaChart>
</ChartContainer> </ChartContainer>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
) );
} }
function DiskChart({ data }: { data: NezhaAPISafe }) {
const [diskChartData, setDiskChartData] = useState([] as diskChartData[]);
const { disk } = formatNezhaInfo(data);
useEffect(() => {
if (data) {
const timestamp = Date.now().toString();
const newData = [...diskChartData, { timeStamp: timestamp, disk: disk }];
if (newData.length > 30) {
newData.shift();
}
setDiskChartData(newData);
}
}, [data]);
const chartConfig = {
disk: {
label: "Disk",
},
} 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 justify-between">
<p className="text-md font-medium">Disk</p>
<section className="flex items-center gap-2">
<p className="text-xs text-end w-10 font-medium">
{disk.toFixed(0)}%
</p>
<AnimatedCircularProgressBar
className="size-3 text-[0px]"
max={100}
min={0}
value={disk}
primaryColor="hsl(var(--chart-5))"
/>
</section>
</div>
<ChartContainer
config={chartConfig}
className="aspect-auto h-[130px] w-full"
>
<AreaChart
accessibilityLayer
data={diskChartData}
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="disk"
type="step"
fill="hsl(var(--chart-5))"
fillOpacity={0.3}
stroke="hsl(var(--chart-5))"
/>
</AreaChart>
</ChartContainer>
</section>
</CardContent>
</Card>
);
}
function NetworkChart({ data }: { data: NezhaAPISafe }) { function NetworkChart({ data }: { data: NezhaAPISafe }) {
const [networkChartData, setNetworkChartData] = useState([] as networkChartData[]); const [networkChartData, setNetworkChartData] = useState(
[] as networkChartData[],
);
const { up, down } = formatNezhaInfo(data); const { up, down } = formatNezhaInfo(data);
@ -308,7 +511,7 @@ function NetworkChart({ data }: { data: NezhaAPISafe }) {
download: { download: {
label: "Download", label: "Download",
}, },
} satisfies ChartConfig } satisfies ChartConfig;
return ( return (
<Card className=" rounded-sm"> <Card className=" rounded-sm">
@ -320,24 +523,22 @@ function NetworkChart({ data }: { data: NezhaAPISafe }) {
<p className="text-xs text-muted-foreground">Upload</p> <p className="text-xs text-muted-foreground">Upload</p>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-1))]"></span> <span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-1))]"></span>
<p className="text-xs font-medium"> <p className="text-xs font-medium">{up.toFixed(2)} M/s</p>
{up.toFixed(2)} M/s
</p>
</div> </div>
</div> </div>
<div className="flex flex-col w-20"> <div className="flex flex-col w-20">
<p className=" text-xs text-muted-foreground">Download</p> <p className=" text-xs text-muted-foreground">Download</p>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-4))]"></span> <span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-4))]"></span>
<p className="text-xs font-medium"> <p className="text-xs font-medium">{down.toFixed(2)} M/s</p>
{down.toFixed(2)} M/s
</p>
</div> </div>
</div> </div>
</section> </section>
</div> </div>
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full"> <ChartContainer
config={chartConfig}
className="aspect-auto h-[130px] w-full"
>
<LineChart <LineChart
accessibilityLayer accessibilityLayer
data={networkChartData} data={networkChartData}
@ -389,5 +590,112 @@ function NetworkChart({ data }: { data: NezhaAPISafe }) {
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
) );
}
function ConnectChart({ data }: { data: NezhaAPISafe }) {
const [connectChartData, setConnectChartData] = useState(
[] as connectChartData[],
);
const { tcp, udp } = formatNezhaInfo(data);
useEffect(() => {
if (data) {
const timestamp = Date.now().toString();
const newData = [
...connectChartData,
{ timeStamp: timestamp, tcp: tcp, udp: udp },
];
if (newData.length > 30) {
newData.shift();
}
setConnectChartData(newData);
}
}, [data]);
const chartConfig = {
tcp: {
label: "TCP",
},
udp: {
label: "UDP",
},
} 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-12">
<p className="text-xs text-muted-foreground">TCP</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">{tcp}</p>
</div>
</div>
<div className="flex flex-col w-12">
<p className=" text-xs text-muted-foreground">UDP</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">{udp}</p>
</div>
</div>
</section>
</div>
<ChartContainer
config={chartConfig}
className="aspect-auto h-[130px] w-full"
>
<LineChart
accessibilityLayer
data={connectChartData}
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"
interval="preserveStartEnd"
/>
<Line
isAnimationActive={false}
dataKey="tcp"
type="linear"
stroke="hsl(var(--chart-1))"
strokeWidth={1}
dot={false}
/>
<Line
isAnimationActive={false}
dataKey="udp"
type="linear"
stroke="hsl(var(--chart-4))"
strokeWidth={1}
dot={false}
/>
</LineChart>
</ChartContainer>
</section>
</CardContent>
</Card>
);
} }

View File

@ -2,7 +2,6 @@
import { NezhaAPISafe } from "@/app/[locale]/types/nezha-api"; import { NezhaAPISafe } from "@/app/[locale]/types/nezha-api";
import { BackIcon } from "@/components/Icon"; import { BackIcon } from "@/components/Icon";
import AnimatedCircularProgressBar from "@/components/ui/animated-circular-progress-bar";
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";
@ -50,14 +49,14 @@ export default function ServerDetailClient({
<BackIcon /> <BackIcon />
{data?.name} {data?.name}
</div> </div>
<section className="flex flex-wrap gap-2 mt-2"> <section className="flex flex-wrap gap-2 mt-3">
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <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 flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">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-[10px] rounded-[6px] w-fit px-1 py-0 dark:text-white",
{ {
" bg-green-800": data?.online_status, " bg-green-800": data?.online_status,
" bg-red-600": !data?.online_status, " bg-red-600": !data?.online_status,
@ -84,9 +83,7 @@ export default function ServerDetailClient({
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">Version</p> <p className="text-xs text-muted-foreground">Version</p>
<div className="text-xs"> <div className="text-xs">{data?.host.Version || "Unknown"} </div>
{data?.host.Version || "Unknown"}{" "}
</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
@ -94,9 +91,7 @@ export default function ServerDetailClient({
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">Arch</p> <p className="text-xs text-muted-foreground">Arch</p>
<div className="text-xs"> <div className="text-xs">{data?.host.Arch || "Unknown"} </div>
{data?.host.Arch || "Unknown"}{" "}
</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
@ -104,9 +99,7 @@ export default function ServerDetailClient({
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">Mem</p> <p className="text-xs text-muted-foreground">Mem</p>
<div className="text-xs"> <div className="text-xs">{formatBytes(data?.host.MemTotal)}</div>
{formatBytes(data?.host.MemTotal)}
</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
@ -114,9 +107,7 @@ export default function ServerDetailClient({
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">Disk</p> <p className="text-xs text-muted-foreground">Disk</p>
<div className="text-xs"> <div className="text-xs">{formatBytes(data?.host.DiskTotal)}</div>
{formatBytes(data?.host.DiskTotal)}
</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
@ -131,7 +122,10 @@ export default function ServerDetailClient({
{" "} {" "}
{data?.host.Platform || "Unknown"} -{" "} {data?.host.Platform || "Unknown"} -{" "}
{data?.host.PlatformVersion}{" "} {data?.host.PlatformVersion}{" "}
</div>) : <div className="text-xs">Unknown</div>} </div>
) : (
<div className="text-xs">Unknown</div>
)}
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
@ -140,10 +134,10 @@ export default function ServerDetailClient({
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">CPU</p> <p className="text-xs text-muted-foreground">CPU</p>
{data?.host.CPU ? ( {data?.host.CPU ? (
<div className="text-xs"> <div className="text-xs"> {data?.host.CPU}</div>
{" "} ) : (
{data?.host.CPU} <div className="text-xs">Unknown</div>
</div>) : <div className="text-xs">Unknown</div>} )}
</section> </section>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -1,12 +1,13 @@
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 ServerDetailClient from "@/app/[locale]/(main)/ClientComponents/ServerDetailClient";
import { Separator } from "@/components/ui/separator"; 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" /> <Separator className="mt-1 mb-2" />
<ServerDetailChartClient server_id={Number(params.id)} /> <ServerDetailChartClient server_id={Number(params.id)} />
</div> </div>
; );
} }

View File

@ -10,11 +10,15 @@ export function formatNezhaInfo(serverInfo: NezhaAPISafe) {
return { return {
...serverInfo, ...serverInfo,
cpu: serverInfo.status.CPU, cpu: serverInfo.status.CPU,
process: serverInfo.status.ProcessCount,
up: serverInfo.status.NetOutSpeed / 1024 / 1024, up: serverInfo.status.NetOutSpeed / 1024 / 1024,
down: serverInfo.status.NetInSpeed / 1024 / 1024, down: serverInfo.status.NetInSpeed / 1024 / 1024,
online: serverInfo.online_status, online: serverInfo.online_status,
tcp: serverInfo.status.TcpConnCount,
udp: serverInfo.status.UdpConnCount,
mem: (serverInfo.status.MemUsed / serverInfo.host.MemTotal) * 100, mem: (serverInfo.status.MemUsed / serverInfo.host.MemTotal) * 100,
swap: (serverInfo.status.SwapUsed / serverInfo.host.SwapTotal) * 100, swap: (serverInfo.status.SwapUsed / serverInfo.host.SwapTotal) * 100,
disk: (serverInfo.status.DiskUsed / serverInfo.host.DiskTotal) * 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,
}; };