mirror of
https://github.com/hamster1963/nezha-dash.git
synced 2025-04-24 21:10:45 +08:00
feat: init cpu chart
This commit is contained in:
parent
d7f36ce144
commit
dbe67ab205
140
app/[locale]/(main)/ClientComponents/ServerDetailChartClient.tsx
Normal file
140
app/[locale]/(main)/ClientComponents/ServerDetailChartClient.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -12,232 +12,217 @@ import { useRouter } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function ServerDetailClient({
|
||||
server_id,
|
||||
server_id,
|
||||
}: {
|
||||
server_id: number;
|
||||
server_id: number;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const locale = useLocale();
|
||||
const { data, error } = useSWR<NezhaAPISafe>(
|
||||
`/api/detail?server_id=${server_id}`,
|
||||
nezhaFetcher,
|
||||
{
|
||||
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>
|
||||
</>
|
||||
const router = useRouter();
|
||||
const locale = useLocale();
|
||||
const { data, error } = useSWR<NezhaAPISafe>(
|
||||
`/api/detail?server_id=${server_id}`,
|
||||
nezhaFetcher,
|
||||
{
|
||||
refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 5000,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (!data) return null;
|
||||
return (
|
||||
<div className="mx-auto grid w-full max-w-5xl gap-1">
|
||||
<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">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}{" "}
|
||||
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>
|
||||
) : (
|
||||
<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>
|
||||
</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">
|
||||
{" "}
|
||||
{data?.host.CPU || "Unknown"}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs w-fit"> Unknown </div>
|
||||
)}
|
||||
<section className="flex flex-wrap gap-2 mt-2">
|
||||
<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 font-medium">
|
||||
{" "}
|
||||
{data?.host.Platform || "Unknown"} -{" "}
|
||||
{data?.host.PlatformVersion}{" "}
|
||||
</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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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>
|
||||
);
|
||||
{/* <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>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,10 @@
|
||||
import ServerDetailClient from "@/app/[locale]/(main)/ClientComponents/ServerDetailClient";
|
||||
import ServerDetailChartClient from "@/app/[locale]/(main)/ClientComponents/ServerDetailChartClient";
|
||||
|
||||
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>
|
||||
;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user