@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,493 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../utils/cn"
|
|
5
|
+
import { Button } from "../button"
|
|
6
|
+
import { Badge } from "../badge"
|
|
7
|
+
import { ScrollArea } from "../scroll-area"
|
|
8
|
+
import {
|
|
9
|
+
Select,
|
|
10
|
+
SelectContent,
|
|
11
|
+
SelectItem,
|
|
12
|
+
SelectTrigger,
|
|
13
|
+
SelectValue,
|
|
14
|
+
} from "../select"
|
|
15
|
+
import {
|
|
16
|
+
DropdownMenu,
|
|
17
|
+
DropdownMenuContent,
|
|
18
|
+
DropdownMenuItem,
|
|
19
|
+
DropdownMenuSeparator,
|
|
20
|
+
DropdownMenuTrigger,
|
|
21
|
+
} from "../dropdown-menu"
|
|
22
|
+
import {
|
|
23
|
+
AlertCircle,
|
|
24
|
+
AlertTriangle,
|
|
25
|
+
Info,
|
|
26
|
+
Bell,
|
|
27
|
+
BellOff,
|
|
28
|
+
Check,
|
|
29
|
+
Clock,
|
|
30
|
+
MoreVertical,
|
|
31
|
+
ExternalLink,
|
|
32
|
+
Eye,
|
|
33
|
+
EyeOff,
|
|
34
|
+
Filter,
|
|
35
|
+
RefreshCw,
|
|
36
|
+
Volume2,
|
|
37
|
+
VolumeX,
|
|
38
|
+
} from "lucide-react"
|
|
39
|
+
|
|
40
|
+
export type AlertSeverity = "critical" | "warning" | "info"
|
|
41
|
+
export type AlertStatus = "firing" | "acknowledged" | "resolved" | "silenced"
|
|
42
|
+
|
|
43
|
+
export interface Alert {
|
|
44
|
+
id: string
|
|
45
|
+
title: string
|
|
46
|
+
description?: string
|
|
47
|
+
severity: AlertSeverity
|
|
48
|
+
status: AlertStatus
|
|
49
|
+
source: string
|
|
50
|
+
startedAt: Date
|
|
51
|
+
endedAt?: Date
|
|
52
|
+
acknowledgedAt?: Date
|
|
53
|
+
acknowledgedBy?: string
|
|
54
|
+
silencedUntil?: Date
|
|
55
|
+
labels?: Record<string, string>
|
|
56
|
+
annotations?: Record<string, string>
|
|
57
|
+
fingerprint?: string
|
|
58
|
+
runbookUrl?: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface WakaAlertPanelProps {
|
|
62
|
+
/** List of alerts */
|
|
63
|
+
alerts: Alert[]
|
|
64
|
+
/** Callback when acknowledging an alert */
|
|
65
|
+
onAcknowledge?: (alertId: string) => void
|
|
66
|
+
/** Callback when silencing an alert */
|
|
67
|
+
onSilence?: (alertId: string, duration: number) => void
|
|
68
|
+
/** Callback when resolving an alert */
|
|
69
|
+
onResolve?: (alertId: string) => void
|
|
70
|
+
/** Callback when viewing alert details */
|
|
71
|
+
onViewDetails?: (alert: Alert) => void
|
|
72
|
+
/** Callback when clicking runbook link */
|
|
73
|
+
onRunbook?: (alert: Alert) => void
|
|
74
|
+
/** Callback when refreshing alerts */
|
|
75
|
+
onRefresh?: () => void
|
|
76
|
+
/** Whether alerts are loading */
|
|
77
|
+
isLoading?: boolean
|
|
78
|
+
/** Show resolved alerts */
|
|
79
|
+
showResolved?: boolean
|
|
80
|
+
/** Custom class name */
|
|
81
|
+
className?: string
|
|
82
|
+
/** Title */
|
|
83
|
+
title?: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const severityConfig: Record<AlertSeverity, { icon: React.ElementType; color: string; bgColor: string }> = {
|
|
87
|
+
critical: { icon: AlertCircle, color: "text-red-500", bgColor: "bg-red-500/10 border-red-500/30" },
|
|
88
|
+
warning: { icon: AlertTriangle, color: "text-yellow-500", bgColor: "bg-yellow-500/10 border-yellow-500/30" },
|
|
89
|
+
info: { icon: Info, color: "text-blue-500", bgColor: "bg-blue-500/10 border-blue-500/30" },
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const statusConfig: Record<AlertStatus, { label: string; variant: "default" | "secondary" | "outline" | "destructive" }> = {
|
|
93
|
+
firing: { label: "Firing", variant: "destructive" },
|
|
94
|
+
acknowledged: { label: "Acknowledged", variant: "secondary" },
|
|
95
|
+
resolved: { label: "Resolved", variant: "outline" },
|
|
96
|
+
silenced: { label: "Silenced", variant: "outline" },
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function formatDuration(date: Date): string {
|
|
100
|
+
const now = new Date()
|
|
101
|
+
const diff = now.getTime() - date.getTime()
|
|
102
|
+
const minutes = Math.floor(diff / 60000)
|
|
103
|
+
const hours = Math.floor(minutes / 60)
|
|
104
|
+
const days = Math.floor(hours / 24)
|
|
105
|
+
|
|
106
|
+
if (days > 0) return `${days}d ${hours % 24}h`
|
|
107
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`
|
|
108
|
+
return `${minutes}m`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function AlertCard({
|
|
112
|
+
alert,
|
|
113
|
+
onAcknowledge,
|
|
114
|
+
onSilence,
|
|
115
|
+
onResolve,
|
|
116
|
+
onViewDetails,
|
|
117
|
+
onRunbook,
|
|
118
|
+
}: {
|
|
119
|
+
alert: Alert
|
|
120
|
+
onAcknowledge?: (alertId: string) => void
|
|
121
|
+
onSilence?: (alertId: string, duration: number) => void
|
|
122
|
+
onResolve?: (alertId: string) => void
|
|
123
|
+
onViewDetails?: (alert: Alert) => void
|
|
124
|
+
onRunbook?: (alert: Alert) => void
|
|
125
|
+
}) {
|
|
126
|
+
const config = severityConfig[alert.severity]
|
|
127
|
+
const statusCfg = statusConfig[alert.status]
|
|
128
|
+
const Icon = config.icon
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div
|
|
132
|
+
className={cn(
|
|
133
|
+
"border rounded-lg p-4 transition-all",
|
|
134
|
+
config.bgColor,
|
|
135
|
+
alert.status === "firing" && alert.severity === "critical" && "animate-pulse"
|
|
136
|
+
)}
|
|
137
|
+
>
|
|
138
|
+
<div className="flex items-start gap-3">
|
|
139
|
+
<Icon className={cn("h-5 w-5 mt-0.5 shrink-0", config.color)} />
|
|
140
|
+
|
|
141
|
+
<div className="flex-1 min-w-0">
|
|
142
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
143
|
+
<h4 className="font-semibold truncate">{alert.title}</h4>
|
|
144
|
+
<Badge variant={statusCfg.variant}>{statusCfg.label}</Badge>
|
|
145
|
+
<Badge variant="outline" className="text-xs">
|
|
146
|
+
{alert.source}
|
|
147
|
+
</Badge>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
{alert.description && (
|
|
151
|
+
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">
|
|
152
|
+
{alert.description}
|
|
153
|
+
</p>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
<div className="flex items-center gap-4 mt-2 text-xs text-muted-foreground">
|
|
157
|
+
<span className="flex items-center gap-1">
|
|
158
|
+
<Clock className="h-3 w-3" />
|
|
159
|
+
{formatDuration(alert.startedAt)}
|
|
160
|
+
</span>
|
|
161
|
+
{alert.acknowledgedBy && (
|
|
162
|
+
<span>
|
|
163
|
+
Ack by {alert.acknowledgedBy}
|
|
164
|
+
</span>
|
|
165
|
+
)}
|
|
166
|
+
{alert.silencedUntil && alert.silencedUntil > new Date() && (
|
|
167
|
+
<span className="flex items-center gap-1">
|
|
168
|
+
<VolumeX className="h-3 w-3" />
|
|
169
|
+
Silenced until {alert.silencedUntil.toLocaleTimeString()}
|
|
170
|
+
</span>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{alert.labels && Object.keys(alert.labels).length > 0 && (
|
|
175
|
+
<div className="flex items-center gap-1 mt-2 flex-wrap">
|
|
176
|
+
{Object.entries(alert.labels).slice(0, 3).map(([key, value]) => (
|
|
177
|
+
<Badge key={key} variant="outline" className="text-xs">
|
|
178
|
+
{key}={value}
|
|
179
|
+
</Badge>
|
|
180
|
+
))}
|
|
181
|
+
{Object.keys(alert.labels).length > 3 && (
|
|
182
|
+
<Badge variant="outline" className="text-xs">
|
|
183
|
+
+{Object.keys(alert.labels).length - 3}
|
|
184
|
+
</Badge>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<DropdownMenu>
|
|
191
|
+
<DropdownMenuTrigger asChild>
|
|
192
|
+
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
|
193
|
+
<MoreVertical className="h-4 w-4" />
|
|
194
|
+
</Button>
|
|
195
|
+
</DropdownMenuTrigger>
|
|
196
|
+
<DropdownMenuContent align="end">
|
|
197
|
+
<DropdownMenuItem onClick={() => onViewDetails?.(alert)}>
|
|
198
|
+
<Eye className="h-4 w-4 mr-2" />
|
|
199
|
+
View Details
|
|
200
|
+
</DropdownMenuItem>
|
|
201
|
+
{alert.runbookUrl && (
|
|
202
|
+
<DropdownMenuItem onClick={() => onRunbook?.(alert)}>
|
|
203
|
+
<ExternalLink className="h-4 w-4 mr-2" />
|
|
204
|
+
View Runbook
|
|
205
|
+
</DropdownMenuItem>
|
|
206
|
+
)}
|
|
207
|
+
<DropdownMenuSeparator />
|
|
208
|
+
{alert.status === "firing" && (
|
|
209
|
+
<>
|
|
210
|
+
<DropdownMenuItem onClick={() => onAcknowledge?.(alert.id)}>
|
|
211
|
+
<Check className="h-4 w-4 mr-2" />
|
|
212
|
+
Acknowledge
|
|
213
|
+
</DropdownMenuItem>
|
|
214
|
+
<DropdownMenuItem onClick={() => onSilence?.(alert.id, 30)}>
|
|
215
|
+
<VolumeX className="h-4 w-4 mr-2" />
|
|
216
|
+
Silence 30m
|
|
217
|
+
</DropdownMenuItem>
|
|
218
|
+
<DropdownMenuItem onClick={() => onSilence?.(alert.id, 60)}>
|
|
219
|
+
<VolumeX className="h-4 w-4 mr-2" />
|
|
220
|
+
Silence 1h
|
|
221
|
+
</DropdownMenuItem>
|
|
222
|
+
<DropdownMenuItem onClick={() => onSilence?.(alert.id, 240)}>
|
|
223
|
+
<VolumeX className="h-4 w-4 mr-2" />
|
|
224
|
+
Silence 4h
|
|
225
|
+
</DropdownMenuItem>
|
|
226
|
+
</>
|
|
227
|
+
)}
|
|
228
|
+
{alert.status !== "resolved" && (
|
|
229
|
+
<DropdownMenuItem onClick={() => onResolve?.(alert.id)}>
|
|
230
|
+
<CheckCircle className="h-4 w-4 mr-2" />
|
|
231
|
+
Mark Resolved
|
|
232
|
+
</DropdownMenuItem>
|
|
233
|
+
)}
|
|
234
|
+
</DropdownMenuContent>
|
|
235
|
+
</DropdownMenu>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Add missing import
|
|
242
|
+
import { CheckCircle } from "lucide-react"
|
|
243
|
+
|
|
244
|
+
export function WakaAlertPanel({
|
|
245
|
+
alerts,
|
|
246
|
+
onAcknowledge,
|
|
247
|
+
onSilence,
|
|
248
|
+
onResolve,
|
|
249
|
+
onViewDetails,
|
|
250
|
+
onRunbook,
|
|
251
|
+
onRefresh,
|
|
252
|
+
isLoading = false,
|
|
253
|
+
showResolved = false,
|
|
254
|
+
className,
|
|
255
|
+
title = "Alerts",
|
|
256
|
+
}: WakaAlertPanelProps) {
|
|
257
|
+
const [severityFilter, setSeverityFilter] = React.useState<AlertSeverity | "all">("all")
|
|
258
|
+
const [statusFilter, setStatusFilter] = React.useState<AlertStatus | "all">("all")
|
|
259
|
+
|
|
260
|
+
// Filter alerts
|
|
261
|
+
const filteredAlerts = React.useMemo(() => {
|
|
262
|
+
return alerts.filter((alert) => {
|
|
263
|
+
if (!showResolved && alert.status === "resolved") return false
|
|
264
|
+
if (severityFilter !== "all" && alert.severity !== severityFilter) return false
|
|
265
|
+
if (statusFilter !== "all" && alert.status !== statusFilter) return false
|
|
266
|
+
return true
|
|
267
|
+
})
|
|
268
|
+
}, [alerts, showResolved, severityFilter, statusFilter])
|
|
269
|
+
|
|
270
|
+
// Count by severity
|
|
271
|
+
const severityCounts = React.useMemo(() => {
|
|
272
|
+
const active = alerts.filter((a) => a.status !== "resolved")
|
|
273
|
+
return {
|
|
274
|
+
critical: active.filter((a) => a.severity === "critical").length,
|
|
275
|
+
warning: active.filter((a) => a.severity === "warning").length,
|
|
276
|
+
info: active.filter((a) => a.severity === "info").length,
|
|
277
|
+
}
|
|
278
|
+
}, [alerts])
|
|
279
|
+
|
|
280
|
+
// Group by status
|
|
281
|
+
const groupedAlerts = React.useMemo(() => {
|
|
282
|
+
const firing = filteredAlerts.filter((a) => a.status === "firing")
|
|
283
|
+
const acknowledged = filteredAlerts.filter((a) => a.status === "acknowledged")
|
|
284
|
+
const silenced = filteredAlerts.filter((a) => a.status === "silenced")
|
|
285
|
+
const resolved = filteredAlerts.filter((a) => a.status === "resolved")
|
|
286
|
+
return { firing, acknowledged, silenced, resolved }
|
|
287
|
+
}, [filteredAlerts])
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<div className={cn("flex flex-col border rounded-lg bg-background", className)}>
|
|
291
|
+
{/* Header */}
|
|
292
|
+
<div className="flex items-center justify-between gap-4 p-3 border-b">
|
|
293
|
+
<div className="flex items-center gap-3">
|
|
294
|
+
<Bell className="h-5 w-5" />
|
|
295
|
+
<h3 className="font-semibold">{title}</h3>
|
|
296
|
+
|
|
297
|
+
{/* Severity counts */}
|
|
298
|
+
<div className="flex items-center gap-1">
|
|
299
|
+
{severityCounts.critical > 0 && (
|
|
300
|
+
<Badge variant="destructive" className="text-xs">
|
|
301
|
+
{severityCounts.critical} critical
|
|
302
|
+
</Badge>
|
|
303
|
+
)}
|
|
304
|
+
{severityCounts.warning > 0 && (
|
|
305
|
+
<Badge className="bg-yellow-500 text-xs">
|
|
306
|
+
{severityCounts.warning} warning
|
|
307
|
+
</Badge>
|
|
308
|
+
)}
|
|
309
|
+
{severityCounts.info > 0 && (
|
|
310
|
+
<Badge variant="secondary" className="text-xs">
|
|
311
|
+
{severityCounts.info} info
|
|
312
|
+
</Badge>
|
|
313
|
+
)}
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<div className="flex items-center gap-2">
|
|
318
|
+
{onRefresh && (
|
|
319
|
+
<Button
|
|
320
|
+
variant="ghost"
|
|
321
|
+
size="sm"
|
|
322
|
+
onClick={onRefresh}
|
|
323
|
+
disabled={isLoading}
|
|
324
|
+
>
|
|
325
|
+
<RefreshCw className={cn("h-4 w-4", isLoading && "animate-spin")} />
|
|
326
|
+
</Button>
|
|
327
|
+
)}
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
{/* Filters */}
|
|
332
|
+
<div className="flex items-center gap-2 p-2 border-b bg-muted/30">
|
|
333
|
+
<Select value={severityFilter} onValueChange={(v) => setSeverityFilter(v as AlertSeverity | "all")}>
|
|
334
|
+
<SelectTrigger className="w-[130px] h-8">
|
|
335
|
+
<Filter className="h-4 w-4 mr-2" />
|
|
336
|
+
<SelectValue placeholder="Severity" />
|
|
337
|
+
</SelectTrigger>
|
|
338
|
+
<SelectContent>
|
|
339
|
+
<SelectItem value="all">All Severity</SelectItem>
|
|
340
|
+
<SelectItem value="critical">Critical</SelectItem>
|
|
341
|
+
<SelectItem value="warning">Warning</SelectItem>
|
|
342
|
+
<SelectItem value="info">Info</SelectItem>
|
|
343
|
+
</SelectContent>
|
|
344
|
+
</Select>
|
|
345
|
+
|
|
346
|
+
<Select value={statusFilter} onValueChange={(v) => setStatusFilter(v as AlertStatus | "all")}>
|
|
347
|
+
<SelectTrigger className="w-[140px] h-8">
|
|
348
|
+
<SelectValue placeholder="Status" />
|
|
349
|
+
</SelectTrigger>
|
|
350
|
+
<SelectContent>
|
|
351
|
+
<SelectItem value="all">All Status</SelectItem>
|
|
352
|
+
<SelectItem value="firing">Firing</SelectItem>
|
|
353
|
+
<SelectItem value="acknowledged">Acknowledged</SelectItem>
|
|
354
|
+
<SelectItem value="silenced">Silenced</SelectItem>
|
|
355
|
+
{showResolved && <SelectItem value="resolved">Resolved</SelectItem>}
|
|
356
|
+
</SelectContent>
|
|
357
|
+
</Select>
|
|
358
|
+
</div>
|
|
359
|
+
|
|
360
|
+
{/* Alerts list */}
|
|
361
|
+
<ScrollArea className="flex-1">
|
|
362
|
+
<div className="p-3 space-y-4">
|
|
363
|
+
{filteredAlerts.length === 0 ? (
|
|
364
|
+
<div className="flex flex-col items-center justify-center h-32 text-muted-foreground">
|
|
365
|
+
<BellOff className="h-8 w-8 mb-2" />
|
|
366
|
+
<span>No alerts to display</span>
|
|
367
|
+
</div>
|
|
368
|
+
) : (
|
|
369
|
+
<>
|
|
370
|
+
{groupedAlerts.firing.length > 0 && (
|
|
371
|
+
<div className="space-y-2">
|
|
372
|
+
<h4 className="text-sm font-medium text-red-500 flex items-center gap-2">
|
|
373
|
+
<Volume2 className="h-4 w-4" />
|
|
374
|
+
Firing ({groupedAlerts.firing.length})
|
|
375
|
+
</h4>
|
|
376
|
+
{groupedAlerts.firing.map((alert) => (
|
|
377
|
+
<AlertCard
|
|
378
|
+
key={alert.id}
|
|
379
|
+
alert={alert}
|
|
380
|
+
onAcknowledge={onAcknowledge}
|
|
381
|
+
onSilence={onSilence}
|
|
382
|
+
onResolve={onResolve}
|
|
383
|
+
onViewDetails={onViewDetails}
|
|
384
|
+
onRunbook={onRunbook}
|
|
385
|
+
/>
|
|
386
|
+
))}
|
|
387
|
+
</div>
|
|
388
|
+
)}
|
|
389
|
+
|
|
390
|
+
{groupedAlerts.acknowledged.length > 0 && (
|
|
391
|
+
<div className="space-y-2">
|
|
392
|
+
<h4 className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
|
393
|
+
<Check className="h-4 w-4" />
|
|
394
|
+
Acknowledged ({groupedAlerts.acknowledged.length})
|
|
395
|
+
</h4>
|
|
396
|
+
{groupedAlerts.acknowledged.map((alert) => (
|
|
397
|
+
<AlertCard
|
|
398
|
+
key={alert.id}
|
|
399
|
+
alert={alert}
|
|
400
|
+
onAcknowledge={onAcknowledge}
|
|
401
|
+
onSilence={onSilence}
|
|
402
|
+
onResolve={onResolve}
|
|
403
|
+
onViewDetails={onViewDetails}
|
|
404
|
+
onRunbook={onRunbook}
|
|
405
|
+
/>
|
|
406
|
+
))}
|
|
407
|
+
</div>
|
|
408
|
+
)}
|
|
409
|
+
|
|
410
|
+
{groupedAlerts.silenced.length > 0 && (
|
|
411
|
+
<div className="space-y-2">
|
|
412
|
+
<h4 className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
|
413
|
+
<VolumeX className="h-4 w-4" />
|
|
414
|
+
Silenced ({groupedAlerts.silenced.length})
|
|
415
|
+
</h4>
|
|
416
|
+
{groupedAlerts.silenced.map((alert) => (
|
|
417
|
+
<AlertCard
|
|
418
|
+
key={alert.id}
|
|
419
|
+
alert={alert}
|
|
420
|
+
onAcknowledge={onAcknowledge}
|
|
421
|
+
onSilence={onSilence}
|
|
422
|
+
onResolve={onResolve}
|
|
423
|
+
onViewDetails={onViewDetails}
|
|
424
|
+
onRunbook={onRunbook}
|
|
425
|
+
/>
|
|
426
|
+
))}
|
|
427
|
+
</div>
|
|
428
|
+
)}
|
|
429
|
+
|
|
430
|
+
{showResolved && groupedAlerts.resolved.length > 0 && (
|
|
431
|
+
<div className="space-y-2">
|
|
432
|
+
<h4 className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
|
433
|
+
<CheckCircle className="h-4 w-4" />
|
|
434
|
+
Resolved ({groupedAlerts.resolved.length})
|
|
435
|
+
</h4>
|
|
436
|
+
{groupedAlerts.resolved.map((alert) => (
|
|
437
|
+
<AlertCard
|
|
438
|
+
key={alert.id}
|
|
439
|
+
alert={alert}
|
|
440
|
+
onAcknowledge={onAcknowledge}
|
|
441
|
+
onSilence={onSilence}
|
|
442
|
+
onResolve={onResolve}
|
|
443
|
+
onViewDetails={onViewDetails}
|
|
444
|
+
onRunbook={onRunbook}
|
|
445
|
+
/>
|
|
446
|
+
))}
|
|
447
|
+
</div>
|
|
448
|
+
)}
|
|
449
|
+
</>
|
|
450
|
+
)}
|
|
451
|
+
</div>
|
|
452
|
+
</ScrollArea>
|
|
453
|
+
</div>
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Default sample alerts for demo
|
|
458
|
+
export const defaultAlerts: Alert[] = [
|
|
459
|
+
{
|
|
460
|
+
id: "1",
|
|
461
|
+
title: "High CPU Usage on prod-api-1",
|
|
462
|
+
description: "CPU usage has exceeded 90% for more than 5 minutes",
|
|
463
|
+
severity: "critical",
|
|
464
|
+
status: "firing",
|
|
465
|
+
source: "prometheus",
|
|
466
|
+
startedAt: new Date(Date.now() - 15 * 60000),
|
|
467
|
+
labels: { instance: "prod-api-1", job: "api-server" },
|
|
468
|
+
runbookUrl: "https://runbooks.example.com/high-cpu",
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
id: "2",
|
|
472
|
+
title: "Database Connection Pool Exhausted",
|
|
473
|
+
description: "PostgreSQL connection pool is at 95% capacity",
|
|
474
|
+
severity: "warning",
|
|
475
|
+
status: "acknowledged",
|
|
476
|
+
source: "prometheus",
|
|
477
|
+
startedAt: new Date(Date.now() - 45 * 60000),
|
|
478
|
+
acknowledgedAt: new Date(Date.now() - 30 * 60000),
|
|
479
|
+
acknowledgedBy: "john.doe",
|
|
480
|
+
labels: { db: "primary", pool: "main" },
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
id: "3",
|
|
484
|
+
title: "SSL Certificate Expiring Soon",
|
|
485
|
+
description: "Certificate for api.example.com expires in 7 days",
|
|
486
|
+
severity: "info",
|
|
487
|
+
status: "silenced",
|
|
488
|
+
source: "cert-manager",
|
|
489
|
+
startedAt: new Date(Date.now() - 2 * 3600000),
|
|
490
|
+
silencedUntil: new Date(Date.now() + 24 * 3600000),
|
|
491
|
+
labels: { domain: "api.example.com" },
|
|
492
|
+
},
|
|
493
|
+
]
|