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.

technical4 min readBy Klivvr Engineering
Share:

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

business

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.

6 min read
technical

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.

5 min read