Update frontend dashboard and health service
This commit is contained in:
162
app/frontend/components/ECGMonitorCard.tsx
Normal file
162
app/frontend/components/ECGMonitorCard.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { PollState } from "@/types/monitor";
|
||||
|
||||
type Tone = "mint" | "blue" | "coral";
|
||||
|
||||
const toneStyles: Record<Tone, string> = {
|
||||
mint: "from-white via-white to-emerald-50 ring-emerald-100",
|
||||
blue: "from-white via-white to-sky-50 ring-sky-100",
|
||||
coral: "from-white via-white to-rose-50 ring-rose-100",
|
||||
};
|
||||
|
||||
interface ECGMonitorCardProps<T> {
|
||||
title: string;
|
||||
endpoint: string;
|
||||
intervalLabel: string;
|
||||
tone?: Tone;
|
||||
now: string;
|
||||
wavePoints: string;
|
||||
waveHeight: number;
|
||||
healthy: boolean;
|
||||
loading: boolean;
|
||||
statusLabel: string;
|
||||
statusDetail: string;
|
||||
lastUpdatedLabel: string;
|
||||
state: PollState<T>;
|
||||
children?: ReactNode;
|
||||
onManualTrigger: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Presentational card that renders endpoint status and the shared ECG waveform with
|
||||
* consistent tone, status badges, and contextual metadata.
|
||||
*/
|
||||
export function ECGMonitorCard<T>({
|
||||
title,
|
||||
endpoint,
|
||||
intervalLabel,
|
||||
tone = "mint",
|
||||
now,
|
||||
wavePoints,
|
||||
waveHeight,
|
||||
healthy,
|
||||
loading,
|
||||
statusLabel,
|
||||
statusDetail,
|
||||
lastUpdatedLabel,
|
||||
state,
|
||||
children,
|
||||
onManualTrigger,
|
||||
}: ECGMonitorCardProps<T>) {
|
||||
const signalId = `${endpoint.replace(/[^a-z0-9]/gi, "-")}-stroke`;
|
||||
|
||||
const badgeColor = loading
|
||||
? "border-amber-200 bg-amber-50 text-amber-800"
|
||||
: healthy
|
||||
? "border-emerald-200 bg-emerald-50 text-emerald-800"
|
||||
: "border-rose-200 bg-rose-50 text-rose-800";
|
||||
|
||||
const dotColor = loading ? "bg-pulse" : healthy ? "bg-success" : "bg-danger";
|
||||
const strokeColor = loading ? "#f59e0b" : healthy ? "#22c55e" : "#ef4444";
|
||||
|
||||
return (
|
||||
<article
|
||||
className={`relative overflow-hidden rounded-3xl border border-black/5 bg-gradient-to-br ${toneStyles[tone]} p-5 shadow-card ring-4 ring-transparent transition hover:-translate-y-1 hover:shadow-[0_24px_60px_rgba(15,23,42,0.16)]`}
|
||||
>
|
||||
<div className="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_20%_10%,rgba(96,165,250,0.08),transparent_30%),radial-gradient(circle_at_80%_0%,rgba(52,211,153,0.1),transparent_30%)]" />
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="space-y-2 min-w-0">
|
||||
<div className="inline-flex max-w-full items-center gap-2 rounded-full border border-black/5 bg-black/5 px-3 py-1 text-xs font-semibold uppercase tracking-[0.12em] text-inkMuted">
|
||||
<span className="break-words">{endpoint}</span>
|
||||
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-gradient-to-r from-accent-blue to-accent-mint" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-ink">{title}</h3>
|
||||
<p className="text-sm text-inkMuted">every {intervalLabel}</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2 text-right">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`inline-flex items-center gap-2 rounded-full border px-3 py-2 text-sm font-semibold shadow-sm ${badgeColor}`}
|
||||
>
|
||||
<span className={`h-2.5 w-2.5 rounded-full ${dotColor}`} />
|
||||
{statusLabel}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onManualTrigger}
|
||||
className="inline-flex items-center gap-2 rounded-full bg-ink text-white px-3 py-2 text-xs font-semibold shadow-card transition hover:bg-inkSoft focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ink"
|
||||
>
|
||||
<span className="h-2 w-2 rounded-full bg-white" aria-hidden />
|
||||
Poll now
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-xs font-semibold uppercase tracking-[0.08em] text-inkMuted">
|
||||
{now}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-col gap-3">
|
||||
<div className="rounded-2xl border border-black/5 bg-white/80 px-3 py-2">
|
||||
<p className="text-xs uppercase tracking-[0.08em] text-inkMuted">
|
||||
Last updated
|
||||
</p>
|
||||
<p className="text-base font-semibold text-ink">{lastUpdatedLabel}</p>
|
||||
</div>
|
||||
|
||||
<div className="relative overflow-hidden rounded-2xl border border-black/5 bg-gradient-to-br from-white via-white to-slate-50 p-4">
|
||||
<div className="pointer-events-none absolute inset-0 opacity-60 mix-blend-multiply">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_10%_20%,rgba(96,165,250,0.12),transparent_35%),radial-gradient(circle_at_80%_0%,rgba(52,211,153,0.12),transparent_30%)]" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs text-inkMuted">
|
||||
<span>ECG signal</span>
|
||||
<span className="inline-flex items-center gap-2 rounded-full bg-black/5 px-2 py-1 font-semibold">
|
||||
<span className={`h-2 w-2 rounded-full ${dotColor}`} />
|
||||
{loading ? "Polling" : healthy ? "Healthy" : "Unhealthy"}
|
||||
</span>
|
||||
</div>
|
||||
<svg
|
||||
viewBox="0 0 360 120"
|
||||
preserveAspectRatio="none"
|
||||
className="mt-2 h-28 w-full text-success drop-shadow-sm"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={signalId} x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stopColor={strokeColor} stopOpacity="0.9" />
|
||||
<stop offset="50%" stopColor={strokeColor} stopOpacity="0.75" />
|
||||
<stop offset="100%" stopColor={strokeColor} stopOpacity="0.95" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<polyline
|
||||
points={wavePoints}
|
||||
fill="none"
|
||||
stroke={`url(#${signalId})`}
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<circle
|
||||
cx="360"
|
||||
cy={waveHeight}
|
||||
r="6"
|
||||
className={`stroke-white/40 ${dotColor}`}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div className="rounded-2xl border border-black/5 bg-white/80 px-3 py-2">
|
||||
<p className="text-xs uppercase tracking-[0.08em] text-inkMuted">Status</p>
|
||||
<p className="text-base font-semibold text-ink">{statusDetail}</p>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-black/5 bg-white/80 px-3 py-2">
|
||||
<p className="text-xs uppercase tracking-[0.08em] text-inkMuted">Endpoint</p>
|
||||
<p className="text-xs font-mono text-ink">{state.attemptedUrl}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user