Skip to content

Commit 18e3c74

Browse files
committed
feat: add chart loading skeletons and enhance translation for time periods
1 parent 6dae6cc commit 18e3c74

File tree

10 files changed

+354
-253
lines changed

10 files changed

+354
-253
lines changed

src/components/NetworkChart.tsx

Lines changed: 190 additions & 171 deletions
Large diffs are not rendered by default.

src/components/ServerDetailChart.tsx

Lines changed: 69 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useQuery } from "@tanstack/react-query";
2+
import { m } from "framer-motion";
23
import { useEffect, useMemo, useRef, useState } from "react";
34
import { useTranslation } from "react-i18next";
45
import {
@@ -32,6 +33,7 @@ import type {
3233
NezhaWebsocketResponse,
3334
} from "@/types/nezha-api";
3435

36+
import ChartSkeleton from "./loading/ChartSkeleton";
3537
import { ServerDetailChartLoading } from "./loading/ServerDetailLoading";
3638
import AnimatedCircularProgressBar from "./ui/animated-circular-progress-bar";
3739

@@ -94,31 +96,41 @@ function PeriodSelector({
9496
];
9597

9698
return (
97-
<div className="flex gap-1 mb-3 flex-wrap -mt-5">
99+
<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">
98100
{periods.map((period) => {
99101
// Only realtime and 1d are available for non-logged-in users
100102
const isLocked =
101103
!isLogin && period.value !== "realtime" && period.value !== "1d";
102104
return (
103-
<button
105+
<div
104106
key={period.value}
105-
type="button"
106-
disabled={isLocked}
107107
onClick={() => {
108108
if (!isLocked) {
109109
onPeriodChange(period.value);
110110
}
111111
}}
112112
className={cn(
113-
"px-2.5 py-1 text-xs rounded-md transition-all",
113+
"relative cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors duration-300",
114114
selectedPeriod === period.value
115-
? "bg-primary text-primary-foreground font-medium"
116-
: "bg-muted/50 hover:bg-muted text-muted-foreground hover:text-foreground",
117-
isLocked && "cursor-not-allowed opacity-50",
115+
? "text-foreground"
116+
: "text-muted-foreground hover:text-foreground",
117+
isLocked && "cursor-not-allowed opacity-40 grayscale",
118118
)}
119119
>
120-
{period.label}
121-
</button>
120+
{selectedPeriod === period.value && (
121+
<m.div
122+
layoutId="period-selector-active"
123+
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"
124+
transition={{ type: "spring", stiffness: 250, damping: 30 }}
125+
/>
126+
)}
127+
<div className="relative z-20 flex items-center gap-1.5">
128+
{period.value === "realtime" && (
129+
<span className="inline-flex rounded-full h-1.5 w-1.5 bg-emerald-500 dark:bg-emerald-400"></span>
130+
)}
131+
{period.label}
132+
</div>
133+
</div>
122134
);
123135
})}
124136
</div>
@@ -261,15 +273,24 @@ function useHistoricalData<T>(
261273
) {
262274
const [historicalData, setHistoricalData] = useState<T[]>([]);
263275
const [isLoading, setIsLoading] = useState(false);
276+
const [displayData, setDisplayData] = useState<T[]>([]);
277+
const loadingTimerRef = useRef<NodeJS.Timeout | null>(null);
264278

265279
useEffect(() => {
266280
if (period === "realtime") {
267281
setHistoricalData([]);
282+
setDisplayData([]);
283+
if (loadingTimerRef.current) {
284+
clearTimeout(loadingTimerRef.current);
285+
}
268286
return;
269287
}
270288

271289
const fetchData = async () => {
272-
setIsLoading(true);
290+
loadingTimerRef.current = setTimeout(() => {
291+
setIsLoading(true);
292+
}, 200);
293+
273294
try {
274295
const response = await fetchServerMetrics(
275296
serverId,
@@ -281,18 +302,23 @@ function useHistoricalData<T>(
281302
transformData(point.ts, point.value),
282303
);
283304
setHistoricalData(transformedData);
305+
setDisplayData(transformedData);
284306
}
285307
} catch (error) {
286308
console.error(`Failed to fetch ${metricName} metrics:`, error);
287309
} finally {
310+
if (loadingTimerRef.current) {
311+
clearTimeout(loadingTimerRef.current);
312+
loadingTimerRef.current = null;
313+
}
288314
setIsLoading(false);
289315
}
290316
};
291317

292318
fetchData();
293319
}, [serverId, metricName, period, transformData]);
294320

295-
return { historicalData, isLoading };
321+
return { historicalData, displayData, isLoading };
296322
}
297323

298324
function GpuChart({
@@ -328,12 +354,8 @@ function GpuChart({
328354
[],
329355
);
330356

331-
const { historicalData, isLoading } = useHistoricalData<gpuChartData>(
332-
id,
333-
"gpu",
334-
period,
335-
transformGpuData,
336-
);
357+
const { displayData: gpuHistoricalData, isLoading } =
358+
useHistoricalData<gpuChartData>(id, "gpu", period, transformGpuData);
337359

338360
// 初始化历史数据
339361
useEffect(() => {
@@ -397,7 +419,7 @@ function GpuChart({
397419
},
398420
} satisfies ChartConfig;
399421

400-
const displayData = period === "realtime" ? gpuChartData : historicalData;
422+
const displayData = period === "realtime" ? gpuChartData : gpuHistoricalData;
401423

402424
return (
403425
<Card
@@ -430,11 +452,7 @@ function GpuChart({
430452
className="aspect-auto h-[130px] w-full"
431453
>
432454
{isLoading ? (
433-
<div className="flex items-center justify-center h-full">
434-
<span className="text-xs text-muted-foreground">
435-
Loading...
436-
</span>
437-
</div>
455+
<ChartSkeleton />
438456
) : (
439457
<AreaChart
440458
syncId="serverDetailCharts"
@@ -532,12 +550,8 @@ function CpuChart({
532550
[],
533551
);
534552

535-
const { historicalData, isLoading } = useHistoricalData<cpuChartData>(
536-
data.id,
537-
"cpu",
538-
period,
539-
transformCpuData,
540-
);
553+
const { displayData: cpuHistoricalData, isLoading } =
554+
useHistoricalData<cpuChartData>(data.id, "cpu", period, transformCpuData);
541555

542556
// 初始化历史数据
543557
useEffect(() => {
@@ -602,7 +616,7 @@ function CpuChart({
602616
},
603617
} satisfies ChartConfig;
604618

605-
const displayData = period === "realtime" ? cpuChartData : historicalData;
619+
const displayData = period === "realtime" ? cpuChartData : cpuHistoricalData;
606620

607621
return (
608622
<Card
@@ -632,11 +646,7 @@ function CpuChart({
632646
className="aspect-auto h-[130px] w-full"
633647
>
634648
{isLoading ? (
635-
<div className="flex items-center justify-center h-full">
636-
<span className="text-xs text-muted-foreground">
637-
Loading...
638-
</span>
639-
</div>
649+
<ChartSkeleton />
640650
) : (
641651
<AreaChart
642652
syncId="serverDetailCharts"
@@ -737,12 +747,13 @@ function ProcessChart({
737747
[],
738748
);
739749

740-
const { historicalData, isLoading } = useHistoricalData<processChartData>(
741-
data.id,
742-
"process_count",
743-
period,
744-
transformProcessData,
745-
);
750+
const { displayData: processHistoricalData, isLoading } =
751+
useHistoricalData<processChartData>(
752+
data.id,
753+
"process_count",
754+
period,
755+
transformProcessData,
756+
);
746757

747758
// 初始化历史数据
748759
useEffect(() => {
@@ -807,7 +818,8 @@ function ProcessChart({
807818
},
808819
} satisfies ChartConfig;
809820

810-
const displayData = period === "realtime" ? processChartData : historicalData;
821+
const displayData =
822+
period === "realtime" ? processChartData : processHistoricalData;
811823

812824
return (
813825
<Card
@@ -830,11 +842,7 @@ function ProcessChart({
830842
className="aspect-auto h-[130px] w-full"
831843
>
832844
{isLoading ? (
833-
<div className="flex items-center justify-center h-full">
834-
<span className="text-xs text-muted-foreground">
835-
Loading...
836-
</span>
837-
</div>
845+
<ChartSkeleton />
838846
) : (
839847
<AreaChart
840848
syncId="serverDetailCharts"
@@ -1114,11 +1122,7 @@ function MemChart({
11141122
className="aspect-auto h-[130px] w-full"
11151123
>
11161124
{isLoadingMem ? (
1117-
<div className="flex items-center justify-center h-full">
1118-
<span className="text-xs text-muted-foreground">
1119-
Loading...
1120-
</span>
1121-
</div>
1125+
<ChartSkeleton />
11221126
) : (
11231127
<AreaChart
11241128
syncId="serverDetailCharts"
@@ -1238,12 +1242,13 @@ function DiskChart({
12381242
[data.host.disk_total],
12391243
);
12401244

1241-
const { historicalData, isLoading } = useHistoricalData<diskChartData>(
1242-
data.id,
1243-
"disk",
1244-
period,
1245-
transformDiskData,
1246-
);
1245+
const { displayData: diskHistoricalData, isLoading } =
1246+
useHistoricalData<diskChartData>(
1247+
data.id,
1248+
"disk",
1249+
period,
1250+
transformDiskData,
1251+
);
12471252

12481253
// 初始化历史数据
12491254
useEffect(() => {
@@ -1308,7 +1313,8 @@ function DiskChart({
13081313
},
13091314
} satisfies ChartConfig;
13101315

1311-
const displayData = period === "realtime" ? diskChartData : historicalData;
1316+
const displayData =
1317+
period === "realtime" ? diskChartData : diskHistoricalData;
13121318

13131319
return (
13141320
<Card
@@ -1344,11 +1350,7 @@ function DiskChart({
13441350
className="aspect-auto h-[130px] w-full"
13451351
>
13461352
{isLoading ? (
1347-
<div className="flex items-center justify-center h-full">
1348-
<span className="text-xs text-muted-foreground">
1349-
Loading...
1350-
</span>
1351-
</div>
1353+
<ChartSkeleton />
13521354
) : (
13531355
<AreaChart
13541356
syncId="serverDetailCharts"
@@ -1615,11 +1617,7 @@ function NetworkChart({
16151617
className="aspect-auto h-[130px] w-full"
16161618
>
16171619
{isLoadingNetwork ? (
1618-
<div className="flex items-center justify-center h-full">
1619-
<span className="text-xs text-muted-foreground">
1620-
Loading...
1621-
</span>
1622-
</div>
1620+
<ChartSkeleton />
16231621
) : (
16241622
<LineChart
16251623
syncId="serverDetailCharts"
@@ -1876,11 +1874,7 @@ function ConnectChart({
18761874
className="aspect-auto h-[130px] w-full"
18771875
>
18781876
{isLoadingConnect ? (
1879-
<div className="flex items-center justify-center h-full">
1880-
<span className="text-xs text-muted-foreground">
1881-
Loading...
1882-
</span>
1883-
</div>
1877+
<ChartSkeleton />
18841878
) : (
18851879
<LineChart
18861880
syncId="serverDetailCharts"
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
export default function ChartSkeleton() {
2+
return (
3+
<div className="h-[130px] w-full px-6 py-3">
4+
<div className="relative h-full w-full animate-pulse">
5+
<div className="absolute bottom-0 left-0 right-0 h-px bg-muted-foreground/20" />
6+
<div className="absolute bottom-1 left-0 right-0 flex items-end justify-between gap-1 px-2">
7+
<div
8+
className="w-2 bg-muted-foreground/20 rounded-t"
9+
style={{ height: "40%" }}
10+
/>
11+
<div
12+
className="w-2 bg-muted-foreground/30 rounded-t"
13+
style={{ height: "65%" }}
14+
/>
15+
<div
16+
className="w-2 bg-muted-foreground/25 rounded-t"
17+
style={{ height: "45%" }}
18+
/>
19+
<div
20+
className="w-2 bg-muted-foreground/35 rounded-t"
21+
style={{ height: "80%" }}
22+
/>
23+
<div
24+
className="w-2 bg-muted-foreground/20 rounded-t"
25+
style={{ height: "55%" }}
26+
/>
27+
<div
28+
className="w-2 bg-muted-foreground/30 rounded-t"
29+
style={{ height: "70%" }}
30+
/>
31+
<div
32+
className="w-2 bg-muted-foreground/25 rounded-t"
33+
style={{ height: "50%" }}
34+
/>
35+
<div
36+
className="w-2 bg-muted-foreground/20 rounded-t"
37+
style={{ height: "35%" }}
38+
/>
39+
<div
40+
className="w-2 bg-muted-foreground/30 rounded-t"
41+
style={{ height: "60%" }}
42+
/>
43+
<div
44+
className="w-2 bg-muted-foreground/25 rounded-t"
45+
style={{ height: "45%" }}
46+
/>
47+
<div
48+
className="w-2 bg-muted-foreground/35 rounded-t"
49+
style={{ height: "75%" }}
50+
/>
51+
<div
52+
className="w-2 bg-muted-foreground/20 rounded-t"
53+
style={{ height: "55%" }}
54+
/>
55+
</div>
56+
<div className="absolute inset-0 bg-gradient-to-t from-background/20 via-transparent to-background/10 rounded-md" />
57+
</div>
58+
</div>
59+
);
60+
}

src/locales/de/translation.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@
4747
"noData": "Kein Server Monitoring Daten, bitte fügen sie zuerst einen Monitor hinzu",
4848
"avgDelay": "Latenz",
4949
"packetLoss": "Paketverlust",
50-
"clearSelections": "Löschen"
50+
"clearSelections": "Löschen",
51+
"peakCut": "Peak cut",
52+
"period1d": "1 Tag",
53+
"period7d": "7 Tage",
54+
"period30d": "30 Tage"
5155
},
5256
"billingInfo": {
5357
"error": "Fehler",

0 commit comments

Comments
 (0)