feat: init cpu chart

This commit is contained in:
hamster1963 2024-10-18 18:01:32 +08:00
parent d7f36ce144
commit dbe67ab205
3 changed files with 353 additions and 223 deletions

View File

@ -0,0 +1,140 @@
"use client"
import AnimatedCircularProgressBar from "@/components/ui/animated-circular-progress-bar";
import { Card, CardContent } from "@/components/ui/card";
import useSWR from "swr";
import { NezhaAPISafe } from "../../types/nezha-api";
import { formatRelativeTime, formatTime, nezhaFetcher } from "@/lib/utils";
import getEnv from "@/lib/env-entry";
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
import { useEffect, useState } from "react";
type cpuChartData = {
timeStamp: string;
cpu: number;
}
export default function ServerDetailChartClient({
server_id,
}: {
server_id: number;
}) {
const { data, error } = useSWR<NezhaAPISafe>(
`/api/detail?server_id=${server_id}`,
nezhaFetcher,
{
refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 5000,
},
);
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) {
return (
<>
<div className="flex flex-col items-center justify-center">
<p className="text-sm font-medium opacity-40">{error.message}</p>
<p className="text-sm font-medium opacity-40">
{/* {t("chart_fetch_error_message")} */}
fetch_error_message
</p>
</div>
</>
);
}
if (!data) return null;
const chartConfig = {
cpu: {
label: "CPU",
},
} satisfies ChartConfig
return (
<section className="grid md:grid-cols-3 grid-cols-1">
<Card className=" rounded-sm">
<CardContent className="px-6 py-3 h-[217px]">
<section className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<p className="text-sm font-medium md:text-base">
CPU
</p>
<section className="flex items-center gap-2">
<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>
</div>
<ChartContainer config={chartConfig}>
<AreaChart
accessibilityLayer
data={chartData}
margin={{
top: 12,
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="timeStamp"
tickLine={false}
axisLine={false}
ticks={[chartData[0]?.timeStamp, chartData[chartData.length - 1]?.timeStamp]}
tickFormatter={(value) => formatRelativeTime(value)}
/>
<YAxis
tickLine={false}
axisLine={false}
mirror={true}
tickMargin={-15}
domain={[0, 100]}
tickFormatter={(value) => `${value}%`}
/>
{/* <ChartTooltip
isAnimationActive={false}
cursor={false}
content={<ChartTooltipContent indicator={"line"} labelFormatter={(_, payload) => {
return formatTime(Number(payload[0].payload.timeStamp));
}} />}
/> */}
<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>
</section>
)
}

View File

@ -12,232 +12,217 @@ import { useRouter } from "next/navigation";
import useSWR from "swr"; import useSWR from "swr";
export default function ServerDetailClient({ export default function ServerDetailClient({
server_id, server_id,
}: { }: {
server_id: number; server_id: number;
}) { }) {
const router = useRouter(); const router = useRouter();
const locale = useLocale(); const locale = useLocale();
const { data, error } = useSWR<NezhaAPISafe>( const { data, error } = useSWR<NezhaAPISafe>(
`/api/detail?server_id=${server_id}`, `/api/detail?server_id=${server_id}`,
nezhaFetcher, nezhaFetcher,
{ {
refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 5000, refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 5000,
}, },
);
if (error) {
return (
<>
<div className="flex flex-col items-center justify-center">
<p className="text-sm font-medium opacity-40">{error.message}</p>
<p className="text-sm font-medium opacity-40">
{/* {t("chart_fetch_error_message")} */}
fetch_error_message
</p>
</div>
</>
); );
} if (error) {
if (!data) return null; return (
return ( <>
<div className="mx-auto grid w-full max-w-5xl gap-1"> <div className="flex flex-col items-center justify-center">
<div <p className="text-sm font-medium opacity-40">{error.message}</p>
onClick={() => { <p className="text-sm font-medium opacity-40">
router.push(`/${locale}/`); {/* {t("chart_fetch_error_message")} */}
}} fetch_error_message
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl" </p>
>
<BackIcon />
{data?.name}
</div>
<section className="flex flex-wrap gap-2 mt-2">
<Card className="rounded-[10px]">
<CardContent className="px-1.5 py-1">
<section className="flex items-center gap-2">
<p className="text-xs font-semibold">Status</p>
<Badge
className={cn(
"text-xs rounded-[6px] w-fit px-1 py-0 dark:text-white",
{
" bg-green-800": data?.online_status,
" bg-red-600": !data?.online_status,
},
)}
>
{data?.online_status ? "Online" : "Offline"}
</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">Uptime</p>
<Badge
className="text-xs rounded-[6px] w-fit px-1 py-0"
variant="secondary"
>
{" "}
{(data?.status.Uptime / 86400).toFixed(0)} Days{" "}
</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">Tag</p>
<Badge
className="text-xs rounded-[6px] w-fit px-1 py-0"
variant="secondary"
>
{" "}
{data?.tag || "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">Arch</p>
<Badge
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"}{" "}
</Badge>
</section>
</CardContent>
</Card>
</section>
<section className="flex flex-wrap gap-2 mt-1">
<Card className="rounded-[10px] flex flex-col justify-center">
<CardContent className="px-1.5 py-1">
<section className="flex items-center gap-2">
<p className="text-xs font-semibold">System</p>
{data?.host.Platform ? (
<div className="text-xs w-fit">
{" "}
{data?.host.Platform || "Unknown"} -{" "}
{data?.host.PlatformVersion}{" "}
</div> </div>
) : ( </>
<div className="text-xs w-fit"> Unknown </div> );
)} }
if (!data) return null;
return (
<div>
<div
onClick={() => {
router.push(`/${locale}/`);
}}
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl"
>
<BackIcon />
{data?.name}
</div>
<section className="flex flex-wrap gap-2 mt-2">
<Card className="rounded-[10px]">
<CardContent className="px-1.5 py-1">
<section className="flex items-center gap-2">
<p className="text-xs font-semibold">Status</p>
<Badge
className={cn(
"text-xs rounded-[6px] w-fit px-1 py-0 dark:text-white",
{
" bg-green-800": data?.online_status,
" bg-red-600": !data?.online_status,
},
)}
>
{data?.online_status ? "Online" : "Offline"}
</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">Uptime</p>
<Badge
className="text-xs rounded-[6px] w-fit px-1 py-0"
variant="secondary"
>
{" "}
{(data?.status.Uptime / 86400).toFixed(0)} Days{" "}
</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">Arch</p>
<Badge
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"}{" "}
</Badge>
</section>
</CardContent>
</Card>
</section> </section>
</CardContent> <section className="flex flex-wrap gap-2 mt-2">
</Card> <Card className="rounded-[10px] flex flex-col justify-center">
<Card className="rounded-[10px] flex flex-col justify-center"> <CardContent className="px-1.5 py-1">
<CardContent className="px-1.5 py-1"> <section className="flex items-center gap-2">
<section className="flex items-center gap-2"> <p className="text-xs font-semibold">System</p>
<p className="text-xs font-semibold">CPU</p> {data?.host.Platform ? (
{data?.host.CPU ? ( <div className="text-xs w-fit font-medium">
<div className="text-xs w-fit"> {" "}
{" "} {data?.host.Platform || "Unknown"} -{" "}
{data?.host.CPU || "Unknown"} {data?.host.PlatformVersion}{" "}
</div> </div>
) : ( ) : (
<div className="text-xs w-fit"> Unknown </div> <div className="text-xs w-fit font-medium"> Unknown </div>
)} )}
</section>
</CardContent>
</Card>
<Card className="rounded-[10px] flex flex-col justify-center">
<CardContent className="px-1.5 py-1">
<section className="flex items-center gap-2">
<p className="text-xs font-semibold">CPU</p>
{data?.host.CPU ? (
<div className="text-xs w-fit font-medium">
{" "}
{data?.host.CPU || "Unknown"}
</div>
) : (
<div className="text-xs w-fit font-medium"> Unknown </div>
)}
</section>
</CardContent>
</Card>
</section> </section>
</CardContent> {/* <section className="flex flex-wrap gap-2 mt-1">
</Card> <Card className="rounded-[10px]">
</section> <CardContent className="px-1.5 py-1">
<section className="flex flex-wrap gap-2 mt-1"> <section className="flex items-center gap-2">
<Card className="rounded-[10px]"> <p className="text-xs font-semibold">CPU</p>
<CardContent className="px-1.5 py-1"> <p className="text-xs text-end w-10 font-medium">
<section className="flex items-center gap-2"> {data?.status.CPU.toFixed(0)}%
<p className="text-xs font-semibold">CPU</p> </p>
<p className="text-xs text-end w-10 font-medium"> <AnimatedCircularProgressBar
{data?.status.CPU.toFixed(0)}% className="size-3 text-[0px]"
</p> max={100}
<AnimatedCircularProgressBar min={0}
className="size-3 text-[0px]" value={data?.status.CPU}
max={100} />
min={0} </section>
value={data?.status.CPU} </CardContent>
/> </Card>
</section> <Card className="rounded-[10px]">
</CardContent> <CardContent className="px-1.5 py-1">
</Card> <section className="flex items-center gap-2">
<Card className="rounded-[10px]"> <p className="text-xs font-semibold">Mem</p>
<CardContent className="px-1.5 py-1"> <p className="text-xs w-10 text-end font-medium">
<section className="flex items-center gap-2"> {((data?.status.MemUsed / data?.host.MemTotal) * 100).toFixed(
<p className="text-xs font-semibold">Mem</p> 0,
<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]"
</p> max={100}
<AnimatedCircularProgressBar min={0}
className="size-3 text-[0px]" value={(data?.status.MemUsed / data?.host.MemTotal) * 100}
max={100} />
min={0} </section>
value={(data?.status.MemUsed / data?.host.MemTotal) * 100} </CardContent>
/> </Card>
</section> <Card className="rounded-[10px]">
</CardContent> <CardContent className="px-1.5 py-1">
</Card> <section className="flex items-center gap-2">
<Card className="rounded-[10px]"> <p className="text-xs font-semibold">Swap</p>
<CardContent className="px-1.5 py-1"> <p className="text-xs w-10 text-end font-medium">
<section className="flex items-center gap-2"> {data?.status.SwapUsed
<p className="text-xs font-semibold">Swap</p> ? (
<p className="text-xs w-10 text-end font-medium"> (data?.status.SwapUsed / data?.host.SwapTotal) *
{data?.status.SwapUsed 100
? ( ).toFixed(0)
(data?.status.SwapUsed / data?.host.SwapTotal) * : 0}
100 %
).toFixed(0) </p>
: 0} <AnimatedCircularProgressBar
% className="size-3 text-[0px]"
</p> max={100}
<AnimatedCircularProgressBar min={0}
className="size-3 text-[0px]" value={(data?.status.SwapUsed / data?.host.SwapTotal) * 100}
max={100} />
min={0} </section>
value={(data?.status.SwapUsed / data?.host.SwapTotal) * 100} </CardContent>
/> </Card>
</section> <Card className="rounded-[10px]">
</CardContent> <CardContent className="px-1.5 py-1">
</Card> <section className="flex items-center gap-2">
<Card className="rounded-[10px]"> <p className="text-xs font-semibold">Disk</p>
<CardContent className="px-1.5 py-1"> <p className="text-xs w-10 text-end font-medium">
<section className="flex items-center gap-2"> {((data?.status.DiskUsed / data?.host.DiskTotal) * 100).toFixed(
<p className="text-xs font-semibold">Disk</p> 0,
<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]"
</p> max={100}
<AnimatedCircularProgressBar min={0}
className="size-3 text-[0px]" value={(data?.status.DiskUsed / data?.host.DiskTotal) * 100}
max={100} />
min={0} </section>
value={(data?.status.DiskUsed / data?.host.DiskTotal) * 100} </CardContent>
/> </Card>
</section> </section> */}
</CardContent> </div>
</Card> );
</section>
</div>
);
} }

View File

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