Skip to content

Commit 0d8e17d

Browse files
Merge pull request #405 from datum-cloud/feat/dashboard-update
2 parents 7b8452c + 1816889 commit 0d8e17d

22 files changed

+1785
-7
lines changed
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import { useClusterHealth } from '../hooks/use-cluster-health';
2+
import type { ClusterEntry } from '../hooks/use-cluster-health';
3+
import { Button } from '@datum-cloud/datum-ui/button';
4+
import { Card, CardContent } from '@datum-cloud/datum-ui/card';
5+
import { Skeleton } from '@datum-cloud/datum-ui/skeleton';
6+
import { Tooltip } from '@datum-cloud/datum-ui/tooltip';
7+
import { Text, Title } from '@datum-cloud/datum-ui/typography';
8+
import { Trans } from '@lingui/react/macro';
9+
import {
10+
AlertCircle,
11+
CheckCircle2,
12+
Cpu,
13+
Globe,
14+
HardDrive,
15+
MemoryStick,
16+
RefreshCw,
17+
ShieldAlert,
18+
XCircle,
19+
Zap,
20+
} from 'lucide-react';
21+
22+
function LoadingSkeleton() {
23+
return (
24+
<div className="space-y-3">
25+
<div className="flex items-center gap-3">
26+
<Skeleton className="h-5 w-5 rounded-full" />
27+
<Skeleton className="h-4 w-28" />
28+
<Skeleton className="h-4 w-32" />
29+
</div>
30+
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8">
31+
{Array.from({ length: 8 }).map((_, i) => (
32+
<Skeleton key={i} className="h-9 rounded-md" />
33+
))}
34+
</div>
35+
</div>
36+
);
37+
}
38+
39+
function ErrorState({ onRetry }: { onRetry: () => void }) {
40+
return (
41+
<div className="flex flex-col items-center justify-center py-6 text-center">
42+
<AlertCircle className="text-muted-foreground mb-3 h-8 w-8" />
43+
<Title level={5} className="mb-1">
44+
<Trans>Cluster metrics unavailable</Trans>
45+
</Title>
46+
<Text size="sm" textColor="muted" className="mb-3">
47+
<Trans>Could not query cluster health metrics</Trans>
48+
</Text>
49+
<Button type="secondary" size="small" onClick={onRetry}>
50+
<Trans>Retry</Trans>
51+
</Button>
52+
</div>
53+
);
54+
}
55+
56+
function isClusterHealthy(c: ClusterEntry) {
57+
return (
58+
c.nodesReady &&
59+
c.gatewayHealthy !== false &&
60+
!c.memoryPressure &&
61+
!c.diskPressure &&
62+
!c.pidPressure
63+
);
64+
}
65+
66+
function PressurePill({
67+
active,
68+
label,
69+
icon: Icon,
70+
}: {
71+
active: boolean;
72+
label: string;
73+
icon: React.ComponentType<{ className?: string }>;
74+
}) {
75+
if (!active) return null;
76+
return (
77+
<Tooltip message={`${label} pressure detected`} side="top">
78+
<span className="inline-flex items-center gap-0.5 text-red-600 dark:text-red-400">
79+
<Icon className="h-3 w-3" />
80+
<span className="text-[10px] leading-none font-medium">{label}</span>
81+
</span>
82+
</Tooltip>
83+
);
84+
}
85+
86+
function MetricChip({
87+
icon: Icon,
88+
value,
89+
warn,
90+
tooltip,
91+
}: {
92+
icon: React.ComponentType<{ className?: string }>;
93+
value: string;
94+
warn?: boolean;
95+
tooltip: string;
96+
}) {
97+
const color = warn ? 'text-amber-600 dark:text-amber-400' : 'text-muted-foreground';
98+
return (
99+
<Tooltip message={tooltip} side="top">
100+
<span className={`inline-flex items-center gap-0.5 ${color}`}>
101+
<Icon className="h-3 w-3" />
102+
<span className="text-[10px] leading-none font-medium">{value}</span>
103+
</span>
104+
</Tooltip>
105+
);
106+
}
107+
108+
function ClusterCell({ cluster }: { cluster: ClusterEntry }) {
109+
const healthy = isClusterHealthy(cluster);
110+
const hasCritical = !cluster.nodesReady || cluster.gatewayHealthy === false;
111+
const hasPressure = cluster.memoryPressure || cluster.diskPressure || cluster.pidPressure;
112+
const certWarn = cluster.certExpiryDays !== null && cluster.certExpiryDays < 14;
113+
114+
const bg = healthy
115+
? 'bg-green-50 dark:bg-green-950/20'
116+
: hasCritical
117+
? 'bg-red-50 dark:bg-red-950/20'
118+
: 'bg-amber-50 dark:bg-amber-950/20';
119+
const cls = `flex min-w-0 gap-1.5 overflow-hidden rounded-md px-2 py-1.5 ${bg}`;
120+
121+
return (
122+
<div className={cls}>
123+
<div className="pt-0.5">
124+
{healthy ? (
125+
<CheckCircle2 className="h-3.5 w-3.5 shrink-0 text-green-600 dark:text-green-400" />
126+
) : hasCritical ? (
127+
<XCircle className="h-3.5 w-3.5 shrink-0 text-red-600 dark:text-red-400" />
128+
) : (
129+
<AlertCircle className="h-3.5 w-3.5 shrink-0 text-amber-600 dark:text-amber-400" />
130+
)}
131+
</div>
132+
<div className="min-w-0 flex-1">
133+
<p className="truncate text-xs font-medium">{cluster.region ?? cluster.name}</p>
134+
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5">
135+
{cluster.requestRate !== null && (
136+
<MetricChip
137+
icon={Zap}
138+
value={`${cluster.requestRate} rps`}
139+
tooltip="Envoy request rate (req/s)"
140+
/>
141+
)}
142+
{cluster.certExpiryDays !== null && (
143+
<MetricChip
144+
icon={ShieldAlert}
145+
value={`${cluster.certExpiryDays}d`}
146+
warn={certWarn}
147+
tooltip={`Certificate expires in ${cluster.certExpiryDays} days`}
148+
/>
149+
)}
150+
{cluster.restartingContainers > 0 && (
151+
<MetricChip
152+
icon={RefreshCw}
153+
value={`${cluster.restartingContainers}`}
154+
warn
155+
tooltip={`${cluster.restartingContainers} container(s) with >5 restarts`}
156+
/>
157+
)}
158+
</div>
159+
{hasPressure && (
160+
<div className="mt-0.5 flex items-center gap-1.5">
161+
<PressurePill active={cluster.memoryPressure} label="MEM" icon={MemoryStick} />
162+
<PressurePill active={cluster.diskPressure} label="DISK" icon={HardDrive} />
163+
<PressurePill active={cluster.pidPressure} label="PID" icon={Cpu} />
164+
</div>
165+
)}
166+
</div>
167+
</div>
168+
);
169+
}
170+
171+
export function ClusterHealthWidget() {
172+
const { data, isLoading, isError, refetch } = useClusterHealth();
173+
174+
const summary = data?.summary;
175+
const allHealthy = summary && summary.healthy === summary.total && summary.total > 0;
176+
177+
return (
178+
<Card className="gap-0 py-0">
179+
<CardContent className="px-4 py-3">
180+
{isLoading ? (
181+
<LoadingSkeleton />
182+
) : isError || !summary || summary.total === 0 ? (
183+
<ErrorState onRetry={refetch} />
184+
) : (
185+
<div className="space-y-3">
186+
<div className="flex flex-wrap items-center gap-x-4 gap-y-1">
187+
<div className="flex items-center gap-2">
188+
<Globe className="text-muted-foreground h-4 w-4" />
189+
<Title level={5}>
190+
<Trans>Cluster Health</Trans>
191+
</Title>
192+
</div>
193+
194+
<div className="bg-border hidden h-5 w-px sm:block" />
195+
196+
<div className="flex items-center gap-2">
197+
{allHealthy ? (
198+
<CheckCircle2 className="h-4 w-4 text-green-600 dark:text-green-400" />
199+
) : (
200+
<XCircle className="h-4 w-4 text-red-600 dark:text-red-400" />
201+
)}
202+
<Text size="sm" className="font-medium">
203+
{summary.healthy}/{summary.total}{' '}
204+
{summary.total === 1 ? (
205+
<Trans>cluster healthy</Trans>
206+
) : (
207+
<Trans>clusters healthy</Trans>
208+
)}
209+
</Text>
210+
</div>
211+
</div>
212+
213+
<div className="grid grid-cols-2 gap-1.5 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8">
214+
{data?.clusters.map((cluster) => (
215+
<ClusterCell key={cluster.name} cluster={cluster} />
216+
))}
217+
</div>
218+
</div>
219+
)}
220+
</CardContent>
221+
</Card>
222+
);
223+
}

0 commit comments

Comments
 (0)