@wakastellar/ui 2.1.2 → 2.3.0
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 +19 -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-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-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-test-report/index.d.ts +60 -0
- package/dist/components/waka-trace-viewer/index.d.ts +36 -0
- package/dist/index.cjs.js +239 -194
- package/dist/index.es.js +45560 -35791
- package/package.json +1 -1
- 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/components/index.ts +189 -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-build-matrix/index.tsx +396 -0
- package/src/components/waka-config-comparator/index.tsx +416 -0
- package/src/components/waka-container-list/index.tsx +475 -0
- 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-kubernetes-overview/index.tsx +536 -0
- package/src/components/waka-log-viewer/index.tsx +386 -0
- package/src/components/waka-migration-list/index.tsx +487 -0
- package/src/components/waka-pod-card/index.tsx +528 -0
- package/src/components/waka-query-explain/index.tsx +657 -0
- 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-service-graph/index.tsx +445 -0
- package/src/components/waka-test-report/index.tsx +469 -0
- package/src/components/waka-trace-viewer/index.tsx +490 -0
package/src/components/index.ts
CHANGED
|
@@ -366,3 +366,192 @@ export {
|
|
|
366
366
|
type UseTooltipTourOptions,
|
|
367
367
|
type UseTooltipTourReturn,
|
|
368
368
|
} from './waka-tooltip-tour'
|
|
369
|
+
|
|
370
|
+
// DevOps - Monitoring & Logs Components
|
|
371
|
+
export {
|
|
372
|
+
WakaLogViewer,
|
|
373
|
+
defaultLogs,
|
|
374
|
+
type LogEntry,
|
|
375
|
+
type LogLevel as WakaLogLevel,
|
|
376
|
+
type WakaLogViewerProps,
|
|
377
|
+
} from './waka-log-viewer'
|
|
378
|
+
|
|
379
|
+
export {
|
|
380
|
+
WakaTraceViewer,
|
|
381
|
+
defaultTraceSpans,
|
|
382
|
+
type TraceSpan,
|
|
383
|
+
type SpanStatus,
|
|
384
|
+
type WakaTraceViewerProps,
|
|
385
|
+
} from './waka-trace-viewer'
|
|
386
|
+
|
|
387
|
+
export {
|
|
388
|
+
WakaAlertPanel,
|
|
389
|
+
defaultAlerts as defaultPanelAlerts,
|
|
390
|
+
type Alert as WakaPanelAlert,
|
|
391
|
+
type AlertSeverity as WakaPanelAlertSeverity,
|
|
392
|
+
type AlertStatus as WakaPanelAlertStatus,
|
|
393
|
+
type WakaAlertPanelProps,
|
|
394
|
+
} from './waka-alert-panel'
|
|
395
|
+
|
|
396
|
+
// DevOps - Containers & Kubernetes Components
|
|
397
|
+
export {
|
|
398
|
+
WakaContainerList,
|
|
399
|
+
defaultContainers,
|
|
400
|
+
type Container,
|
|
401
|
+
type ContainerStatus as WakaContainerStatus,
|
|
402
|
+
type ContainerResource,
|
|
403
|
+
type WakaContainerListProps,
|
|
404
|
+
} from './waka-container-list'
|
|
405
|
+
|
|
406
|
+
export {
|
|
407
|
+
WakaKubernetesOverview,
|
|
408
|
+
defaultK8sNodes,
|
|
409
|
+
defaultK8sPods,
|
|
410
|
+
defaultK8sDeployments,
|
|
411
|
+
defaultK8sServices,
|
|
412
|
+
defaultK8sNamespaces,
|
|
413
|
+
type K8sNode,
|
|
414
|
+
type K8sPod,
|
|
415
|
+
type K8sDeployment,
|
|
416
|
+
type K8sService,
|
|
417
|
+
type K8sNamespace,
|
|
418
|
+
type NodeStatus as K8sNodeStatus,
|
|
419
|
+
type PodPhase as K8sPodPhase,
|
|
420
|
+
type DeploymentStatus as K8sDeploymentStatus,
|
|
421
|
+
type WakaKubernetesOverviewProps,
|
|
422
|
+
} from './waka-kubernetes-overview'
|
|
423
|
+
|
|
424
|
+
export {
|
|
425
|
+
WakaPodCard,
|
|
426
|
+
defaultPodDetails,
|
|
427
|
+
type PodDetails,
|
|
428
|
+
type PodContainer,
|
|
429
|
+
type PodEvent,
|
|
430
|
+
type PodPhase,
|
|
431
|
+
type ContainerState,
|
|
432
|
+
type WakaPodCardProps,
|
|
433
|
+
} from './waka-pod-card'
|
|
434
|
+
|
|
435
|
+
export {
|
|
436
|
+
WakaServiceGraph,
|
|
437
|
+
defaultServices,
|
|
438
|
+
defaultConnections,
|
|
439
|
+
type ServiceNode,
|
|
440
|
+
type ServiceConnection,
|
|
441
|
+
type ServiceHealth,
|
|
442
|
+
type ServiceType,
|
|
443
|
+
type WakaServiceGraphProps,
|
|
444
|
+
} from './waka-service-graph'
|
|
445
|
+
|
|
446
|
+
// DevOps - CI/CD Components
|
|
447
|
+
export {
|
|
448
|
+
WakaBuildMatrix,
|
|
449
|
+
defaultBuildMatrixColumns,
|
|
450
|
+
defaultBuildMatrixRows,
|
|
451
|
+
type BuildMatrixRow,
|
|
452
|
+
type BuildCell,
|
|
453
|
+
type BuildStatus as WakaBuildStatus,
|
|
454
|
+
type WakaBuildMatrixProps,
|
|
455
|
+
} from './waka-build-matrix'
|
|
456
|
+
|
|
457
|
+
export {
|
|
458
|
+
WakaTestReport,
|
|
459
|
+
defaultTestSuites,
|
|
460
|
+
defaultCoverage,
|
|
461
|
+
type TestSuite,
|
|
462
|
+
type TestCase,
|
|
463
|
+
type CoverageReport,
|
|
464
|
+
type TestStatus,
|
|
465
|
+
type WakaTestReportProps,
|
|
466
|
+
} from './waka-test-report'
|
|
467
|
+
|
|
468
|
+
export {
|
|
469
|
+
WakaArtifactList,
|
|
470
|
+
defaultArtifacts,
|
|
471
|
+
type Artifact,
|
|
472
|
+
type WakaArtifactListProps,
|
|
473
|
+
} from './waka-artifact-list'
|
|
474
|
+
|
|
475
|
+
export {
|
|
476
|
+
WakaSecurityScanResult,
|
|
477
|
+
defaultVulnerabilities,
|
|
478
|
+
type Vulnerability,
|
|
479
|
+
type VulnerabilitySeverity,
|
|
480
|
+
type ScanType,
|
|
481
|
+
type ScanSummary,
|
|
482
|
+
type WakaSecurityScanResultProps,
|
|
483
|
+
} from './waka-security-scan-result'
|
|
484
|
+
|
|
485
|
+
export {
|
|
486
|
+
WakaDependencyTree,
|
|
487
|
+
defaultDependencies,
|
|
488
|
+
type Dependency,
|
|
489
|
+
type DependencyType,
|
|
490
|
+
type VulnSeverity as DependencyVulnSeverity,
|
|
491
|
+
type WakaDependencyTreeProps,
|
|
492
|
+
} from './waka-dependency-tree'
|
|
493
|
+
|
|
494
|
+
// DevOps - Configuration Components
|
|
495
|
+
export {
|
|
496
|
+
WakaEnvVarEditor,
|
|
497
|
+
defaultEnvVariables,
|
|
498
|
+
type EnvVariable,
|
|
499
|
+
type WakaEnvVarEditorProps,
|
|
500
|
+
} from './waka-env-var-editor'
|
|
501
|
+
|
|
502
|
+
export {
|
|
503
|
+
WakaSecretCard,
|
|
504
|
+
defaultSecret,
|
|
505
|
+
type Secret,
|
|
506
|
+
type SecretAccess,
|
|
507
|
+
type WakaSecretCardProps,
|
|
508
|
+
} from './waka-secret-card'
|
|
509
|
+
|
|
510
|
+
export {
|
|
511
|
+
WakaConfigComparator,
|
|
512
|
+
defaultConfigEnvironments,
|
|
513
|
+
type ConfigEnvironment,
|
|
514
|
+
type ConfigValue,
|
|
515
|
+
type ConfigDiff,
|
|
516
|
+
type DiffType,
|
|
517
|
+
type WakaConfigComparatorProps,
|
|
518
|
+
} from './waka-config-comparator'
|
|
519
|
+
|
|
520
|
+
export {
|
|
521
|
+
WakaFeatureFlagRow,
|
|
522
|
+
defaultFeatureFlags,
|
|
523
|
+
type FeatureFlag,
|
|
524
|
+
type FeatureFlagStatus,
|
|
525
|
+
type FeatureFlagSegment,
|
|
526
|
+
type WakaFeatureFlagRowProps,
|
|
527
|
+
} from './waka-feature-flag-row'
|
|
528
|
+
|
|
529
|
+
// DevOps - Database Components
|
|
530
|
+
export {
|
|
531
|
+
WakaDatabaseCard,
|
|
532
|
+
defaultDatabase,
|
|
533
|
+
defaultDatabases,
|
|
534
|
+
type DatabaseInfo,
|
|
535
|
+
type DatabaseMetrics,
|
|
536
|
+
type DatabaseType as WakaDatabaseType,
|
|
537
|
+
type DatabaseStatus as WakaDatabaseStatus,
|
|
538
|
+
type ReplicationRole,
|
|
539
|
+
type WakaDatabaseCardProps,
|
|
540
|
+
} from './waka-database-card'
|
|
541
|
+
|
|
542
|
+
export {
|
|
543
|
+
WakaMigrationList,
|
|
544
|
+
defaultMigrations,
|
|
545
|
+
type Migration,
|
|
546
|
+
type MigrationStatus,
|
|
547
|
+
type WakaMigrationListProps,
|
|
548
|
+
} from './waka-migration-list'
|
|
549
|
+
|
|
550
|
+
export {
|
|
551
|
+
WakaQueryExplain,
|
|
552
|
+
defaultQueryPlan,
|
|
553
|
+
type QueryPlan,
|
|
554
|
+
type ExplainNode,
|
|
555
|
+
type NodeType,
|
|
556
|
+
type WakaQueryExplainProps,
|
|
557
|
+
} from './waka-query-explain'
|
|
@@ -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
|
+
]
|