27 lines
3.3 KiB
TypeScript
27 lines
3.3 KiB
TypeScript
import { useState } from 'react';
|
|
import type { Incident, IncidentStage, Locale } from '../types';
|
|
import { formatTime, translations } from '../lib/i18n';
|
|
import { Check, Clock, History, Warning } from './Icons';
|
|
|
|
export function Incidents({ incidents, locale, compact = false }: { incidents: Incident[]; locale: Locale; compact?: boolean }) {
|
|
const t = translations[locale];
|
|
const stages: Record<IncidentStage, string> = { detected: t.stageDetected, reported: t.stageReported, investigating: t.stageInvestigating, identified: t.stageIdentified, monitoring: t.stageMonitoring, resolved: t.stageResolved, postmortem: t.stagePostmortem };
|
|
const [filter, setFilter] = useState('all');
|
|
const filtered = incidents.filter(incident => filter === 'all' || (filter === 'active' ? !incident.resolvedAt : Boolean(incident.resolvedAt)));
|
|
const visible = compact ? filtered.slice(0, 3) : filtered;
|
|
return <>
|
|
{!compact && <div className="incidentFilters" role="group" aria-label={t.incidentsTab}>{(['all', 'active', 'resolved'] as const).map(key => <button key={key} className={filter === key ? 'selected' : ''} aria-pressed={filter === key} onClick={() => setFilter(key)}>{key === 'all' ? t.allFilter : key === 'active' ? t.activeFilter : t.resolvedFilter}</button>)}</div>}
|
|
{!visible.length ? <div className="emptyState"><span className="emptyIcon"><History size={30} /></span><h3>{filter === 'all' ? t.noIncidents : t.noFiltered}</h3><p>{t.noIncidentsDescription}</p></div> : <div className="incidentList">{visible.map(incident => {
|
|
const latest = incident.updates.at(-1);
|
|
const stage = incident.resolvedAt ? 'resolved' : latest?.stage || (incident.source === 'monitor' ? 'detected' : 'reported');
|
|
return <article className="incidentCard" key={incident.id} id={`incident-${incident.id}`}>
|
|
<div className="incidentTop"><span className={`incidentPill ${incident.resolvedAt ? 'resolved' : 'active'}`}>{incident.resolvedAt ? <Check size={17} /> : <Warning size={17} />}{stages[stage]}</span><time className="incidentDate" dateTime={new Date(incident.startedAt).toISOString()}>{formatTime(incident.startedAt, locale, true)}</time></div>
|
|
<h3>{incident.title[locale]}</h3><p>{latest?.message[locale] || incident.message[locale]}</p>
|
|
{incident.impact && <div className="incidentImpact"><strong>{incident.resolvedAt ? t.impactPast : t.impactTitle}</strong><p>{incident.impact[locale]}</p></div>}
|
|
<div className="incidentMeta"><span><Clock size={16} />{t.duration}: {incident.resolvedAt ? `${Math.max(1, Math.round((incident.resolvedAt - incident.startedAt) / 60000))} ${t.minutes}` : t.ongoing}</span><span>{incident.scope === 'scenario' ? t.scenarioTitle : incident.source === 'monitor' ? t.automated : t.teamUpdate}</span></div>
|
|
<details className="incidentTimeline" open={!compact && !incident.resolvedAt}><summary><History size={17} />{t.timeline}<span>{t.updates}: {incident.updates.length}</span></summary><ol>{incident.updates.map(update => <li key={update.id} className={update.source}><div className="timelineMeta"><time dateTime={new Date(update.createdAt).toISOString()}>{formatTime(update.createdAt, locale, true)}</time><span>{update.source === 'monitor' ? t.automated : t.teamUpdate}</span></div><h4>{stages[update.stage]}</h4><p>{update.message[locale]}</p></li>)}</ol></details>
|
|
</article>;
|
|
})}</div>}
|
|
</>;
|
|
}
|