@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.
Files changed (123) hide show
  1. package/dist/blocks/apm-overview/index.d.ts +58 -0
  2. package/dist/blocks/cicd-builder/index.d.ts +47 -0
  3. package/dist/blocks/cloud-cost-dashboard/index.d.ts +49 -0
  4. package/dist/blocks/container-orchestrator/index.d.ts +63 -0
  5. package/dist/blocks/database-admin/index.d.ts +84 -0
  6. package/dist/blocks/gitops-sync-status/index.d.ts +45 -0
  7. package/dist/blocks/incident-manager/index.d.ts +44 -0
  8. package/dist/blocks/index.d.ts +10 -0
  9. package/dist/blocks/infrastructure-map/index.d.ts +32 -0
  10. package/dist/blocks/on-call-schedule/index.d.ts +43 -0
  11. package/dist/blocks/release-notes/index.d.ts +49 -0
  12. package/dist/components/index.d.ts +34 -0
  13. package/dist/components/waka-ad-banner/index.d.ts +36 -0
  14. package/dist/components/waka-ad-fallback/index.d.ts +33 -0
  15. package/dist/components/waka-ad-inline/index.d.ts +15 -0
  16. package/dist/components/waka-ad-interstitial/index.d.ts +26 -0
  17. package/dist/components/waka-ad-placeholder/index.d.ts +17 -0
  18. package/dist/components/waka-ad-provider/index.d.ts +103 -0
  19. package/dist/components/waka-ad-sidebar/index.d.ts +18 -0
  20. package/dist/components/waka-ad-sticky-footer/index.d.ts +17 -0
  21. package/dist/components/waka-alert-panel/index.d.ts +45 -0
  22. package/dist/components/waka-artifact-list/index.d.ts +32 -0
  23. package/dist/components/waka-build-matrix/index.d.ts +36 -0
  24. package/dist/components/waka-config-comparator/index.d.ts +37 -0
  25. package/dist/components/waka-container-list/index.d.ts +51 -0
  26. package/dist/components/waka-content-recommendation/index.d.ts +23 -0
  27. package/dist/components/waka-database-card/index.d.ts +46 -0
  28. package/dist/components/waka-dependency-tree/index.d.ts +38 -0
  29. package/dist/components/waka-env-var-editor/index.d.ts +30 -0
  30. package/dist/components/waka-feature-flag-row/index.d.ts +45 -0
  31. package/dist/components/waka-kubernetes-overview/index.d.ts +98 -0
  32. package/dist/components/waka-log-viewer/index.d.ts +38 -0
  33. package/dist/components/waka-migration-list/index.d.ts +36 -0
  34. package/dist/components/waka-outstream-video/index.d.ts +24 -0
  35. package/dist/components/waka-pod-card/index.d.ts +73 -0
  36. package/dist/components/waka-query-explain/index.d.ts +48 -0
  37. package/dist/components/waka-secret-card/index.d.ts +43 -0
  38. package/dist/components/waka-security-scan-result/index.d.ts +45 -0
  39. package/dist/components/waka-service-graph/index.d.ts +44 -0
  40. package/dist/components/waka-sponsored-badge/index.d.ts +20 -0
  41. package/dist/components/waka-sponsored-card/index.d.ts +25 -0
  42. package/dist/components/waka-sponsored-feed/index.d.ts +31 -0
  43. package/dist/components/waka-test-report/index.d.ts +60 -0
  44. package/dist/components/waka-trace-viewer/index.d.ts +36 -0
  45. package/dist/components/waka-video-ad/index.d.ts +32 -0
  46. package/dist/components/waka-video-overlay/index.d.ts +26 -0
  47. package/dist/index.cjs.js +251 -200
  48. package/dist/index.d.ts +1 -0
  49. package/dist/index.es.js +47315 -35823
  50. package/dist/utils/security.d.ts +96 -0
  51. package/package.json +4 -4
  52. package/src/blocks/apm-overview/index.tsx +672 -0
  53. package/src/blocks/cicd-builder/index.tsx +738 -0
  54. package/src/blocks/cloud-cost-dashboard/index.tsx +597 -0
  55. package/src/blocks/container-orchestrator/index.tsx +729 -0
  56. package/src/blocks/database-admin/index.tsx +679 -0
  57. package/src/blocks/gitops-sync-status/index.tsx +557 -0
  58. package/src/blocks/incident-manager/index.tsx +586 -0
  59. package/src/blocks/index.ts +119 -0
  60. package/src/blocks/infrastructure-map/index.tsx +638 -0
  61. package/src/blocks/on-call-schedule/index.tsx +615 -0
  62. package/src/blocks/release-notes/index.tsx +643 -0
  63. package/src/blocks/sidebar/index.tsx +6 -6
  64. package/src/components/DataTable/templates/index.tsx +3 -2
  65. package/src/components/index.ts +283 -0
  66. package/src/components/waka-3d-pie-chart/index.tsx +11 -11
  67. package/src/components/waka-achievement-unlock/index.tsx +16 -16
  68. package/src/components/waka-ad-banner/index.tsx +275 -0
  69. package/src/components/waka-ad-fallback/index.tsx +181 -0
  70. package/src/components/waka-ad-inline/index.tsx +103 -0
  71. package/src/components/waka-ad-interstitial/index.tsx +278 -0
  72. package/src/components/waka-ad-placeholder/index.tsx +84 -0
  73. package/src/components/waka-ad-provider/index.tsx +329 -0
  74. package/src/components/waka-ad-sidebar/index.tsx +113 -0
  75. package/src/components/waka-ad-sticky-footer/index.tsx +125 -0
  76. package/src/components/waka-alert-panel/index.tsx +493 -0
  77. package/src/components/waka-artifact-list/index.tsx +416 -0
  78. package/src/components/waka-badge-showcase/index.tsx +12 -11
  79. package/src/components/waka-build-matrix/index.tsx +396 -0
  80. package/src/components/waka-command-bar/index.tsx +2 -1
  81. package/src/components/waka-config-comparator/index.tsx +416 -0
  82. package/src/components/waka-container-list/index.tsx +475 -0
  83. package/src/components/waka-content-recommendation/index.tsx +294 -0
  84. package/src/components/waka-cost-breakdown/index.tsx +10 -10
  85. package/src/components/waka-database-card/index.tsx +473 -0
  86. package/src/components/waka-dependency-tree/index.tsx +542 -0
  87. package/src/components/waka-env-var-editor/index.tsx +417 -0
  88. package/src/components/waka-feature-flag-row/index.tsx +386 -0
  89. package/src/components/waka-funnel-chart/index.tsx +8 -8
  90. package/src/components/waka-health-pulse/index.tsx +6 -6
  91. package/src/components/waka-kubernetes-overview/index.tsx +536 -0
  92. package/src/components/waka-leaderboard/index.tsx +9 -9
  93. package/src/components/waka-log-viewer/index.tsx +386 -0
  94. package/src/components/waka-loot-box/index.tsx +20 -20
  95. package/src/components/waka-migration-list/index.tsx +487 -0
  96. package/src/components/waka-outstream-video/index.tsx +240 -0
  97. package/src/components/waka-player-card/index.tsx +5 -5
  98. package/src/components/waka-pod-card/index.tsx +528 -0
  99. package/src/components/waka-query-explain/index.tsx +657 -0
  100. package/src/components/waka-quota-bar/index.tsx +4 -4
  101. package/src/components/waka-radar-score/index.tsx +10 -10
  102. package/src/components/waka-scratch-card/index.tsx +5 -4
  103. package/src/components/waka-secret-card/index.tsx +371 -0
  104. package/src/components/waka-security-scan-result/index.tsx +473 -0
  105. package/src/components/waka-server-rack/index.tsx +28 -27
  106. package/src/components/waka-service-graph/index.tsx +445 -0
  107. package/src/components/waka-sponsored-badge/index.tsx +97 -0
  108. package/src/components/waka-sponsored-card/index.tsx +275 -0
  109. package/src/components/waka-sponsored-feed/index.tsx +127 -0
  110. package/src/components/waka-spotlight/index.tsx +2 -1
  111. package/src/components/waka-success-explosion/index.tsx +4 -4
  112. package/src/components/waka-test-report/index.tsx +469 -0
  113. package/src/components/waka-trace-viewer/index.tsx +490 -0
  114. package/src/components/waka-video-ad/index.tsx +406 -0
  115. package/src/components/waka-video-overlay/index.tsx +257 -0
  116. package/src/components/waka-xp-bar/index.tsx +13 -13
  117. package/src/styles/base.css +16 -0
  118. package/src/styles/tailwind.preset.js +12 -0
  119. package/src/styles/themes/forest.css +16 -0
  120. package/src/styles/themes/monochrome.css +16 -0
  121. package/src/styles/themes/perpetuity.css +16 -0
  122. package/src/styles/themes/sunset.css +16 -0
  123. 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
+ ]