diff --git a/app/[locale]/(main)/ClientComponents/NetworkChart.tsx b/app/[locale]/(main)/ClientComponents/NetworkChart.tsx new file mode 100644 index 0000000..e9b9bf0 --- /dev/null +++ b/app/[locale]/(main)/ClientComponents/NetworkChart.tsx @@ -0,0 +1,183 @@ +"use client"; + +import * as React from "react"; +import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"; + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "@/components/ui/chart"; +import useSWR from "swr"; +import { ServerMonitorChart } from "../../types/nezha-api"; +import { formatTime, nezhaFetcher } from "@/lib/utils"; +import { formatRelativeTime } from "@/lib/utils"; +import { BackIcon } from "@/components/Icon"; +import { useRouter } from "next/navigation"; +import { useLocale } from "next-intl"; +import { useTranslations } from "next-intl"; +export function NetworkChartClient({ server_id }: { server_id: number }) { + const t = useTranslations("NetworkChartClient"); + const { data, error } = useSWR( + `/api/monitor?server_id=${server_id}`, + nezhaFetcher, + ); + + if (error) + return ( +
+

{error.message}

+
+ ); + if (!data) return null; + + const initChartConfig = { + avg_delay: { + label: t("avg_delay"), + }, + } satisfies ChartConfig; + + const chartDataKey = Object.keys(data); + + const generateChartConfig = chartDataKey.reduce((config, key, index) => { + return { + ...config, + [key]: { + label: key, + color: `hsl(var(--chart-${index + 1}))`, + }, + }; + }, {} as ChartConfig); + + const chartConfig = { ...initChartConfig, ...generateChartConfig }; + + return ( + + ); +} + +export function NetworkChart({ + chartDataKey, + chartConfig, + chartData, +}: { + chartDataKey: string[]; + chartConfig: ChartConfig; + chartData: ServerMonitorChart; +}) { + const t = useTranslations("NetworkChart"); + const router = useRouter(); + const locale = useLocale(); + + const [activeChart, setActiveChart] = React.useState< + keyof typeof chartConfig + >(chartDataKey[0]); + + return ( + + +
+ { + router.push(`/${locale}/`); + }} + className="flex cursor-pointer items-center gap-0.5 text-xl" + > + + {chartData[chartDataKey[0]][0].server_name} + + + {chartDataKey.length} {t("ServerMonitorCount")} + +
+
+ {chartDataKey.map((key, index) => { + const chart = key as keyof typeof chartConfig; + return ( + + ); + })} +
+
+ + + + + formatRelativeTime(value)} + /> + `${value}ms`} + /> + { + return formatTime(payload[0].payload.created_at); + }} + /> + } + /> + + + + +
+ ); +} diff --git a/app/[locale]/(main)/[id]/page.tsx b/app/[locale]/(main)/[id]/page.tsx new file mode 100644 index 0000000..de1a312 --- /dev/null +++ b/app/[locale]/(main)/[id]/page.tsx @@ -0,0 +1,9 @@ +import { NetworkChartClient } from "../ClientComponents/NetworkChart"; + +export default function Page({ params }: { params: { id: string } }) { + return ( +
+ +
+ ); +} diff --git a/app/[locale]/types/nezha-api.ts b/app/[locale]/types/nezha-api.ts index bc91e7f..40998b3 100644 --- a/app/[locale]/types/nezha-api.ts +++ b/app/[locale]/types/nezha-api.ts @@ -55,3 +55,20 @@ export interface NezhaAPIStatus { Temperatures: number; GPU: number; } + +export type ServerMonitorChart = { + [key: string]: { + server_name: string; + created_at: number; + avg_delay: number; + }[]; +}; + +export interface NezhaAPIMonitor { + monitor_id: number; + monitor_name: string; + server_id: number; + server_name: string; + created_at: number[]; + avg_delay: number[]; +} diff --git a/app/api/monitor/route.ts b/app/api/monitor/route.ts new file mode 100644 index 0000000..2db4d45 --- /dev/null +++ b/app/api/monitor/route.ts @@ -0,0 +1,27 @@ +import { ServerMonitorChart } from "@/app/[locale]/types/nezha-api"; +import { GetServerMonitor } from "@/lib/serverFetch"; +import { NextResponse } from "next/server"; + +interface NezhaDataResponse { + error?: string; + data?: ServerMonitorChart; +} + +export async function GET(req: Request) { + const { searchParams } = new URL(req.url); + const server_id = searchParams.get("server_id"); + if (!server_id) { + return NextResponse.json( + { error: "server_id is required" }, + { status: 400 }, + ); + } + const response = (await GetServerMonitor({ + server_id: parseInt(server_id), + })) as NezhaDataResponse; + if (response.error) { + console.log(response.error); + return NextResponse.json({ error: response.error }, { status: 400 }); + } + return NextResponse.json(response, { status: 200 }); +} diff --git a/bun.lockb b/bun.lockb index cf1eb4e..699a001 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/Icon.tsx b/components/Icon.tsx index aa97669..f690beb 100644 --- a/components/Icon.tsx +++ b/components/Icon.tsx @@ -1,3 +1,5 @@ +import Image from "next/image"; + export function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) { return ( @@ -5,3 +7,28 @@ export function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) { ); } + +export function BackIcon() { + return ( + <> + BackIcon + BackIcon + + ); +} diff --git a/components/ServerCard.tsx b/components/ServerCard.tsx index afbe314..79cefc8 100644 --- a/components/ServerCard.tsx +++ b/components/ServerCard.tsx @@ -1,4 +1,4 @@ -import { useTranslations } from "next-intl"; +import { useLocale, useTranslations } from "next-intl"; import { NezhaAPISafe } from "../app/[locale]/types/nezha-api"; import ServerUsageBar from "@/components/ServerUsageBar"; import { Card } from "@/components/ui/card"; @@ -12,6 +12,7 @@ import ServerCardPopover from "./ServerCardPopover"; import { env } from "next-runtime-env"; import ServerFlag from "./ServerFlag"; +import { useRouter } from "next/navigation"; export default function ServerCard({ serverInfo, @@ -19,11 +20,14 @@ export default function ServerCard({ serverInfo: NezhaAPISafe; }) { const t = useTranslations("ServerCard"); - const { name, country_code, online, cpu, up, down, mem, stg, ...props } = + const router = useRouter(); + const { id, name, country_code, online, cpu, up, down, mem, stg, ...props } = formatNezhaInfo(serverInfo); const showFlag = env("NEXT_PUBLIC_ShowFlag") === "true"; + const locale = useLocale(); + return online ? ( -
+
{ + router.push(`/${locale}/${id}`); + }} + className={"grid cursor-pointer grid-cols-5 items-center gap-3"} + >

{t("CPU")}

diff --git a/components/ui/card.tsx b/components/ui/card.tsx index c5b32be..fc2d9c7 100644 --- a/components/ui/card.tsx +++ b/components/ui/card.tsx @@ -78,9 +78,9 @@ CardFooter.displayName = "CardFooter"; export { Card, - CardContent, - CardDescription, - CardFooter, CardHeader, + CardFooter, CardTitle, + CardDescription, + CardContent, }; diff --git a/components/ui/chart.tsx b/components/ui/chart.tsx new file mode 100644 index 0000000..74551d7 --- /dev/null +++ b/components/ui/chart.tsx @@ -0,0 +1,365 @@ +"use client"; + +import * as React from "react"; +import * as RechartsPrimitive from "recharts"; + +import { cn } from "@/lib/utils"; + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const; + +export type ChartConfig = { + [k in string]: { + label?: React.ReactNode; + icon?: React.ComponentType; + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ); +}; + +type ChartContextProps = { + config: ChartConfig; +}; + +const ChartContext = React.createContext(null); + +function useChart() { + const context = React.useContext(ChartContext); + + if (!context) { + throw new Error("useChart must be used within a "); + } + + return context; +} + +const ChartContainer = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + config: ChartConfig; + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"]; + } +>(({ id, className, children, config, ...props }, ref) => { + const uniqueId = React.useId(); + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; + + return ( + +
+ + + {children} + +
+
+ ); +}); +ChartContainer.displayName = "Chart"; + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([_, config]) => config.theme || config.color, + ); + + if (!colorConfig.length) { + return null; + } + + return ( +