fix: enhance NetworkChart with user login state and period selection (#54)

This commit is contained in:
仓鼠
2026-01-29 09:29:00 +08:00
committed by GitHub
parent 76590a6bd0
commit 1aa66f98ed
4 changed files with 131 additions and 20 deletions
+1
View File
@@ -35,6 +35,7 @@ jobs:
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: dist.zip files: dist.zip
prerelease: ${{ contains(github.ref_name, '-alpha') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-rc') }}
- name: Changelog - name: Changelog
run: bun x changelogithub run: bun x changelogithub
+1
View File
@@ -1,5 +1,6 @@
{ {
"lockfileVersion": 1, "lockfileVersion": 1,
"configVersion": 0,
"workspaces": { "workspaces": {
"": { "": {
"name": "nazha-dashboard-vite", "name": "nazha-dashboard-vite",
+124 -19
View File
@@ -27,7 +27,11 @@ import {
ChartTooltip, ChartTooltip,
ChartTooltipContent, ChartTooltipContent,
} from "@/components/ui/chart"; } 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 { cn, formatTime } from "@/lib/utils";
import type { NezhaMonitor, ServerMonitorChart } from "@/types/nezha-api"; import type { NezhaMonitor, ServerMonitorChart } from "@/types/nezha-api";
@@ -40,6 +44,12 @@ interface ResultItem {
[key: string]: number; [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 * Helper method to calculate packet loss from delay data
*/ */
@@ -115,10 +125,31 @@ export function NetworkChart({
show: boolean; show: boolean;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [period, setPeriod] = React.useState<MonitorPeriod>("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({ const { data: monitorData } = useQuery({
queryKey: ["monitor", server_id], queryKey: ["monitor", server_id, period],
queryFn: () => fetchMonitor(server_id), queryFn: () => fetchMonitor(server_id, period),
enabled: show, enabled: show,
refetchOnMount: true, refetchOnMount: true,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
@@ -145,7 +176,19 @@ export function NetworkChart({
const formattedData = formatData(monitorData.data); 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 = { const initChartConfig = {
avg_delay: { avg_delay: {
@@ -166,6 +209,9 @@ export function NetworkChart({
chartData={transformedData} chartData={transformedData}
serverName={monitorData.data[0].server_name} serverName={monitorData.data[0].server_name}
formattedData={formattedData} formattedData={formattedData}
period={period}
onPeriodChange={setPeriod}
isLogin={isLogin}
/> />
); );
} }
@@ -176,12 +222,18 @@ export const NetworkChartClient = React.memo(function NetworkChart({
chartData, chartData,
serverName, serverName,
formattedData, formattedData,
period,
onPeriodChange,
isLogin,
}: { }: {
chartDataKey: string[]; chartDataKey: string[];
chartConfig: ChartConfig; chartConfig: ChartConfig;
chartData: ServerMonitorChart; chartData: ServerMonitorChart;
serverName: string; serverName: string;
formattedData: ResultItem[]; formattedData: ResultItem[];
period: MonitorPeriod;
onPeriodChange: (period: MonitorPeriod) => void;
isLogin: boolean;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -221,11 +273,30 @@ export const NetworkChartClient = React.memo(function NetworkChart({
[chartDataKey], [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( const chartButtons = useMemo(
() => () =>
chartDataKey.map((key) => { chartDataKey.map((key) => {
const monitorData = chartData[key]; const monitorData = chartData[key];
const lastDelay = monitorData[monitorData.length - 1].avg_delay; const lastDelay = monitorData[monitorData.length - 1].avg_delay;
const stats = chartStats[key];
// Calculate average packet loss if available // Calculate average packet loss if available
const packetLossData = monitorData.reduce<number[]>((acc, item) => { const packetLossData = monitorData.reduce<number[]>((acc, item) => {
@@ -251,19 +322,27 @@ export const NetworkChartClient = React.memo(function NetworkChart({
{key} {key}
</span> </span>
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<span className="text-md font-bold leading-none sm:text-lg"> <span className="text-md font-semibold leading-none sm:text-xl">
{lastDelay.toFixed(2)}ms {lastDelay.toFixed(2)}ms
</span> </span>
{avgPacketLoss !== null && ( <div className="flex items-center gap-2 text-[12px]">
<span className="text-xs text-muted-foreground"> <span className="text-green-600 dark:text-green-400">
{avgPacketLoss.toFixed(2)}% avg loss {stats.minDelay.toFixed(0)}
</span> </span>
)} <span className="text-red-600 dark:text-red-500">
{stats.maxDelay.toFixed(0)}
</span>
{avgPacketLoss !== null && (
<span className="text-muted-foreground">
{avgPacketLoss.toFixed(2)}% avg loss
</span>
)}
</div>
</div> </div>
</button> </button>
); );
}), }),
[chartDataKey, activeCharts, chartData, handleButtonClick], [chartDataKey, activeCharts, chartData, chartStats, handleButtonClick],
); );
const chartElements = useMemo(() => { const chartElements = useMemo(() => {
@@ -463,15 +542,41 @@ export const NetworkChartClient = React.memo(function NetworkChart({
<CardDescription className="text-xs"> <CardDescription className="text-xs">
{chartDataKey.length} {t("monitor.monitorCount")} {chartDataKey.length} {t("monitor.monitorCount")}
</CardDescription> </CardDescription>
<div className="flex items-center mt-0.5 space-x-2"> <div className="mt-0.5 flex items-center gap-3">
<Switch <div className="flex items-center gap-1 rounded-lg bg-muted/50 p-1">
id="Peak" {TIME_RANGE_OPTIONS.map((option) => {
checked={isPeakEnabled} const isLocked = !isLogin && option.value !== "1d";
onCheckedChange={setIsPeakEnabled} return (
/> <button
<Label className="text-xs" htmlFor="Peak"> key={option.value}
Peak cut type="button"
</Label> disabled={isLocked}
onClick={() => {
if (!isLocked) {
onPeriodChange(option.value);
}
}}
className={`rounded-md px-2.5 py-1 text-xs font-medium transition-all duration-200 ${
period === option.value
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
} ${isLocked ? "cursor-not-allowed opacity-50" : ""}`}
>
{option.label}
</button>
);
})}
</div>
<div className="flex items-center space-x-2">
<Switch
id="Peak"
checked={isPeakEnabled}
onCheckedChange={setIsPeakEnabled}
/>
<Label className="text-xs" htmlFor="Peak">
Peak cut
</Label>
</div>
</div> </div>
</div> </div>
<div className="flex flex-wrap w-full">{chartButtons}</div> <div className="flex flex-wrap w-full">{chartButtons}</div>
+5 -1
View File
@@ -37,10 +37,14 @@ export const fetchLoginUser = async (): Promise<LoginUserResponse> => {
return data; return data;
}; };
export type MonitorPeriod = "1d" | "7d" | "30d";
export const fetchMonitor = async ( export const fetchMonitor = async (
server_id: number, server_id: number,
period?: MonitorPeriod,
): Promise<MonitorResponse> => { ): Promise<MonitorResponse> => {
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(); const data = await response.json();
if (data.error) { if (data.error) {
throw new Error(data.error); throw new Error(data.error);