This commit is contained in:
ryan
2026-07-13 15:08:40 +08:00
parent 08e8eea932
commit 1eff7878a1
441 changed files with 26491 additions and 18405 deletions
@@ -1,13 +1,7 @@
'use client';
import {TrendChart} from '@/components/data/trend-chart';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {Card, CardContent, CardDescription, CardHeader, CardTitle,} from '@/components/ui/card';
import type {CapacityTrendPoint} from '@/lib/services/openflare';
import {formatPercent, formatTrendHour} from './dashboard-utils';
@@ -22,14 +16,16 @@ export function CapacityTrendChart({
description?: string;
}) {
return (
<Card className="border-dashed shadow-none">
<Card className='border-dashed shadow-none'>
<CardHeader>
<CardTitle className="text-sm font-semibold">{title}</CardTitle>
<CardDescription className="text-xs">{description}</CardDescription>
<CardTitle className='text-sm font-semibold'>{title}</CardTitle>
<CardDescription className='text-xs'>{description}</CardDescription>
</CardHeader>
<CardContent>
<TrendChart
labels={points.map((point) => formatTrendHour(point.bucket_started_at))}
labels={points.map((point) =>
formatTrendHour(point.bucket_started_at),
)}
yAxisValueFormatter={formatPercent}
series={[
{
@@ -51,4 +47,4 @@ export function CapacityTrendChart({
</CardContent>
</Card>
);
}
}
@@ -12,4 +12,4 @@ export function formatTrendHour(value: string) {
return '—';
}
return `${date.getHours().toString().padStart(2, '0')}:00`;
}
}
@@ -1,682 +1,172 @@
{
"Afghanistan": [
65.99254367483304,
33.83349410470984
],
"Angola": [
17.563198329790602,
-12.334557680834592
],
"Albania": [
20.035803636476235,
41.179283471608585
],
"United Arab Emirates": [
54.30345828396012,
23.905698042805522
],
"Argentina": [
-65.15670576062132,
-35.178771590782524
],
"Armenia": [
44.929379894609674,
40.29884592895426
],
"France": [
165.43817082439585,
-21.285375782391018
],
"Australia": [
134.361180723562,
-25.574955774991995
],
"Austria": [
14.10165807418182,
47.57042831528493
],
"Azerbaijan": [
47.636777782137415,
40.3311508093757
],
"Burundi": [
29.873065896828848,
-3.3802825292327188
],
"Belgium": [
4.628129604331132,
50.646494813466056
],
"Benin": [
2.323507643298733,
9.63048313950481
],
"Burkina Faso": [
-1.7757430610210276,
12.260370131848955
],
"Bangladesh": [
90.21009840315206,
23.88793565572049
],
"Bulgaria": [
25.225364036830744,
42.77785706066581
],
"The Bahamas": [
-78.02733865351972,
24.719920319716515
],
"Bosnia and Herzegovina": [
17.762461846176542,
44.16398853722456
],
"Belarus": [
28.04895281233843,
53.53432933090482
],
"Belize": [
-88.72096138346826,
17.17073148225863
],
"Bolivia": [
-64.69104972918655,
-16.70141507507877
],
"Brazil": [
-53.113858151998855,
-10.827995867833263
],
"Brunei": [
114.54229268997476,
4.451218476864121
],
"Bhutan": [
90.40562361681495,
27.40133623508629
],
"Botswana": [
23.791788508036337,
-22.185333985163894
],
"Central African Republic": [
20.490781591297914,
6.563625165826992
],
"Canada": [
-102.31777616484295,
57.94457487676815
],
"Switzerland": [
8.205812860319684,
46.805544047201984
],
"Chile": [
-71.34891661948937,
-35.98028631773855
],
"China": [
103.79804331240364,
36.61094167680319
],
"Ivory Coast": [
-5.569771898429842,
7.626172775954684
],
"Cameroon": [
12.739868035578002,
5.692692453534944
],
"Democratic Republic of the Congo": [
23.64811688254194,
-2.8775533309965056
],
"Republic of the Congo": [
15.21207418503586,
-0.8564004333096924
],
"Colombia": [
-73.07794094917405,
3.9235618261252663
],
"Costa Rica": [
-84.21742467558424,
9.986463158070535
],
"Cuba": [
-78.8954025482625,
21.62136808001903
],
"Northern Cyprus": [
33.39079188058263,
35.225753269439686
],
"Cyprus": [
33.02409208254226,
34.91304424540028
],
"Czech Republic": [
15.330767057694615,
49.70160772219921
],
"Germany": [
10.380496796525602,
51.083853487429664
],
"Djibouti": [
42.56788631653962,
11.745988995518626
],
"Denmark": [
-41.29088868109839,
74.72535220460091
],
"Dominican Republic": [
-70.48595892120278,
18.896879062003393
],
"Algeria": [
2.6175172555729325,
28.158202786113698
],
"Ecuador": [
-78.38384779532852,
-1.4454571869341688
],
"Egypt": [
29.86739912685152,
26.49286512797635
],
"Eritrea": [
38.835037148217566,
15.359809600209143
],
"Spain": [
-3.5708365129134876,
40.391564705240064
],
"Estonia": [
25.811350172874032,
58.69303019372469
],
"Ethiopia": [
39.60372599651433,
8.621595995236198
],
"Finland": [
26.28958351526606,
64.52279699210293
],
"Fiji": [
177.98415179869738,
-17.827004078184217
],
"United Kingdom": [
-36.367082569942795,
-54.468348288665524
],
"Gabon": [
11.774048593462979,
-0.5859798536474902
],
"Georgia": [
43.48650319000057,
42.175027555818694
],
"Ghana": [
-1.2150159067556041,
7.94078880601944
],
"Guinea": [
-10.943568403941383,
10.44078010915303
],
"Gambia": [
-16.21304964555284,
13.37062695465985
],
"Guinea Bissau": [
-14.929646023453557,
12.05088889829685
],
"Equatorial Guinea": [
10.481670786028316,
1.5660796013647418
],
"Greece": [
22.53435219348475,
39.517734832602365
],
"Guatemala": [
-90.35344030476544,
15.679044168855697
],
"Guyana": [
-58.978019347627985,
4.800329726037504
],
"Honduras": [
-86.65651796671759,
14.821029518594349
],
"Croatia": [
16.458685366615978,
45.18567759026696
],
"Haiti": [
-72.67866559866681,
18.916195586860297
],
"Hungary": [
19.410867145831855,
47.168542742918525
],
"Indonesia": [
101.52237404492608,
-0.44681277438152994
],
"India": [
79.58636900833257,
22.901586798523514
],
"Ireland": [
-8.117695195030898,
53.2341315203433
],
"Iran": [
54.266246557433305,
32.57616445788996
],
"Iraq": [
43.74006501790808,
33.0341456153028
],
"Iceland": [
-18.509670948264166,
65.03034972428229
],
"Israel": [
35.241573878810335,
31.94758544099519
],
"Italy": [
12.16233842748749,
43.51281131035235
],
"Jamaica": [
-77.32850498837172,
18.172957893241634
],
"Jordan": [
36.76131486689134,
31.237145277490367
],
"Japan": [
137.97352453982398,
36.589925009528365
],
"Kashmir": [
77.08733723957188,
35.415738932286466
],
"Kazakhstan": [
67.27815427190085,
48.155989088140196
],
"Kenya": [
37.795078751163835,
0.5967194222965451
],
"Kyrgyzstan": [
74.58224715420873,
41.48363475100716
],
"Cambodia": [
104.90702345739547,
12.717932969523966
],
"South Korea": [
127.87280447602005,
36.475157799464604
],
"Kosovo": [
20.851519165302378,
42.51783231278167
],
"Kuwait": [
47.566177395781736,
29.316145203902064
],
"Laos": [
103.75263811609112,
18.494354885305256
],
"Lebanon": [
35.88502277707876,
33.91528991779158
],
"Liberia": [
-9.325867879201954,
6.453996218817175
],
"Libya": [
18.008785486000946,
27.031457363755713
],
"Sri Lanka": [
80.70055059693914,
7.649883097905021
],
"Lesotho": [
28.24303757633595,
-29.57351232727891
],
"Lithuania": [
23.872793668340726,
55.3142460584835
],
"Luxembourg": [
6.070340274408408,
49.76746140228473
],
"Latvia": [
24.89496154477673,
56.853143698564296
],
"Morocco": [
-8.459192831832421,
29.83932361976915
],
"Moldova": [
28.446472517363826,
47.2017924696845
],
"Madagascar": [
46.70172536870331,
-19.374776628372288
],
"Mexico": [
-102.51849775988345,
23.943713432682014
],
"Macedonia": [
21.704642152886493,
41.60373573782414
],
"Mali": [
-3.5321833143642736,
17.347810147640345
],
"Myanmar": [
96.49588816533908,
21.22201469728619
],
"Montenegro": [
19.233365413523355,
42.78395456212118
],
"Mongolia": [
103.06423533091326,
46.82560864032323
],
"Mozambique": [
35.53173663088568,
-17.27297477254694
],
"Mauritania": [
-10.34307896458181,
20.259793585678146
],
"Malawi": [
34.288911351438394,
-13.200363393826267
],
"Malaysia": [
114.69201508943004,
3.57724156754415
],
"Namibia": [
17.208945346875193,
-22.132302890947404
],
"Niger": [
9.384787308614277,
17.41758190683491
],
"Nigeria": [
8.074235448092892,
9.58958974174772
],
"Nicaragua": [
-85.03229930746807,
12.835548864379284
],
"Netherlands": [
5.646194278986089,
52.27734132368374
],
"Norway": [
13.994247649210363,
64.32249798565987
],
"Nepal": [
83.9234099472594,
28.243335814044734
],
"New Zealand": [
170.54047059084724,
-43.955381757717255
],
"Oman": [
56.08271927344639,
20.573738446088598
],
"Pakistan": [
69.32988114530586,
29.951585862562073
],
"Panama": [
-80.05970573973497,
8.521692369504816
],
"Peru": [
-74.38850244798846,
-9.156316252308201
],
"Philippines": [
121.45754828266669,
15.948824272939182
],
"Papua New Guinea": [
144.22896759838426,
-6.607045399379529
],
"Poland": [
19.379892383012464,
52.11963086628078
],
"United States of America": [
-99.14669730710926,
39.52689381707972
],
"North Korea": [
127.21551783512186,
40.144856985239926
],
"Portugal": [
-7.977838817691485,
39.6854900148663
],
"Paraguay": [
-58.39900641582761,
-23.223895588708068
],
"Qatar": [
51.170816022848,
25.32876608728452
],
"Romania": [
24.971632366248883,
45.84560772065747
],
"Russia": [
99.05228241954488,
61.677043847105494
],
"Rwanda": [
29.91765145538139,
-1.9953628964377972
],
"Western Sahara": [
-12.218085234851758,
24.221135708939354
],
"Saudi Arabia": [
44.5342871199886,
24.122854227726673
],
"Sudan": [
29.946243665370346,
15.99205876641883
],
"South Sudan": [
30.253827921202955,
7.307277152400154
],
"Senegal": [
-14.472411019597182,
14.338613732351318
],
"Solomon Islands": [
160.156396875585,
-9.628589831637283
],
"Sierra Leone": [
-11.82013204896783,
8.575093438325089
],
"El Salvador": [
-88.86593554283397,
13.734573052971413
],
"Somaliland": [
46.25513292885051,
9.729995248781385
],
"Somalia": [
45.706539856671256,
4.747960517798881
],
"Republic of Serbia": [
20.766249889622852,
44.19998417744864
],
"Suriname": [
-55.91029765824739,
4.126766010517478
],
"Slovakia": [
19.47296461136286,
48.71540683451534
],
"Slovenia": [
14.771193461021065,
46.10807043333343
],
"Sweden": [
16.752551082675275,
62.8354879894394
],
"Swaziland": [
31.47664514492297,
-26.563545981108437
],
"Syria": [
38.50449031631095,
35.01769370592341
],
"Chad": [
18.645815630757973,
15.32729896930035
],
"Togo": [
0.949513544830056,
8.576560980195756
],
"Thailand": [
101.01060253224972,
15.132319216387051
],
"Tajikistan": [
70.98712700685866,
38.55204366339685
],
"Turkmenistan": [
59.39292286662992,
39.1098173228857
],
"East Timor": [
125.78222644041995,
-8.858373836074785
],
"Trinidad and Tobago": [
-61.270458833911896,
10.339930045317969
],
"Tunisia": [
9.546832905099203,
34.12673069029241
],
"Turkey": [
35.424986274948736,
38.998407240706534
],
"United Republic of Tanzania": [
34.79490109621088,
-6.276146393386636
],
"Uganda": [
32.36575488492427,
1.2757695499320103
],
"Ukraine": [
31.381193375918794,
49.01345298489402
],
"Uruguay": [
-56.01665573850212,
-32.80651677440885
],
"Uzbekistan": [
63.148826727968626,
41.7502616897204
],
"Venezuela": [
-66.1882174671549,
7.118299933056809
],
"Vietnam": [
106.29939096235873,
16.63088189612042
],
"Vanuatu": [
166.84228659210697,
-15.266627310669035
],
"Yemen": [
47.546351590294876,
15.935825615412996
],
"South Africa": [
25.160630055943862,
-29.008774283210798
],
"Zambia": [
27.775321721593084,
-13.460954386993254
],
"Zimbabwe": [
29.850564118889274,
-19.00677451616593
]
"Afghanistan": [65.99254367483304, 33.83349410470984],
"Angola": [17.563198329790602, -12.334557680834592],
"Albania": [20.035803636476235, 41.179283471608585],
"United Arab Emirates": [54.30345828396012, 23.905698042805522],
"Argentina": [-65.15670576062132, -35.178771590782524],
"Armenia": [44.929379894609674, 40.29884592895426],
"France": [165.43817082439585, -21.285375782391018],
"Australia": [134.361180723562, -25.574955774991995],
"Austria": [14.10165807418182, 47.57042831528493],
"Azerbaijan": [47.636777782137415, 40.3311508093757],
"Burundi": [29.873065896828848, -3.3802825292327188],
"Belgium": [4.628129604331132, 50.646494813466056],
"Benin": [2.323507643298733, 9.63048313950481],
"Burkina Faso": [-1.7757430610210276, 12.260370131848955],
"Bangladesh": [90.21009840315206, 23.88793565572049],
"Bulgaria": [25.225364036830744, 42.77785706066581],
"The Bahamas": [-78.02733865351972, 24.719920319716515],
"Bosnia and Herzegovina": [17.762461846176542, 44.16398853722456],
"Belarus": [28.04895281233843, 53.53432933090482],
"Belize": [-88.72096138346826, 17.17073148225863],
"Bolivia": [-64.69104972918655, -16.70141507507877],
"Brazil": [-53.113858151998855, -10.827995867833263],
"Brunei": [114.54229268997476, 4.451218476864121],
"Bhutan": [90.40562361681495, 27.40133623508629],
"Botswana": [23.791788508036337, -22.185333985163894],
"Central African Republic": [20.490781591297914, 6.563625165826992],
"Canada": [-102.31777616484295, 57.94457487676815],
"Switzerland": [8.205812860319684, 46.805544047201984],
"Chile": [-71.34891661948937, -35.98028631773855],
"China": [103.79804331240364, 36.61094167680319],
"Ivory Coast": [-5.569771898429842, 7.626172775954684],
"Cameroon": [12.739868035578002, 5.692692453534944],
"Democratic Republic of the Congo": [23.64811688254194, -2.8775533309965056],
"Republic of the Congo": [15.21207418503586, -0.8564004333096924],
"Colombia": [-73.07794094917405, 3.9235618261252663],
"Costa Rica": [-84.21742467558424, 9.986463158070535],
"Cuba": [-78.8954025482625, 21.62136808001903],
"Northern Cyprus": [33.39079188058263, 35.225753269439686],
"Cyprus": [33.02409208254226, 34.91304424540028],
"Czech Republic": [15.330767057694615, 49.70160772219921],
"Germany": [10.380496796525602, 51.083853487429664],
"Djibouti": [42.56788631653962, 11.745988995518626],
"Denmark": [-41.29088868109839, 74.72535220460091],
"Dominican Republic": [-70.48595892120278, 18.896879062003393],
"Algeria": [2.6175172555729325, 28.158202786113698],
"Ecuador": [-78.38384779532852, -1.4454571869341688],
"Egypt": [29.86739912685152, 26.49286512797635],
"Eritrea": [38.835037148217566, 15.359809600209143],
"Spain": [-3.5708365129134876, 40.391564705240064],
"Estonia": [25.811350172874032, 58.69303019372469],
"Ethiopia": [39.60372599651433, 8.621595995236198],
"Finland": [26.28958351526606, 64.52279699210293],
"Fiji": [177.98415179869738, -17.827004078184217],
"United Kingdom": [-36.367082569942795, -54.468348288665524],
"Gabon": [11.774048593462979, -0.5859798536474902],
"Georgia": [43.48650319000057, 42.175027555818694],
"Ghana": [-1.2150159067556041, 7.94078880601944],
"Guinea": [-10.943568403941383, 10.44078010915303],
"Gambia": [-16.21304964555284, 13.37062695465985],
"Guinea Bissau": [-14.929646023453557, 12.05088889829685],
"Equatorial Guinea": [10.481670786028316, 1.5660796013647418],
"Greece": [22.53435219348475, 39.517734832602365],
"Guatemala": [-90.35344030476544, 15.679044168855697],
"Guyana": [-58.978019347627985, 4.800329726037504],
"Honduras": [-86.65651796671759, 14.821029518594349],
"Croatia": [16.458685366615978, 45.18567759026696],
"Haiti": [-72.67866559866681, 18.916195586860297],
"Hungary": [19.410867145831855, 47.168542742918525],
"Indonesia": [101.52237404492608, -0.44681277438152994],
"India": [79.58636900833257, 22.901586798523514],
"Ireland": [-8.117695195030898, 53.2341315203433],
"Iran": [54.266246557433305, 32.57616445788996],
"Iraq": [43.74006501790808, 33.0341456153028],
"Iceland": [-18.509670948264166, 65.03034972428229],
"Israel": [35.241573878810335, 31.94758544099519],
"Italy": [12.16233842748749, 43.51281131035235],
"Jamaica": [-77.32850498837172, 18.172957893241634],
"Jordan": [36.76131486689134, 31.237145277490367],
"Japan": [137.97352453982398, 36.589925009528365],
"Kashmir": [77.08733723957188, 35.415738932286466],
"Kazakhstan": [67.27815427190085, 48.155989088140196],
"Kenya": [37.795078751163835, 0.5967194222965451],
"Kyrgyzstan": [74.58224715420873, 41.48363475100716],
"Cambodia": [104.90702345739547, 12.717932969523966],
"South Korea": [127.87280447602005, 36.475157799464604],
"Kosovo": [20.851519165302378, 42.51783231278167],
"Kuwait": [47.566177395781736, 29.316145203902064],
"Laos": [103.75263811609112, 18.494354885305256],
"Lebanon": [35.88502277707876, 33.91528991779158],
"Liberia": [-9.325867879201954, 6.453996218817175],
"Libya": [18.008785486000946, 27.031457363755713],
"Sri Lanka": [80.70055059693914, 7.649883097905021],
"Lesotho": [28.24303757633595, -29.57351232727891],
"Lithuania": [23.872793668340726, 55.3142460584835],
"Luxembourg": [6.070340274408408, 49.76746140228473],
"Latvia": [24.89496154477673, 56.853143698564296],
"Morocco": [-8.459192831832421, 29.83932361976915],
"Moldova": [28.446472517363826, 47.2017924696845],
"Madagascar": [46.70172536870331, -19.374776628372288],
"Mexico": [-102.51849775988345, 23.943713432682014],
"Macedonia": [21.704642152886493, 41.60373573782414],
"Mali": [-3.5321833143642736, 17.347810147640345],
"Myanmar": [96.49588816533908, 21.22201469728619],
"Montenegro": [19.233365413523355, 42.78395456212118],
"Mongolia": [103.06423533091326, 46.82560864032323],
"Mozambique": [35.53173663088568, -17.27297477254694],
"Mauritania": [-10.34307896458181, 20.259793585678146],
"Malawi": [34.288911351438394, -13.200363393826267],
"Malaysia": [114.69201508943004, 3.57724156754415],
"Namibia": [17.208945346875193, -22.132302890947404],
"Niger": [9.384787308614277, 17.41758190683491],
"Nigeria": [8.074235448092892, 9.58958974174772],
"Nicaragua": [-85.03229930746807, 12.835548864379284],
"Netherlands": [5.646194278986089, 52.27734132368374],
"Norway": [13.994247649210363, 64.32249798565987],
"Nepal": [83.9234099472594, 28.243335814044734],
"New Zealand": [170.54047059084724, -43.955381757717255],
"Oman": [56.08271927344639, 20.573738446088598],
"Pakistan": [69.32988114530586, 29.951585862562073],
"Panama": [-80.05970573973497, 8.521692369504816],
"Peru": [-74.38850244798846, -9.156316252308201],
"Philippines": [121.45754828266669, 15.948824272939182],
"Papua New Guinea": [144.22896759838426, -6.607045399379529],
"Poland": [19.379892383012464, 52.11963086628078],
"United States of America": [-99.14669730710926, 39.52689381707972],
"North Korea": [127.21551783512186, 40.144856985239926],
"Portugal": [-7.977838817691485, 39.6854900148663],
"Paraguay": [-58.39900641582761, -23.223895588708068],
"Qatar": [51.170816022848, 25.32876608728452],
"Romania": [24.971632366248883, 45.84560772065747],
"Russia": [99.05228241954488, 61.677043847105494],
"Rwanda": [29.91765145538139, -1.9953628964377972],
"Western Sahara": [-12.218085234851758, 24.221135708939354],
"Saudi Arabia": [44.5342871199886, 24.122854227726673],
"Sudan": [29.946243665370346, 15.99205876641883],
"South Sudan": [30.253827921202955, 7.307277152400154],
"Senegal": [-14.472411019597182, 14.338613732351318],
"Solomon Islands": [160.156396875585, -9.628589831637283],
"Sierra Leone": [-11.82013204896783, 8.575093438325089],
"El Salvador": [-88.86593554283397, 13.734573052971413],
"Somaliland": [46.25513292885051, 9.729995248781385],
"Somalia": [45.706539856671256, 4.747960517798881],
"Republic of Serbia": [20.766249889622852, 44.19998417744864],
"Suriname": [-55.91029765824739, 4.126766010517478],
"Slovakia": [19.47296461136286, 48.71540683451534],
"Slovenia": [14.771193461021065, 46.10807043333343],
"Sweden": [16.752551082675275, 62.8354879894394],
"Swaziland": [31.47664514492297, -26.563545981108437],
"Syria": [38.50449031631095, 35.01769370592341],
"Chad": [18.645815630757973, 15.32729896930035],
"Togo": [0.949513544830056, 8.576560980195756],
"Thailand": [101.01060253224972, 15.132319216387051],
"Tajikistan": [70.98712700685866, 38.55204366339685],
"Turkmenistan": [59.39292286662992, 39.1098173228857],
"East Timor": [125.78222644041995, -8.858373836074785],
"Trinidad and Tobago": [-61.270458833911896, 10.339930045317969],
"Tunisia": [9.546832905099203, 34.12673069029241],
"Turkey": [35.424986274948736, 38.998407240706534],
"United Republic of Tanzania": [34.79490109621088, -6.276146393386636],
"Uganda": [32.36575488492427, 1.2757695499320103],
"Ukraine": [31.381193375918794, 49.01345298489402],
"Uruguay": [-56.01665573850212, -32.80651677440885],
"Uzbekistan": [63.148826727968626, 41.7502616897204],
"Venezuela": [-66.1882174671549, 7.118299933056809],
"Vietnam": [106.29939096235873, 16.63088189612042],
"Vanuatu": [166.84228659210697, -15.266627310669035],
"Yemen": [47.546351590294876, 15.935825615412996],
"South Africa": [25.160630055943862, -29.008774283210798],
"Zambia": [27.775321721593084, -13.460954386993254],
"Zimbabwe": [29.850564118889274, -19.00677451616593]
}
@@ -1,14 +1,8 @@
'use client';
import {RankChart} from '@/components/data/rank-chart';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import type {DistributionItem, TrafficDistributions} from '@/lib/services/openflare';
import {Card, CardContent, CardDescription, CardHeader, CardTitle,} from '@/components/ui/card';
import type {DistributionItem, TrafficDistributions,} from '@/lib/services/openflare';
function toRankItems(items: DistributionItem[]) {
return items.map((item) => ({
@@ -23,18 +17,18 @@ export function SourceDistributionChart({
items: TrafficDistributions['source_countries'];
}) {
return (
<Card className="border-dashed shadow-none">
<Card className='border-dashed shadow-none'>
<CardHeader>
<CardTitle className="text-sm font-semibold">来源分布</CardTitle>
<CardDescription className="text-xs">
<CardTitle className='text-sm font-semibold'>来源分布</CardTitle>
<CardDescription className='text-xs'>
聚合最近 24 小时主要来源国家。
</CardDescription>
</CardHeader>
<CardContent>
<RankChart
items={toRankItems(items)}
color="#38bdf8"
emptyMessage="暂无来源分布数据"
color='#38bdf8'
emptyMessage='暂无来源分布数据'
/>
</CardContent>
</Card>
@@ -47,10 +41,10 @@ export function StatusCodeDistributionChart({
items: TrafficDistributions['status_codes'];
}) {
return (
<Card className="border-dashed shadow-none">
<Card className='border-dashed shadow-none'>
<CardHeader>
<CardTitle className="text-sm font-semibold">状态码分布</CardTitle>
<CardDescription className="text-xs">
<CardTitle className='text-sm font-semibold'>状态码分布</CardTitle>
<CardDescription className='text-xs'>
快速判断成功响应是否仍是主流,以及错误码是否有抬升。
</CardDescription>
</CardHeader>
@@ -60,8 +54,8 @@ export function StatusCodeDistributionChart({
...item,
label: `HTTP ${item.label}`,
}))}
color="#f59e0b"
emptyMessage="暂无状态码分布"
color='#f59e0b'
emptyMessage='暂无状态码分布'
/>
</CardContent>
</Card>
@@ -74,20 +68,20 @@ export function TopDomainChart({
items: TrafficDistributions['top_domains'];
}) {
return (
<Card className="border-dashed shadow-none">
<Card className='border-dashed shadow-none'>
<CardHeader>
<CardTitle className="text-sm font-semibold">Top Domain</CardTitle>
<CardDescription className="text-xs">
<CardTitle className='text-sm font-semibold'>Top Domain</CardTitle>
<CardDescription className='text-xs'>
观察主要流量集中在哪些域名。
</CardDescription>
</CardHeader>
<CardContent>
<RankChart
items={toRankItems(items)}
color="#34d399"
emptyMessage="暂无域名分布"
color='#34d399'
emptyMessage='暂无域名分布'
/>
</CardContent>
</Card>
);
}
}
@@ -15,34 +15,34 @@ export function GeoDistributionList({ items }: { items: DistributionItem[] }) {
const maxValue = sortedItems[0]?.value ?? 0;
return (
<Card className="border-dashed shadow-none">
<Card className='border-dashed shadow-none'>
<CardHeader>
<CardTitle className="text-sm font-semibold flex items-center gap-1.5">
<Globe2 className="size-4 text-primary" />
<CardTitle className='text-sm font-semibold flex items-center gap-1.5'>
<Globe2 className='size-4 text-primary' />
来源国家分布
</CardTitle>
<CardDescription className="text-xs">
<CardDescription className='text-xs'>
聚合最近 24 小时主要来源国家。
</CardDescription>
</CardHeader>
<CardContent>
{sortedItems.length === 0 ? (
<div className="flex min-h-[180px] items-center justify-center text-xs text-muted-foreground">
<div className='flex min-h-[180px] items-center justify-center text-xs text-muted-foreground'>
暂无来源分布数据
</div>
) : (
<div className="space-y-3">
<div className='space-y-3'>
{sortedItems.map((item) => {
const ratio = maxValue > 0 ? (item.value / maxValue) * 100 : 0;
return (
<div key={item.key} className="space-y-1.5">
<div className="flex items-center justify-between text-xs">
<span className="font-medium">{item.key || '未知'}</span>
<span className="font-mono tabular-nums text-muted-foreground">
<div key={item.key} className='space-y-1.5'>
<div className='flex items-center justify-between text-xs'>
<span className='font-medium'>{item.key || '未知'}</span>
<span className='font-mono tabular-nums text-muted-foreground'>
{formatCompactNumber(item.value)}
</span>
</div>
<Progress value={ratio} className="h-1.5" />
<Progress value={ratio} className='h-1.5' />
</div>
);
})}
@@ -1,10 +1,13 @@
'use client';
import {TrendChart} from '@/components/data/trend-chart';
import {Card, CardContent, CardHeader, CardTitle,} from '@/components/ui/card';
import type {DiskIOTrendPoint, NetworkTrendPoint} from '@/lib/services/openflare';
import { TrendChart } from '@/components/data/trend-chart';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import type {
DiskIOTrendPoint,
NetworkTrendPoint,
} from '@/lib/services/openflare';
import {formatBytes, formatTrendHour} from './dashboard-utils';
import { formatBytes, formatTrendHour } from './dashboard-utils';
export function NetworkDiskTrendChart({
networkPoints,
@@ -14,16 +17,20 @@ export function NetworkDiskTrendChart({
diskPoints: DiskIOTrendPoint[];
}) {
return (
<Card className="border-dashed shadow-none">
<Card className='border-dashed shadow-none'>
<CardHeader>
<CardTitle className="text-sm font-semibold">24 小时网络与磁盘趋势</CardTitle>
<CardTitle className='text-sm font-semibold'>
24 小时网络与磁盘趋势
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<CardContent className='space-y-6'>
<TrendChart
labels={networkPoints.map((point) => formatTrendHour(point.bucket_started_at))}
labels={networkPoints.map((point) =>
formatTrendHour(point.bucket_started_at),
)}
height={180}
summaryScope="total"
summaryHint="近 24 小时"
summaryScope='total'
summaryHint='近 24 小时'
yAxisValueFormatter={formatBytes}
series={[
{
@@ -44,10 +51,12 @@ export function NetworkDiskTrendChart({
/>
<TrendChart
labels={diskPoints.map((point) => formatTrendHour(point.bucket_started_at))}
labels={diskPoints.map((point) =>
formatTrendHour(point.bucket_started_at),
)}
height={180}
summaryScope="total"
summaryHint="近 24 小时"
summaryScope='total'
summaryHint='近 24 小时'
yAxisValueFormatter={formatBytes}
series={[
{
@@ -2,13 +2,26 @@
import Link from 'next/link';
import {EmptyStateWithBorder} from '@/components/layout/empty';
import {Button} from '@/components/ui/button';
import {Card, CardContent, CardDescription, CardHeader, CardTitle,} from '@/components/ui/card';
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from '@/components/ui/table';
import type {DashboardNodeHealth} from '@/lib/services/openflare';
import { EmptyStateWithBorder } from '@/components/layout/empty';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import type { DashboardNodeHealth } from '@/lib/services/openflare';
import {NodeStatusBadge} from '../../nodes/components/node-status-badge';
import { NodeStatusBadge } from '../../nodes/components/node-status-badge';
import {
formatRelativeTime,
getNodeStatusLabel,
@@ -17,7 +30,7 @@ import {
getOpenrestyStatusTone,
isWSConnectedLastSeen,
} from '../../nodes/components/node-utils';
import {formatCompactNumber, formatPercent} from './dashboard-utils';
import { formatCompactNumber, formatPercent } from './dashboard-utils';
export function NodeHealthTable({ nodes }: { nodes: DashboardNodeHealth[] }) {
const sortedNodes = [...nodes].sort((left, right) => {
@@ -38,52 +51,57 @@ export function NodeHealthTable({ nodes }: { nodes: DashboardNodeHealth[] }) {
});
return (
<Card className="border-dashed shadow-none">
<CardHeader className="flex flex-row items-start justify-between gap-4">
<Card className='border-dashed shadow-none'>
<CardHeader className='flex flex-row items-start justify-between gap-4'>
<div>
<CardTitle className="text-sm font-semibold">节点健康列表</CardTitle>
<CardDescription className="text-xs">
<CardTitle className='text-sm font-semibold'>节点健康列表</CardTitle>
<CardDescription className='text-xs'>
按异常数量和资源压力排序。
</CardDescription>
</div>
<Button variant="outline" size="sm" className="h-8 text-xs" asChild>
<Link href="/nodes">进入节点页</Link>
<Button variant='outline' size='sm' className='h-8 text-xs' asChild>
<Link href='/nodes'>进入节点页</Link>
</Button>
</CardHeader>
<CardContent>
{sortedNodes.length === 0 ? (
<EmptyStateWithBorder
title="暂无节点"
description="节点接入后,这里会展示系统健康与容量摘要。"
title='暂无节点'
description='节点接入后,这里会展示系统健康与容量摘要。'
/>
) : (
<div className="border border-dashed rounded-lg overflow-hidden">
<div className='border border-dashed rounded-lg overflow-hidden'>
<Table>
<TableHeader>
<TableRow className="border-dashed hover:bg-transparent">
<TableHead className="py-2 h-8">节点</TableHead>
<TableHead className="py-2 h-8">状态</TableHead>
<TableHead className="py-2 h-8">CPU / 内存</TableHead>
<TableHead className="py-2 h-8">请求 / 错误</TableHead>
<TableHead className="py-2 h-8">活动异常</TableHead>
<TableHead className="py-2 h-8">最近心跳</TableHead>
<TableHead className="py-2 h-8 text-right">操作</TableHead>
<TableRow className='border-dashed hover:bg-transparent'>
<TableHead className='py-2 h-8'>节点</TableHead>
<TableHead className='py-2 h-8'>状态</TableHead>
<TableHead className='py-2 h-8'>CPU / 内存</TableHead>
<TableHead className='py-2 h-8'>请求 / 错误</TableHead>
<TableHead className='py-2 h-8'>活动异常</TableHead>
<TableHead className='py-2 h-8'>最近心跳</TableHead>
<TableHead className='py-2 h-8 text-right'>操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sortedNodes.slice(0, 12).map((node) => (
<TableRow key={node.node_id} className="border-dashed align-top">
<TableCell className="py-3">
<div className="space-y-1">
<p className="font-medium">{node.name}</p>
<p className="text-xs text-muted-foreground">{node.node_id}</p>
<p className="text-xs text-muted-foreground">
<TableRow
key={node.node_id}
className='border-dashed align-top'
>
<TableCell className='py-3'>
<div className='space-y-1'>
<p className='font-medium'>{node.name}</p>
<p className='text-xs text-muted-foreground'>
{node.node_id}
</p>
<p className='text-xs text-muted-foreground'>
{node.geo_name || '未配置地图点位'}
</p>
</div>
</TableCell>
<TableCell className="py-3">
<div className="flex flex-wrap gap-1.5">
<TableCell className='py-3'>
<div className='flex flex-wrap gap-1.5'>
<NodeStatusBadge
label={getNodeStatusLabel(node.status)}
tone={getNodeStatusTone(node.status)}
@@ -94,29 +112,36 @@ export function NodeHealthTable({ nodes }: { nodes: DashboardNodeHealth[] }) {
/>
</div>
</TableCell>
<TableCell className="py-3 text-sm text-muted-foreground">
<TableCell className='py-3 text-sm text-muted-foreground'>
{formatPercent(node.cpu_usage_percent)} /{' '}
{formatPercent(node.memory_usage_percent)}
</TableCell>
<TableCell className="py-3 text-sm text-muted-foreground">
<TableCell className='py-3 text-sm text-muted-foreground'>
{formatCompactNumber(node.request_count)} /{' '}
{formatCompactNumber(node.error_count)}
</TableCell>
<TableCell className="py-3">
<TableCell className='py-3'>
<NodeStatusBadge
label={String(node.active_event_count)}
tone={node.active_event_count > 0 ? 'danger' : 'success'}
tone={
node.active_event_count > 0 ? 'danger' : 'success'
}
/>
</TableCell>
<TableCell className="py-3 text-xs text-muted-foreground">
<TableCell className='py-3 text-xs text-muted-foreground'>
{isWSConnectedLastSeen(node.last_seen_at)
? 'WS 已连接'
: node.last_seen_at
? formatRelativeTime(node.last_seen_at)
: '暂无'}
</TableCell>
<TableCell className="py-3 text-right">
<Button variant="ghost" size="sm" className="h-8 text-xs" asChild>
<TableCell className='py-3 text-right'>
<Button
variant='ghost'
size='sm'
className='h-8 text-xs'
asChild
>
<Link href={`/nodes/detail?id=${node.id}`}>详情</Link>
</Button>
</TableCell>
@@ -1,13 +1,8 @@
'use client';
import {RankChart} from '@/components/data/rank-chart';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import type {DashboardNodeHealth} from '@/lib/services/openflare';
import { RankChart } from '@/components/data/rank-chart';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import type { DashboardNodeHealth } from '@/lib/services/openflare';
function buildNodeRankItems(
nodes: DashboardNodeHealth[],
@@ -31,25 +26,25 @@ function buildNodeRankItems(
}));
}
export function NodeRankPanel({nodes}: {nodes: DashboardNodeHealth[]}) {
export function NodeRankPanel({ nodes }: { nodes: DashboardNodeHealth[] }) {
return (
<Card className="border-dashed shadow-none">
<Card className='border-dashed shadow-none'>
<CardHeader>
<CardTitle className="text-sm font-semibold">Top 节点榜单</CardTitle>
<CardTitle className='text-sm font-semibold'>Top 节点榜单</CardTitle>
</CardHeader>
<CardContent className="grid gap-6">
<CardContent className='grid gap-6'>
<div>
<p className="mb-3 text-xs tracking-[0.22em] text-muted-foreground uppercase">
<p className='mb-3 text-xs tracking-[0.22em] text-muted-foreground uppercase'>
流量最高节点
</p>
<RankChart
items={buildNodeRankItems(nodes, (node) => node.request_count)}
color="#38bdf8"
emptyMessage="暂无流量榜单"
color='#38bdf8'
emptyMessage='暂无流量榜单'
/>
</div>
<div>
<p className="mb-3 text-xs tracking-[0.22em] text-muted-foreground uppercase">
<p className='mb-3 text-xs tracking-[0.22em] text-muted-foreground uppercase'>
容量压力节点
</p>
<RankChart
@@ -62,12 +57,12 @@ export function NodeRankPanel({nodes}: {nodes: DashboardNodeHealth[]}) {
),
),
)}
color="#ef4444"
color='#ef4444'
valueFormatter={(value) => `${value}%`}
emptyMessage="暂无容量压力数据"
emptyMessage='暂无容量压力数据'
/>
</div>
</CardContent>
</Card>
);
}
}
@@ -1,12 +1,16 @@
'use client';
import {Activity, Cpu, Gauge, Server, Users} from 'lucide-react';
import { Activity, Cpu, Gauge, Server, Users } from 'lucide-react';
import {Card, CardContent, CardHeader} from '@/components/ui/card';
import {Progress} from '@/components/ui/progress';
import type {DashboardCapacity, DashboardSummary, DashboardTraffic,} from '@/lib/services/openflare';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import type {
DashboardCapacity,
DashboardSummary,
DashboardTraffic,
} from '@/lib/services/openflare';
import {formatCompactNumber, formatPercent} from './dashboard-utils';
import { formatCompactNumber, formatPercent } from './dashboard-utils';
export function DashboardStatCards({
summary,
@@ -23,17 +27,19 @@ export function DashboardStatCards({
: 0;
return (
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<Card className="border-dashed shadow-none">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<span className="text-xs font-medium text-muted-foreground">24h 请求量</span>
<Activity className="size-4 text-primary" />
<div className='grid gap-4 md:grid-cols-2 xl:grid-cols-3'>
<Card className='border-dashed shadow-none'>
<CardHeader className='flex flex-row items-center justify-between pb-2'>
<span className='text-xs font-medium text-muted-foreground'>
24h 请求量
</span>
<Activity className='size-4 text-primary' />
</CardHeader>
<CardContent className="space-y-2">
<div className="text-2xl font-semibold tracking-tight">
<CardContent className='space-y-2'>
<div className='text-2xl font-semibold tracking-tight'>
{formatCompactNumber(traffic.request_count)}
</div>
<p className="text-[10px] text-muted-foreground">
<p className='text-[10px] text-muted-foreground'>
窗口UV(估) {formatCompactNumber(traffic.unique_visitors)} · 错误{' '}
{formatCompactNumber(traffic.error_count)} · 估算 QPS{' '}
{traffic.estimated_qps.toFixed(2)}
@@ -41,48 +47,55 @@ export function DashboardStatCards({
</CardContent>
</Card>
<Card className="border-dashed shadow-none">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<span className="text-xs font-medium text-muted-foreground">集群容量</span>
<Cpu className="size-4 text-primary" />
<Card className='border-dashed shadow-none'>
<CardHeader className='flex flex-row items-center justify-between pb-2'>
<span className='text-xs font-medium text-muted-foreground'>
集群容量
</span>
<Cpu className='size-4 text-primary' />
</CardHeader>
<CardContent className="space-y-2">
<div className="text-2xl font-semibold tracking-tight">
<CardContent className='space-y-2'>
<div className='text-2xl font-semibold tracking-tight'>
{formatPercent(capacity.average_cpu_usage_percent)}
<span className="ml-2 text-sm font-normal text-muted-foreground">
<span className='ml-2 text-sm font-normal text-muted-foreground'>
CPU
</span>
</div>
<Progress value={capacity.average_cpu_usage_percent} className="h-1.5" />
<p className="text-[10px] text-muted-foreground">
平均内存 {formatPercent(capacity.average_memory_usage_percent)} · 高负载节点 CPU{' '}
{capacity.high_cpu_nodes} / 内存 {capacity.high_memory_nodes} / 存储{' '}
{capacity.high_storage_nodes}
<Progress
value={capacity.average_cpu_usage_percent}
className='h-1.5'
/>
<p className='text-[10px] text-muted-foreground'>
平均内存 {formatPercent(capacity.average_memory_usage_percent)} ·
高负载节点 CPU {capacity.high_cpu_nodes} / 内存{' '}
{capacity.high_memory_nodes} / 存储 {capacity.high_storage_nodes}
</p>
</CardContent>
</Card>
<Card className="border-dashed shadow-none md:col-span-2 xl:col-span-1">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<span className="text-xs font-medium text-muted-foreground">节点概览</span>
<Server className="size-4 text-primary" />
<Card className='border-dashed shadow-none md:col-span-2 xl:col-span-1'>
<CardHeader className='flex flex-row items-center justify-between pb-2'>
<span className='text-xs font-medium text-muted-foreground'>
节点概览
</span>
<Server className='size-4 text-primary' />
</CardHeader>
<CardContent className="space-y-2">
<div className="text-2xl font-semibold tracking-tight">
<CardContent className='space-y-2'>
<div className='text-2xl font-semibold tracking-tight'>
{summary.online_nodes}
<span className="text-sm font-normal text-muted-foreground">
<span className='text-sm font-normal text-muted-foreground'>
{' '}
/ {summary.total_nodes} 在线
</span>
</div>
<Progress value={onlineRate} className="h-1.5" />
<p className="text-[10px] text-muted-foreground flex flex-wrap items-center gap-x-3 gap-y-1">
<span className="inline-flex items-center gap-1">
<Users className="size-3" />
<Progress value={onlineRate} className='h-1.5' />
<p className='text-[10px] text-muted-foreground flex flex-wrap items-center gap-x-3 gap-y-1'>
<span className='inline-flex items-center gap-1'>
<Users className='size-3' />
待接入 {summary.pending_nodes}
</span>
<span className="inline-flex items-center gap-1">
<Gauge className="size-3" />
<span className='inline-flex items-center gap-1'>
<Gauge className='size-3' />
异常 {summary.unhealthy_nodes} · 离线 {summary.offline_nodes}
</span>
</p>
@@ -1,10 +1,16 @@
'use client';
import {TrendChart} from '@/components/data/trend-chart';
import {Card, CardContent, CardDescription, CardHeader, CardTitle,} from '@/components/ui/card';
import type {TrafficTrendPoint} from '@/lib/services/openflare';
import { TrendChart } from '@/components/data/trend-chart';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import type { TrafficTrendPoint } from '@/lib/services/openflare';
import {formatTrendHour} from './dashboard-utils';
import { formatTrendHour } from './dashboard-utils';
export function TrafficTrendChart({
points,
@@ -16,16 +22,18 @@ export function TrafficTrendChart({
description?: string;
}) {
return (
<Card className="border-dashed shadow-none">
<Card className='border-dashed shadow-none'>
<CardHeader>
<CardTitle className="text-sm font-semibold">{title}</CardTitle>
<CardDescription className="text-xs">{description}</CardDescription>
<CardTitle className='text-sm font-semibold'>{title}</CardTitle>
<CardDescription className='text-xs'>{description}</CardDescription>
</CardHeader>
<CardContent>
<TrendChart
labels={points.map((point) => formatTrendHour(point.bucket_started_at))}
summaryScope="total"
summaryHint="近 24 小时"
labels={points.map((point) =>
formatTrendHour(point.bucket_started_at),
)}
summaryScope='total'
summaryHint='近 24 小时'
series={[
{
label: '请求量',
@@ -1,19 +1,25 @@
'use client';
import ReactEChartsCore from 'echarts-for-react/lib/core';
import type {EChartsCoreOption} from 'echarts/core';
import type { EChartsCoreOption } from 'echarts/core';
import * as echarts from 'echarts/core';
import {MapChart, ScatterChart} from 'echarts/charts';
import {GeoComponent, TooltipComponent} from 'echarts/components';
import {CanvasRenderer} from 'echarts/renderers';
import {useTheme} from 'next-themes';
import {useRouter} from 'next/navigation';
import {useEffect, useMemo, useRef, useState} from 'react';
import { MapChart, ScatterChart } from 'echarts/charts';
import { GeoComponent, TooltipComponent } from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import { useTheme } from 'next-themes';
import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useRef, useState } from 'react';
import {EmptyState} from '@/components/layout/empty';
import type {DashboardNodeHealth, DistributionItem} from '@/lib/services/openflare';
import { EmptyState } from '@/components/layout/empty';
import type {
DashboardNodeHealth,
DistributionItem,
} from '@/lib/services/openflare';
import {getNodeStatusLabel, getOpenrestyStatusLabel,} from '../../nodes/components/node-utils';
import {
getNodeStatusLabel,
getOpenrestyStatusLabel,
} from '../../nodes/components/node-utils';
import countryCentroidsJson from './data/country-centroids.json';
import worldGeoJson from './data/world-geo.json';
@@ -133,7 +139,9 @@ const countryCentroids = countryCentroidsJson as Record<string, number[]>;
type CountryCentroid = [number, number];
function toCountryCentroid(coords: number[] | undefined): CountryCentroid | null {
function toCountryCentroid(
coords: number[] | undefined,
): CountryCentroid | null {
if (!coords || coords.length < 2) {
return null;
}
@@ -235,7 +243,7 @@ export function WorldStageMap({
nodes: DashboardNodeHealth[];
sourceCountries: DistributionItem[];
}) {
const {resolvedTheme} = useTheme();
const { resolvedTheme } = useTheme();
const isDark = resolvedTheme === 'dark';
const router = useRouter();
const chartContainerRef = useRef<HTMLDivElement | null>(null);
@@ -268,7 +276,7 @@ export function WorldStageMap({
setContainerSize((previous) =>
previous.width === nextWidth && previous.height === nextHeight
? previous
: {width: nextWidth, height: nextHeight},
: { width: nextWidth, height: nextHeight },
);
};
@@ -423,8 +431,7 @@ export function WorldStageMap({
}, [containerSize]);
const computedLayoutSize = useMemo(
() =>
`${Math.round(baseWorldMapLayoutSizePercent * responsiveMapScale)}%`,
() => `${Math.round(baseWorldMapLayoutSizePercent * responsiveMapScale)}%`,
[responsiveMapScale],
);
const computedZoom = useMemo(
@@ -442,9 +449,7 @@ export function WorldStageMap({
backgroundColor: isDark
? 'rgba(24,24,27,0.96)'
: 'rgba(255,255,255,0.98)',
borderColor: isDark
? 'rgba(63,63,70,0.8)'
: 'rgba(228,228,231,0.9)',
borderColor: isDark ? 'rgba(63,63,70,0.8)' : 'rgba(228,228,231,0.9)',
borderWidth: 1,
textStyle: {
color: isDark ? '#fafafa' : '#18181b',
@@ -577,7 +582,7 @@ export function WorldStageMap({
if (!mapReady) {
return (
<div className="flex h-full items-center justify-center">
<div className='flex h-full items-center justify-center'>
<EmptyState
title={mapFailed ? '全球地图加载失败' : '全球地图加载中'}
description={
@@ -593,16 +598,16 @@ export function WorldStageMap({
const chartReady = containerSize.width > 0 && containerSize.height > 0;
return (
<div ref={chartContainerRef} className="h-full min-h-0 w-full">
<div ref={chartContainerRef} className='h-full min-h-0 w-full'>
{chartReady ? (
<ReactEChartsCore
echarts={echarts}
option={mapOption}
notMerge
lazyUpdate
opts={{renderer: 'canvas'}}
opts={{ renderer: 'canvas' }}
onEvents={{
click: (params: {data?: MapNodeDatum}) => {
click: (params: { data?: MapNodeDatum }) => {
if (params.data?.route) {
router.push(params.data.route);
}
@@ -614,11 +619,11 @@ export function WorldStageMap({
}}
/>
) : (
<div className="flex h-full items-center justify-center">
<div className='flex h-full items-center justify-center'>
<EmptyState
title="全球地图加载中"
description="正在测量地图容器尺寸..."
iconSize="sm"
title='全球地图加载中'
description='正在测量地图容器尺寸...'
iconSize='sm'
/>
</div>
)}
@@ -1,13 +1,28 @@
'use client';
import dynamic from 'next/dynamic';
import {type ComponentType, useEffect, useRef, useState} from 'react';
import {Activity, Cpu, Globe2, HardDrive, MemoryStick, Network, Server, ShieldCheck,} from 'lucide-react';
import { type ComponentType, useEffect, useRef, useState } from 'react';
import {
Activity,
Cpu,
Globe2,
HardDrive,
MemoryStick,
Network,
Server,
ShieldCheck,
} from 'lucide-react';
import {EmptyState} from '@/components/layout/empty';
import {Badge} from '@/components/ui/badge';
import {Card, CardContent, CardDescription, CardHeader, CardTitle,} from '@/components/ui/card';
import {Progress} from '@/components/ui/progress';
import { EmptyState } from '@/components/layout/empty';
import { Badge } from '@/components/ui/badge';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import type {
DashboardCapacity,
DashboardNodeHealth,
@@ -15,20 +30,20 @@ import type {
DashboardTraffic,
DistributionItem,
} from '@/lib/services/openflare';
import {cn} from '@/lib/utils';
import { cn } from '@/lib/utils';
import {formatCompactNumber, formatPercent} from './dashboard-utils';
import { formatCompactNumber, formatPercent } from './dashboard-utils';
const WorldStageMap = dynamic(
() => import('./world-stage-map').then((module) => module.WorldStageMap),
{ssr: false},
{ ssr: false },
);
const LEGEND_ITEMS = [
{dot: 'bg-blue-500', label: '来源'},
{dot: 'bg-emerald-500', label: '正常'},
{dot: 'bg-amber-500', label: '承压'},
{dot: 'bg-destructive', label: '异常'},
{ dot: 'bg-blue-500', label: '来源' },
{ dot: 'bg-emerald-500', label: '正常' },
{ dot: 'bg-amber-500', label: '承压' },
{ dot: 'bg-destructive', label: '异常' },
] as const;
function SummaryMetric({
@@ -42,7 +57,7 @@ function SummaryMetric({
label: string;
value: string;
hint: string;
icon: ComponentType<{className?: string}>;
icon: ComponentType<{ className?: string }>;
progress?: number;
className?: string;
}) {
@@ -53,17 +68,17 @@ function SummaryMetric({
className,
)}
>
<div className="flex items-center justify-between gap-2">
<span className="truncate text-xs text-muted-foreground">{label}</span>
<Icon className="size-3.5 shrink-0 text-primary/60" />
<div className='flex items-center justify-between gap-2'>
<span className='truncate text-xs text-muted-foreground'>{label}</span>
<Icon className='size-3.5 shrink-0 text-primary/60' />
</div>
<span className="text-xl font-semibold tabular-nums leading-none tracking-tight">
<span className='text-xl font-semibold tabular-nums leading-none tracking-tight'>
{value}
</span>
{typeof progress === 'number' ? (
<Progress value={progress} className="h-1" />
<Progress value={progress} className='h-1' />
) : null}
<span className="truncate text-[10px] text-muted-foreground">{hint}</span>
<span className='truncate text-[10px] text-muted-foreground'>{hint}</span>
</div>
);
}
@@ -78,24 +93,24 @@ function DetailMetric({
label: string;
value: string;
hint: string;
icon: ComponentType<{className?: string}>;
icon: ComponentType<{ className?: string }>;
progress?: number;
}) {
return (
<div className="flex min-w-0 flex-col gap-1 rounded-lg border border-dashed bg-background/60 px-3 py-2.5">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="truncate text-[11px] text-muted-foreground">{label}</p>
<p className="mt-1 text-lg font-semibold tabular-nums leading-none tracking-tight">
<div className='flex min-w-0 flex-col gap-1 rounded-lg border border-dashed bg-background/60 px-3 py-2.5'>
<div className='flex items-start justify-between gap-3'>
<div className='min-w-0'>
<p className='truncate text-[11px] text-muted-foreground'>{label}</p>
<p className='mt-1 text-lg font-semibold tabular-nums leading-none tracking-tight'>
{value}
</p>
</div>
<Icon className="size-3.5 shrink-0 text-muted-foreground" />
<Icon className='size-3.5 shrink-0 text-muted-foreground' />
</div>
{typeof progress === 'number' ? (
<Progress value={progress} className="h-1" />
<Progress value={progress} className='h-1' />
) : null}
<p className="truncate text-[10px] text-muted-foreground">{hint}</p>
<p className='truncate text-[10px] text-muted-foreground'>{hint}</p>
</div>
);
}
@@ -134,7 +149,7 @@ export function WorldStage({
observer.disconnect();
}
},
{rootMargin: '120px 0px'},
{ rootMargin: '120px 0px' },
);
observer.observe(mapViewport);
@@ -223,29 +238,29 @@ export function WorldStage({
] as const;
return (
<Card className="overflow-hidden border-dashed shadow-none">
<CardHeader className="gap-3 space-y-0 pb-3">
<div className="flex flex-wrap items-start justify-between gap-x-4 gap-y-3">
<div className="min-w-0">
<CardTitle className="flex items-center gap-1.5 text-sm font-semibold">
<Globe2 className="size-4 shrink-0 text-primary" />
<Card className='overflow-hidden border-dashed shadow-none'>
<CardHeader className='gap-3 space-y-0 pb-3'>
<div className='flex flex-wrap items-start justify-between gap-x-4 gap-y-3'>
<div className='min-w-0'>
<CardTitle className='flex items-center gap-1.5 text-sm font-semibold'>
<Globe2 className='size-4 shrink-0 text-primary' />
全球态势板
<Badge
variant="outline"
className="ml-1 text-[10px] font-normal text-muted-foreground"
variant='outline'
className='ml-1 text-[10px] font-normal text-muted-foreground'
>
{mapModeLabel}
</Badge>
</CardTitle>
<CardDescription className="mt-1 text-xs">
<CardDescription className='mt-1 text-xs'>
汇总节点分布、来源国家热度与最近窗口运行状态。
</CardDescription>
</div>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 rounded-full border border-dashed bg-muted/15 px-3 py-1.5">
<div className='flex flex-wrap items-center gap-x-3 gap-y-1 rounded-full border border-dashed bg-muted/15 px-3 py-1.5'>
{LEGEND_ITEMS.map((item) => (
<span
key={item.label}
className="inline-flex items-center gap-1 text-[10px] text-muted-foreground"
className='inline-flex items-center gap-1 text-[10px] text-muted-foreground'
>
<span className={cn('size-1.5 rounded-full', item.dot)} />
{item.label}
@@ -255,34 +270,37 @@ export function WorldStage({
</div>
</CardHeader>
<CardContent className="flex flex-col gap-4 pt-0">
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<CardContent className='flex flex-col gap-4 pt-0'>
<div className='grid gap-3 sm:grid-cols-2 xl:grid-cols-4'>
{headlineMetrics.map((metric) => (
<SummaryMetric key={metric.label} {...metric} />
))}
</div>
<div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_18rem] xl:items-stretch">
<div className="flex min-w-0 items-center justify-center rounded-lg border border-dashed bg-muted/10 p-3">
<div className='grid gap-4 xl:grid-cols-[minmax(0,1fr)_18rem] xl:items-stretch'>
<div className='flex min-w-0 items-center justify-center rounded-lg border border-dashed bg-muted/10 p-3'>
<div
ref={mapViewportRef}
className="relative aspect-[5/3] min-h-[260px] w-full max-w-[720px] overflow-hidden rounded-md bg-background/60 sm:min-h-0"
className='relative aspect-[5/3] min-h-[260px] w-full max-w-[720px] overflow-hidden rounded-md bg-background/60 sm:min-h-0'
>
{shouldRenderMap ? (
<WorldStageMap nodes={nodes} sourceCountries={sourceCountries} />
<WorldStageMap
nodes={nodes}
sourceCountries={sourceCountries}
/>
) : (
<div className="flex h-full items-center justify-center px-4">
<div className='flex h-full items-center justify-center px-4'>
<EmptyState
title="地图准备中"
description="进入可视区域后加载"
iconSize="sm"
title='地图准备中'
description='进入可视区域后加载'
iconSize='sm'
/>
</div>
)}
{shouldRenderMap && nodes.length === 0 ? (
<div className="pointer-events-none absolute inset-x-3 bottom-2 z-10">
<p className="rounded-md border border-dashed bg-background/90 px-2.5 py-1.5 text-[10px] text-muted-foreground backdrop-blur-sm">
<div className='pointer-events-none absolute inset-x-3 bottom-2 z-10'>
<p className='rounded-md border border-dashed bg-background/90 px-2.5 py-1.5 text-[10px] text-muted-foreground backdrop-blur-sm'>
暂无节点,接入后将展示地理分布
</p>
</div>
@@ -290,7 +308,7 @@ export function WorldStage({
</div>
</div>
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-1">
<div className='grid gap-2 sm:grid-cols-2 xl:grid-cols-1'>
{detailMetrics.map((metric) => (
<DetailMetric key={metric.label} {...metric} />
))}
@@ -1,26 +1,31 @@
import type {LucideIcon} from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import {Card, CardDescription, CardHeader, CardTitle} from '@/components/ui/card';
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
interface PlaceholderPageProps {
title: string;
icon: LucideIcon;
}
export function PlaceholderPage({title, icon: Icon}: PlaceholderPageProps) {
export function PlaceholderPage({ title, icon: Icon }: PlaceholderPageProps) {
return (
<div className="py-6 px-1 space-y-6">
<div className="flex items-center gap-2">
<Icon className="size-5 text-primary" />
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
<div className='py-6 px-1 space-y-6'>
<div className='flex items-center gap-2'>
<Icon className='size-5 text-primary' />
<h1 className='text-2xl font-semibold tracking-tight'>{title}</h1>
</div>
<Card>
<CardHeader>
<CardTitle className="text-base">开发中</CardTitle>
<CardTitle className='text-base'>开发中</CardTitle>
<CardDescription>该功能正在迁移开发中,敬请期待。</CardDescription>
</CardHeader>
</Card>
</div>
);
}
}