Import source: web/src/components/IncidentList
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,92 @@
|
|||||||
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
|
import type { Incident, LiveTarget } from "../lib/types";
|
||||||
|
import { fmtDate, fmtDuration } from "../lib/format";
|
||||||
|
import { springs, useUI } from "../lib/ui";
|
||||||
|
|
||||||
|
interface IncidentListProps {
|
||||||
|
incidents: Incident[];
|
||||||
|
targets: LiveTarget[];
|
||||||
|
compact?: boolean;
|
||||||
|
hideTarget?: boolean;
|
||||||
|
/** When set, clicking a row opens that target's detail. */
|
||||||
|
onSelect?: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IncidentList({ incidents, targets, compact, hideTarget, onSelect }: IncidentListProps) {
|
||||||
|
const { t, lang, reduced } = useUI();
|
||||||
|
const nameOf = (id: string) => targets.find((x) => x.id === id)?.name ?? id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{!compact ? null : (
|
||||||
|
<div className="microlabel mb-3 flex items-baseline gap-4">
|
||||||
|
<span>{t.incidents}</span>
|
||||||
|
<span className="rule-fade flex-1" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{incidents.length === 0 ? (
|
||||||
|
<p className="text-[11px] text-paper-faint py-3">{t.noIncidents}</p>
|
||||||
|
) : (
|
||||||
|
<ul className="flex flex-col">
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
|
{incidents.map((inc) => {
|
||||||
|
const ongoing = inc.ended_at === undefined || inc.ended_at === null;
|
||||||
|
const duration = (ongoing ? Date.now() : inc.ended_at!) - inc.started_at;
|
||||||
|
return (
|
||||||
|
<motion.li
|
||||||
|
key={inc.id}
|
||||||
|
layout={!reduced}
|
||||||
|
initial={reduced ? false : { opacity: 0, x: -14 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={reduced ? undefined : { opacity: 0, x: 14 }}
|
||||||
|
transition={reduced ? { duration: 0 } : springs.gentle}
|
||||||
|
onClick={onSelect ? () => onSelect(inc.target_id) : undefined}
|
||||||
|
className={`flex items-center gap-3 py-[9px] border-b border-ink-800/70 last:border-0 ${
|
||||||
|
onSelect
|
||||||
|
? "cursor-pointer -mx-2 px-2 rounded-sm transition-colors hover:bg-ink-850/60"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="relative flex size-[7px] shrink-0">
|
||||||
|
{ongoing && !reduced && (
|
||||||
|
<motion.span
|
||||||
|
className="absolute inset-0 rounded-full bg-down"
|
||||||
|
animate={{ scale: [1, 2.6], opacity: [0.7, 0] }}
|
||||||
|
transition={{ duration: 1.6, repeat: Infinity, ease: "easeOut" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={`relative size-[7px] rounded-full ${
|
||||||
|
ongoing ? "bg-down" : "bg-idle"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{!hideTarget && (
|
||||||
|
<span className="text-[11px] text-paper font-medium shrink-0 max-w-[34%] truncate">
|
||||||
|
{nameOf(inc.target_id)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="text-[10px] text-paper-faint truncate flex-1 font-mono" dir="ltr">
|
||||||
|
{inc.error || "—"}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-paper-faint tabular shrink-0 hidden sm:inline font-mono">
|
||||||
|
{fmtDate(lang, inc.started_at)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`text-[10px] tabular shrink-0 font-mono ${
|
||||||
|
ongoing ? "text-down" : "text-paper-dim"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{ongoing ? t.ongoing : fmtDuration(lang, duration)}
|
||||||
|
</span>
|
||||||
|
</motion.li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</AnimatePresence>
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user