From 1aa66f98ed8a979d0bf1baad801898eaf942d636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=93=E9=BC=A0?= <71394853+hamster1963@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:29:00 +0800 Subject: [PATCH] fix: enhance NetworkChart with user login state and period selection (#54) --- .github/workflows/Build.yml | 1 + bun.lock | 1 + src/components/NetworkChart.tsx | 143 +++++++++++++++++++++++++++----- src/lib/nezha-api.ts | 6 +- 4 files changed, 131 insertions(+), 20 deletions(-) diff --git a/.github/workflows/Build.yml b/.github/workflows/Build.yml index 7714867..a680929 100644 --- a/.github/workflows/Build.yml +++ b/.github/workflows/Build.yml @@ -35,6 +35,7 @@ jobs: uses: softprops/action-gh-release@v2 with: files: dist.zip + prerelease: ${{ contains(github.ref_name, '-alpha') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-rc') }} - name: Changelog run: bun x changelogithub diff --git a/bun.lock b/bun.lock index d48c74a..0d6bdf2 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "nazha-dashboard-vite", diff --git a/src/components/NetworkChart.tsx b/src/components/NetworkChart.tsx index 396fc4b..01b7b10 100644 --- a/src/components/NetworkChart.tsx +++ b/src/components/NetworkChart.tsx @@ -27,7 +27,11 @@ import { ChartTooltip, ChartTooltipContent, } from "@/components/ui/chart"; -import { fetchMonitor } from "@/lib/nezha-api"; +import { + fetchLoginUser, + fetchMonitor, + type MonitorPeriod, +} from "@/lib/nezha-api"; import { cn, formatTime } from "@/lib/utils"; import type { NezhaMonitor, ServerMonitorChart } from "@/types/nezha-api"; @@ -40,6 +44,12 @@ interface ResultItem { [key: string]: number; } +const TIME_RANGE_OPTIONS: { value: MonitorPeriod; label: string }[] = [ + { value: "1d", label: "1D" }, + { value: "7d", label: "7D" }, + { value: "30d", label: "30D" }, +]; + /** * Helper method to calculate packet loss from delay data */ @@ -115,10 +125,31 @@ export function NetworkChart({ show: boolean; }) { const { t } = useTranslation(); + const [period, setPeriod] = React.useState("30d"); + const { data: userData, isError: isLoginError } = useQuery({ + queryKey: ["login-user"], + queryFn: () => fetchLoginUser(), + refetchOnMount: false, + refetchOnWindowFocus: true, + refetchIntervalInBackground: true, + refetchInterval: 1000 * 30, + retry: 0, + }); + const isLogin = isLoginError + ? false + : userData + ? !!userData?.data?.id && !!document.cookie + : false; + + React.useEffect(() => { + if (!isLogin && period !== "1d") { + setPeriod("1d"); + } + }, [isLogin, period]); const { data: monitorData } = useQuery({ - queryKey: ["monitor", server_id], - queryFn: () => fetchMonitor(server_id), + queryKey: ["monitor", server_id, period], + queryFn: () => fetchMonitor(server_id, period), enabled: show, refetchOnMount: true, refetchOnWindowFocus: true, @@ -145,7 +176,19 @@ export function NetworkChart({ const formattedData = formatData(monitorData.data); - const chartDataKey = Object.keys(transformedData); + const monitorIdByName = new Map( + monitorData.data.map((item) => [item.monitor_name, item.monitor_id]), + ); + const chartDataKey = Object.keys(transformedData).sort((a, b) => { + const aId = monitorIdByName.get(a); + const bId = monitorIdByName.get(b); + if (aId === undefined && bId === undefined) { + return a.localeCompare(b); + } + if (aId === undefined) return 1; + if (bId === undefined) return -1; + return aId - bId; + }); const initChartConfig = { avg_delay: { @@ -166,6 +209,9 @@ export function NetworkChart({ chartData={transformedData} serverName={monitorData.data[0].server_name} formattedData={formattedData} + period={period} + onPeriodChange={setPeriod} + isLogin={isLogin} /> ); } @@ -176,12 +222,18 @@ export const NetworkChartClient = React.memo(function NetworkChart({ chartData, serverName, formattedData, + period, + onPeriodChange, + isLogin, }: { chartDataKey: string[]; chartConfig: ChartConfig; chartData: ServerMonitorChart; serverName: string; formattedData: ResultItem[]; + period: MonitorPeriod; + onPeriodChange: (period: MonitorPeriod) => void; + isLogin: boolean; }) { const { t } = useTranslation(); @@ -221,11 +273,30 @@ export const NetworkChartClient = React.memo(function NetworkChart({ [chartDataKey], ); + const chartStats = useMemo(() => { + const stats: { [key: string]: { minDelay: number; maxDelay: number } } = {}; + + for (const key of chartDataKey) { + const data = chartData[key] || []; + if (data.length > 0) { + const delays = data.map((item) => item.avg_delay); + const minDelay = Math.min(...delays); + const maxDelay = Math.max(...delays); + stats[key] = { minDelay, maxDelay }; + } else { + stats[key] = { minDelay: 0, maxDelay: 0 }; + } + } + + return stats; + }, [chartDataKey, chartData]); + const chartButtons = useMemo( () => chartDataKey.map((key) => { const monitorData = chartData[key]; const lastDelay = monitorData[monitorData.length - 1].avg_delay; + const stats = chartStats[key]; // Calculate average packet loss if available const packetLossData = monitorData.reduce((acc, item) => { @@ -251,19 +322,27 @@ export const NetworkChartClient = React.memo(function NetworkChart({ {key}
- + {lastDelay.toFixed(2)}ms - {avgPacketLoss !== null && ( - - {avgPacketLoss.toFixed(2)}% avg loss +
+ + ↓{stats.minDelay.toFixed(0)} - )} + + ↑{stats.maxDelay.toFixed(0)} + + {avgPacketLoss !== null && ( + + {avgPacketLoss.toFixed(2)}% avg loss + + )} +
); }), - [chartDataKey, activeCharts, chartData, handleButtonClick], + [chartDataKey, activeCharts, chartData, chartStats, handleButtonClick], ); const chartElements = useMemo(() => { @@ -463,15 +542,41 @@ export const NetworkChartClient = React.memo(function NetworkChart({ {chartDataKey.length} {t("monitor.monitorCount")} -
- - +
+
+ {TIME_RANGE_OPTIONS.map((option) => { + const isLocked = !isLogin && option.value !== "1d"; + return ( + + ); + })} +
+
+ + +
{chartButtons}
diff --git a/src/lib/nezha-api.ts b/src/lib/nezha-api.ts index 83ad072..92d526e 100644 --- a/src/lib/nezha-api.ts +++ b/src/lib/nezha-api.ts @@ -37,10 +37,14 @@ export const fetchLoginUser = async (): Promise => { return data; }; +export type MonitorPeriod = "1d" | "7d" | "30d"; + export const fetchMonitor = async ( server_id: number, + period?: MonitorPeriod, ): Promise => { - const response = await fetch(`/api/v1/service/${server_id}`); + const query = period ? `?period=${period}` : ""; + const response = await fetch(`/api/v1/server/${server_id}/service${query}`); const data = await response.json(); if (data.error) { throw new Error(data.error);