mirror of
https://github.com/Buriburizaem0n/nezha-dash-v1.git
synced 2026-05-06 13:58:43 +00:00
fix: enhance NetworkChart with user login state and period selection (#54)
This commit is contained in:
@@ -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,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 0,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "nazha-dashboard-vite",
|
"name": "nazha-dashboard-vite",
|
||||||
|
|||||||
+124
-19
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user