Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
actions-user committed Oct 10, 2024
2 parents 61e1c1e + a07113a commit 5ce29eb
Show file tree
Hide file tree
Showing 25 changed files with 871 additions and 21 deletions.
Binary file added .github/shot-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/shot-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/shot-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/shot-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,7 @@
| 英语 | en ||
| 日语 | ja ||

![screen-shot-one](/.github/shotOne.png)
![screen-shot-two](/.github/shotTwo.png)
![screen-shot-one](/.github/shot-1.png)
![screen-shot-two](/.github/shot-2.png)
![screen-shot-three](/.github/shot-3.png)
![screen-shot-four](/.github/shot-4.png)
263 changes: 263 additions & 0 deletions app/[locale]/(main)/ClientComponents/NetworkChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
"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,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import useSWR from "swr";
import { NezhaAPIMonitor, 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";
import NetworkChartLoading from "./NetworkChartLoading";

interface ResultItem {
created_at: number;
[key: string]: number;
}

export function NetworkChartClient({ server_id }: { server_id: number }) {
const t = useTranslations("NetworkChartClient");
const { data, error } = useSWR<NezhaAPIMonitor[]>(
`/api/monitor?server_id=${server_id}`,
nezhaFetcher,
);

if (error)
return (
<div className="flex flex-col items-center justify-center">
<p className="text-sm font-medium opacity-40">{error.message}</p>
</div>
);
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 = {
avg_delay: {
label: t("avg_delay"),
},
} satisfies ChartConfig;

const chartDataKey = Object.keys(transformedData);

return (
<NetworkChart
chartDataKey={chartDataKey}
chartConfig={initChartConfig}
chartData={transformedData}
serverName={data[0].server_name}
formattedData={formattedData}
/>
);
}

export function NetworkChart({
chartDataKey,
chartConfig,
chartData,
serverName,
formattedData,
}: {
chartDataKey: string[];
chartConfig: ChartConfig;
chartData: ServerMonitorChart;
serverName: string;
formattedData: ResultItem[];
}) {
const t = useTranslations("NetworkChart");
const router = useRouter();
const locale = useLocale();

const defaultChart = "All";

const [activeChart, setActiveChart] = React.useState(defaultChart);

const handleButtonClick = (chart: string) => {
if (chart === activeChart) {
setActiveChart(defaultChart);
} else {
setActiveChart(chart);
}
};

const getColorByIndex = (chart: string) => {
const index = chartDataKey.indexOf(chart);
return `hsl(var(--chart-${(index % 10) + 1}))`;
};

return (
<Card>
<CardHeader className="flex flex-col items-stretch space-y-0 p-0 sm:flex-row">
<div className="flex flex-none flex-col justify-center gap-1 border-b px-6 py-4">
<CardTitle
onClick={() => {
router.push(`/${locale}/`);
}}
className="flex flex-none cursor-pointer items-center gap-0.5 text-xl"
>
<BackIcon />
{serverName}
</CardTitle>
<CardDescription className="text-xs">
{chartDataKey.length} {t("ServerMonitorCount")}
</CardDescription>
</div>
<div className="flex flex-wrap">
{chartDataKey.map((key) => {
return (
<button
key={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`}
onClick={() => handleButtonClick(key)}
>
<span className="whitespace-nowrap text-xs text-muted-foreground">
{key}
</span>
<span className="text-md font-bold leading-none sm:text-lg">
{chartData[key][chartData[key].length - 1].avg_delay.toFixed(
2,
)}
ms
</span>
</button>
);
})}
</div>
</CardHeader>
<CardContent className="px-2 sm:p-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<LineChart
accessibilityLayer
data={
activeChart === defaultChart
? formattedData
: chartData[activeChart]
}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="created_at"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => formatRelativeTime(value)}
/>
<YAxis
tickLine={false}
axisLine={false}
mirror={true}
tickMargin={-15}
minTickGap={20}
tickFormatter={(value) => `${value}ms`}
/>
<ChartTooltip
content={
<ChartTooltipContent
indicator={"dot"}
className="gap-2"
labelKey="created_at"
labelClassName="text-muted-foreground"
labelFormatter={(_, payload) => {
return formatTime(payload[0].payload.created_at);
}}
/>
}
/>
{activeChart === defaultChart && (
<ChartLegend content={<ChartLegendContent />} />
)}
{activeChart !== defaultChart && (
<Line
isAnimationActive={false}
strokeWidth={2}
type="linear"
dot={false}
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>
</ChartContainer>
</CardContent>
</Card>
);
}
25 changes: 25 additions & 0 deletions app/[locale]/(main)/ClientComponents/NetworkChartLoading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { BackIcon } from "@/components/Icon";
import { Loader } from "@/components/loading/Loader";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";

export default function NetworkChartLoading() {
return (
<Card>
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5">
<CardTitle className="flex items-center gap-0.5 text-xl">
<BackIcon />
<div className="aspect-auto h-[20px] w-24 bg-muted"></div>
</CardTitle>
<div className="mt-[2px] aspect-auto h-[14px] w-32 bg-muted"></div>
</div>
<div className="hidden pr-4 pt-4 sm:block">
<Loader visible={true} />
</div>
</CardHeader>
<CardContent className="px-2 sm:p-6">
<div className="aspect-auto h-[250px] w-full"></div>
</CardContent>
</Card>
);
}
9 changes: 9 additions & 0 deletions app/[locale]/(main)/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { NetworkChartClient } from "../ClientComponents/NetworkChart";

export default function Page({ params }: { params: { id: string } }) {
return (
<div className="mx-auto grid w-full max-w-5xl gap-4 md:gap-6">
<NetworkChartClient server_id={Number(params.id)} />
</div>
);
}
3 changes: 0 additions & 3 deletions app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,6 @@ export const viewport: Viewport = {
userScalable: false,
};

// optimization: force static for vercel
export const dynamic = process.env.VERCEL ? "force-static" : "auto";

export async function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
Expand Down
16 changes: 16 additions & 0 deletions app/[locale]/types/nezha-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,19 @@ export interface NezhaAPIStatus {
Temperatures: number;
GPU: number;
}

export type ServerMonitorChart = {
[key: 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[];
}
27 changes: 27 additions & 0 deletions app/api/monitor/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
Binary file modified bun.lockb
Binary file not shown.
Loading

0 comments on commit 5ce29eb

Please sign in to comment.