mirror of
https://github.com/hamster1963/nezha-dash.git
synced 2025-04-24 21:10:45 +08:00
commit
e3ce8ad574
@ -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>
|
||||||
|
@ -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;
|
||||||
}[];
|
}[];
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user