Incident Management UI Patterns
How to build incident management interfaces for operations dashboards, covering incident lifecycle, status pages, alert routing, and escalation workflows in React.
When an incident occurs, the operations team needs a structured workflow: detect the problem, assess its severity, coordinate the response, communicate status, and resolve the issue. An incident management UI provides this structure, transforming chaotic incident response into a repeatable process.
This article covers the incident management patterns in Klivvr's Web Ops Console.
Incident Data Model
Every incident follows a lifecycle from detection through resolution. The data model captures the incident's state at each stage.
interface Incident {
id: string;
title: string;
description: string;
severity: "critical" | "major" | "minor" | "informational";
status: "detected" | "investigating" | "identified" | "monitoring" | "resolved";
affectedServices: string[];
assignee?: string;
commander?: string;
timeline: TimelineEntry[];
createdAt: string;
updatedAt: string;
resolvedAt?: string;
postmortemUrl?: string;
}
interface TimelineEntry {
id: string;
timestamp: string;
author: string;
type: "status_change" | "update" | "action" | "escalation" | "resolution";
content: string;
metadata?: Record<string, unknown>;
}
interface AlertRule {
id: string;
name: string;
condition: string;
severity: Incident["severity"];
notifyChannels: string[];
autoCreateIncident: boolean;
cooldownMinutes: number;
}Incident List and Dashboard
The incident dashboard provides an at-a-glance view of all active incidents with severity-based prioritization.
function IncidentDashboard() {
const [incidents, setIncidents] = useState<Incident[]>([]);
const [filter, setFilter] = useState<"active" | "resolved" | "all">("active");
useEffect(() => {
fetchIncidents(filter).then(setIncidents);
}, [filter]);
const activeIncidents = incidents.filter((i) => i.status !== "resolved");
const criticalCount = activeIncidents.filter((i) => i.severity === "critical").length;
return (
<div>
{criticalCount > 0 && (
<div className="mb-4 rounded-lg bg-red-600 px-4 py-3 text-white">
<span className="font-bold">{criticalCount} critical incident(s)</span>
{" "}currently active — immediate attention required.
</div>
)}
<div className="mb-4 flex items-center justify-between">
<div className="flex gap-2">
{(["active", "resolved", "all"] as const).map((f) => (
<button
key={f}
onClick={() => setFilter(f)}
className={`rounded px-3 py-1.5 text-sm ${
filter === f ? "bg-gray-900 text-white" : "bg-gray-100"
}`}
>
{f.charAt(0).toUpperCase() + f.slice(1)}
</button>
))}
</div>
<RequirePermission permission="incidents:create">
<button className="btn-primary" onClick={() => openCreateModal()}>
Declare Incident
</button>
</RequirePermission>
</div>
<div className="space-y-3">
{incidents.map((incident) => (
<IncidentCard key={incident.id} incident={incident} />
))}
</div>
</div>
);
}
function IncidentCard({ incident }: { incident: Incident }) {
const severityStyles = {
critical: "border-l-red-600 bg-red-50",
major: "border-l-orange-500 bg-orange-50",
minor: "border-l-yellow-400 bg-yellow-50",
informational: "border-l-blue-400 bg-blue-50",
};
return (
<a
href={`/incidents/${incident.id}`}
className={`block rounded-lg border border-l-4 p-4 hover:shadow-md transition ${
severityStyles[incident.severity]
}`}
>
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-2">
<SeverityBadge severity={incident.severity} />
<StatusBadge status={incident.status} />
<h3 className="font-medium text-gray-900">{incident.title}</h3>
</div>
<p className="mt-1 text-sm text-gray-600">{incident.description}</p>
<div className="mt-2 flex gap-4 text-xs text-gray-500">
<span>Affected: {incident.affectedServices.join(", ")}</span>
{incident.assignee && <span>Assigned: {incident.assignee}</span>}
<span>
Duration: {formatDuration(incident.createdAt, incident.resolvedAt)}
</span>
</div>
</div>
</div>
</a>
);
}Incident Timeline
The incident detail page centers on a timeline that records every status change, update, and action taken during the incident.
function IncidentTimeline({ incident }: { incident: Incident }) {
const [newUpdate, setNewUpdate] = useState("");
const addUpdate = async () => {
if (!newUpdate.trim()) return;
await postIncidentUpdate(incident.id, {
type: "update",
content: newUpdate,
});
setNewUpdate("");
};
return (
<div className="space-y-4">
<RequirePermission permission="incidents:create">
<div className="flex gap-2">
<input
type="text"
value={newUpdate}
onChange={(e) => setNewUpdate(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && addUpdate()}
placeholder="Add an update..."
className="flex-1 rounded border px-3 py-2 text-sm"
/>
<button onClick={addUpdate} className="btn-primary text-sm">
Post Update
</button>
</div>
</RequirePermission>
<div className="relative ml-4 border-l-2 border-gray-200 pl-6">
{incident.timeline.map((entry) => (
<div key={entry.id} className="relative mb-6">
<div className="absolute -left-8 mt-1.5 h-3 w-3 rounded-full border-2 border-white bg-gray-400" />
<div className="text-xs text-gray-400">
{new Date(entry.timestamp).toLocaleString()} — {entry.author}
</div>
<TimelineEntryContent entry={entry} />
</div>
))}
</div>
</div>
);
}
function TimelineEntryContent({ entry }: { entry: TimelineEntry }) {
const typeStyles: Record<string, string> = {
status_change: "text-blue-700 bg-blue-50",
update: "text-gray-700",
action: "text-green-700 bg-green-50",
escalation: "text-orange-700 bg-orange-50",
resolution: "text-purple-700 bg-purple-50",
};
return (
<div className={`mt-1 rounded p-2 text-sm ${typeStyles[entry.type] ?? ""}`}>
{entry.type === "status_change" && (
<span className="font-medium">Status changed: </span>
)}
{entry.type === "escalation" && (
<span className="font-medium">Escalated: </span>
)}
{entry.content}
</div>
);
}Status Update Controls
During an incident, the assigned responder needs quick controls to update the incident status and severity.
function IncidentControls({ incident }: { incident: Incident }) {
const statusFlow: Record<string, Incident["status"][]> = {
detected: ["investigating"],
investigating: ["identified", "resolved"],
identified: ["monitoring", "resolved"],
monitoring: ["resolved"],
resolved: [],
};
const nextStatuses = statusFlow[incident.status] ?? [];
return (
<div className="flex flex-wrap gap-2">
{nextStatuses.map((status) => (
<button
key={status}
onClick={() => updateIncidentStatus(incident.id, status)}
className={`rounded px-3 py-1.5 text-sm font-medium ${
status === "resolved"
? "bg-green-600 text-white"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
Move to: {status}
</button>
))}
</div>
);
}Conclusion
Incident management UI patterns transform chaotic incident response into structured workflows. The Web Ops Console provides incident declaration, severity-based prioritization, timeline-driven communication, status progression controls, and integrated alerting. These patterns ensure that when something goes wrong in Klivvr's infrastructure, the response is coordinated, documented, and efficient — reducing mean time to resolution and building a knowledge base of past incidents for future reference.
Related Articles
Build vs Buy for Internal Operations Tools
A framework for deciding whether to build or buy internal operations tools, covering total cost of ownership, customization needs, and the strategic value of purpose-built tooling.
Improving Incident Response with Ops Dashboards
How operations dashboards improve incident response efficiency through faster detection, structured workflows, MTTR reduction, and postmortem processes.
Data Visualization Patterns for Ops Dashboards
How to choose and implement effective data visualizations for operations dashboards, covering chart selection, color systems, responsive layouts, and accessibility.