feat: enhance tooltips and loading states in NetworkChart and ServerDetailChart; add translations for TSDB and login requirements

This commit is contained in:
hamster1963
2026-02-14 15:04:37 +08:00
parent 7d59371ee3
commit 0c7c6a1378
6 changed files with 300 additions and 158 deletions
+55 -35
View File
@@ -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"
+217 -66
View File
@@ -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
+19 -55
View File
@@ -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>
); );
+4 -1
View File
@@ -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"
+4 -1
View File
@@ -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 天"
+1
View File
@@ -156,6 +156,7 @@ export interface SettingResponse {
data: { data: {
config: SettingConfig; config: SettingConfig;
version: string; version: string;
tsdb_enabled?: boolean;
}; };
} }