All files / src/components/monitors UptimeChart.tsx

100% Statements 59/59
100% Branches 22/22
100% Functions 1/1
100% Lines 59/59

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 741x           24x   24x 24x   24x 5x 5x 5x 100x 100x 100x 100x 100x 5x 5x   5x   24x 4x 4x 4x 4x   4x   15x 15x 15x 15x   15x 15x 15x 15x 15x 15x 15x 24x   24x 24x 24x 24x 24x 171x 171x 171x 171x 171x 171x 171x 165x 6x 171x 171x 171x 24x 24x 24x 24x 24x 24x 24x   24x  
import { useMonitorPings } from "../../hooks/usePings";
 
interface UptimeChartProps {
  monitorId: string;
}
 
export default function UptimeChart({ monitorId }: UptimeChartProps) {
  // Use a high limit for the chart to show meaningful bars, paginated list handles display
  const { data: pingsResponse, isLoading } = useMonitorPings(monitorId, 0, 50);
  const pings = pingsResponse?.items ?? [];
 
  if (isLoading) {
    return (
      <div className="h-8 flex items-end gap-1 animate-pulse">
        {Array.from({ length: 20 }).map((_, i) => (
          <div
            key={i}
            className="flex-1 bg-gray-200 rounded-sm"
            style={{ height: "60%" }}
          />
        ))}
      </div>
    );
  }
 
  if (!pings || pings.length === 0) {
    return (
      <div className="rounded-lg border border-gray-200 p-6 text-center">
        <p className="text-sm text-gray-500">No ping data available yet.</p>
      </div>
    );
  }
 
  const recentChecks = [...pings].reverse();
  const uptimePercent = Math.round(
    (recentChecks.filter((d) => d.is_up).length / recentChecks.length) * 100,
  );
 
  return (
    <div className="space-y-3">
      <div className="flex items-center justify-between">
        <span className="text-sm font-medium text-gray-700">
          Last {recentChecks.length} Checks
        </span>
        <span
          className={`text-sm font-semibold ${uptimePercent >= 99 ? "text-emerald-600" : uptimePercent >= 95 ? "text-yellow-600" : "text-red-600"}`}
        >
          {uptimePercent}% uptime
        </span>
      </div>
      <div className="flex gap-0.5 h-10 items-end">
        {recentChecks.map((check, i) => (
          <div
            key={i}
            className={`flex-1 rounded-[1px] transition-all hover:opacity-80 ${
              check.is_up ? "bg-emerald-400" : "bg-red-400"
            }`}
            style={{
              height: check.response_ms
                ? `${Math.min((check.response_ms / 1000) * 100, 100)}%`
                : "100%",
            }}
            title={`${check.is_up ? "UP" : "DOWN"} — ${check.response_ms ?? "N/A"}ms — ${new Date(check.timestamp).toLocaleString()}`}
          />
        ))}
      </div>
      <div className="flex justify-between text-xs text-gray-400">
        <span>{recentChecks.length} checks</span>
        <span>{recentChecks.filter((c) => c.is_up).length} passed</span>
      </div>
    </div>
  );
}