@wakastellar/ui 2.1.2 → 2.3.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/dist/blocks/apm-overview/index.d.ts +58 -0
- package/dist/blocks/cicd-builder/index.d.ts +47 -0
- package/dist/blocks/cloud-cost-dashboard/index.d.ts +49 -0
- package/dist/blocks/container-orchestrator/index.d.ts +63 -0
- package/dist/blocks/database-admin/index.d.ts +84 -0
- package/dist/blocks/gitops-sync-status/index.d.ts +45 -0
- package/dist/blocks/incident-manager/index.d.ts +44 -0
- package/dist/blocks/index.d.ts +10 -0
- package/dist/blocks/infrastructure-map/index.d.ts +32 -0
- package/dist/blocks/on-call-schedule/index.d.ts +43 -0
- package/dist/blocks/release-notes/index.d.ts +49 -0
- package/dist/components/index.d.ts +34 -0
- package/dist/components/waka-ad-banner/index.d.ts +36 -0
- package/dist/components/waka-ad-fallback/index.d.ts +33 -0
- package/dist/components/waka-ad-inline/index.d.ts +15 -0
- package/dist/components/waka-ad-interstitial/index.d.ts +26 -0
- package/dist/components/waka-ad-placeholder/index.d.ts +17 -0
- package/dist/components/waka-ad-provider/index.d.ts +103 -0
- package/dist/components/waka-ad-sidebar/index.d.ts +18 -0
- package/dist/components/waka-ad-sticky-footer/index.d.ts +17 -0
- package/dist/components/waka-alert-panel/index.d.ts +45 -0
- package/dist/components/waka-artifact-list/index.d.ts +32 -0
- package/dist/components/waka-build-matrix/index.d.ts +36 -0
- package/dist/components/waka-config-comparator/index.d.ts +37 -0
- package/dist/components/waka-container-list/index.d.ts +51 -0
- package/dist/components/waka-content-recommendation/index.d.ts +23 -0
- package/dist/components/waka-database-card/index.d.ts +46 -0
- package/dist/components/waka-dependency-tree/index.d.ts +38 -0
- package/dist/components/waka-env-var-editor/index.d.ts +30 -0
- package/dist/components/waka-feature-flag-row/index.d.ts +45 -0
- package/dist/components/waka-kubernetes-overview/index.d.ts +98 -0
- package/dist/components/waka-log-viewer/index.d.ts +38 -0
- package/dist/components/waka-migration-list/index.d.ts +36 -0
- package/dist/components/waka-outstream-video/index.d.ts +24 -0
- package/dist/components/waka-pod-card/index.d.ts +73 -0
- package/dist/components/waka-query-explain/index.d.ts +48 -0
- package/dist/components/waka-secret-card/index.d.ts +43 -0
- package/dist/components/waka-security-scan-result/index.d.ts +45 -0
- package/dist/components/waka-service-graph/index.d.ts +44 -0
- package/dist/components/waka-sponsored-badge/index.d.ts +20 -0
- package/dist/components/waka-sponsored-card/index.d.ts +25 -0
- package/dist/components/waka-sponsored-feed/index.d.ts +31 -0
- package/dist/components/waka-test-report/index.d.ts +60 -0
- package/dist/components/waka-trace-viewer/index.d.ts +36 -0
- package/dist/components/waka-video-ad/index.d.ts +32 -0
- package/dist/components/waka-video-overlay/index.d.ts +26 -0
- package/dist/index.cjs.js +251 -200
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +47315 -35823
- package/dist/utils/security.d.ts +96 -0
- package/package.json +4 -4
- package/src/blocks/apm-overview/index.tsx +672 -0
- package/src/blocks/cicd-builder/index.tsx +738 -0
- package/src/blocks/cloud-cost-dashboard/index.tsx +597 -0
- package/src/blocks/container-orchestrator/index.tsx +729 -0
- package/src/blocks/database-admin/index.tsx +679 -0
- package/src/blocks/gitops-sync-status/index.tsx +557 -0
- package/src/blocks/incident-manager/index.tsx +586 -0
- package/src/blocks/index.ts +119 -0
- package/src/blocks/infrastructure-map/index.tsx +638 -0
- package/src/blocks/on-call-schedule/index.tsx +615 -0
- package/src/blocks/release-notes/index.tsx +643 -0
- package/src/blocks/sidebar/index.tsx +6 -6
- package/src/components/DataTable/templates/index.tsx +3 -2
- package/src/components/index.ts +283 -0
- package/src/components/waka-3d-pie-chart/index.tsx +11 -11
- package/src/components/waka-achievement-unlock/index.tsx +16 -16
- package/src/components/waka-ad-banner/index.tsx +275 -0
- package/src/components/waka-ad-fallback/index.tsx +181 -0
- package/src/components/waka-ad-inline/index.tsx +103 -0
- package/src/components/waka-ad-interstitial/index.tsx +278 -0
- package/src/components/waka-ad-placeholder/index.tsx +84 -0
- package/src/components/waka-ad-provider/index.tsx +329 -0
- package/src/components/waka-ad-sidebar/index.tsx +113 -0
- package/src/components/waka-ad-sticky-footer/index.tsx +125 -0
- package/src/components/waka-alert-panel/index.tsx +493 -0
- package/src/components/waka-artifact-list/index.tsx +416 -0
- package/src/components/waka-badge-showcase/index.tsx +12 -11
- package/src/components/waka-build-matrix/index.tsx +396 -0
- package/src/components/waka-command-bar/index.tsx +2 -1
- package/src/components/waka-config-comparator/index.tsx +416 -0
- package/src/components/waka-container-list/index.tsx +475 -0
- package/src/components/waka-content-recommendation/index.tsx +294 -0
- package/src/components/waka-cost-breakdown/index.tsx +10 -10
- package/src/components/waka-database-card/index.tsx +473 -0
- package/src/components/waka-dependency-tree/index.tsx +542 -0
- package/src/components/waka-env-var-editor/index.tsx +417 -0
- package/src/components/waka-feature-flag-row/index.tsx +386 -0
- package/src/components/waka-funnel-chart/index.tsx +8 -8
- package/src/components/waka-health-pulse/index.tsx +6 -6
- package/src/components/waka-kubernetes-overview/index.tsx +536 -0
- package/src/components/waka-leaderboard/index.tsx +9 -9
- package/src/components/waka-log-viewer/index.tsx +386 -0
- package/src/components/waka-loot-box/index.tsx +20 -20
- package/src/components/waka-migration-list/index.tsx +487 -0
- package/src/components/waka-outstream-video/index.tsx +240 -0
- package/src/components/waka-player-card/index.tsx +5 -5
- package/src/components/waka-pod-card/index.tsx +528 -0
- package/src/components/waka-query-explain/index.tsx +657 -0
- package/src/components/waka-quota-bar/index.tsx +4 -4
- package/src/components/waka-radar-score/index.tsx +10 -10
- package/src/components/waka-scratch-card/index.tsx +5 -4
- package/src/components/waka-secret-card/index.tsx +371 -0
- package/src/components/waka-security-scan-result/index.tsx +473 -0
- package/src/components/waka-server-rack/index.tsx +28 -27
- package/src/components/waka-service-graph/index.tsx +445 -0
- package/src/components/waka-sponsored-badge/index.tsx +97 -0
- package/src/components/waka-sponsored-card/index.tsx +275 -0
- package/src/components/waka-sponsored-feed/index.tsx +127 -0
- package/src/components/waka-spotlight/index.tsx +2 -1
- package/src/components/waka-success-explosion/index.tsx +4 -4
- package/src/components/waka-test-report/index.tsx +469 -0
- package/src/components/waka-trace-viewer/index.tsx +490 -0
- package/src/components/waka-video-ad/index.tsx +406 -0
- package/src/components/waka-video-overlay/index.tsx +257 -0
- package/src/components/waka-xp-bar/index.tsx +13 -13
- package/src/styles/base.css +16 -0
- package/src/styles/tailwind.preset.js +12 -0
- package/src/styles/themes/forest.css +16 -0
- package/src/styles/themes/monochrome.css +16 -0
- package/src/styles/themes/perpetuity.css +16 -0
- package/src/styles/themes/sunset.css +16 -0
- package/src/styles/themes/twilight.css +16 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../utils/cn"
|
|
5
|
+
import { Badge } from "../../components/badge"
|
|
6
|
+
import { Button } from "../../components/button"
|
|
7
|
+
import { Card, CardContent, CardHeader, CardTitle } from "../../components/card"
|
|
8
|
+
import { Input } from "../../components/input"
|
|
9
|
+
import { Avatar, AvatarFallback, AvatarImage } from "../../components/avatar"
|
|
10
|
+
import { ScrollArea } from "../../components/scroll-area"
|
|
11
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../components/tabs"
|
|
12
|
+
import { Progress } from "../../components/progress"
|
|
13
|
+
import {
|
|
14
|
+
Select,
|
|
15
|
+
SelectContent,
|
|
16
|
+
SelectItem,
|
|
17
|
+
SelectTrigger,
|
|
18
|
+
SelectValue,
|
|
19
|
+
} from "../../components/select"
|
|
20
|
+
import {
|
|
21
|
+
AlertTriangle,
|
|
22
|
+
AlertCircle,
|
|
23
|
+
Bell,
|
|
24
|
+
CheckCircle2,
|
|
25
|
+
Clock,
|
|
26
|
+
MessageSquare,
|
|
27
|
+
Phone,
|
|
28
|
+
Plus,
|
|
29
|
+
Search,
|
|
30
|
+
Shield,
|
|
31
|
+
User,
|
|
32
|
+
Users,
|
|
33
|
+
Activity,
|
|
34
|
+
TrendingUp,
|
|
35
|
+
TrendingDown,
|
|
36
|
+
ExternalLink,
|
|
37
|
+
MoreVertical,
|
|
38
|
+
Play,
|
|
39
|
+
Pause,
|
|
40
|
+
XCircle,
|
|
41
|
+
} from "lucide-react"
|
|
42
|
+
|
|
43
|
+
export type IncidentSeverity = "critical" | "high" | "medium" | "low"
|
|
44
|
+
export type IncidentStatus = "triggered" | "acknowledged" | "investigating" | "identified" | "resolved" | "closed"
|
|
45
|
+
|
|
46
|
+
export interface IncidentResponder {
|
|
47
|
+
id: string
|
|
48
|
+
name: string
|
|
49
|
+
avatar?: string
|
|
50
|
+
role: string
|
|
51
|
+
status: "available" | "engaged" | "offline"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface IncidentEvent {
|
|
55
|
+
id: string
|
|
56
|
+
type: "status_change" | "comment" | "assignment" | "escalation" | "runbook"
|
|
57
|
+
timestamp: Date
|
|
58
|
+
user?: string
|
|
59
|
+
content: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface Incident {
|
|
63
|
+
id: string
|
|
64
|
+
title: string
|
|
65
|
+
description?: string
|
|
66
|
+
severity: IncidentSeverity
|
|
67
|
+
status: IncidentStatus
|
|
68
|
+
service?: string
|
|
69
|
+
createdAt: Date
|
|
70
|
+
acknowledgedAt?: Date
|
|
71
|
+
resolvedAt?: Date
|
|
72
|
+
responders: IncidentResponder[]
|
|
73
|
+
events: IncidentEvent[]
|
|
74
|
+
impact?: string
|
|
75
|
+
affectedUsers?: number
|
|
76
|
+
slackChannel?: string
|
|
77
|
+
runbookUrl?: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface IncidentManagerProps {
|
|
81
|
+
incidents: Incident[]
|
|
82
|
+
onCreateIncident?: () => void
|
|
83
|
+
onAcknowledge?: (incident: Incident) => void
|
|
84
|
+
onResolve?: (incident: Incident) => void
|
|
85
|
+
onEscalate?: (incident: Incident) => void
|
|
86
|
+
onAssign?: (incident: Incident, responder: IncidentResponder) => void
|
|
87
|
+
className?: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const severityConfig: Record<IncidentSeverity, { label: string; color: string; bgColor: string }> = {
|
|
91
|
+
critical: { label: "Critical", color: "text-red-500", bgColor: "bg-red-500" },
|
|
92
|
+
high: { label: "High", color: "text-orange-500", bgColor: "bg-orange-500" },
|
|
93
|
+
medium: { label: "Medium", color: "text-yellow-500", bgColor: "bg-yellow-500" },
|
|
94
|
+
low: { label: "Low", color: "text-blue-500", bgColor: "bg-blue-500" },
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const statusConfig: Record<IncidentStatus, { label: string; color: string; icon: React.ElementType }> = {
|
|
98
|
+
triggered: { label: "Triggered", color: "text-red-500", icon: AlertCircle },
|
|
99
|
+
acknowledged: { label: "Acknowledged", color: "text-orange-500", icon: Bell },
|
|
100
|
+
investigating: { label: "Investigating", color: "text-yellow-500", icon: Search },
|
|
101
|
+
identified: { label: "Identified", color: "text-blue-500", icon: Activity },
|
|
102
|
+
resolved: { label: "Resolved", color: "text-green-500", icon: CheckCircle2 },
|
|
103
|
+
closed: { label: "Closed", color: "text-gray-500", icon: XCircle },
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function formatDuration(start: Date, end?: Date): string {
|
|
107
|
+
const diff = (end || new Date()).getTime() - start.getTime()
|
|
108
|
+
const minutes = Math.floor(diff / 60000)
|
|
109
|
+
const hours = Math.floor(minutes / 60)
|
|
110
|
+
const days = Math.floor(hours / 24)
|
|
111
|
+
|
|
112
|
+
if (days > 0) return `${days}d ${hours % 24}h`
|
|
113
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`
|
|
114
|
+
return `${minutes}m`
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatTime(date: Date): string {
|
|
118
|
+
return date.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" })
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function IncidentCard({
|
|
122
|
+
incident,
|
|
123
|
+
onAcknowledge,
|
|
124
|
+
onResolve,
|
|
125
|
+
onSelect,
|
|
126
|
+
}: {
|
|
127
|
+
incident: Incident
|
|
128
|
+
onAcknowledge?: () => void
|
|
129
|
+
onResolve?: () => void
|
|
130
|
+
onSelect?: () => void
|
|
131
|
+
}) {
|
|
132
|
+
const sevConf = severityConfig[incident.severity]
|
|
133
|
+
const statConf = statusConfig[incident.status]
|
|
134
|
+
const StatusIcon = statConf.icon
|
|
135
|
+
const isActive = !["resolved", "closed"].includes(incident.status)
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<Card
|
|
139
|
+
className={cn(
|
|
140
|
+
"cursor-pointer transition-all hover:shadow-md",
|
|
141
|
+
incident.severity === "critical" && isActive && "border-red-500/50 bg-red-500/5",
|
|
142
|
+
incident.severity === "high" && isActive && "border-orange-500/30"
|
|
143
|
+
)}
|
|
144
|
+
onClick={onSelect}
|
|
145
|
+
>
|
|
146
|
+
<CardContent className="p-4">
|
|
147
|
+
<div className="flex items-start gap-3">
|
|
148
|
+
<div className={cn("p-2 rounded-lg shrink-0", `${sevConf.bgColor}/10`)}>
|
|
149
|
+
<AlertTriangle className={cn("h-5 w-5", sevConf.color)} />
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div className="flex-1 min-w-0">
|
|
153
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
154
|
+
<Badge className={cn("text-white text-xs", sevConf.bgColor)}>
|
|
155
|
+
{sevConf.label}
|
|
156
|
+
</Badge>
|
|
157
|
+
<Badge variant="outline" className={cn("text-xs", statConf.color)}>
|
|
158
|
+
<StatusIcon className="h-3 w-3 mr-1" />
|
|
159
|
+
{statConf.label}
|
|
160
|
+
</Badge>
|
|
161
|
+
{incident.service && (
|
|
162
|
+
<Badge variant="secondary" className="text-xs">
|
|
163
|
+
{incident.service}
|
|
164
|
+
</Badge>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<h4 className="font-semibold mt-2 truncate">{incident.title}</h4>
|
|
169
|
+
|
|
170
|
+
{incident.description && (
|
|
171
|
+
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">
|
|
172
|
+
{incident.description}
|
|
173
|
+
</p>
|
|
174
|
+
)}
|
|
175
|
+
|
|
176
|
+
<div className="flex items-center gap-4 mt-3 text-xs text-muted-foreground">
|
|
177
|
+
<span className="flex items-center gap-1">
|
|
178
|
+
<Clock className="h-3 w-3" />
|
|
179
|
+
{formatDuration(incident.createdAt, incident.resolvedAt)}
|
|
180
|
+
</span>
|
|
181
|
+
{incident.affectedUsers !== undefined && (
|
|
182
|
+
<span className="flex items-center gap-1">
|
|
183
|
+
<Users className="h-3 w-3" />
|
|
184
|
+
{incident.affectedUsers} affected
|
|
185
|
+
</span>
|
|
186
|
+
)}
|
|
187
|
+
<span className="flex items-center gap-1">
|
|
188
|
+
<MessageSquare className="h-3 w-3" />
|
|
189
|
+
{incident.events.length}
|
|
190
|
+
</span>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
{/* Responders */}
|
|
194
|
+
{incident.responders.length > 0 && (
|
|
195
|
+
<div className="flex items-center gap-2 mt-3">
|
|
196
|
+
<div className="flex -space-x-2">
|
|
197
|
+
{incident.responders.slice(0, 3).map((responder) => (
|
|
198
|
+
<Avatar key={responder.id} className="h-6 w-6 border-2 border-background">
|
|
199
|
+
<AvatarImage src={responder.avatar} />
|
|
200
|
+
<AvatarFallback className="text-xs">
|
|
201
|
+
{responder.name.slice(0, 2).toUpperCase()}
|
|
202
|
+
</AvatarFallback>
|
|
203
|
+
</Avatar>
|
|
204
|
+
))}
|
|
205
|
+
{incident.responders.length > 3 && (
|
|
206
|
+
<div className="h-6 w-6 rounded-full bg-muted flex items-center justify-center text-xs border-2 border-background">
|
|
207
|
+
+{incident.responders.length - 3}
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
)}
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{/* Quick actions */}
|
|
216
|
+
{isActive && (
|
|
217
|
+
<div className="flex flex-col gap-1 shrink-0">
|
|
218
|
+
{incident.status === "triggered" && onAcknowledge && (
|
|
219
|
+
<Button
|
|
220
|
+
size="sm"
|
|
221
|
+
variant="outline"
|
|
222
|
+
className="h-7 text-xs"
|
|
223
|
+
onClick={(e) => {
|
|
224
|
+
e.stopPropagation()
|
|
225
|
+
onAcknowledge()
|
|
226
|
+
}}
|
|
227
|
+
>
|
|
228
|
+
Ack
|
|
229
|
+
</Button>
|
|
230
|
+
)}
|
|
231
|
+
{incident.status !== "triggered" && incident.status !== "resolved" && onResolve && (
|
|
232
|
+
<Button
|
|
233
|
+
size="sm"
|
|
234
|
+
variant="outline"
|
|
235
|
+
className="h-7 text-xs text-green-500"
|
|
236
|
+
onClick={(e) => {
|
|
237
|
+
e.stopPropagation()
|
|
238
|
+
onResolve()
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
Resolve
|
|
242
|
+
</Button>
|
|
243
|
+
)}
|
|
244
|
+
</div>
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
</CardContent>
|
|
248
|
+
</Card>
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function IncidentStats({ incidents }: { incidents: Incident[] }) {
|
|
253
|
+
const active = incidents.filter((i) => !["resolved", "closed"].includes(i.status))
|
|
254
|
+
const critical = active.filter((i) => i.severity === "critical")
|
|
255
|
+
const mttr = React.useMemo(() => {
|
|
256
|
+
const resolved = incidents.filter((i) => i.resolvedAt)
|
|
257
|
+
if (resolved.length === 0) return 0
|
|
258
|
+
const total = resolved.reduce((acc, i) => {
|
|
259
|
+
return acc + (i.resolvedAt!.getTime() - i.createdAt.getTime())
|
|
260
|
+
}, 0)
|
|
261
|
+
return Math.round(total / resolved.length / 60000) // in minutes
|
|
262
|
+
}, [incidents])
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<div className="grid grid-cols-4 gap-4">
|
|
266
|
+
<Card>
|
|
267
|
+
<CardContent className="p-4 text-center">
|
|
268
|
+
<div className="text-3xl font-bold text-red-500">{active.length}</div>
|
|
269
|
+
<div className="text-sm text-muted-foreground">Active Incidents</div>
|
|
270
|
+
</CardContent>
|
|
271
|
+
</Card>
|
|
272
|
+
<Card>
|
|
273
|
+
<CardContent className="p-4 text-center">
|
|
274
|
+
<div className="text-3xl font-bold text-orange-500">{critical.length}</div>
|
|
275
|
+
<div className="text-sm text-muted-foreground">Critical</div>
|
|
276
|
+
</CardContent>
|
|
277
|
+
</Card>
|
|
278
|
+
<Card>
|
|
279
|
+
<CardContent className="p-4 text-center">
|
|
280
|
+
<div className="text-3xl font-bold">{mttr}m</div>
|
|
281
|
+
<div className="text-sm text-muted-foreground">Avg MTTR</div>
|
|
282
|
+
</CardContent>
|
|
283
|
+
</Card>
|
|
284
|
+
<Card>
|
|
285
|
+
<CardContent className="p-4 text-center">
|
|
286
|
+
<div className="text-3xl font-bold text-green-500">
|
|
287
|
+
{incidents.filter((i) => i.status === "resolved").length}
|
|
288
|
+
</div>
|
|
289
|
+
<div className="text-sm text-muted-foreground">Resolved Today</div>
|
|
290
|
+
</CardContent>
|
|
291
|
+
</Card>
|
|
292
|
+
</div>
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function IncidentManager({
|
|
297
|
+
incidents,
|
|
298
|
+
onCreateIncident,
|
|
299
|
+
onAcknowledge,
|
|
300
|
+
onResolve,
|
|
301
|
+
onEscalate,
|
|
302
|
+
onAssign,
|
|
303
|
+
className,
|
|
304
|
+
}: IncidentManagerProps) {
|
|
305
|
+
const [searchQuery, setSearchQuery] = React.useState("")
|
|
306
|
+
const [severityFilter, setSeverityFilter] = React.useState<IncidentSeverity | "all">("all")
|
|
307
|
+
const [statusFilter, setStatusFilter] = React.useState<"active" | "resolved" | "all">("active")
|
|
308
|
+
const [selectedIncident, setSelectedIncident] = React.useState<Incident | null>(null)
|
|
309
|
+
|
|
310
|
+
// Filter incidents
|
|
311
|
+
const filteredIncidents = React.useMemo(() => {
|
|
312
|
+
return incidents
|
|
313
|
+
.filter((i) => {
|
|
314
|
+
if (searchQuery && !i.title.toLowerCase().includes(searchQuery.toLowerCase())) return false
|
|
315
|
+
if (severityFilter !== "all" && i.severity !== severityFilter) return false
|
|
316
|
+
if (statusFilter === "active" && ["resolved", "closed"].includes(i.status)) return false
|
|
317
|
+
if (statusFilter === "resolved" && !["resolved", "closed"].includes(i.status)) return false
|
|
318
|
+
return true
|
|
319
|
+
})
|
|
320
|
+
.sort((a, b) => {
|
|
321
|
+
// Sort by severity first, then by date
|
|
322
|
+
const sevOrder = { critical: 0, high: 1, medium: 2, low: 3 }
|
|
323
|
+
const sevDiff = sevOrder[a.severity] - sevOrder[b.severity]
|
|
324
|
+
if (sevDiff !== 0) return sevDiff
|
|
325
|
+
return b.createdAt.getTime() - a.createdAt.getTime()
|
|
326
|
+
})
|
|
327
|
+
}, [incidents, searchQuery, severityFilter, statusFilter])
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
<div className={cn("flex flex-col gap-6", className)}>
|
|
331
|
+
{/* Stats */}
|
|
332
|
+
<IncidentStats incidents={incidents} />
|
|
333
|
+
|
|
334
|
+
{/* Main content */}
|
|
335
|
+
<div className="flex gap-6">
|
|
336
|
+
{/* Incidents list */}
|
|
337
|
+
<div className="flex-1 space-y-4">
|
|
338
|
+
{/* Toolbar */}
|
|
339
|
+
<div className="flex items-center gap-3">
|
|
340
|
+
<div className="relative flex-1 max-w-sm">
|
|
341
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
342
|
+
<Input
|
|
343
|
+
placeholder="Search incidents..."
|
|
344
|
+
value={searchQuery}
|
|
345
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
346
|
+
className="pl-9"
|
|
347
|
+
/>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
<Select value={severityFilter} onValueChange={(v) => setSeverityFilter(v as IncidentSeverity | "all")}>
|
|
351
|
+
<SelectTrigger className="w-32">
|
|
352
|
+
<SelectValue placeholder="Severity" />
|
|
353
|
+
</SelectTrigger>
|
|
354
|
+
<SelectContent>
|
|
355
|
+
<SelectItem value="all">All Severity</SelectItem>
|
|
356
|
+
<SelectItem value="critical">Critical</SelectItem>
|
|
357
|
+
<SelectItem value="high">High</SelectItem>
|
|
358
|
+
<SelectItem value="medium">Medium</SelectItem>
|
|
359
|
+
<SelectItem value="low">Low</SelectItem>
|
|
360
|
+
</SelectContent>
|
|
361
|
+
</Select>
|
|
362
|
+
|
|
363
|
+
<Select value={statusFilter} onValueChange={(v) => setStatusFilter(v as "active" | "resolved" | "all")}>
|
|
364
|
+
<SelectTrigger className="w-32">
|
|
365
|
+
<SelectValue placeholder="Status" />
|
|
366
|
+
</SelectTrigger>
|
|
367
|
+
<SelectContent>
|
|
368
|
+
<SelectItem value="active">Active</SelectItem>
|
|
369
|
+
<SelectItem value="resolved">Resolved</SelectItem>
|
|
370
|
+
<SelectItem value="all">All</SelectItem>
|
|
371
|
+
</SelectContent>
|
|
372
|
+
</Select>
|
|
373
|
+
|
|
374
|
+
{onCreateIncident && (
|
|
375
|
+
<Button onClick={onCreateIncident}>
|
|
376
|
+
<Plus className="h-4 w-4 mr-1" />
|
|
377
|
+
Create Incident
|
|
378
|
+
</Button>
|
|
379
|
+
)}
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
{/* Incidents */}
|
|
383
|
+
<ScrollArea className="h-[600px]">
|
|
384
|
+
<div className="space-y-3 pr-4">
|
|
385
|
+
{filteredIncidents.length === 0 ? (
|
|
386
|
+
<div className="flex flex-col items-center justify-center h-48 text-muted-foreground">
|
|
387
|
+
<Shield className="h-12 w-12 mb-4 text-green-500" />
|
|
388
|
+
<p className="text-lg font-medium">No incidents</p>
|
|
389
|
+
<p className="text-sm">All systems are operational</p>
|
|
390
|
+
</div>
|
|
391
|
+
) : (
|
|
392
|
+
filteredIncidents.map((incident) => (
|
|
393
|
+
<IncidentCard
|
|
394
|
+
key={incident.id}
|
|
395
|
+
incident={incident}
|
|
396
|
+
onAcknowledge={onAcknowledge ? () => onAcknowledge(incident) : undefined}
|
|
397
|
+
onResolve={onResolve ? () => onResolve(incident) : undefined}
|
|
398
|
+
onSelect={() => setSelectedIncident(incident)}
|
|
399
|
+
/>
|
|
400
|
+
))
|
|
401
|
+
)}
|
|
402
|
+
</div>
|
|
403
|
+
</ScrollArea>
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
{/* Incident detail panel */}
|
|
407
|
+
{selectedIncident && (
|
|
408
|
+
<Card className="w-96 shrink-0">
|
|
409
|
+
<CardHeader className="pb-3">
|
|
410
|
+
<div className="flex items-center justify-between">
|
|
411
|
+
<Badge className={cn("text-white", severityConfig[selectedIncident.severity].bgColor)}>
|
|
412
|
+
{severityConfig[selectedIncident.severity].label}
|
|
413
|
+
</Badge>
|
|
414
|
+
<Button variant="ghost" size="sm" onClick={() => setSelectedIncident(null)}>
|
|
415
|
+
<XCircle className="h-4 w-4" />
|
|
416
|
+
</Button>
|
|
417
|
+
</div>
|
|
418
|
+
<CardTitle className="mt-2">{selectedIncident.title}</CardTitle>
|
|
419
|
+
<div className="flex items-center gap-2 mt-2">
|
|
420
|
+
<Badge variant="outline" className={statusConfig[selectedIncident.status].color}>
|
|
421
|
+
{statusConfig[selectedIncident.status].label}
|
|
422
|
+
</Badge>
|
|
423
|
+
{selectedIncident.service && (
|
|
424
|
+
<Badge variant="secondary">{selectedIncident.service}</Badge>
|
|
425
|
+
)}
|
|
426
|
+
</div>
|
|
427
|
+
</CardHeader>
|
|
428
|
+
<CardContent className="space-y-4">
|
|
429
|
+
{selectedIncident.description && (
|
|
430
|
+
<p className="text-sm text-muted-foreground">{selectedIncident.description}</p>
|
|
431
|
+
)}
|
|
432
|
+
|
|
433
|
+
{/* Impact */}
|
|
434
|
+
{selectedIncident.impact && (
|
|
435
|
+
<div>
|
|
436
|
+
<div className="text-xs font-medium text-muted-foreground mb-1">Impact</div>
|
|
437
|
+
<p className="text-sm">{selectedIncident.impact}</p>
|
|
438
|
+
</div>
|
|
439
|
+
)}
|
|
440
|
+
|
|
441
|
+
{/* Timeline */}
|
|
442
|
+
<div>
|
|
443
|
+
<div className="text-xs font-medium text-muted-foreground mb-2">Timeline</div>
|
|
444
|
+
<div className="space-y-2">
|
|
445
|
+
{selectedIncident.events.slice(0, 5).map((event) => (
|
|
446
|
+
<div key={event.id} className="flex items-start gap-2 text-xs">
|
|
447
|
+
<span className="text-muted-foreground shrink-0">
|
|
448
|
+
{formatTime(event.timestamp)}
|
|
449
|
+
</span>
|
|
450
|
+
<span>{event.content}</span>
|
|
451
|
+
</div>
|
|
452
|
+
))}
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
|
|
456
|
+
{/* Responders */}
|
|
457
|
+
<div>
|
|
458
|
+
<div className="text-xs font-medium text-muted-foreground mb-2">Responders</div>
|
|
459
|
+
<div className="space-y-2">
|
|
460
|
+
{selectedIncident.responders.map((responder) => (
|
|
461
|
+
<div key={responder.id} className="flex items-center gap-2">
|
|
462
|
+
<Avatar className="h-6 w-6">
|
|
463
|
+
<AvatarImage src={responder.avatar} />
|
|
464
|
+
<AvatarFallback className="text-xs">
|
|
465
|
+
{responder.name.slice(0, 2).toUpperCase()}
|
|
466
|
+
</AvatarFallback>
|
|
467
|
+
</Avatar>
|
|
468
|
+
<span className="text-sm">{responder.name}</span>
|
|
469
|
+
<Badge variant="outline" className="text-xs ml-auto">
|
|
470
|
+
{responder.role}
|
|
471
|
+
</Badge>
|
|
472
|
+
</div>
|
|
473
|
+
))}
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
|
|
477
|
+
{/* Actions */}
|
|
478
|
+
<div className="flex flex-col gap-2 pt-2 border-t">
|
|
479
|
+
{selectedIncident.slackChannel && (
|
|
480
|
+
<Button variant="outline" size="sm" className="justify-start">
|
|
481
|
+
<MessageSquare className="h-4 w-4 mr-2" />
|
|
482
|
+
Join #{selectedIncident.slackChannel}
|
|
483
|
+
</Button>
|
|
484
|
+
)}
|
|
485
|
+
{selectedIncident.runbookUrl && (
|
|
486
|
+
<Button variant="outline" size="sm" className="justify-start">
|
|
487
|
+
<ExternalLink className="h-4 w-4 mr-2" />
|
|
488
|
+
View Runbook
|
|
489
|
+
</Button>
|
|
490
|
+
)}
|
|
491
|
+
{onEscalate && !["resolved", "closed"].includes(selectedIncident.status) && (
|
|
492
|
+
<Button
|
|
493
|
+
variant="outline"
|
|
494
|
+
size="sm"
|
|
495
|
+
className="justify-start text-orange-500"
|
|
496
|
+
onClick={() => onEscalate(selectedIncident)}
|
|
497
|
+
>
|
|
498
|
+
<Phone className="h-4 w-4 mr-2" />
|
|
499
|
+
Escalate
|
|
500
|
+
</Button>
|
|
501
|
+
)}
|
|
502
|
+
</div>
|
|
503
|
+
</CardContent>
|
|
504
|
+
</Card>
|
|
505
|
+
)}
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Default sample data
|
|
512
|
+
export const defaultIncidents: Incident[] = [
|
|
513
|
+
{
|
|
514
|
+
id: "1",
|
|
515
|
+
title: "Database connection pool exhausted",
|
|
516
|
+
description: "Production database is experiencing connection pool exhaustion, causing request timeouts",
|
|
517
|
+
severity: "critical",
|
|
518
|
+
status: "investigating",
|
|
519
|
+
service: "api-gateway",
|
|
520
|
+
createdAt: new Date(Date.now() - 45 * 60000),
|
|
521
|
+
acknowledgedAt: new Date(Date.now() - 40 * 60000),
|
|
522
|
+
impact: "50% of API requests failing with 504 errors",
|
|
523
|
+
affectedUsers: 12500,
|
|
524
|
+
slackChannel: "incident-db-001",
|
|
525
|
+
runbookUrl: "https://runbooks.example.com/db-pool",
|
|
526
|
+
responders: [
|
|
527
|
+
{ id: "1", name: "John Doe", role: "Incident Commander", status: "engaged" },
|
|
528
|
+
{ id: "2", name: "Jane Smith", role: "DBA", status: "engaged" },
|
|
529
|
+
],
|
|
530
|
+
events: [
|
|
531
|
+
{ id: "e1", type: "status_change", timestamp: new Date(Date.now() - 45 * 60000), content: "Incident triggered by alerting system" },
|
|
532
|
+
{ id: "e2", type: "assignment", timestamp: new Date(Date.now() - 43 * 60000), content: "John Doe assigned as IC", user: "System" },
|
|
533
|
+
{ id: "e3", type: "status_change", timestamp: new Date(Date.now() - 40 * 60000), content: "Incident acknowledged", user: "John Doe" },
|
|
534
|
+
{ id: "e4", type: "comment", timestamp: new Date(Date.now() - 35 * 60000), content: "Investigating connection pool settings", user: "Jane Smith" },
|
|
535
|
+
{ id: "e5", type: "status_change", timestamp: new Date(Date.now() - 30 * 60000), content: "Status changed to investigating", user: "John Doe" },
|
|
536
|
+
],
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
id: "2",
|
|
540
|
+
title: "High latency on payment service",
|
|
541
|
+
description: "Payment processing latency increased to >2s, above SLA threshold",
|
|
542
|
+
severity: "high",
|
|
543
|
+
status: "acknowledged",
|
|
544
|
+
service: "payment-service",
|
|
545
|
+
createdAt: new Date(Date.now() - 20 * 60000),
|
|
546
|
+
acknowledgedAt: new Date(Date.now() - 15 * 60000),
|
|
547
|
+
affectedUsers: 3200,
|
|
548
|
+
responders: [
|
|
549
|
+
{ id: "3", name: "Alice Johnson", role: "On-Call Engineer", status: "engaged" },
|
|
550
|
+
],
|
|
551
|
+
events: [
|
|
552
|
+
{ id: "e6", type: "status_change", timestamp: new Date(Date.now() - 20 * 60000), content: "Incident triggered" },
|
|
553
|
+
{ id: "e7", type: "status_change", timestamp: new Date(Date.now() - 15 * 60000), content: "Acknowledged by Alice Johnson" },
|
|
554
|
+
],
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
id: "3",
|
|
558
|
+
title: "Elevated error rate on search API",
|
|
559
|
+
severity: "medium",
|
|
560
|
+
status: "triggered",
|
|
561
|
+
service: "search-service",
|
|
562
|
+
createdAt: new Date(Date.now() - 5 * 60000),
|
|
563
|
+
responders: [],
|
|
564
|
+
events: [
|
|
565
|
+
{ id: "e8", type: "status_change", timestamp: new Date(Date.now() - 5 * 60000), content: "Incident triggered by alerting system" },
|
|
566
|
+
],
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
id: "4",
|
|
570
|
+
title: "Memory leak in recommendation engine",
|
|
571
|
+
description: "Gradual memory increase detected, auto-scaling keeping up but needs investigation",
|
|
572
|
+
severity: "low",
|
|
573
|
+
status: "resolved",
|
|
574
|
+
service: "recommendation-engine",
|
|
575
|
+
createdAt: new Date(Date.now() - 4 * 3600000),
|
|
576
|
+
acknowledgedAt: new Date(Date.now() - 3.5 * 3600000),
|
|
577
|
+
resolvedAt: new Date(Date.now() - 2 * 3600000),
|
|
578
|
+
responders: [
|
|
579
|
+
{ id: "4", name: "Bob Wilson", role: "Backend Engineer", status: "available" },
|
|
580
|
+
],
|
|
581
|
+
events: [
|
|
582
|
+
{ id: "e9", type: "status_change", timestamp: new Date(Date.now() - 4 * 3600000), content: "Incident triggered" },
|
|
583
|
+
{ id: "e10", type: "status_change", timestamp: new Date(Date.now() - 2 * 3600000), content: "Resolved - deployed fix v2.3.1" },
|
|
584
|
+
],
|
|
585
|
+
},
|
|
586
|
+
]
|