Merge pull request #46 from hamster1963/new-chart

feat: overview chart
This commit is contained in:
仓鼠 2024-10-09 22:51:47 +08:00 committed by GitHub
commit e3ce8ad574
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 124 additions and 65 deletions

View File

@ -17,7 +17,7 @@ import {
ChartTooltipContent, ChartTooltipContent,
} from "@/components/ui/chart"; } from "@/components/ui/chart";
import useSWR from "swr"; import useSWR from "swr";
import { ServerMonitorChart } from "../../types/nezha-api"; import { NezhaAPIMonitor, ServerMonitorChart } from "../../types/nezha-api";
import { formatTime, nezhaFetcher } from "@/lib/utils"; import { formatTime, nezhaFetcher } from "@/lib/utils";
import { formatRelativeTime } from "@/lib/utils"; import { formatRelativeTime } from "@/lib/utils";
import { BackIcon } from "@/components/Icon"; import { BackIcon } from "@/components/Icon";
@ -26,9 +26,14 @@ import { useLocale } from "next-intl";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import NetworkChartLoading from "./NetworkChartLoading"; import NetworkChartLoading from "./NetworkChartLoading";
interface ResultItem {
created_at: number;
[key: string]: number;
}
export function NetworkChartClient({ server_id }: { server_id: number }) { export function NetworkChartClient({ server_id }: { server_id: number }) {
const t = useTranslations("NetworkChartClient"); const t = useTranslations("NetworkChartClient");
const { data, error } = useSWR<ServerMonitorChart>( const { data, error } = useSWR<NezhaAPIMonitor[]>(
`/api/monitor?server_id=${server_id}`, `/api/monitor?server_id=${server_id}`,
nezhaFetcher, nezhaFetcher,
); );
@ -41,19 +46,64 @@ export function NetworkChartClient({ server_id }: { server_id: number }) {
); );
if (!data) return <NetworkChartLoading />; if (!data) return <NetworkChartLoading />;
function transformData(data: NezhaAPIMonitor[]) {
const monitorData: ServerMonitorChart = {};
data.forEach((item) => {
const monitorName = item.monitor_name;
if (!monitorData[monitorName]) {
monitorData[monitorName] = [];
}
for (let i = 0; i < item.created_at.length; i++) {
monitorData[monitorName].push({
created_at: item.created_at[i],
avg_delay: item.avg_delay[i],
});
}
});
return monitorData;
}
const formatData = (rawData: NezhaAPIMonitor[]) => {
const result: { [time: number]: ResultItem } = {};
// 遍历每个监控项
rawData.forEach((item) => {
const { monitor_name, created_at, avg_delay } = item;
created_at.forEach((time, index) => {
if (!result[time]) {
result[time] = { created_at: time };
}
result[time][monitor_name] = parseFloat(avg_delay[index].toFixed(2));
});
});
return Object.values(result).sort((a, b) => a.created_at - b.created_at);
};
const transformedData = transformData(data);
const formattedData = formatData(data);
const initChartConfig = { const initChartConfig = {
avg_delay: { avg_delay: {
label: t("avg_delay"), label: t("avg_delay"),
}, },
} satisfies ChartConfig; } satisfies ChartConfig;
const chartDataKey = Object.keys(data); const chartDataKey = Object.keys(transformedData);
return ( return (
<NetworkChart <NetworkChart
chartDataKey={chartDataKey} chartDataKey={chartDataKey}
chartConfig={initChartConfig} chartConfig={initChartConfig}
chartData={data} chartData={transformedData}
serverName={data[0].server_name}
formattedData={formattedData}
/> />
); );
} }
@ -62,22 +112,34 @@ export function NetworkChart({
chartDataKey, chartDataKey,
chartConfig, chartConfig,
chartData, chartData,
serverName,
formattedData,
}: { }: {
chartDataKey: string[]; chartDataKey: string[];
chartConfig: ChartConfig; chartConfig: ChartConfig;
chartData: ServerMonitorChart; chartData: ServerMonitorChart;
serverName: string;
formattedData: ResultItem[];
}) { }) {
const t = useTranslations("NetworkChart"); const t = useTranslations("NetworkChart");
const router = useRouter(); const router = useRouter();
const locale = useLocale(); const locale = useLocale();
const [activeChart, setActiveChart] = React.useState< const defaultChart = "All";
keyof typeof chartConfig
>(chartDataKey[0]); const [activeChart, setActiveChart] = React.useState(defaultChart);
const handleButtonClick = (chart: string) => {
if (chart === activeChart) {
setActiveChart(defaultChart);
} else {
setActiveChart(chart);
}
};
const getColorByIndex = (chart: string) => { const getColorByIndex = (chart: string) => {
const index = chartDataKey.indexOf(chart); const index = chartDataKey.indexOf(chart);
return `hsl(var(--chart-${(index % 5) + 1}))`; return `hsl(var(--chart-${(index % 10) + 1}))`;
}; };
return ( return (
@ -91,24 +153,23 @@ export function NetworkChart({
className="flex flex-none cursor-pointer items-center gap-0.5 text-xl" className="flex flex-none cursor-pointer items-center gap-0.5 text-xl"
> >
<BackIcon /> <BackIcon />
{chartData[chartDataKey[0]][0].server_name} {serverName}
</CardTitle> </CardTitle>
<CardDescription className="text-xs"> <CardDescription className="text-xs">
{chartDataKey.length} {t("ServerMonitorCount")} {chartDataKey.length} {t("ServerMonitorCount")}
</CardDescription> </CardDescription>
</div> </div>
<div className="flex flex-wrap"> <div className="flex flex-wrap">
{chartDataKey.map((key, index) => { {chartDataKey.map((key) => {
const chart = key as keyof typeof chartConfig;
return ( return (
<button <button
key={key} key={key}
data-active={activeChart === key} data-active={activeChart === key}
className={`relative z-30 flex flex-1 flex-col justify-center gap-1 border-b px-6 py-4 text-left data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-6`} className={`relative z-30 flex flex-1 flex-col justify-center gap-1 border-b px-6 py-4 text-left data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-6`}
onClick={() => setActiveChart(key as keyof typeof chartConfig)} onClick={() => handleButtonClick(key)}
> >
<span className="whitespace-nowrap text-xs text-muted-foreground"> <span className="whitespace-nowrap text-xs text-muted-foreground">
{chart} {key}
</span> </span>
<span className="text-md font-bold leading-none sm:text-lg"> <span className="text-md font-bold leading-none sm:text-lg">
{chartData[key][chartData[key].length - 1].avg_delay.toFixed( {chartData[key][chartData[key].length - 1].avg_delay.toFixed(
@ -128,7 +189,11 @@ export function NetworkChart({
> >
<LineChart <LineChart
accessibilityLayer accessibilityLayer
data={chartData[activeChart]} data={
activeChart === defaultChart
? formattedData
: chartData[activeChart]
}
margin={{ margin={{
left: 12, left: 12,
right: 12, right: 12,
@ -143,7 +208,6 @@ export function NetworkChart({
tickFormatter={(value) => formatRelativeTime(value)} tickFormatter={(value) => formatRelativeTime(value)}
/> />
<YAxis <YAxis
dataKey="avg_delay"
tickLine={false} tickLine={false}
axisLine={false} axisLine={false}
mirror={true} mirror={true}
@ -155,8 +219,7 @@ export function NetworkChart({
content={ content={
<ChartTooltipContent <ChartTooltipContent
indicator={"dot"} indicator={"dot"}
className="w-fit" className="gap-2"
nameKey="avg_delay"
labelKey="created_at" labelKey="created_at"
labelClassName="text-muted-foreground" labelClassName="text-muted-foreground"
labelFormatter={(_, payload) => { labelFormatter={(_, payload) => {
@ -165,14 +228,28 @@ export function NetworkChart({
/> />
} }
/> />
<Line {activeChart !== defaultChart && (
isAnimationActive={false} <Line
strokeWidth={2} isAnimationActive={false}
type="linear" strokeWidth={2}
dot={false} type="linear"
dataKey="avg_delay" dot={false}
stroke={getColorByIndex(activeChart)} dataKey="avg_delay"
/> stroke={getColorByIndex(activeChart)}
/>
)}
{activeChart === defaultChart &&
chartDataKey.map((key) => (
<Line
key={key}
isAnimationActive={false}
strokeWidth={2}
type="linear"
dot={false}
dataKey={key}
stroke={getColorByIndex(key)}
/>
))}
</LineChart> </LineChart>
</ChartContainer> </ChartContainer>
</CardContent> </CardContent>

View File

@ -58,7 +58,6 @@ export interface NezhaAPIStatus {
export type ServerMonitorChart = { export type ServerMonitorChart = {
[key: string]: { [key: string]: {
server_name: string;
created_at: number; created_at: number;
avg_delay: number; avg_delay: number;
}[]; }[];

BIN
bun.lockb

Binary file not shown.

View File

@ -239,7 +239,7 @@ const ChartTooltipContent = React.forwardRef<
</span> </span>
</div> </div>
{item.value && ( {item.value && (
<span className="font-mono font-medium tabular-nums text-foreground"> <span className="ml-2 font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()} {item.value.toLocaleString()}
</span> </span>
)} )}

View File

@ -1,11 +1,6 @@
"use server"; "use server";
import { import { NezhaAPI, ServerApi } from "../app/[locale]/types/nezha-api";
NezhaAPI,
NezhaAPIMonitor,
ServerApi,
ServerMonitorChart,
} from "../app/[locale]/types/nezha-api";
import { MakeOptional } from "../app/[locale]/types/utils"; import { MakeOptional } from "../app/[locale]/types/utils";
import { unstable_noStore as noStore } from "next/cache"; import { unstable_noStore as noStore } from "next/cache";
import getEnv from "./env-entry"; import getEnv from "./env-entry";
@ -71,28 +66,6 @@ export async function GetNezhaData() {
} }
export async function GetServerMonitor({ server_id }: { server_id: number }) { export async function GetServerMonitor({ server_id }: { server_id: number }) {
function transformData(data: NezhaAPIMonitor[]) {
const monitorData: ServerMonitorChart = {};
data.forEach((item) => {
const monitorName = item.monitor_name;
if (!monitorData[monitorName]) {
monitorData[monitorName] = [];
}
for (let i = 0; i < item.created_at.length; i++) {
monitorData[monitorName].push({
server_name: item.server_name,
created_at: item.created_at[i],
avg_delay: item.avg_delay[i],
});
}
});
return monitorData;
}
var nezhaBaseUrl = getEnv("NezhaBaseUrl"); var nezhaBaseUrl = getEnv("NezhaBaseUrl");
if (!nezhaBaseUrl) { if (!nezhaBaseUrl) {
console.log("NezhaBaseUrl is not set"); console.log("NezhaBaseUrl is not set");
@ -122,7 +95,7 @@ export async function GetServerMonitor({ server_id }: { server_id: number }) {
console.log(resData); console.log(resData);
return { error: "MonitorData fetch failed" }; return { error: "MonitorData fetch failed" };
} }
return transformData(monitorData); return monitorData;
} catch (error) { } catch (error) {
return error; return error;
} }

View File

@ -28,11 +28,11 @@
"country-flag-icons": "^1.5.13", "country-flag-icons": "^1.5.13",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"flag-icons": "^7.2.3", "flag-icons": "^7.2.3",
"framer-motion": "^11.11.4", "framer-motion": "^11.11.7",
"lucide-react": "^0.451.0", "lucide-react": "^0.451.0",
"luxon": "^3.5.0", "luxon": "^3.5.0",
"next": "^14.2.15", "next": "^14.2.15",
"next-intl": "^3.20.0", "next-intl": "^3.21.1",
"next-runtime-env": "^3.2.2", "next-runtime-env": "^3.2.2",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"react": "^18.3.1", "react": "^18.3.1",

View File

@ -24,11 +24,16 @@
--input: 20 5.9% 90%; --input: 20 5.9% 90%;
--ring: 20 14.3% 4.1%; --ring: 20 14.3% 4.1%;
--radius: 1rem; --radius: 1rem;
--chart-1: 173 58% 39%; --chart-1: 220 70% 50%;
--chart-2: 12 76% 61%; --chart-2: 340 75% 55%;
--chart-3: 197 37% 24%; --chart-3: 30 80% 55%;
--chart-4: 43 74% 66%; --chart-4: 280 65% 60%;
--chart-5: 27 87% 67%; --chart-5: 160 60% 45%;
--chart-6: 180 50% 50%;
--chart-7: 216 50% 50%;
--chart-8: 252 50% 50%;
--chart-9: 288 50% 50%;
--chart-10: 324 50% 50%;
} }
.dark { .dark {
@ -52,10 +57,15 @@
--input: 12 6.5% 15.1%; --input: 12 6.5% 15.1%;
--ring: 24 5.7% 82.9%; --ring: 24 5.7% 82.9%;
--chart-1: 220 70% 50%; --chart-1: 220 70% 50%;
--chart-5: 160 60% 45%; --chart-2: 340 75% 55%;
--chart-3: 30 80% 55%; --chart-3: 30 80% 55%;
--chart-4: 280 65% 60%; --chart-4: 280 65% 60%;
--chart-2: 340 75% 55%; --chart-5: 160 60% 45%;
--chart-6: 180 50% 50%;
--chart-7: 216 50% 50%;
--chart-8: 252 50% 50%;
--chart-9: 288 50% 50%;
--chart-10: 324 50% 50%;
} }
} }