feat: add chart loading skeletons and enhance translation for time periods

This commit is contained in:
hamster1963
2026-01-30 11:42:14 +08:00
parent 6dae6cce8f
commit 18e3c74178
10 changed files with 358 additions and 257 deletions
+194 -175
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { m } from "framer-motion";
import * as React from "react"; import * as React from "react";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -44,12 +45,6 @@ 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
*/ */
@@ -237,6 +232,12 @@ export const NetworkChartClient = React.memo(function NetworkChart({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const TIME_RANGE_OPTIONS: { value: MonitorPeriod; label: string }[] = [
{ value: "1d", label: t("monitor.period1d") },
{ value: "7d", label: t("monitor.period7d") },
{ value: "30d", label: t("monitor.period30d") },
];
const customBackgroundImage = const customBackgroundImage =
(window.CustomBackgroundImage as string) !== "" (window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage ? window.CustomBackgroundImage
@@ -529,183 +530,201 @@ export const NetworkChartClient = React.memo(function NetworkChart({
}, [isPeakEnabled, activeCharts, formattedData, chartData, chartDataKey]); }, [isPeakEnabled, activeCharts, formattedData, chartData, chartDataKey]);
return ( return (
<Card <div className="flex flex-col gap-3">
className={cn({ <div className="flex items-center gap-3 -mt-5 flex-wrap">
"bg-card/70": customBackgroundImage, <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">
})} {TIME_RANGE_OPTIONS.map((option) => {
> const isLocked = !isLogin && option.value !== "1d";
<CardHeader className="flex flex-col items-stretch space-y-0 overflow-hidden rounded-t-lg p-0 sm:flex-row"> return (
<div className="flex flex-none flex-col justify-center gap-1 border-b px-6 py-4"> <div
<CardTitle className="flex flex-none items-center gap-0.5 text-md"> key={option.value}
{serverName} onClick={() => {
</CardTitle> if (!isLocked) {
<CardDescription className="text-xs"> onPeriodChange(option.value);
{chartDataKey.length} {t("monitor.monitorCount")} }
</CardDescription>
<div className="mt-0.5 flex items-center gap-3">
<div className="flex items-center gap-1 rounded-lg bg-muted/50 p-1">
{TIME_RANGE_OPTIONS.map((option) => {
const isLocked = !isLogin && option.value !== "1d";
return (
<button
key={option.value}
type="button"
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 className="flex flex-wrap w-full">{chartButtons}</div>
</CardHeader>
<CardContent className="pr-2 pl-0 py-4 sm:pt-6 sm:pb-6 sm:pr-6 sm:pl-2">
<div className="relative">
{activeCharts.length > 0 && (
<button
className="absolute -top-2 right-1 z-10 text-xs px-2 py-1 bg-stone-100/80 dark:bg-stone-800/80 backdrop-blur-xs rounded-[5px] text-muted-foreground hover:text-foreground transition-colors"
onClick={clearAllSelections}
>
{t("monitor.clearSelections", "Clear")} ({activeCharts.length})
</button>
)}
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<ComposedChart
accessibilityLayer
data={processedData}
margin={{ left: 12, right: 12 }}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="created_at"
tickLine={true}
tickSize={3}
axisLine={false}
tickMargin={8}
minTickGap={80}
ticks={processedData
.filter((item, index, array) => {
if (array.length < 6) {
return index === 0 || index === array.length - 1;
}
// 计算数据的总时间跨度(毫秒)
const timeSpan =
array[array.length - 1].created_at - array[0].created_at;
const hours = timeSpan / (1000 * 60 * 60);
// 根据时间跨度调整显示间隔
if (hours <= 12) {
// 12小时内,每60分钟显示一个刻度
return (
index === 0 ||
index === array.length - 1 ||
new Date(item.created_at).getMinutes() % 60 === 0
);
}
// 超过12小时,每2小时显示一个刻度
const date = new Date(item.created_at);
return date.getMinutes() === 0 && date.getHours() % 2 === 0;
})
.map((item) => item.created_at)}
tickFormatter={(value) => {
const date = new Date(value);
const minutes = date.getMinutes();
return minutes === 0
? `${date.getHours()}:00`
: `${date.getHours()}:${minutes}`;
}} }}
/> className={cn(
<YAxis "relative cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors duration-300",
yAxisId="delay" period === option.value
tickLine={false} ? "text-foreground"
axisLine={false} : "text-muted-foreground hover:text-foreground",
tickMargin={15} isLocked && "cursor-not-allowed opacity-40 grayscale",
minTickGap={20} )}
tickFormatter={(value) => `${value}ms`} >
/> {period === option.value && (
{activeCharts.length === 1 && ( <m.div
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"
transition={{
type: "spring",
stiffness: 400,
damping: 30,
}}
/>
)}
<span className="relative z-20">{option.label}</span>
</div>
);
})}
</div>
<div className="flex items-center space-x-2">
<Switch
id="Peak"
checked={isPeakEnabled}
onCheckedChange={setIsPeakEnabled}
/>
<Label className="text-xs" htmlFor="Peak">
{t("monitor.peakCut")}
</Label>
</div>
</div>
<Card
className={cn({
"bg-card/70": customBackgroundImage,
})}
>
<CardHeader className="flex flex-col items-stretch space-y-0 overflow-hidden rounded-t-lg p-0 sm:flex-row">
<div className="flex flex-none flex-col justify-center gap-1 border-b px-6 py-4">
<CardTitle className="flex flex-none items-center gap-0.5 text-md">
{serverName}
</CardTitle>
<CardDescription className="text-xs">
{chartDataKey.length} {t("monitor.monitorCount")}
</CardDescription>
</div>
<div className="flex flex-wrap w-full">{chartButtons}</div>
</CardHeader>
<CardContent className="pr-2 pl-0 py-4 sm:pt-6 sm:pb-6 sm:pr-6 sm:pl-2">
<div className="relative">
{activeCharts.length > 0 && (
<button
className="absolute -top-2 right-1 z-10 text-xs px-2 py-1 bg-stone-100/80 dark:bg-stone-800/80 backdrop-blur-xs rounded-[5px] text-muted-foreground hover:text-foreground transition-colors"
onClick={clearAllSelections}
>
{t("monitor.clearSelections", "Clear")} ({activeCharts.length})
</button>
)}
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<ComposedChart
accessibilityLayer
data={processedData}
margin={{ left: 12, right: 12 }}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="created_at"
tickLine={true}
tickSize={3}
axisLine={false}
tickMargin={8}
minTickGap={80}
ticks={processedData
.filter((item, index, array) => {
if (array.length < 6) {
return index === 0 || index === array.length - 1;
}
// 计算数据的总时间跨度(毫秒)
const timeSpan =
array[array.length - 1].created_at -
array[0].created_at;
const hours = timeSpan / (1000 * 60 * 60);
// 根据时间跨度调整显示间隔
if (hours <= 12) {
// 12小时内,每60分钟显示一个刻度
return (
index === 0 ||
index === array.length - 1 ||
new Date(item.created_at).getMinutes() % 60 === 0
);
}
// 超过12小时,每2小时显示一个刻度
const date = new Date(item.created_at);
return (
date.getMinutes() === 0 && date.getHours() % 2 === 0
);
})
.map((item) => item.created_at)}
tickFormatter={(value) => {
const date = new Date(value);
const minutes = date.getMinutes();
return minutes === 0
? `${date.getHours()}:00`
: `${date.getHours()}:${minutes}`;
}}
/>
<YAxis <YAxis
yAxisId="packet-loss" yAxisId="delay"
orientation="right"
tickLine={false} tickLine={false}
axisLine={false} axisLine={false}
tickMargin={15} tickMargin={15}
minTickGap={20} minTickGap={20}
tickFormatter={(value) => `${value}%`} tickFormatter={(value) => `${value}ms`}
/> />
)} {activeCharts.length === 1 && (
<ChartTooltip <YAxis
isAnimationActive={false} yAxisId="packet-loss"
content={ orientation="right"
<ChartTooltipContent tickLine={false}
indicator={"line"} axisLine={false}
labelKey="created_at" tickMargin={15}
labelFormatter={(_, payload) => { minTickGap={20}
return formatTime(payload[0].payload.created_at); tickFormatter={(value) => `${value}%`}
}}
formatter={(value, name) => {
let formattedValue: string;
let label: string;
if (name === "packet_loss") {
formattedValue = `${Number(value).toFixed(2)}%`;
label = t("monitor.packetLoss", "Packet Loss");
} else if (name === "avg_delay") {
formattedValue = `${Number(value).toFixed(2)}ms`;
label = t("monitor.avgDelay", "Avg Delay");
} else {
// For monitor names (in multi-chart view) - delay data
formattedValue = `${Number(value).toFixed(2)}ms`;
label = name as string;
}
return (
<div className="flex flex-1 items-center justify-between leading-none">
<span className="text-muted-foreground">{label}</span>
<span className="ml-2 font-medium text-foreground tabular-nums">
{formattedValue}
</span>
</div>
);
}}
/> />
} )}
/> <ChartTooltip
{activeCharts.length !== 1 && ( isAnimationActive={false}
<ChartLegend content={<ChartLegendContent />} /> content={
)} <ChartTooltipContent
{chartElements} indicator={"line"}
</ComposedChart> labelKey="created_at"
</ChartContainer> labelFormatter={(_, payload) => {
</div> return formatTime(payload[0].payload.created_at);
</CardContent> }}
</Card> formatter={(value, name) => {
let formattedValue: string;
let label: string;
if (name === "packet_loss") {
formattedValue = `${Number(value).toFixed(2)}%`;
label = t("monitor.packetLoss", "Packet Loss");
} else if (name === "avg_delay") {
formattedValue = `${Number(value).toFixed(2)}ms`;
label = t("monitor.avgDelay", "Avg Delay");
} else {
// For monitor names (in multi-chart view) - delay data
formattedValue = `${Number(value).toFixed(2)}ms`;
label = name as string;
}
return (
<div className="flex flex-1 items-center justify-between leading-none">
<span className="text-muted-foreground">
{label}
</span>
<span className="ml-2 font-medium text-foreground tabular-nums">
{formattedValue}
</span>
</div>
);
}}
/>
}
/>
{activeCharts.length !== 1 && (
<ChartLegend content={<ChartLegendContent />} />
)}
{chartElements}
</ComposedChart>
</ChartContainer>
</div>
</CardContent>
</Card>
</div>
); );
}); });
+69 -75
View File
@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { m } from "framer-motion";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
@@ -32,6 +33,7 @@ import type {
NezhaWebsocketResponse, NezhaWebsocketResponse,
} from "@/types/nezha-api"; } from "@/types/nezha-api";
import ChartSkeleton from "./loading/ChartSkeleton";
import { ServerDetailChartLoading } from "./loading/ServerDetailLoading"; import { ServerDetailChartLoading } from "./loading/ServerDetailLoading";
import AnimatedCircularProgressBar from "./ui/animated-circular-progress-bar"; import AnimatedCircularProgressBar from "./ui/animated-circular-progress-bar";
@@ -94,31 +96,41 @@ function PeriodSelector({
]; ];
return ( return (
<div className="flex gap-1 mb-3 flex-wrap -mt-5"> <div className="flex gap-0.5 mb-3 flex-wrap -mt-5 p-0.5 bg-muted dark:bg-muted/40 rounded-full w-fit border border-border/60 dark:border-border">
{periods.map((period) => { {periods.map((period) => {
// Only realtime and 1d are available for non-logged-in users // Only realtime and 1d are available for non-logged-in users
const isLocked = const isLocked =
!isLogin && period.value !== "realtime" && period.value !== "1d"; !isLogin && period.value !== "realtime" && period.value !== "1d";
return ( return (
<button <div
key={period.value} key={period.value}
type="button"
disabled={isLocked}
onClick={() => { onClick={() => {
if (!isLocked) { if (!isLocked) {
onPeriodChange(period.value); onPeriodChange(period.value);
} }
}} }}
className={cn( className={cn(
"px-2.5 py-1 text-xs rounded-md transition-all", "relative cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors duration-300",
selectedPeriod === period.value selectedPeriod === period.value
? "bg-primary text-primary-foreground font-medium" ? "text-foreground"
: "bg-muted/50 hover:bg-muted text-muted-foreground hover:text-foreground", : "text-muted-foreground hover:text-foreground",
isLocked && "cursor-not-allowed opacity-50", isLocked && "cursor-not-allowed opacity-40 grayscale",
)} )}
> >
{period.label} {selectedPeriod === period.value && (
</button> <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>
@@ -261,15 +273,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 loadingTimerRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => { useEffect(() => {
if (period === "realtime") { if (period === "realtime") {
setHistoricalData([]); setHistoricalData([]);
setDisplayData([]);
if (loadingTimerRef.current) {
clearTimeout(loadingTimerRef.current);
}
return; return;
} }
const fetchData = async () => { const fetchData = async () => {
setIsLoading(true); loadingTimerRef.current = setTimeout(() => {
setIsLoading(true);
}, 200);
try { try {
const response = await fetchServerMetrics( const response = await fetchServerMetrics(
serverId, serverId,
@@ -281,10 +302,15 @@ function useHistoricalData<T>(
transformData(point.ts, point.value), transformData(point.ts, point.value),
); );
setHistoricalData(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) {
clearTimeout(loadingTimerRef.current);
loadingTimerRef.current = null;
}
setIsLoading(false); setIsLoading(false);
} }
}; };
@@ -292,7 +318,7 @@ function useHistoricalData<T>(
fetchData(); fetchData();
}, [serverId, metricName, period, transformData]); }, [serverId, metricName, period, transformData]);
return { historicalData, isLoading }; return { historicalData, displayData, isLoading };
} }
function GpuChart({ function GpuChart({
@@ -328,12 +354,8 @@ function GpuChart({
[], [],
); );
const { historicalData, isLoading } = useHistoricalData<gpuChartData>( const { displayData: gpuHistoricalData, isLoading } =
id, useHistoricalData<gpuChartData>(id, "gpu", period, transformGpuData);
"gpu",
period,
transformGpuData,
);
// 初始化历史数据 // 初始化历史数据
useEffect(() => { useEffect(() => {
@@ -397,7 +419,7 @@ function GpuChart({
}, },
} satisfies ChartConfig; } satisfies ChartConfig;
const displayData = period === "realtime" ? gpuChartData : historicalData; const displayData = period === "realtime" ? gpuChartData : gpuHistoricalData;
return ( return (
<Card <Card
@@ -430,11 +452,7 @@ function GpuChart({
className="aspect-auto h-[130px] w-full" className="aspect-auto h-[130px] w-full"
> >
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center h-full"> <ChartSkeleton />
<span className="text-xs text-muted-foreground">
Loading...
</span>
</div>
) : ( ) : (
<AreaChart <AreaChart
syncId="serverDetailCharts" syncId="serverDetailCharts"
@@ -532,12 +550,8 @@ function CpuChart({
[], [],
); );
const { historicalData, isLoading } = useHistoricalData<cpuChartData>( const { displayData: cpuHistoricalData, isLoading } =
data.id, useHistoricalData<cpuChartData>(data.id, "cpu", period, transformCpuData);
"cpu",
period,
transformCpuData,
);
// 初始化历史数据 // 初始化历史数据
useEffect(() => { useEffect(() => {
@@ -602,7 +616,7 @@ function CpuChart({
}, },
} satisfies ChartConfig; } satisfies ChartConfig;
const displayData = period === "realtime" ? cpuChartData : historicalData; const displayData = period === "realtime" ? cpuChartData : cpuHistoricalData;
return ( return (
<Card <Card
@@ -632,11 +646,7 @@ function CpuChart({
className="aspect-auto h-[130px] w-full" className="aspect-auto h-[130px] w-full"
> >
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center h-full"> <ChartSkeleton />
<span className="text-xs text-muted-foreground">
Loading...
</span>
</div>
) : ( ) : (
<AreaChart <AreaChart
syncId="serverDetailCharts" syncId="serverDetailCharts"
@@ -737,12 +747,13 @@ function ProcessChart({
[], [],
); );
const { historicalData, isLoading } = useHistoricalData<processChartData>( const { displayData: processHistoricalData, isLoading } =
data.id, useHistoricalData<processChartData>(
"process_count", data.id,
period, "process_count",
transformProcessData, period,
); transformProcessData,
);
// 初始化历史数据 // 初始化历史数据
useEffect(() => { useEffect(() => {
@@ -807,7 +818,8 @@ function ProcessChart({
}, },
} satisfies ChartConfig; } satisfies ChartConfig;
const displayData = period === "realtime" ? processChartData : historicalData; const displayData =
period === "realtime" ? processChartData : processHistoricalData;
return ( return (
<Card <Card
@@ -830,11 +842,7 @@ function ProcessChart({
className="aspect-auto h-[130px] w-full" className="aspect-auto h-[130px] w-full"
> >
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center h-full"> <ChartSkeleton />
<span className="text-xs text-muted-foreground">
Loading...
</span>
</div>
) : ( ) : (
<AreaChart <AreaChart
syncId="serverDetailCharts" syncId="serverDetailCharts"
@@ -1114,11 +1122,7 @@ function MemChart({
className="aspect-auto h-[130px] w-full" className="aspect-auto h-[130px] w-full"
> >
{isLoadingMem ? ( {isLoadingMem ? (
<div className="flex items-center justify-center h-full"> <ChartSkeleton />
<span className="text-xs text-muted-foreground">
Loading...
</span>
</div>
) : ( ) : (
<AreaChart <AreaChart
syncId="serverDetailCharts" syncId="serverDetailCharts"
@@ -1238,12 +1242,13 @@ function DiskChart({
[data.host.disk_total], [data.host.disk_total],
); );
const { historicalData, isLoading } = useHistoricalData<diskChartData>( const { displayData: diskHistoricalData, isLoading } =
data.id, useHistoricalData<diskChartData>(
"disk", data.id,
period, "disk",
transformDiskData, period,
); transformDiskData,
);
// 初始化历史数据 // 初始化历史数据
useEffect(() => { useEffect(() => {
@@ -1308,7 +1313,8 @@ function DiskChart({
}, },
} satisfies ChartConfig; } satisfies ChartConfig;
const displayData = period === "realtime" ? diskChartData : historicalData; const displayData =
period === "realtime" ? diskChartData : diskHistoricalData;
return ( return (
<Card <Card
@@ -1344,11 +1350,7 @@ function DiskChart({
className="aspect-auto h-[130px] w-full" className="aspect-auto h-[130px] w-full"
> >
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center h-full"> <ChartSkeleton />
<span className="text-xs text-muted-foreground">
Loading...
</span>
</div>
) : ( ) : (
<AreaChart <AreaChart
syncId="serverDetailCharts" syncId="serverDetailCharts"
@@ -1615,11 +1617,7 @@ function NetworkChart({
className="aspect-auto h-[130px] w-full" className="aspect-auto h-[130px] w-full"
> >
{isLoadingNetwork ? ( {isLoadingNetwork ? (
<div className="flex items-center justify-center h-full"> <ChartSkeleton />
<span className="text-xs text-muted-foreground">
Loading...
</span>
</div>
) : ( ) : (
<LineChart <LineChart
syncId="serverDetailCharts" syncId="serverDetailCharts"
@@ -1876,11 +1874,7 @@ function ConnectChart({
className="aspect-auto h-[130px] w-full" className="aspect-auto h-[130px] w-full"
> >
{isLoadingConnect ? ( {isLoadingConnect ? (
<div className="flex items-center justify-center h-full"> <ChartSkeleton />
<span className="text-xs text-muted-foreground">
Loading...
</span>
</div>
) : ( ) : (
<LineChart <LineChart
syncId="serverDetailCharts" syncId="serverDetailCharts"
+60
View File
@@ -0,0 +1,60 @@
export default function ChartSkeleton() {
return (
<div className="h-[130px] w-full px-6 py-3">
<div className="relative h-full w-full animate-pulse">
<div className="absolute bottom-0 left-0 right-0 h-px bg-muted-foreground/20" />
<div className="absolute bottom-1 left-0 right-0 flex items-end justify-between gap-1 px-2">
<div
className="w-2 bg-muted-foreground/20 rounded-t"
style={{ height: "40%" }}
/>
<div
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>
);
}
+5 -1
View File
@@ -47,7 +47,11 @@
"noData": "Kein Server Monitoring Daten, bitte fügen sie zuerst einen Monitor hinzu", "noData": "Kein Server Monitoring Daten, bitte fügen sie zuerst einen Monitor hinzu",
"avgDelay": "Latenz", "avgDelay": "Latenz",
"packetLoss": "Paketverlust", "packetLoss": "Paketverlust",
"clearSelections": "Löschen" "clearSelections": "Löschen",
"peakCut": "Peak cut",
"period1d": "1 Tag",
"period7d": "7 Tage",
"period30d": "30 Tage"
}, },
"billingInfo": { "billingInfo": {
"error": "Fehler", "error": "Fehler",
+5 -1
View File
@@ -113,7 +113,11 @@
"avgDelay": "Latency", "avgDelay": "Latency",
"monitorCount": "Services", "monitorCount": "Services",
"packetLoss": "Packet Loss", "packetLoss": "Packet Loss",
"clearSelections": "Clear" "clearSelections": "Clear",
"peakCut": "Peak cut",
"period1d": "1 Day",
"period7d": "7 Days",
"period30d": "30 Days"
}, },
"pwa": { "pwa": {
"offlineReady": "App ready to work offline", "offlineReady": "App ready to work offline",
+5 -1
View File
@@ -93,7 +93,11 @@
"noData": "No hay datos de servidores, primero agregue un monitor de servicio", "noData": "No hay datos de servidores, primero agregue un monitor de servicio",
"monitorCount": "Servicios", "monitorCount": "Servicios",
"packetLoss": "Pérdida de paquetes", "packetLoss": "Pérdida de paquetes",
"clearSelections": "Limpiar" "clearSelections": "Limpiar",
"peakCut": "Peak cut",
"period1d": "1 Día",
"period7d": "7 Días",
"period30d": "30 Días"
}, },
"error": { "error": {
"pageNotFound": "Página no encontrada", "pageNotFound": "Página no encontrada",
+5 -1
View File
@@ -100,7 +100,11 @@
"avgDelay": "Задержка", "avgDelay": "Задержка",
"monitorCount": "Сервисы", "monitorCount": "Сервисы",
"packetLoss": "Потеря пакетов", "packetLoss": "Потеря пакетов",
"clearSelections": "Очистить" "clearSelections": "Очистить",
"peakCut": "Peak cut",
"period1d": "1 день",
"period7d": "7 дней",
"period30d": "30 дней"
}, },
"pwa": { "pwa": {
"newContent": "Доступен новый контент", "newContent": "Доступен новый контент",
+5 -1
View File
@@ -95,7 +95,11 @@
"avgDelay": "சுணக்கம்", "avgDelay": "சுணக்கம்",
"monitorCount": "சேவைகள்", "monitorCount": "சேவைகள்",
"packetLoss": "தொகுப்பு இழப்பு", "packetLoss": "தொகுப்பு இழப்பு",
"clearSelections": "அழி" "clearSelections": "அழி",
"peakCut": "Peak cut",
"period1d": "1 நாள்",
"period7d": "7 நாட்கள்",
"period30d": "30 நாட்கள்"
}, },
"pwa": { "pwa": {
"offlineReady": "ஆஃப்லைனில் வேலை செய்ய பயன்பாடு தயாராக உள்ளது", "offlineReady": "ஆஃப்லைனில் வேலை செய்ய பயன்பாடு தயாராக உள்ளது",
+5 -1
View File
@@ -114,7 +114,11 @@
"avgDelay": "延迟", "avgDelay": "延迟",
"monitorCount": "个监控服务", "monitorCount": "个监控服务",
"packetLoss": "丢包率", "packetLoss": "丢包率",
"clearSelections": "清除" "clearSelections": "清除",
"peakCut": "削峰",
"period1d": "1 天",
"period7d": "7 天",
"period30d": "30 天"
}, },
"pwa": { "pwa": {
"offlineReady": "应用可以离线使用了", "offlineReady": "应用可以离线使用了",
+5 -1
View File
@@ -112,7 +112,11 @@
"avgDelay": "延遲", "avgDelay": "延遲",
"monitorCount": "個監控", "monitorCount": "個監控",
"packetLoss": "丟包率", "packetLoss": "丟包率",
"clearSelections": "清除" "clearSelections": "清除",
"peakCut": "削峰",
"period1d": "1 天",
"period7d": "7 天",
"period30d": "30 天"
}, },
"billingInfo": { "billingInfo": {
"remaining": "剩餘天數", "remaining": "剩餘天數",