@checkmate-monitor/incident-frontend 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/package.json +29 -0
- package/src/api.ts +10 -0
- package/src/components/IncidentEditor.tsx +311 -0
- package/src/components/IncidentMenuItems.tsx +27 -0
- package/src/components/IncidentUpdateForm.tsx +124 -0
- package/src/components/SystemIncidentBadge.tsx +68 -0
- package/src/components/SystemIncidentPanel.tsx +240 -0
- package/src/index.tsx +70 -0
- package/src/pages/IncidentConfigPage.tsx +387 -0
- package/src/pages/IncidentDetailPage.tsx +269 -0
- package/src/pages/SystemIncidentHistoryPage.tsx +200 -0
- package/src/utils/badges.tsx +58 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import React, { useEffect, useState, useCallback, useMemo } from "react";
|
|
2
|
+
import { useParams } from "react-router-dom";
|
|
3
|
+
import {
|
|
4
|
+
useApi,
|
|
5
|
+
rpcApiRef,
|
|
6
|
+
permissionApiRef,
|
|
7
|
+
wrapInSuspense,
|
|
8
|
+
} from "@checkmate-monitor/frontend-api";
|
|
9
|
+
import { useSignal } from "@checkmate-monitor/signal-frontend";
|
|
10
|
+
import { resolveRoute } from "@checkmate-monitor/common";
|
|
11
|
+
import { incidentApiRef } from "../api";
|
|
12
|
+
import {
|
|
13
|
+
incidentRoutes,
|
|
14
|
+
INCIDENT_UPDATED,
|
|
15
|
+
type IncidentDetail,
|
|
16
|
+
} from "@checkmate-monitor/incident-common";
|
|
17
|
+
import { CatalogApi, type System } from "@checkmate-monitor/catalog-common";
|
|
18
|
+
import {
|
|
19
|
+
Card,
|
|
20
|
+
CardHeader,
|
|
21
|
+
CardTitle,
|
|
22
|
+
CardContent,
|
|
23
|
+
Button,
|
|
24
|
+
Badge,
|
|
25
|
+
LoadingSpinner,
|
|
26
|
+
BackLink,
|
|
27
|
+
useToast,
|
|
28
|
+
StatusUpdateTimeline,
|
|
29
|
+
} from "@checkmate-monitor/ui";
|
|
30
|
+
import {
|
|
31
|
+
AlertTriangle,
|
|
32
|
+
Clock,
|
|
33
|
+
Calendar,
|
|
34
|
+
MessageSquare,
|
|
35
|
+
CheckCircle2,
|
|
36
|
+
Server,
|
|
37
|
+
Plus,
|
|
38
|
+
} from "lucide-react";
|
|
39
|
+
import { format, formatDistanceToNow } from "date-fns";
|
|
40
|
+
import { IncidentUpdateForm } from "../components/IncidentUpdateForm";
|
|
41
|
+
import {
|
|
42
|
+
getIncidentStatusBadge,
|
|
43
|
+
getIncidentSeverityBadge,
|
|
44
|
+
} from "../utils/badges";
|
|
45
|
+
|
|
46
|
+
const IncidentDetailPageContent: React.FC = () => {
|
|
47
|
+
const { incidentId } = useParams<{ incidentId: string }>();
|
|
48
|
+
const api = useApi(incidentApiRef);
|
|
49
|
+
const rpcApi = useApi(rpcApiRef);
|
|
50
|
+
const permissionApi = useApi(permissionApiRef);
|
|
51
|
+
const toast = useToast();
|
|
52
|
+
|
|
53
|
+
const catalogApi = useMemo(() => rpcApi.forPlugin(CatalogApi), [rpcApi]);
|
|
54
|
+
|
|
55
|
+
const { allowed: canManage } = permissionApi.useResourcePermission(
|
|
56
|
+
"incident",
|
|
57
|
+
"manage"
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const [incident, setIncident] = useState<IncidentDetail | undefined>();
|
|
61
|
+
const [systems, setSystems] = useState<System[]>([]);
|
|
62
|
+
const [loading, setLoading] = useState(true);
|
|
63
|
+
const [showUpdateForm, setShowUpdateForm] = useState(false);
|
|
64
|
+
|
|
65
|
+
const loadData = useCallback(async () => {
|
|
66
|
+
if (!incidentId) return;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const [incidentData, systemList] = await Promise.all([
|
|
70
|
+
api.getIncident({ id: incidentId }),
|
|
71
|
+
catalogApi.getSystems(),
|
|
72
|
+
]);
|
|
73
|
+
setIncident(incidentData ?? undefined);
|
|
74
|
+
setSystems(systemList);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error("Failed to load incident:", error);
|
|
77
|
+
} finally {
|
|
78
|
+
setLoading(false);
|
|
79
|
+
}
|
|
80
|
+
}, [incidentId, api, catalogApi]);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
loadData();
|
|
84
|
+
}, [loadData]);
|
|
85
|
+
|
|
86
|
+
// Listen for realtime updates
|
|
87
|
+
useSignal(INCIDENT_UPDATED, ({ incidentId: updatedId }) => {
|
|
88
|
+
if (incidentId === updatedId) {
|
|
89
|
+
loadData();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const handleUpdateSuccess = () => {
|
|
94
|
+
setShowUpdateForm(false);
|
|
95
|
+
loadData();
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const handleResolve = async () => {
|
|
99
|
+
if (!incidentId) return;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
await api.resolveIncident({ id: incidentId });
|
|
103
|
+
toast.success("Incident resolved");
|
|
104
|
+
await loadData();
|
|
105
|
+
} catch (error) {
|
|
106
|
+
const message =
|
|
107
|
+
error instanceof Error ? error.message : "Failed to resolve";
|
|
108
|
+
toast.error(message);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (loading) {
|
|
113
|
+
return (
|
|
114
|
+
<div className="p-12 flex justify-center">
|
|
115
|
+
<LoadingSpinner />
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!incident) {
|
|
121
|
+
return (
|
|
122
|
+
<div className="p-12 text-center">
|
|
123
|
+
<p className="text-muted-foreground">Incident not found</p>
|
|
124
|
+
<BackLink
|
|
125
|
+
to={resolveRoute(incidentRoutes.routes.config, {})}
|
|
126
|
+
className="mt-4"
|
|
127
|
+
>
|
|
128
|
+
Back to Incidents
|
|
129
|
+
</BackLink>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const affectedSystems = systems.filter((s) =>
|
|
135
|
+
incident.systemIds.includes(s.id)
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div className="space-y-6 p-6">
|
|
140
|
+
{/* Incident Header */}
|
|
141
|
+
<Card
|
|
142
|
+
className={
|
|
143
|
+
incident.severity === "critical"
|
|
144
|
+
? "border-destructive/30 bg-destructive/5"
|
|
145
|
+
: incident.severity === "major"
|
|
146
|
+
? "border-warning/30 bg-warning/5"
|
|
147
|
+
: ""
|
|
148
|
+
}
|
|
149
|
+
>
|
|
150
|
+
<CardHeader className="border-b border-border">
|
|
151
|
+
<div className="flex items-start justify-between">
|
|
152
|
+
<div className="flex items-center gap-3">
|
|
153
|
+
<AlertTriangle
|
|
154
|
+
className={`h-6 w-6 ${
|
|
155
|
+
incident.severity === "critical"
|
|
156
|
+
? "text-destructive"
|
|
157
|
+
: incident.severity === "major"
|
|
158
|
+
? "text-warning"
|
|
159
|
+
: "text-muted-foreground"
|
|
160
|
+
}`}
|
|
161
|
+
/>
|
|
162
|
+
<div>
|
|
163
|
+
<CardTitle className="text-xl">{incident.title}</CardTitle>
|
|
164
|
+
<div className="flex items-center gap-2 mt-2">
|
|
165
|
+
{getIncidentSeverityBadge(incident.severity)}
|
|
166
|
+
{getIncidentStatusBadge(incident.status)}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
{canManage && incident.status !== "resolved" && (
|
|
171
|
+
<Button variant="outline" onClick={handleResolve}>
|
|
172
|
+
<CheckCircle2 className="h-4 w-4 mr-2" />
|
|
173
|
+
Resolve Incident
|
|
174
|
+
</Button>
|
|
175
|
+
)}
|
|
176
|
+
<BackLink to={resolveRoute(incidentRoutes.routes.config, {})}>
|
|
177
|
+
Back to Incidents
|
|
178
|
+
</BackLink>
|
|
179
|
+
</div>
|
|
180
|
+
</CardHeader>
|
|
181
|
+
<CardContent className="p-4 space-y-4">
|
|
182
|
+
{incident.description && (
|
|
183
|
+
<p className="text-muted-foreground">{incident.description}</p>
|
|
184
|
+
)}
|
|
185
|
+
|
|
186
|
+
<div className="flex gap-6 text-sm text-muted-foreground">
|
|
187
|
+
<div className="flex items-center gap-1">
|
|
188
|
+
<Calendar className="h-4 w-4" />
|
|
189
|
+
<span>
|
|
190
|
+
Started {format(new Date(incident.createdAt), "MMM d, HH:mm")}
|
|
191
|
+
</span>
|
|
192
|
+
</div>
|
|
193
|
+
<div className="flex items-center gap-1">
|
|
194
|
+
<Clock className="h-4 w-4" />
|
|
195
|
+
<span>
|
|
196
|
+
Duration:{" "}
|
|
197
|
+
{formatDistanceToNow(new Date(incident.createdAt), {
|
|
198
|
+
addSuffix: false,
|
|
199
|
+
})}
|
|
200
|
+
</span>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</CardContent>
|
|
204
|
+
</Card>
|
|
205
|
+
|
|
206
|
+
{/* Affected Systems */}
|
|
207
|
+
<Card>
|
|
208
|
+
<CardHeader>
|
|
209
|
+
<div className="flex items-center gap-2">
|
|
210
|
+
<Server className="h-5 w-5 text-muted-foreground" />
|
|
211
|
+
<CardTitle className="text-lg">Affected Systems</CardTitle>
|
|
212
|
+
</div>
|
|
213
|
+
</CardHeader>
|
|
214
|
+
<CardContent>
|
|
215
|
+
<div className="flex flex-wrap gap-2">
|
|
216
|
+
{affectedSystems.map((system) => (
|
|
217
|
+
<Badge key={system.id} variant="outline">
|
|
218
|
+
{system.name}
|
|
219
|
+
</Badge>
|
|
220
|
+
))}
|
|
221
|
+
</div>
|
|
222
|
+
</CardContent>
|
|
223
|
+
</Card>
|
|
224
|
+
|
|
225
|
+
{/* Status Updates Timeline */}
|
|
226
|
+
<Card>
|
|
227
|
+
<CardHeader className="border-b border-border">
|
|
228
|
+
<div className="flex items-center justify-between">
|
|
229
|
+
<div className="flex items-center gap-2">
|
|
230
|
+
<MessageSquare className="h-5 w-5 text-muted-foreground" />
|
|
231
|
+
<CardTitle className="text-lg">Status Updates</CardTitle>
|
|
232
|
+
</div>
|
|
233
|
+
{canManage && !showUpdateForm && (
|
|
234
|
+
<Button
|
|
235
|
+
variant="outline"
|
|
236
|
+
size="sm"
|
|
237
|
+
onClick={() => setShowUpdateForm(true)}
|
|
238
|
+
>
|
|
239
|
+
<Plus className="h-4 w-4 mr-1" />
|
|
240
|
+
Add Update
|
|
241
|
+
</Button>
|
|
242
|
+
)}
|
|
243
|
+
</div>
|
|
244
|
+
</CardHeader>
|
|
245
|
+
<CardContent className="p-4">
|
|
246
|
+
{/* Add Update Form */}
|
|
247
|
+
{showUpdateForm && incidentId && (
|
|
248
|
+
<div className="mb-4">
|
|
249
|
+
<IncidentUpdateForm
|
|
250
|
+
incidentId={incidentId}
|
|
251
|
+
onSuccess={handleUpdateSuccess}
|
|
252
|
+
onCancel={() => setShowUpdateForm(false)}
|
|
253
|
+
/>
|
|
254
|
+
</div>
|
|
255
|
+
)}
|
|
256
|
+
|
|
257
|
+
<StatusUpdateTimeline
|
|
258
|
+
updates={incident.updates}
|
|
259
|
+
renderStatusBadge={getIncidentStatusBadge}
|
|
260
|
+
emptyTitle="No status updates"
|
|
261
|
+
emptyDescription="No status updates have been posted yet."
|
|
262
|
+
/>
|
|
263
|
+
</CardContent>
|
|
264
|
+
</Card>
|
|
265
|
+
</div>
|
|
266
|
+
);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export const IncidentDetailPage = wrapInSuspense(IncidentDetailPageContent);
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import React, { useEffect, useState, useMemo } from "react";
|
|
2
|
+
import { useParams, Link } from "react-router-dom";
|
|
3
|
+
import {
|
|
4
|
+
useApi,
|
|
5
|
+
rpcApiRef,
|
|
6
|
+
wrapInSuspense,
|
|
7
|
+
} from "@checkmate-monitor/frontend-api";
|
|
8
|
+
import { useSignal } from "@checkmate-monitor/signal-frontend";
|
|
9
|
+
import { resolveRoute } from "@checkmate-monitor/common";
|
|
10
|
+
import { incidentApiRef } from "../api";
|
|
11
|
+
import {
|
|
12
|
+
incidentRoutes,
|
|
13
|
+
INCIDENT_UPDATED,
|
|
14
|
+
type IncidentWithSystems,
|
|
15
|
+
type IncidentStatus,
|
|
16
|
+
} from "@checkmate-monitor/incident-common";
|
|
17
|
+
import {
|
|
18
|
+
CatalogApi,
|
|
19
|
+
type System,
|
|
20
|
+
catalogRoutes,
|
|
21
|
+
} from "@checkmate-monitor/catalog-common";
|
|
22
|
+
import {
|
|
23
|
+
Card,
|
|
24
|
+
CardHeader,
|
|
25
|
+
CardTitle,
|
|
26
|
+
CardContent,
|
|
27
|
+
Badge,
|
|
28
|
+
LoadingSpinner,
|
|
29
|
+
EmptyState,
|
|
30
|
+
BackLink,
|
|
31
|
+
} from "@checkmate-monitor/ui";
|
|
32
|
+
import { AlertTriangle, Clock, ChevronRight } from "lucide-react";
|
|
33
|
+
import { formatDistanceToNow } from "date-fns";
|
|
34
|
+
|
|
35
|
+
const SystemIncidentHistoryPageContent: React.FC = () => {
|
|
36
|
+
const { systemId } = useParams<{ systemId: string }>();
|
|
37
|
+
const api = useApi(incidentApiRef);
|
|
38
|
+
const rpcApi = useApi(rpcApiRef);
|
|
39
|
+
|
|
40
|
+
const catalogApi = useMemo(() => rpcApi.forPlugin(CatalogApi), [rpcApi]);
|
|
41
|
+
|
|
42
|
+
const [incidents, setIncidents] = useState<IncidentWithSystems[]>([]);
|
|
43
|
+
const [system, setSystem] = useState<System | undefined>();
|
|
44
|
+
const [loading, setLoading] = useState(true);
|
|
45
|
+
|
|
46
|
+
const loadData = async () => {
|
|
47
|
+
if (!systemId) return;
|
|
48
|
+
|
|
49
|
+
setLoading(true);
|
|
50
|
+
try {
|
|
51
|
+
const [incidentList, systemList] = await Promise.all([
|
|
52
|
+
api.listIncidents({ systemId, includeResolved: true }),
|
|
53
|
+
catalogApi.getSystems(),
|
|
54
|
+
]);
|
|
55
|
+
const systemData = systemList.find((s) => s.id === systemId);
|
|
56
|
+
setIncidents(incidentList);
|
|
57
|
+
setSystem(systemData);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error("Failed to load incidents:", error);
|
|
60
|
+
} finally {
|
|
61
|
+
setLoading(false);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
loadData();
|
|
67
|
+
}, [systemId]);
|
|
68
|
+
|
|
69
|
+
// Listen for realtime updates
|
|
70
|
+
useSignal(INCIDENT_UPDATED, ({ systemIds }) => {
|
|
71
|
+
if (systemId && systemIds.includes(systemId)) {
|
|
72
|
+
loadData();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const getStatusBadge = (status: IncidentStatus) => {
|
|
77
|
+
switch (status) {
|
|
78
|
+
case "investigating": {
|
|
79
|
+
return <Badge variant="destructive">Investigating</Badge>;
|
|
80
|
+
}
|
|
81
|
+
case "identified": {
|
|
82
|
+
return <Badge variant="warning">Identified</Badge>;
|
|
83
|
+
}
|
|
84
|
+
case "fixing": {
|
|
85
|
+
return <Badge variant="warning">Fixing</Badge>;
|
|
86
|
+
}
|
|
87
|
+
case "monitoring": {
|
|
88
|
+
return <Badge variant="info">Monitoring</Badge>;
|
|
89
|
+
}
|
|
90
|
+
case "resolved": {
|
|
91
|
+
return <Badge variant="success">Resolved</Badge>;
|
|
92
|
+
}
|
|
93
|
+
default: {
|
|
94
|
+
return <Badge>{status}</Badge>;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const getSeverityBadge = (severity: string) => {
|
|
100
|
+
switch (severity) {
|
|
101
|
+
case "critical": {
|
|
102
|
+
return <Badge variant="destructive">Critical</Badge>;
|
|
103
|
+
}
|
|
104
|
+
case "major": {
|
|
105
|
+
return <Badge variant="warning">Major</Badge>;
|
|
106
|
+
}
|
|
107
|
+
default: {
|
|
108
|
+
return <Badge variant="secondary">Minor</Badge>;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (loading) {
|
|
114
|
+
return (
|
|
115
|
+
<div className="p-12 flex justify-center">
|
|
116
|
+
<LoadingSpinner />
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div className="space-y-6 p-6">
|
|
123
|
+
<Card>
|
|
124
|
+
<CardHeader className="border-b border-border">
|
|
125
|
+
<div className="flex items-center justify-between">
|
|
126
|
+
<div className="flex items-center gap-2">
|
|
127
|
+
<AlertTriangle className="h-5 w-5 text-muted-foreground" />
|
|
128
|
+
<CardTitle>
|
|
129
|
+
Incident History{system ? ` - ${system.name}` : ""}
|
|
130
|
+
</CardTitle>
|
|
131
|
+
</div>
|
|
132
|
+
{system && (
|
|
133
|
+
<BackLink
|
|
134
|
+
to={resolveRoute(catalogRoutes.routes.systemDetail, {
|
|
135
|
+
systemId: system.id,
|
|
136
|
+
})}
|
|
137
|
+
>
|
|
138
|
+
Back to {system.name}
|
|
139
|
+
</BackLink>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
</CardHeader>
|
|
143
|
+
<CardContent className="p-0">
|
|
144
|
+
{incidents.length === 0 ? (
|
|
145
|
+
<EmptyState
|
|
146
|
+
title="No incidents"
|
|
147
|
+
description="This system has no recorded incidents."
|
|
148
|
+
/>
|
|
149
|
+
) : (
|
|
150
|
+
<div className="divide-y divide-border">
|
|
151
|
+
{incidents.map((incident) => (
|
|
152
|
+
<Link
|
|
153
|
+
key={incident.id}
|
|
154
|
+
to={resolveRoute(incidentRoutes.routes.detail, {
|
|
155
|
+
incidentId: incident.id,
|
|
156
|
+
})}
|
|
157
|
+
className="block p-4 hover:bg-muted/50 transition-colors"
|
|
158
|
+
>
|
|
159
|
+
<div className="flex items-start justify-between">
|
|
160
|
+
<div className="flex-1">
|
|
161
|
+
<div className="flex items-center gap-2 mb-1">
|
|
162
|
+
<h4 className="font-medium text-foreground">
|
|
163
|
+
{incident.title}
|
|
164
|
+
</h4>
|
|
165
|
+
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
|
166
|
+
</div>
|
|
167
|
+
{incident.description && (
|
|
168
|
+
<p className="text-sm text-muted-foreground line-clamp-2 mb-2">
|
|
169
|
+
{incident.description}
|
|
170
|
+
</p>
|
|
171
|
+
)}
|
|
172
|
+
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
|
173
|
+
<div className="flex items-center gap-1">
|
|
174
|
+
<Clock className="h-3 w-3" />
|
|
175
|
+
<span>
|
|
176
|
+
{formatDistanceToNow(new Date(incident.createdAt), {
|
|
177
|
+
addSuffix: true,
|
|
178
|
+
})}
|
|
179
|
+
</span>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
184
|
+
{getSeverityBadge(incident.severity)}
|
|
185
|
+
{getStatusBadge(incident.status)}
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</Link>
|
|
189
|
+
))}
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
</CardContent>
|
|
193
|
+
</Card>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export const SystemIncidentHistoryPage = wrapInSuspense(
|
|
199
|
+
SystemIncidentHistoryPageContent
|
|
200
|
+
);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Badge } from "@checkmate-monitor/ui";
|
|
3
|
+
import type {
|
|
4
|
+
IncidentStatus,
|
|
5
|
+
IncidentSeverity,
|
|
6
|
+
} from "@checkmate-monitor/incident-common";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns a styled badge for the given incident status.
|
|
10
|
+
* Use this utility to ensure consistent status badge styling across the plugin.
|
|
11
|
+
*/
|
|
12
|
+
export function getIncidentStatusBadge(
|
|
13
|
+
status: IncidentStatus
|
|
14
|
+
): React.ReactNode {
|
|
15
|
+
switch (status) {
|
|
16
|
+
case "investigating": {
|
|
17
|
+
return <Badge variant="destructive">Investigating</Badge>;
|
|
18
|
+
}
|
|
19
|
+
case "identified": {
|
|
20
|
+
return <Badge variant="warning">Identified</Badge>;
|
|
21
|
+
}
|
|
22
|
+
case "fixing": {
|
|
23
|
+
return <Badge variant="warning">Fixing</Badge>;
|
|
24
|
+
}
|
|
25
|
+
case "monitoring": {
|
|
26
|
+
return <Badge variant="info">Monitoring</Badge>;
|
|
27
|
+
}
|
|
28
|
+
case "resolved": {
|
|
29
|
+
return <Badge variant="success">Resolved</Badge>;
|
|
30
|
+
}
|
|
31
|
+
default: {
|
|
32
|
+
return <Badge>{status}</Badge>;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns a styled badge for the given incident severity.
|
|
39
|
+
* Use this utility to ensure consistent severity badge styling across the plugin.
|
|
40
|
+
*/
|
|
41
|
+
export function getIncidentSeverityBadge(
|
|
42
|
+
severity: IncidentSeverity
|
|
43
|
+
): React.ReactNode {
|
|
44
|
+
switch (severity) {
|
|
45
|
+
case "critical": {
|
|
46
|
+
return <Badge variant="destructive">Critical</Badge>;
|
|
47
|
+
}
|
|
48
|
+
case "major": {
|
|
49
|
+
return <Badge variant="warning">Major</Badge>;
|
|
50
|
+
}
|
|
51
|
+
case "minor": {
|
|
52
|
+
return <Badge variant="secondary">Minor</Badge>;
|
|
53
|
+
}
|
|
54
|
+
default: {
|
|
55
|
+
return <Badge>{severity}</Badge>;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|