mirror of
https://github.com/Buriburizaem0n/nezha-dash-v1.git
synced 2026-05-06 13:58:43 +00:00
feat: enhance tooltips and loading states in NetworkChart and ServerDetailChart; add translations for TSDB and login requirements
This commit is contained in:
@@ -28,6 +28,12 @@ import {
|
|||||||
ChartTooltip,
|
ChartTooltip,
|
||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
} from "@/components/ui/chart";
|
} from "@/components/ui/chart";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import {
|
import {
|
||||||
fetchLoginUser,
|
fetchLoginUser,
|
||||||
fetchMonitor,
|
fetchMonitor,
|
||||||
@@ -533,41 +539,55 @@ export const NetworkChartClient = React.memo(function NetworkChart({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex items-center gap-3 sm:-mt-5 -mt-3 flex-wrap">
|
<div className="flex items-center gap-3 sm:-mt-5 -mt-3 flex-wrap">
|
||||||
<div className="flex items-center gap-1 rounded-full bg-muted dark:bg-muted/40 p-0.5 border border-border/60 dark:border-border">
|
<TooltipProvider delayDuration={120}>
|
||||||
{TIME_RANGE_OPTIONS.map((option) => {
|
<div className="flex items-center gap-1 rounded-full bg-muted dark:bg-muted/40 p-0.5 border border-border/60 dark:border-border">
|
||||||
const isLocked = !isLogin && option.value !== "1d";
|
{TIME_RANGE_OPTIONS.map((option) => {
|
||||||
return (
|
const isLocked = !isLogin && option.value !== "1d";
|
||||||
<div
|
const optionItem = (
|
||||||
key={option.value}
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!isLocked) {
|
if (!isLocked) {
|
||||||
onPeriodChange(option.value);
|
onPeriodChange(option.value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors duration-300",
|
"relative cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors duration-300",
|
||||||
period === option.value
|
period === option.value
|
||||||
? "text-foreground"
|
? "text-foreground"
|
||||||
: "text-muted-foreground hover:text-foreground",
|
: "text-muted-foreground hover:text-foreground",
|
||||||
isLocked && "cursor-not-allowed opacity-40 grayscale",
|
isLocked && "cursor-not-allowed opacity-40 grayscale",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{period === option.value && (
|
{period === option.value && (
|
||||||
<m.div
|
<m.div
|
||||||
layoutId="network-period-selector-active"
|
layoutId="network-period-selector-active"
|
||||||
className="absolute inset-0 z-10 h-full w-full bg-white dark:bg-background rounded-full ring-1 ring-border/60 dark:ring-border/40"
|
className="absolute inset-0 z-10 h-full w-full bg-white dark:bg-background rounded-full ring-1 ring-border/60 dark:ring-border/40"
|
||||||
transition={{
|
transition={{
|
||||||
type: "spring",
|
type: "spring",
|
||||||
stiffness: 400,
|
stiffness: 400,
|
||||||
damping: 30,
|
damping: 30,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="relative z-20">{option.label}</span>
|
<span className="relative z-20">{option.label}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
|
||||||
</div>
|
if (isLocked) {
|
||||||
|
return (
|
||||||
|
<Tooltip key={option.value}>
|
||||||
|
<TooltipTrigger asChild>{optionItem}</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{t("monitor.loginRequired", "Please login to view")}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div key={option.value}>{optionItem}</div>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch
|
<Switch
|
||||||
id="Peak"
|
id="Peak"
|
||||||
|
|||||||
@@ -18,9 +18,19 @@ import {
|
|||||||
ChartTooltip,
|
ChartTooltip,
|
||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
} from "@/components/ui/chart";
|
} from "@/components/ui/chart";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { useWebSocketContext } from "@/hooks/use-websocket-context";
|
import { useWebSocketContext } from "@/hooks/use-websocket-context";
|
||||||
import { formatBytes } from "@/lib/format";
|
import { formatBytes } from "@/lib/format";
|
||||||
import { fetchLoginUser, fetchServerMetrics } from "@/lib/nezha-api";
|
import {
|
||||||
|
fetchLoginUser,
|
||||||
|
fetchServerMetrics,
|
||||||
|
fetchSetting,
|
||||||
|
} from "@/lib/nezha-api";
|
||||||
import {
|
import {
|
||||||
cn,
|
cn,
|
||||||
formatNezhaInfo,
|
formatNezhaInfo,
|
||||||
@@ -77,14 +87,22 @@ type connectChartData = {
|
|||||||
udp: number;
|
udp: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MIN_HISTORY_LOADING_MS = 300;
|
||||||
|
const sleep = (ms: number) =>
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
|
||||||
function PeriodSelector({
|
function PeriodSelector({
|
||||||
selectedPeriod,
|
selectedPeriod,
|
||||||
onPeriodChange,
|
onPeriodChange,
|
||||||
isLogin,
|
isLogin,
|
||||||
|
isTsdbEnabled,
|
||||||
}: {
|
}: {
|
||||||
selectedPeriod: ChartPeriod;
|
selectedPeriod: ChartPeriod;
|
||||||
onPeriodChange: (period: ChartPeriod) => void;
|
onPeriodChange: (period: ChartPeriod) => void;
|
||||||
isLogin: boolean;
|
isLogin: boolean;
|
||||||
|
isTsdbEnabled: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -96,44 +114,73 @@ function PeriodSelector({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-0.5 mb-3 flex-wrap sm:-mt-5 -mt-3 p-0.5 bg-muted dark:bg-muted/40 rounded-full w-fit border border-border/60 dark:border-border">
|
<TooltipProvider delayDuration={120}>
|
||||||
{periods.map((period) => {
|
<div className="flex gap-0.5 mb-3 flex-wrap sm:-mt-5 -mt-3 p-0.5 bg-muted dark:bg-muted/40 rounded-full w-fit border border-border/60 dark:border-border">
|
||||||
// Only realtime and 1d are available for non-logged-in users
|
{periods.map((period) => {
|
||||||
const isLocked =
|
const isHistoryPeriod = period.value !== "realtime";
|
||||||
!isLogin && period.value !== "realtime" && period.value !== "1d";
|
const isLockedByTsdb = !isTsdbEnabled && isHistoryPeriod;
|
||||||
return (
|
// Only realtime and 1d are available for non-logged-in users
|
||||||
<div
|
const isLockedByLogin =
|
||||||
key={period.value}
|
!isLockedByTsdb &&
|
||||||
onClick={() => {
|
!isLogin &&
|
||||||
if (!isLocked) {
|
period.value !== "realtime" &&
|
||||||
onPeriodChange(period.value);
|
period.value !== "1d";
|
||||||
}
|
const isLocked = isLockedByTsdb || isLockedByLogin;
|
||||||
}}
|
|
||||||
className={cn(
|
const periodItem = (
|
||||||
"relative cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors duration-300",
|
<div
|
||||||
selectedPeriod === period.value
|
onClick={() => {
|
||||||
? "text-foreground"
|
if (!isLocked) {
|
||||||
: "text-muted-foreground hover:text-foreground",
|
onPeriodChange(period.value);
|
||||||
isLocked && "cursor-not-allowed opacity-40 grayscale",
|
}
|
||||||
)}
|
}}
|
||||||
>
|
className={cn(
|
||||||
{selectedPeriod === period.value && (
|
"relative cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors duration-300",
|
||||||
<m.div
|
selectedPeriod === period.value
|
||||||
layoutId="period-selector-active"
|
? "text-foreground"
|
||||||
className="absolute inset-0 z-10 h-full w-full bg-white dark:bg-background rounded-full ring-1 ring-border/60 dark:ring-border/40"
|
: "text-muted-foreground hover:text-foreground",
|
||||||
transition={{ type: "spring", stiffness: 250, damping: 30 }}
|
isLocked && "cursor-not-allowed opacity-40 grayscale",
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="relative z-20 flex items-center gap-1.5">
|
|
||||||
{period.value === "realtime" && (
|
|
||||||
<span className="inline-flex rounded-full h-1.5 w-1.5 bg-emerald-500 dark:bg-emerald-400"></span>
|
|
||||||
)}
|
)}
|
||||||
{period.label}
|
>
|
||||||
|
{selectedPeriod === period.value && (
|
||||||
|
<m.div
|
||||||
|
layoutId="period-selector-active"
|
||||||
|
className="absolute inset-0 z-10 h-full w-full bg-white dark:bg-background rounded-full ring-1 ring-border/60 dark:ring-border/40"
|
||||||
|
transition={{ type: "spring", stiffness: 250, damping: 30 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="relative z-20 flex items-center gap-1.5">
|
||||||
|
{period.value === "realtime" && (
|
||||||
|
<span className="inline-flex rounded-full h-1.5 w-1.5 bg-emerald-500 dark:bg-emerald-400"></span>
|
||||||
|
)}
|
||||||
|
{period.label}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
|
||||||
})}
|
if (isLockedByTsdb || isLockedByLogin) {
|
||||||
</div>
|
return (
|
||||||
|
<Tooltip key={period.value}>
|
||||||
|
<TooltipTrigger asChild>{periodItem}</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{isLockedByTsdb
|
||||||
|
? t(
|
||||||
|
"serverDetailChart.tsdbRequired",
|
||||||
|
"Enable TSDB to use historical data",
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
"serverDetailChart.loginRequired",
|
||||||
|
"Please login to view",
|
||||||
|
)}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div key={period.value}>{periodItem}</div>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,12 +208,31 @@ export default function ServerDetailChart({
|
|||||||
? !!userData?.data?.id && !!document.cookie
|
? !!userData?.data?.id && !!document.cookie
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
|
const { data: settingData } = useQuery({
|
||||||
|
queryKey: ["setting"],
|
||||||
|
queryFn: () => fetchSetting(),
|
||||||
|
refetchOnMount: true,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
});
|
||||||
|
const isTsdbEnabled = settingData?.data?.tsdb_enabled ?? true;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isTsdbEnabled && selectedPeriod !== "realtime") {
|
||||||
|
setSelectedPeriod("realtime");
|
||||||
|
}
|
||||||
|
}, [isTsdbEnabled, selectedPeriod]);
|
||||||
|
|
||||||
// Reset period if user is not logged in and selected period is restricted
|
// Reset period if user is not logged in and selected period is restricted
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLogin && selectedPeriod !== "realtime" && selectedPeriod !== "1d") {
|
if (
|
||||||
|
isTsdbEnabled &&
|
||||||
|
!isLogin &&
|
||||||
|
selectedPeriod !== "realtime" &&
|
||||||
|
selectedPeriod !== "1d"
|
||||||
|
) {
|
||||||
setSelectedPeriod("1d");
|
setSelectedPeriod("1d");
|
||||||
}
|
}
|
||||||
}, [isLogin, selectedPeriod]);
|
}, [isLogin, isTsdbEnabled, selectedPeriod]);
|
||||||
|
|
||||||
if (!connected && !lastMessage) {
|
if (!connected && !lastMessage) {
|
||||||
return <ServerDetailChartLoading />;
|
return <ServerDetailChartLoading />;
|
||||||
@@ -195,6 +261,7 @@ export default function ServerDetailChart({
|
|||||||
selectedPeriod={selectedPeriod}
|
selectedPeriod={selectedPeriod}
|
||||||
onPeriodChange={setSelectedPeriod}
|
onPeriodChange={setSelectedPeriod}
|
||||||
isLogin={isLogin}
|
isLogin={isLogin}
|
||||||
|
isTsdbEnabled={isTsdbEnabled}
|
||||||
/>
|
/>
|
||||||
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3 server-charts">
|
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3 server-charts">
|
||||||
<CpuChart
|
<CpuChart
|
||||||
@@ -274,22 +341,24 @@ function useHistoricalData<T>(
|
|||||||
const [historicalData, setHistoricalData] = useState<T[]>([]);
|
const [historicalData, setHistoricalData] = useState<T[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [displayData, setDisplayData] = useState<T[]>([]);
|
const [displayData, setDisplayData] = useState<T[]>([]);
|
||||||
const loadingTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const [loadedPeriod, setLoadedPeriod] = useState<ChartPeriod>("realtime");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
if (period === "realtime") {
|
if (period === "realtime") {
|
||||||
setHistoricalData([]);
|
setHistoricalData([]);
|
||||||
setDisplayData([]);
|
setDisplayData([]);
|
||||||
if (loadingTimerRef.current) {
|
setIsLoading(false);
|
||||||
clearTimeout(loadingTimerRef.current);
|
setLoadedPeriod("realtime");
|
||||||
}
|
return () => {
|
||||||
return;
|
cancelled = true;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
loadingTimerRef.current = setTimeout(() => {
|
const loadingStartedAt = Date.now();
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
}, 200);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetchServerMetrics(
|
const response = await fetchServerMetrics(
|
||||||
@@ -301,24 +370,35 @@ function useHistoricalData<T>(
|
|||||||
const transformedData = response.data.data_points.map((point) =>
|
const transformedData = response.data.data_points.map((point) =>
|
||||||
transformData(point.ts, point.value),
|
transformData(point.ts, point.value),
|
||||||
);
|
);
|
||||||
setHistoricalData(transformedData);
|
if (!cancelled) {
|
||||||
setDisplayData(transformedData);
|
setHistoricalData(transformedData);
|
||||||
|
setDisplayData(transformedData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to fetch ${metricName} metrics:`, error);
|
console.error(`Failed to fetch ${metricName} metrics:`, error);
|
||||||
} finally {
|
} finally {
|
||||||
if (loadingTimerRef.current) {
|
const elapsed = Date.now() - loadingStartedAt;
|
||||||
clearTimeout(loadingTimerRef.current);
|
if (elapsed < MIN_HISTORY_LOADING_MS) {
|
||||||
loadingTimerRef.current = null;
|
await sleep(MIN_HISTORY_LOADING_MS - elapsed);
|
||||||
|
}
|
||||||
|
if (!cancelled) {
|
||||||
|
setIsLoading(false);
|
||||||
|
setLoadedPeriod(period);
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchData();
|
fetchData();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}, [serverId, metricName, period, transformData]);
|
}, [serverId, metricName, period, transformData]);
|
||||||
|
|
||||||
return { historicalData, displayData, isLoading };
|
const isHistoricalLoading =
|
||||||
|
period !== "realtime" && (isLoading || loadedPeriod !== period);
|
||||||
|
|
||||||
|
return { historicalData, displayData, isLoading: isHistoricalLoading };
|
||||||
}
|
}
|
||||||
|
|
||||||
function GpuChart({
|
function GpuChart({
|
||||||
@@ -938,14 +1018,23 @@ function MemChart({
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
const [isLoadingMem, setIsLoadingMem] = useState(false);
|
const [isLoadingMem, setIsLoadingMem] = useState(false);
|
||||||
|
const [loadedPeriodMem, setLoadedPeriodMem] =
|
||||||
|
useState<ChartPeriod>("realtime");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
if (period === "realtime") {
|
if (period === "realtime") {
|
||||||
setMemHistoricalData([]);
|
setMemHistoricalData([]);
|
||||||
return;
|
setIsLoadingMem(false);
|
||||||
|
setLoadedPeriodMem("realtime");
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchMemData = async () => {
|
const fetchMemData = async () => {
|
||||||
|
const loadingStartedAt = Date.now();
|
||||||
setIsLoadingMem(true);
|
setIsLoadingMem(true);
|
||||||
try {
|
try {
|
||||||
const [memResponse, swapResponse] = await Promise.all([
|
const [memResponse, swapResponse] = await Promise.all([
|
||||||
@@ -978,16 +1067,28 @@ function MemChart({
|
|||||||
swap: swapMap.get(point.ts) || 0,
|
swap: swapMap.get(point.ts) || 0,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
setMemHistoricalData(combinedData);
|
if (!cancelled) {
|
||||||
|
setMemHistoricalData(combinedData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch memory metrics:", error);
|
console.error("Failed to fetch memory metrics:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingMem(false);
|
const elapsed = Date.now() - loadingStartedAt;
|
||||||
|
if (elapsed < MIN_HISTORY_LOADING_MS) {
|
||||||
|
await sleep(MIN_HISTORY_LOADING_MS - elapsed);
|
||||||
|
}
|
||||||
|
if (!cancelled) {
|
||||||
|
setIsLoadingMem(false);
|
||||||
|
setLoadedPeriodMem(period);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchMemData();
|
fetchMemData();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}, [data.id, period, data.host.mem_total, data.host.swap_total]);
|
}, [data.id, period, data.host.mem_total, data.host.swap_total]);
|
||||||
|
|
||||||
// 初始化历史数据
|
// 初始化历史数据
|
||||||
@@ -1058,6 +1159,8 @@ function MemChart({
|
|||||||
} satisfies ChartConfig;
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
const displayData = period === "realtime" ? memChartData : memHistoricalData;
|
const displayData = period === "realtime" ? memChartData : memHistoricalData;
|
||||||
|
const isMemLoading =
|
||||||
|
period !== "realtime" && (isLoadingMem || loadedPeriodMem !== period);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@@ -1121,7 +1224,7 @@ function MemChart({
|
|||||||
config={chartConfig}
|
config={chartConfig}
|
||||||
className="aspect-auto h-[130px] w-full"
|
className="aspect-auto h-[130px] w-full"
|
||||||
>
|
>
|
||||||
{isLoadingMem ? (
|
{isMemLoading ? (
|
||||||
<ChartSkeleton />
|
<ChartSkeleton />
|
||||||
) : (
|
) : (
|
||||||
<AreaChart
|
<AreaChart
|
||||||
@@ -1450,14 +1553,23 @@ function NetworkChart({
|
|||||||
networkChartData[]
|
networkChartData[]
|
||||||
>([]);
|
>([]);
|
||||||
const [isLoadingNetwork, setIsLoadingNetwork] = useState(false);
|
const [isLoadingNetwork, setIsLoadingNetwork] = useState(false);
|
||||||
|
const [loadedPeriodNetwork, setLoadedPeriodNetwork] =
|
||||||
|
useState<ChartPeriod>("realtime");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
if (period === "realtime") {
|
if (period === "realtime") {
|
||||||
setNetworkHistoricalData([]);
|
setNetworkHistoricalData([]);
|
||||||
return;
|
setIsLoadingNetwork(false);
|
||||||
|
setLoadedPeriodNetwork("realtime");
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchNetworkData = async () => {
|
const fetchNetworkData = async () => {
|
||||||
|
const loadingStartedAt = Date.now();
|
||||||
setIsLoadingNetwork(true);
|
setIsLoadingNetwork(true);
|
||||||
try {
|
try {
|
||||||
const [uploadResponse, downloadResponse] = await Promise.all([
|
const [uploadResponse, downloadResponse] = await Promise.all([
|
||||||
@@ -1479,16 +1591,28 @@ function NetworkChart({
|
|||||||
upload: point.value / 1024 / 1024, // Convert bytes to MB
|
upload: point.value / 1024 / 1024, // Convert bytes to MB
|
||||||
download: downloadMap.get(point.ts) || 0,
|
download: downloadMap.get(point.ts) || 0,
|
||||||
}));
|
}));
|
||||||
setNetworkHistoricalData(combinedData);
|
if (!cancelled) {
|
||||||
|
setNetworkHistoricalData(combinedData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch network metrics:", error);
|
console.error("Failed to fetch network metrics:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingNetwork(false);
|
const elapsed = Date.now() - loadingStartedAt;
|
||||||
|
if (elapsed < MIN_HISTORY_LOADING_MS) {
|
||||||
|
await sleep(MIN_HISTORY_LOADING_MS - elapsed);
|
||||||
|
}
|
||||||
|
if (!cancelled) {
|
||||||
|
setIsLoadingNetwork(false);
|
||||||
|
setLoadedPeriodNetwork(period);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchNetworkData();
|
fetchNetworkData();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}, [data.id, period]);
|
}, [data.id, period]);
|
||||||
|
|
||||||
// 初始化历史数据
|
// 初始化历史数据
|
||||||
@@ -1554,6 +1678,9 @@ function NetworkChart({
|
|||||||
|
|
||||||
const displayData =
|
const displayData =
|
||||||
period === "realtime" ? networkChartData : networkHistoricalData;
|
period === "realtime" ? networkChartData : networkHistoricalData;
|
||||||
|
const isNetworkLoading =
|
||||||
|
period !== "realtime" &&
|
||||||
|
(isLoadingNetwork || loadedPeriodNetwork !== period);
|
||||||
|
|
||||||
let maxDownload = Math.max(...displayData.map((item) => item.download));
|
let maxDownload = Math.max(...displayData.map((item) => item.download));
|
||||||
maxDownload = Math.ceil(maxDownload);
|
maxDownload = Math.ceil(maxDownload);
|
||||||
@@ -1616,7 +1743,7 @@ function NetworkChart({
|
|||||||
config={chartConfig}
|
config={chartConfig}
|
||||||
className="aspect-auto h-[130px] w-full"
|
className="aspect-auto h-[130px] w-full"
|
||||||
>
|
>
|
||||||
{isLoadingNetwork ? (
|
{isNetworkLoading ? (
|
||||||
<ChartSkeleton />
|
<ChartSkeleton />
|
||||||
) : (
|
) : (
|
||||||
<LineChart
|
<LineChart
|
||||||
@@ -1733,14 +1860,23 @@ function ConnectChart({
|
|||||||
connectChartData[]
|
connectChartData[]
|
||||||
>([]);
|
>([]);
|
||||||
const [isLoadingConnect, setIsLoadingConnect] = useState(false);
|
const [isLoadingConnect, setIsLoadingConnect] = useState(false);
|
||||||
|
const [loadedPeriodConnect, setLoadedPeriodConnect] =
|
||||||
|
useState<ChartPeriod>("realtime");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
if (period === "realtime") {
|
if (period === "realtime") {
|
||||||
setConnectHistoricalData([]);
|
setConnectHistoricalData([]);
|
||||||
return;
|
setIsLoadingConnect(false);
|
||||||
|
setLoadedPeriodConnect("realtime");
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchConnectData = async () => {
|
const fetchConnectData = async () => {
|
||||||
|
const loadingStartedAt = Date.now();
|
||||||
setIsLoadingConnect(true);
|
setIsLoadingConnect(true);
|
||||||
try {
|
try {
|
||||||
const [tcpResponse, udpResponse] = await Promise.all([
|
const [tcpResponse, udpResponse] = await Promise.all([
|
||||||
@@ -1761,16 +1897,28 @@ function ConnectChart({
|
|||||||
tcp: point.value,
|
tcp: point.value,
|
||||||
udp: udpMap.get(point.ts) || 0,
|
udp: udpMap.get(point.ts) || 0,
|
||||||
}));
|
}));
|
||||||
setConnectHistoricalData(combinedData);
|
if (!cancelled) {
|
||||||
|
setConnectHistoricalData(combinedData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch connection metrics:", error);
|
console.error("Failed to fetch connection metrics:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingConnect(false);
|
const elapsed = Date.now() - loadingStartedAt;
|
||||||
|
if (elapsed < MIN_HISTORY_LOADING_MS) {
|
||||||
|
await sleep(MIN_HISTORY_LOADING_MS - elapsed);
|
||||||
|
}
|
||||||
|
if (!cancelled) {
|
||||||
|
setIsLoadingConnect(false);
|
||||||
|
setLoadedPeriodConnect(period);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchConnectData();
|
fetchConnectData();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}, [data.id, period]);
|
}, [data.id, period]);
|
||||||
|
|
||||||
// 初始化历史数据
|
// 初始化历史数据
|
||||||
@@ -1842,6 +1990,9 @@ function ConnectChart({
|
|||||||
|
|
||||||
const displayData =
|
const displayData =
|
||||||
period === "realtime" ? connectChartData : connectHistoricalData;
|
period === "realtime" ? connectChartData : connectHistoricalData;
|
||||||
|
const isConnectLoading =
|
||||||
|
period !== "realtime" &&
|
||||||
|
(isLoadingConnect || loadedPeriodConnect !== period);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@@ -1873,7 +2024,7 @@ function ConnectChart({
|
|||||||
config={chartConfig}
|
config={chartConfig}
|
||||||
className="aspect-auto h-[130px] w-full"
|
className="aspect-auto h-[130px] w-full"
|
||||||
>
|
>
|
||||||
{isLoadingConnect ? (
|
{isConnectLoading ? (
|
||||||
<ChartSkeleton />
|
<ChartSkeleton />
|
||||||
) : (
|
) : (
|
||||||
<LineChart
|
<LineChart
|
||||||
|
|||||||
@@ -1,59 +1,23 @@
|
|||||||
export default function ChartSkeleton() {
|
export default function ChartSkeleton({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}: {
|
||||||
|
width?: number | string;
|
||||||
|
height?: number | string;
|
||||||
|
}) {
|
||||||
|
const resolvedWidth = typeof width === "number" ? `${width}px` : width;
|
||||||
|
const resolvedHeight = typeof height === "number" ? `${height}px` : height;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-[130px] w-full px-6 py-3">
|
<div
|
||||||
<div className="relative h-full w-full animate-pulse">
|
className="relative h-full w-full overflow-hidden"
|
||||||
<div className="absolute bottom-0 left-0 right-0 h-px bg-muted-foreground/20" />
|
style={{
|
||||||
<div className="absolute bottom-1 left-0 right-0 flex items-end justify-between gap-1 px-2">
|
width: resolvedWidth || "100%",
|
||||||
<div
|
height: resolvedHeight || "100%",
|
||||||
className="w-2 bg-muted-foreground/20 rounded-t"
|
}}
|
||||||
style={{ height: "40%" }}
|
>
|
||||||
/>
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<div
|
<div className="size-4 rounded-full border-2 border-muted-foreground/20 border-t-muted-foreground/70 animate-spin" />
|
||||||
className="w-2 bg-muted-foreground/30 rounded-t"
|
|
||||||
style={{ height: "65%" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="w-2 bg-muted-foreground/25 rounded-t"
|
|
||||||
style={{ height: "45%" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="w-2 bg-muted-foreground/35 rounded-t"
|
|
||||||
style={{ height: "80%" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="w-2 bg-muted-foreground/20 rounded-t"
|
|
||||||
style={{ height: "55%" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="w-2 bg-muted-foreground/30 rounded-t"
|
|
||||||
style={{ height: "70%" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="w-2 bg-muted-foreground/25 rounded-t"
|
|
||||||
style={{ height: "50%" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="w-2 bg-muted-foreground/20 rounded-t"
|
|
||||||
style={{ height: "35%" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="w-2 bg-muted-foreground/30 rounded-t"
|
|
||||||
style={{ height: "60%" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="w-2 bg-muted-foreground/25 rounded-t"
|
|
||||||
style={{ height: "45%" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="w-2 bg-muted-foreground/35 rounded-t"
|
|
||||||
style={{ height: "75%" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="w-2 bg-muted-foreground/20 rounded-t"
|
|
||||||
style={{ height: "55%" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-background/20 via-transparent to-background/10 rounded-md" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -81,7 +81,9 @@
|
|||||||
"realtime": "Realtime",
|
"realtime": "Realtime",
|
||||||
"period1d": "1 Day",
|
"period1d": "1 Day",
|
||||||
"period7d": "7 Days",
|
"period7d": "7 Days",
|
||||||
"period30d": "30 Days"
|
"period30d": "30 Days",
|
||||||
|
"tsdbRequired": "Enable TSDB to use historical data",
|
||||||
|
"loginRequired": "Please login to view"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"themeBy": "Theme by "
|
"themeBy": "Theme by "
|
||||||
@@ -115,6 +117,7 @@
|
|||||||
"packetLoss": "Packet Loss",
|
"packetLoss": "Packet Loss",
|
||||||
"clearSelections": "Clear",
|
"clearSelections": "Clear",
|
||||||
"peakCut": "Peak cut",
|
"peakCut": "Peak cut",
|
||||||
|
"loginRequired": "Please login to view",
|
||||||
"period1d": "1 Day",
|
"period1d": "1 Day",
|
||||||
"period7d": "7 Days",
|
"period7d": "7 Days",
|
||||||
"period30d": "30 Days"
|
"period30d": "30 Days"
|
||||||
|
|||||||
@@ -81,7 +81,9 @@
|
|||||||
"realtime": "实时",
|
"realtime": "实时",
|
||||||
"period1d": "1 天",
|
"period1d": "1 天",
|
||||||
"period7d": "7 天",
|
"period7d": "7 天",
|
||||||
"period30d": "30 天"
|
"period30d": "30 天",
|
||||||
|
"tsdbRequired": "需要开启 TSDB 才能启用历史记录功能",
|
||||||
|
"loginRequired": "请登录后查看"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"themeBy": "主题-"
|
"themeBy": "主题-"
|
||||||
@@ -116,6 +118,7 @@
|
|||||||
"packetLoss": "丢包率",
|
"packetLoss": "丢包率",
|
||||||
"clearSelections": "清除",
|
"clearSelections": "清除",
|
||||||
"peakCut": "削峰",
|
"peakCut": "削峰",
|
||||||
|
"loginRequired": "请登录后查看",
|
||||||
"period1d": "1 天",
|
"period1d": "1 天",
|
||||||
"period7d": "7 天",
|
"period7d": "7 天",
|
||||||
"period30d": "30 天"
|
"period30d": "30 天"
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ export interface SettingResponse {
|
|||||||
data: {
|
data: {
|
||||||
config: SettingConfig;
|
config: SettingConfig;
|
||||||
version: string;
|
version: string;
|
||||||
|
tsdb_enabled?: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user