@checkstack/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 +86 -0
- package/package.json +29 -0
- package/src/api.ts +10 -0
- package/src/components/IncidentEditor.tsx +311 -0
- package/src/components/IncidentMenuItems.tsx +33 -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 +69 -0
- package/src/pages/IncidentConfigPage.tsx +387 -0
- package/src/pages/IncidentDetailPage.tsx +284 -0
- package/src/pages/SystemIncidentHistoryPage.tsx +200 -0
- package/src/utils/badges.tsx +58 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import React, { useEffect, useState, useCallback, useMemo } from "react";
|
|
2
|
+
import { useParams, useNavigate, useSearchParams } from "react-router-dom";
|
|
3
|
+
import {
|
|
4
|
+
useApi,
|
|
5
|
+
rpcApiRef,
|
|
6
|
+
permissionApiRef,
|
|
7
|
+
wrapInSuspense,
|
|
8
|
+
} from "@checkstack/frontend-api";
|
|
9
|
+
import { useSignal } from "@checkstack/signal-frontend";
|
|
10
|
+
import { resolveRoute } from "@checkstack/common";
|
|
11
|
+
import { incidentApiRef } from "../api";
|
|
12
|
+
import {
|
|
13
|
+
incidentRoutes,
|
|
14
|
+
INCIDENT_UPDATED,
|
|
15
|
+
type IncidentDetail,
|
|
16
|
+
} from "@checkstack/incident-common";
|
|
17
|
+
import { CatalogApi, type System } from "@checkstack/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
|
+
PageLayout,
|
|
30
|
+
} from "@checkstack/ui";
|
|
31
|
+
import {
|
|
32
|
+
AlertTriangle,
|
|
33
|
+
Clock,
|
|
34
|
+
Calendar,
|
|
35
|
+
MessageSquare,
|
|
36
|
+
CheckCircle2,
|
|
37
|
+
Server,
|
|
38
|
+
Plus,
|
|
39
|
+
} from "lucide-react";
|
|
40
|
+
import { format, formatDistanceToNow } from "date-fns";
|
|
41
|
+
import { IncidentUpdateForm } from "../components/IncidentUpdateForm";
|
|
42
|
+
import {
|
|
43
|
+
getIncidentStatusBadge,
|
|
44
|
+
getIncidentSeverityBadge,
|
|
45
|
+
} from "../utils/badges";
|
|
46
|
+
|
|
47
|
+
const IncidentDetailPageContent: React.FC = () => {
|
|
48
|
+
const { incidentId } = useParams<{ incidentId: string }>();
|
|
49
|
+
const navigate = useNavigate();
|
|
50
|
+
const [searchParams] = useSearchParams();
|
|
51
|
+
const api = useApi(incidentApiRef);
|
|
52
|
+
const rpcApi = useApi(rpcApiRef);
|
|
53
|
+
const permissionApi = useApi(permissionApiRef);
|
|
54
|
+
const toast = useToast();
|
|
55
|
+
|
|
56
|
+
const catalogApi = useMemo(() => rpcApi.forPlugin(CatalogApi), [rpcApi]);
|
|
57
|
+
|
|
58
|
+
const { allowed: canManage } = permissionApi.useResourcePermission(
|
|
59
|
+
"incident",
|
|
60
|
+
"manage"
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const [incident, setIncident] = useState<IncidentDetail | undefined>();
|
|
64
|
+
const [systems, setSystems] = useState<System[]>([]);
|
|
65
|
+
const [loading, setLoading] = useState(true);
|
|
66
|
+
const [showUpdateForm, setShowUpdateForm] = useState(false);
|
|
67
|
+
|
|
68
|
+
const loadData = useCallback(async () => {
|
|
69
|
+
if (!incidentId) return;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const [incidentData, systemList] = await Promise.all([
|
|
73
|
+
api.getIncident({ id: incidentId }),
|
|
74
|
+
catalogApi.getSystems(),
|
|
75
|
+
]);
|
|
76
|
+
setIncident(incidentData ?? undefined);
|
|
77
|
+
setSystems(systemList);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error("Failed to load incident:", error);
|
|
80
|
+
} finally {
|
|
81
|
+
setLoading(false);
|
|
82
|
+
}
|
|
83
|
+
}, [incidentId, api, catalogApi]);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
loadData();
|
|
87
|
+
}, [loadData]);
|
|
88
|
+
|
|
89
|
+
// Listen for realtime updates
|
|
90
|
+
useSignal(INCIDENT_UPDATED, ({ incidentId: updatedId }) => {
|
|
91
|
+
if (incidentId === updatedId) {
|
|
92
|
+
loadData();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const handleUpdateSuccess = () => {
|
|
97
|
+
setShowUpdateForm(false);
|
|
98
|
+
loadData();
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const handleResolve = async () => {
|
|
102
|
+
if (!incidentId) return;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
await api.resolveIncident({ id: incidentId });
|
|
106
|
+
toast.success("Incident resolved");
|
|
107
|
+
await loadData();
|
|
108
|
+
} catch (error) {
|
|
109
|
+
const message =
|
|
110
|
+
error instanceof Error ? error.message : "Failed to resolve";
|
|
111
|
+
toast.error(message);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const getSystemName = (systemId: string): string => {
|
|
116
|
+
return systems.find((s) => s.id === systemId)?.name ?? systemId;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (loading) {
|
|
120
|
+
return (
|
|
121
|
+
<div className="p-12 flex justify-center">
|
|
122
|
+
<LoadingSpinner />
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!incident) {
|
|
128
|
+
return (
|
|
129
|
+
<div className="p-12 text-center">
|
|
130
|
+
<p className="text-muted-foreground">Incident not found</p>
|
|
131
|
+
<BackLink
|
|
132
|
+
to={resolveRoute(incidentRoutes.routes.config, {})}
|
|
133
|
+
className="mt-4"
|
|
134
|
+
>
|
|
135
|
+
Back to Incidents
|
|
136
|
+
</BackLink>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const canResolve = canManage && incident.status !== "resolved";
|
|
142
|
+
// Use 'from' query param for back navigation, fallback to first affected system
|
|
143
|
+
const sourceSystemId = searchParams.get("from") ?? incident.systemIds[0];
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<PageLayout
|
|
147
|
+
title={incident.title}
|
|
148
|
+
subtitle="Incident details and status history"
|
|
149
|
+
loading={false}
|
|
150
|
+
allowed={true}
|
|
151
|
+
actions={
|
|
152
|
+
sourceSystemId ? (
|
|
153
|
+
<BackLink
|
|
154
|
+
onClick={() =>
|
|
155
|
+
navigate(
|
|
156
|
+
resolveRoute(incidentRoutes.routes.systemHistory, {
|
|
157
|
+
systemId: sourceSystemId,
|
|
158
|
+
})
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
>
|
|
162
|
+
Back to History
|
|
163
|
+
</BackLink>
|
|
164
|
+
) : undefined
|
|
165
|
+
}
|
|
166
|
+
>
|
|
167
|
+
<div className="space-y-6">
|
|
168
|
+
{/* Incident Info Card */}
|
|
169
|
+
<Card>
|
|
170
|
+
<CardHeader className="border-b border-border">
|
|
171
|
+
<div className="flex items-center justify-between">
|
|
172
|
+
<div className="flex items-center gap-2">
|
|
173
|
+
<AlertTriangle className="h-5 w-5 text-muted-foreground" />
|
|
174
|
+
<CardTitle>Incident Details</CardTitle>
|
|
175
|
+
</div>
|
|
176
|
+
<div className="flex items-center gap-2">
|
|
177
|
+
{getIncidentSeverityBadge(incident.severity)}
|
|
178
|
+
{getIncidentStatusBadge(incident.status)}
|
|
179
|
+
{canResolve && (
|
|
180
|
+
<Button variant="outline" size="sm" onClick={handleResolve}>
|
|
181
|
+
<CheckCircle2 className="h-4 w-4 mr-1" />
|
|
182
|
+
Resolve
|
|
183
|
+
</Button>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
</CardHeader>
|
|
188
|
+
<CardContent className="p-6 space-y-4">
|
|
189
|
+
{incident.description && (
|
|
190
|
+
<div>
|
|
191
|
+
<h4 className="text-sm font-medium text-muted-foreground mb-1">
|
|
192
|
+
Description
|
|
193
|
+
</h4>
|
|
194
|
+
<p className="text-foreground">{incident.description}</p>
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
|
|
198
|
+
<div className="grid grid-cols-2 gap-4">
|
|
199
|
+
<div>
|
|
200
|
+
<h4 className="text-sm font-medium text-muted-foreground mb-1">
|
|
201
|
+
Started
|
|
202
|
+
</h4>
|
|
203
|
+
<div className="flex items-center gap-2 text-foreground">
|
|
204
|
+
<Calendar className="h-4 w-4" />
|
|
205
|
+
<span>{format(new Date(incident.createdAt), "PPpp")}</span>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
<div>
|
|
209
|
+
<h4 className="text-sm font-medium text-muted-foreground mb-1">
|
|
210
|
+
Duration
|
|
211
|
+
</h4>
|
|
212
|
+
<div className="flex items-center gap-2 text-foreground">
|
|
213
|
+
<Clock className="h-4 w-4" />
|
|
214
|
+
<span>
|
|
215
|
+
{formatDistanceToNow(new Date(incident.createdAt), {
|
|
216
|
+
addSuffix: false,
|
|
217
|
+
})}
|
|
218
|
+
</span>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
<div>
|
|
224
|
+
<h4 className="text-sm font-medium text-muted-foreground mb-2">
|
|
225
|
+
Affected Systems
|
|
226
|
+
</h4>
|
|
227
|
+
<div className="flex flex-wrap gap-2">
|
|
228
|
+
{incident.systemIds.map((systemId) => (
|
|
229
|
+
<Badge key={systemId} variant="outline">
|
|
230
|
+
<Server className="h-3 w-3 mr-1" />
|
|
231
|
+
{getSystemName(systemId)}
|
|
232
|
+
</Badge>
|
|
233
|
+
))}
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</CardContent>
|
|
237
|
+
</Card>
|
|
238
|
+
|
|
239
|
+
{/* Status Updates Timeline */}
|
|
240
|
+
<Card>
|
|
241
|
+
<CardHeader className="border-b border-border">
|
|
242
|
+
<div className="flex items-center justify-between">
|
|
243
|
+
<div className="flex items-center gap-2">
|
|
244
|
+
<MessageSquare className="h-5 w-5 text-muted-foreground" />
|
|
245
|
+
<CardTitle>Status Updates</CardTitle>
|
|
246
|
+
</div>
|
|
247
|
+
{canManage && !showUpdateForm && (
|
|
248
|
+
<Button
|
|
249
|
+
variant="outline"
|
|
250
|
+
size="sm"
|
|
251
|
+
onClick={() => setShowUpdateForm(true)}
|
|
252
|
+
>
|
|
253
|
+
<Plus className="h-4 w-4 mr-1" />
|
|
254
|
+
Add Update
|
|
255
|
+
</Button>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
|
+
</CardHeader>
|
|
259
|
+
<CardContent className="p-6">
|
|
260
|
+
{/* Add Update Form */}
|
|
261
|
+
{showUpdateForm && incidentId && (
|
|
262
|
+
<div className="mb-6">
|
|
263
|
+
<IncidentUpdateForm
|
|
264
|
+
incidentId={incidentId}
|
|
265
|
+
onSuccess={handleUpdateSuccess}
|
|
266
|
+
onCancel={() => setShowUpdateForm(false)}
|
|
267
|
+
/>
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
|
|
271
|
+
<StatusUpdateTimeline
|
|
272
|
+
updates={incident.updates}
|
|
273
|
+
renderStatusBadge={getIncidentStatusBadge}
|
|
274
|
+
emptyTitle="No status updates"
|
|
275
|
+
emptyDescription="No status updates have been posted for this incident."
|
|
276
|
+
/>
|
|
277
|
+
</CardContent>
|
|
278
|
+
</Card>
|
|
279
|
+
</div>
|
|
280
|
+
</PageLayout>
|
|
281
|
+
);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
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 "@checkstack/frontend-api";
|
|
8
|
+
import { useSignal } from "@checkstack/signal-frontend";
|
|
9
|
+
import { resolveRoute } from "@checkstack/common";
|
|
10
|
+
import { incidentApiRef } from "../api";
|
|
11
|
+
import {
|
|
12
|
+
incidentRoutes,
|
|
13
|
+
INCIDENT_UPDATED,
|
|
14
|
+
type IncidentWithSystems,
|
|
15
|
+
type IncidentStatus,
|
|
16
|
+
} from "@checkstack/incident-common";
|
|
17
|
+
import {
|
|
18
|
+
CatalogApi,
|
|
19
|
+
type System,
|
|
20
|
+
catalogRoutes,
|
|
21
|
+
} from "@checkstack/catalog-common";
|
|
22
|
+
import {
|
|
23
|
+
Card,
|
|
24
|
+
CardHeader,
|
|
25
|
+
CardTitle,
|
|
26
|
+
CardContent,
|
|
27
|
+
Badge,
|
|
28
|
+
LoadingSpinner,
|
|
29
|
+
EmptyState,
|
|
30
|
+
BackLink,
|
|
31
|
+
} from "@checkstack/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
|
+
})}?from=${systemId}`}
|
|
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 "@checkstack/ui";
|
|
3
|
+
import type {
|
|
4
|
+
IncidentStatus,
|
|
5
|
+
IncidentSeverity,
|
|
6
|
+
} from "@checkstack/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
|
+
}
|