@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,672 @@
|
|
|
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 { Progress } from "../../components/progress"
|
|
9
|
+
import { ScrollArea } from "../../components/scroll-area"
|
|
10
|
+
import {
|
|
11
|
+
Select,
|
|
12
|
+
SelectContent,
|
|
13
|
+
SelectItem,
|
|
14
|
+
SelectTrigger,
|
|
15
|
+
SelectValue,
|
|
16
|
+
} from "../../components/select"
|
|
17
|
+
import {
|
|
18
|
+
Activity,
|
|
19
|
+
TrendingUp,
|
|
20
|
+
TrendingDown,
|
|
21
|
+
Clock,
|
|
22
|
+
AlertTriangle,
|
|
23
|
+
CheckCircle2,
|
|
24
|
+
XCircle,
|
|
25
|
+
Zap,
|
|
26
|
+
BarChart3,
|
|
27
|
+
ArrowRight,
|
|
28
|
+
ExternalLink,
|
|
29
|
+
RefreshCw,
|
|
30
|
+
Search,
|
|
31
|
+
Filter,
|
|
32
|
+
Server,
|
|
33
|
+
Globe,
|
|
34
|
+
Database,
|
|
35
|
+
Cpu,
|
|
36
|
+
MemoryStick,
|
|
37
|
+
HardDrive,
|
|
38
|
+
} from "lucide-react"
|
|
39
|
+
|
|
40
|
+
export type ServiceStatus = "healthy" | "warning" | "critical" | "unknown"
|
|
41
|
+
export type TimeRange = "1h" | "6h" | "24h" | "7d" | "30d"
|
|
42
|
+
|
|
43
|
+
export interface ServiceMetrics {
|
|
44
|
+
requestsPerSecond: number
|
|
45
|
+
avgResponseTime: number // in ms
|
|
46
|
+
p50ResponseTime: number
|
|
47
|
+
p95ResponseTime: number
|
|
48
|
+
p99ResponseTime: number
|
|
49
|
+
errorRate: number // percentage
|
|
50
|
+
successRate: number // percentage
|
|
51
|
+
activeConnections: number
|
|
52
|
+
cpuUsage?: number
|
|
53
|
+
memoryUsage?: number
|
|
54
|
+
throughput?: number // requests per minute
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ServiceEndpoint {
|
|
58
|
+
path: string
|
|
59
|
+
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"
|
|
60
|
+
requestsPerMinute: number
|
|
61
|
+
avgResponseTime: number
|
|
62
|
+
errorRate: number
|
|
63
|
+
status: ServiceStatus
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface APMService {
|
|
67
|
+
id: string
|
|
68
|
+
name: string
|
|
69
|
+
type: "api" | "web" | "worker" | "database" | "cache" | "queue"
|
|
70
|
+
status: ServiceStatus
|
|
71
|
+
metrics: ServiceMetrics
|
|
72
|
+
previousMetrics?: ServiceMetrics
|
|
73
|
+
endpoints?: ServiceEndpoint[]
|
|
74
|
+
instances?: number
|
|
75
|
+
version?: string
|
|
76
|
+
dependencies?: string[]
|
|
77
|
+
tags?: string[]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface APMTransaction {
|
|
81
|
+
id: string
|
|
82
|
+
name: string
|
|
83
|
+
service: string
|
|
84
|
+
duration: number
|
|
85
|
+
timestamp: Date
|
|
86
|
+
status: "success" | "error"
|
|
87
|
+
traceId?: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface APMOverviewProps {
|
|
91
|
+
services: APMService[]
|
|
92
|
+
recentTransactions?: APMTransaction[]
|
|
93
|
+
timeRange?: TimeRange
|
|
94
|
+
onTimeRangeChange?: (range: TimeRange) => void
|
|
95
|
+
onServiceClick?: (service: APMService) => void
|
|
96
|
+
onTransactionClick?: (transaction: APMTransaction) => void
|
|
97
|
+
onRefresh?: () => void
|
|
98
|
+
className?: string
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const statusConfig: Record<ServiceStatus, { color: string; bgColor: string; label: string }> = {
|
|
102
|
+
healthy: { color: "text-green-500", bgColor: "bg-green-500", label: "Healthy" },
|
|
103
|
+
warning: { color: "text-yellow-500", bgColor: "bg-yellow-500", label: "Warning" },
|
|
104
|
+
critical: { color: "text-red-500", bgColor: "bg-red-500", label: "Critical" },
|
|
105
|
+
unknown: { color: "text-gray-500", bgColor: "bg-gray-500", label: "Unknown" },
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const serviceTypeConfig: Record<APMService["type"], { icon: React.ElementType; color: string }> = {
|
|
109
|
+
api: { icon: Globe, color: "text-blue-500" },
|
|
110
|
+
web: { icon: Globe, color: "text-purple-500" },
|
|
111
|
+
worker: { icon: Cpu, color: "text-orange-500" },
|
|
112
|
+
database: { icon: Database, color: "text-green-500" },
|
|
113
|
+
cache: { icon: HardDrive, color: "text-red-500" },
|
|
114
|
+
queue: { icon: Activity, color: "text-cyan-500" },
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatMs(ms: number): string {
|
|
118
|
+
if (ms < 1) return `${(ms * 1000).toFixed(0)}µs`
|
|
119
|
+
if (ms < 1000) return `${ms.toFixed(0)}ms`
|
|
120
|
+
return `${(ms / 1000).toFixed(2)}s`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function formatNumber(n: number): string {
|
|
124
|
+
if (n < 1000) return n.toFixed(0)
|
|
125
|
+
if (n < 1000000) return `${(n / 1000).toFixed(1)}k`
|
|
126
|
+
return `${(n / 1000000).toFixed(1)}M`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function TrendIndicator({ current, previous }: { current: number; previous?: number }) {
|
|
130
|
+
if (previous === undefined) return null
|
|
131
|
+
const change = ((current - previous) / previous) * 100
|
|
132
|
+
const isPositive = change > 0
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<span className={cn(
|
|
136
|
+
"text-xs flex items-center gap-0.5",
|
|
137
|
+
isPositive ? "text-red-500" : "text-green-500"
|
|
138
|
+
)}>
|
|
139
|
+
{isPositive ? <TrendingUp className="h-3 w-3" /> : <TrendingDown className="h-3 w-3" />}
|
|
140
|
+
{Math.abs(change).toFixed(1)}%
|
|
141
|
+
</span>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function MetricCard({
|
|
146
|
+
title,
|
|
147
|
+
value,
|
|
148
|
+
unit,
|
|
149
|
+
previousValue,
|
|
150
|
+
status,
|
|
151
|
+
icon: Icon,
|
|
152
|
+
}: {
|
|
153
|
+
title: string
|
|
154
|
+
value: number
|
|
155
|
+
unit?: string
|
|
156
|
+
previousValue?: number
|
|
157
|
+
status?: ServiceStatus
|
|
158
|
+
icon: React.ElementType
|
|
159
|
+
}) {
|
|
160
|
+
const statusConf = status ? statusConfig[status] : null
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<Card>
|
|
164
|
+
<CardContent className="p-4">
|
|
165
|
+
<div className="flex items-start justify-between">
|
|
166
|
+
<div className="p-2 rounded-lg bg-muted">
|
|
167
|
+
<Icon className="h-4 w-4 text-muted-foreground" />
|
|
168
|
+
</div>
|
|
169
|
+
{statusConf && (
|
|
170
|
+
<div className={cn("w-2 h-2 rounded-full", statusConf.bgColor)} />
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
<div className="mt-3">
|
|
174
|
+
<div className="flex items-baseline gap-1">
|
|
175
|
+
<span className="text-2xl font-bold">{formatNumber(value)}</span>
|
|
176
|
+
{unit && <span className="text-sm text-muted-foreground">{unit}</span>}
|
|
177
|
+
</div>
|
|
178
|
+
<div className="flex items-center justify-between mt-1">
|
|
179
|
+
<span className="text-sm text-muted-foreground">{title}</span>
|
|
180
|
+
<TrendIndicator current={value} previous={previousValue} />
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</CardContent>
|
|
184
|
+
</Card>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function ServiceCard({
|
|
189
|
+
service,
|
|
190
|
+
onClick,
|
|
191
|
+
}: {
|
|
192
|
+
service: APMService
|
|
193
|
+
onClick?: () => void
|
|
194
|
+
}) {
|
|
195
|
+
const statusConf = statusConfig[service.status]
|
|
196
|
+
const typeConf = serviceTypeConfig[service.type]
|
|
197
|
+
const TypeIcon = typeConf.icon
|
|
198
|
+
const { metrics, previousMetrics } = service
|
|
199
|
+
|
|
200
|
+
const apdexScore = (metrics.successRate / 100) * (1 - (metrics.p95ResponseTime / 3000))
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<Card
|
|
204
|
+
className={cn(
|
|
205
|
+
"cursor-pointer transition-all hover:shadow-md",
|
|
206
|
+
service.status === "critical" && "border-red-500/50",
|
|
207
|
+
service.status === "warning" && "border-yellow-500/30"
|
|
208
|
+
)}
|
|
209
|
+
onClick={onClick}
|
|
210
|
+
>
|
|
211
|
+
<CardContent className="p-4">
|
|
212
|
+
<div className="flex items-start gap-4">
|
|
213
|
+
{/* Service icon */}
|
|
214
|
+
<div className={cn("p-2 rounded-lg", `${typeConf.color.replace("text-", "bg-")}/10`)}>
|
|
215
|
+
<TypeIcon className={cn("h-5 w-5", typeConf.color)} />
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
{/* Main info */}
|
|
219
|
+
<div className="flex-1 min-w-0">
|
|
220
|
+
<div className="flex items-center gap-2">
|
|
221
|
+
<h4 className="font-semibold truncate">{service.name}</h4>
|
|
222
|
+
<div className={cn("w-2 h-2 rounded-full shrink-0", statusConf.bgColor)} />
|
|
223
|
+
{service.version && (
|
|
224
|
+
<Badge variant="outline" className="text-xs">
|
|
225
|
+
v{service.version}
|
|
226
|
+
</Badge>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<div className="flex items-center gap-4 mt-2 text-sm">
|
|
231
|
+
<span className="flex items-center gap-1">
|
|
232
|
+
<Zap className="h-3 w-3 text-muted-foreground" />
|
|
233
|
+
{metrics.requestsPerSecond}/s
|
|
234
|
+
</span>
|
|
235
|
+
<span className="flex items-center gap-1">
|
|
236
|
+
<Clock className="h-3 w-3 text-muted-foreground" />
|
|
237
|
+
{formatMs(metrics.avgResponseTime)}
|
|
238
|
+
</span>
|
|
239
|
+
<span className={cn(
|
|
240
|
+
"flex items-center gap-1",
|
|
241
|
+
metrics.errorRate > 5 && "text-red-500",
|
|
242
|
+
metrics.errorRate > 1 && metrics.errorRate <= 5 && "text-yellow-500"
|
|
243
|
+
)}>
|
|
244
|
+
<AlertTriangle className="h-3 w-3" />
|
|
245
|
+
{metrics.errorRate.toFixed(2)}%
|
|
246
|
+
</span>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
{/* Response time breakdown */}
|
|
250
|
+
<div className="flex items-center gap-4 mt-3 text-xs text-muted-foreground">
|
|
251
|
+
<span>p50: {formatMs(metrics.p50ResponseTime)}</span>
|
|
252
|
+
<span>p95: {formatMs(metrics.p95ResponseTime)}</span>
|
|
253
|
+
<span>p99: {formatMs(metrics.p99ResponseTime)}</span>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
{/* Tags */}
|
|
257
|
+
{service.tags && service.tags.length > 0 && (
|
|
258
|
+
<div className="flex items-center gap-1 mt-2">
|
|
259
|
+
{service.tags.slice(0, 3).map((tag) => (
|
|
260
|
+
<Badge key={tag} variant="secondary" className="text-xs">
|
|
261
|
+
{tag}
|
|
262
|
+
</Badge>
|
|
263
|
+
))}
|
|
264
|
+
{service.tags.length > 3 && (
|
|
265
|
+
<span className="text-xs text-muted-foreground">
|
|
266
|
+
+{service.tags.length - 3}
|
|
267
|
+
</span>
|
|
268
|
+
)}
|
|
269
|
+
</div>
|
|
270
|
+
)}
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
{/* Right side metrics */}
|
|
274
|
+
<div className="text-right shrink-0 space-y-1">
|
|
275
|
+
<div className="text-lg font-bold text-green-500">
|
|
276
|
+
{metrics.successRate.toFixed(1)}%
|
|
277
|
+
</div>
|
|
278
|
+
<div className="text-xs text-muted-foreground">Success</div>
|
|
279
|
+
{service.instances && (
|
|
280
|
+
<div className="text-xs text-muted-foreground">
|
|
281
|
+
{service.instances} instances
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
{/* Resource usage */}
|
|
288
|
+
{(metrics.cpuUsage !== undefined || metrics.memoryUsage !== undefined) && (
|
|
289
|
+
<div className="grid grid-cols-2 gap-3 mt-4 pt-3 border-t">
|
|
290
|
+
{metrics.cpuUsage !== undefined && (
|
|
291
|
+
<div>
|
|
292
|
+
<div className="flex items-center justify-between text-xs mb-1">
|
|
293
|
+
<span className="text-muted-foreground flex items-center gap-1">
|
|
294
|
+
<Cpu className="h-3 w-3" /> CPU
|
|
295
|
+
</span>
|
|
296
|
+
<span className={cn(metrics.cpuUsage > 80 && "text-red-500")}>
|
|
297
|
+
{metrics.cpuUsage}%
|
|
298
|
+
</span>
|
|
299
|
+
</div>
|
|
300
|
+
<Progress
|
|
301
|
+
value={metrics.cpuUsage}
|
|
302
|
+
className={cn(
|
|
303
|
+
"h-1.5",
|
|
304
|
+
metrics.cpuUsage > 80 && "[&>div]:bg-red-500"
|
|
305
|
+
)}
|
|
306
|
+
/>
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
{metrics.memoryUsage !== undefined && (
|
|
310
|
+
<div>
|
|
311
|
+
<div className="flex items-center justify-between text-xs mb-1">
|
|
312
|
+
<span className="text-muted-foreground flex items-center gap-1">
|
|
313
|
+
<MemoryStick className="h-3 w-3" /> Memory
|
|
314
|
+
</span>
|
|
315
|
+
<span className={cn(metrics.memoryUsage > 80 && "text-red-500")}>
|
|
316
|
+
{metrics.memoryUsage}%
|
|
317
|
+
</span>
|
|
318
|
+
</div>
|
|
319
|
+
<Progress
|
|
320
|
+
value={metrics.memoryUsage}
|
|
321
|
+
className={cn(
|
|
322
|
+
"h-1.5",
|
|
323
|
+
metrics.memoryUsage > 80 && "[&>div]:bg-red-500"
|
|
324
|
+
)}
|
|
325
|
+
/>
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
328
|
+
</div>
|
|
329
|
+
)}
|
|
330
|
+
</CardContent>
|
|
331
|
+
</Card>
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function RecentTransactions({
|
|
336
|
+
transactions,
|
|
337
|
+
onClick,
|
|
338
|
+
}: {
|
|
339
|
+
transactions: APMTransaction[]
|
|
340
|
+
onClick?: (t: APMTransaction) => void
|
|
341
|
+
}) {
|
|
342
|
+
return (
|
|
343
|
+
<Card>
|
|
344
|
+
<CardHeader className="pb-2">
|
|
345
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
346
|
+
<Activity className="h-4 w-4" />
|
|
347
|
+
Recent Transactions
|
|
348
|
+
</CardTitle>
|
|
349
|
+
</CardHeader>
|
|
350
|
+
<CardContent>
|
|
351
|
+
<ScrollArea className="h-[300px]">
|
|
352
|
+
<div className="space-y-2">
|
|
353
|
+
{transactions.map((tx) => (
|
|
354
|
+
<div
|
|
355
|
+
key={tx.id}
|
|
356
|
+
className="flex items-center gap-3 p-2 rounded-lg hover:bg-muted/50 cursor-pointer transition-colors"
|
|
357
|
+
onClick={() => onClick?.(tx)}
|
|
358
|
+
>
|
|
359
|
+
{tx.status === "success" ? (
|
|
360
|
+
<CheckCircle2 className="h-4 w-4 text-green-500 shrink-0" />
|
|
361
|
+
) : (
|
|
362
|
+
<XCircle className="h-4 w-4 text-red-500 shrink-0" />
|
|
363
|
+
)}
|
|
364
|
+
|
|
365
|
+
<div className="flex-1 min-w-0">
|
|
366
|
+
<div className="font-medium text-sm truncate">{tx.name}</div>
|
|
367
|
+
<div className="text-xs text-muted-foreground">{tx.service}</div>
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
<div className="text-right shrink-0">
|
|
371
|
+
<div className={cn(
|
|
372
|
+
"text-sm font-medium",
|
|
373
|
+
tx.duration > 1000 && "text-yellow-500",
|
|
374
|
+
tx.duration > 3000 && "text-red-500"
|
|
375
|
+
)}>
|
|
376
|
+
{formatMs(tx.duration)}
|
|
377
|
+
</div>
|
|
378
|
+
<div className="text-xs text-muted-foreground">
|
|
379
|
+
{tx.timestamp.toLocaleTimeString()}
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
))}
|
|
384
|
+
</div>
|
|
385
|
+
</ScrollArea>
|
|
386
|
+
</CardContent>
|
|
387
|
+
</Card>
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function APMOverview({
|
|
392
|
+
services,
|
|
393
|
+
recentTransactions = [],
|
|
394
|
+
timeRange = "1h",
|
|
395
|
+
onTimeRangeChange,
|
|
396
|
+
onServiceClick,
|
|
397
|
+
onTransactionClick,
|
|
398
|
+
onRefresh,
|
|
399
|
+
className,
|
|
400
|
+
}: APMOverviewProps) {
|
|
401
|
+
const [statusFilter, setStatusFilter] = React.useState<ServiceStatus | "all">("all")
|
|
402
|
+
|
|
403
|
+
// Aggregate metrics
|
|
404
|
+
const aggregateMetrics = React.useMemo(() => {
|
|
405
|
+
const totalRequests = services.reduce((acc, s) => acc + s.metrics.requestsPerSecond, 0)
|
|
406
|
+
const avgResponseTime = services.reduce((acc, s) => acc + s.metrics.avgResponseTime, 0) / services.length
|
|
407
|
+
const avgErrorRate = services.reduce((acc, s) => acc + s.metrics.errorRate, 0) / services.length
|
|
408
|
+
const healthyCount = services.filter((s) => s.status === "healthy").length
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
totalRequests,
|
|
412
|
+
avgResponseTime,
|
|
413
|
+
avgErrorRate,
|
|
414
|
+
healthyCount,
|
|
415
|
+
totalServices: services.length,
|
|
416
|
+
}
|
|
417
|
+
}, [services])
|
|
418
|
+
|
|
419
|
+
const filteredServices = React.useMemo(() => {
|
|
420
|
+
if (statusFilter === "all") return services
|
|
421
|
+
return services.filter((s) => s.status === statusFilter)
|
|
422
|
+
}, [services, statusFilter])
|
|
423
|
+
|
|
424
|
+
return (
|
|
425
|
+
<div className={cn("space-y-6", className)}>
|
|
426
|
+
{/* Header */}
|
|
427
|
+
<Card>
|
|
428
|
+
<CardHeader>
|
|
429
|
+
<div className="flex items-center justify-between">
|
|
430
|
+
<div className="flex items-center gap-3">
|
|
431
|
+
<Activity className="h-6 w-6" />
|
|
432
|
+
<div>
|
|
433
|
+
<CardTitle>Application Performance</CardTitle>
|
|
434
|
+
<p className="text-sm text-muted-foreground">
|
|
435
|
+
Monitor service health and performance metrics
|
|
436
|
+
</p>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
<div className="flex items-center gap-2">
|
|
441
|
+
<Select value={timeRange} onValueChange={(v) => onTimeRangeChange?.(v as TimeRange)}>
|
|
442
|
+
<SelectTrigger className="w-28">
|
|
443
|
+
<Clock className="h-4 w-4 mr-2" />
|
|
444
|
+
<SelectValue />
|
|
445
|
+
</SelectTrigger>
|
|
446
|
+
<SelectContent>
|
|
447
|
+
<SelectItem value="1h">Last 1h</SelectItem>
|
|
448
|
+
<SelectItem value="6h">Last 6h</SelectItem>
|
|
449
|
+
<SelectItem value="24h">Last 24h</SelectItem>
|
|
450
|
+
<SelectItem value="7d">Last 7d</SelectItem>
|
|
451
|
+
<SelectItem value="30d">Last 30d</SelectItem>
|
|
452
|
+
</SelectContent>
|
|
453
|
+
</Select>
|
|
454
|
+
|
|
455
|
+
{onRefresh && (
|
|
456
|
+
<Button variant="outline" onClick={onRefresh}>
|
|
457
|
+
<RefreshCw className="h-4 w-4" />
|
|
458
|
+
</Button>
|
|
459
|
+
)}
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
</CardHeader>
|
|
463
|
+
</Card>
|
|
464
|
+
|
|
465
|
+
{/* Key metrics */}
|
|
466
|
+
<div className="grid grid-cols-4 gap-4">
|
|
467
|
+
<MetricCard
|
|
468
|
+
title="Total Requests"
|
|
469
|
+
value={aggregateMetrics.totalRequests}
|
|
470
|
+
unit="/s"
|
|
471
|
+
icon={Zap}
|
|
472
|
+
/>
|
|
473
|
+
<MetricCard
|
|
474
|
+
title="Avg Response Time"
|
|
475
|
+
value={aggregateMetrics.avgResponseTime}
|
|
476
|
+
unit="ms"
|
|
477
|
+
icon={Clock}
|
|
478
|
+
status={aggregateMetrics.avgResponseTime > 500 ? "warning" : "healthy"}
|
|
479
|
+
/>
|
|
480
|
+
<MetricCard
|
|
481
|
+
title="Error Rate"
|
|
482
|
+
value={aggregateMetrics.avgErrorRate}
|
|
483
|
+
unit="%"
|
|
484
|
+
icon={AlertTriangle}
|
|
485
|
+
status={aggregateMetrics.avgErrorRate > 5 ? "critical" : aggregateMetrics.avgErrorRate > 1 ? "warning" : "healthy"}
|
|
486
|
+
/>
|
|
487
|
+
<MetricCard
|
|
488
|
+
title="Services Healthy"
|
|
489
|
+
value={aggregateMetrics.healthyCount}
|
|
490
|
+
unit={`/ ${aggregateMetrics.totalServices}`}
|
|
491
|
+
icon={CheckCircle2}
|
|
492
|
+
status={aggregateMetrics.healthyCount === aggregateMetrics.totalServices ? "healthy" : "warning"}
|
|
493
|
+
/>
|
|
494
|
+
</div>
|
|
495
|
+
|
|
496
|
+
<div className="grid grid-cols-3 gap-6">
|
|
497
|
+
{/* Services */}
|
|
498
|
+
<div className="col-span-2 space-y-4">
|
|
499
|
+
<div className="flex items-center justify-between">
|
|
500
|
+
<h3 className="font-semibold">Services</h3>
|
|
501
|
+
<div className="flex items-center gap-2">
|
|
502
|
+
{(["all", "healthy", "warning", "critical"] as const).map((status) => (
|
|
503
|
+
<Button
|
|
504
|
+
key={status}
|
|
505
|
+
variant={statusFilter === status ? "default" : "ghost"}
|
|
506
|
+
size="sm"
|
|
507
|
+
onClick={() => setStatusFilter(status)}
|
|
508
|
+
>
|
|
509
|
+
{status === "all" ? "All" : statusConfig[status].label}
|
|
510
|
+
</Button>
|
|
511
|
+
))}
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
|
|
515
|
+
<ScrollArea className="h-[500px]">
|
|
516
|
+
<div className="space-y-4 pr-4">
|
|
517
|
+
{filteredServices.map((service) => (
|
|
518
|
+
<ServiceCard
|
|
519
|
+
key={service.id}
|
|
520
|
+
service={service}
|
|
521
|
+
onClick={() => onServiceClick?.(service)}
|
|
522
|
+
/>
|
|
523
|
+
))}
|
|
524
|
+
</div>
|
|
525
|
+
</ScrollArea>
|
|
526
|
+
</div>
|
|
527
|
+
|
|
528
|
+
{/* Recent transactions */}
|
|
529
|
+
<div>
|
|
530
|
+
<RecentTransactions
|
|
531
|
+
transactions={recentTransactions}
|
|
532
|
+
onClick={onTransactionClick}
|
|
533
|
+
/>
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Default sample data
|
|
541
|
+
export const defaultAPMServices: APMService[] = [
|
|
542
|
+
{
|
|
543
|
+
id: "1",
|
|
544
|
+
name: "api-gateway",
|
|
545
|
+
type: "api",
|
|
546
|
+
status: "healthy",
|
|
547
|
+
version: "2.4.1",
|
|
548
|
+
instances: 3,
|
|
549
|
+
metrics: {
|
|
550
|
+
requestsPerSecond: 1250,
|
|
551
|
+
avgResponseTime: 45,
|
|
552
|
+
p50ResponseTime: 32,
|
|
553
|
+
p95ResponseTime: 120,
|
|
554
|
+
p99ResponseTime: 250,
|
|
555
|
+
errorRate: 0.12,
|
|
556
|
+
successRate: 99.88,
|
|
557
|
+
activeConnections: 450,
|
|
558
|
+
cpuUsage: 45,
|
|
559
|
+
memoryUsage: 62,
|
|
560
|
+
},
|
|
561
|
+
tags: ["production", "critical"],
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
id: "2",
|
|
565
|
+
name: "user-service",
|
|
566
|
+
type: "api",
|
|
567
|
+
status: "healthy",
|
|
568
|
+
version: "1.8.0",
|
|
569
|
+
instances: 2,
|
|
570
|
+
metrics: {
|
|
571
|
+
requestsPerSecond: 350,
|
|
572
|
+
avgResponseTime: 85,
|
|
573
|
+
p50ResponseTime: 65,
|
|
574
|
+
p95ResponseTime: 180,
|
|
575
|
+
p99ResponseTime: 450,
|
|
576
|
+
errorRate: 0.5,
|
|
577
|
+
successRate: 99.5,
|
|
578
|
+
activeConnections: 120,
|
|
579
|
+
cpuUsage: 35,
|
|
580
|
+
memoryUsage: 55,
|
|
581
|
+
},
|
|
582
|
+
tags: ["production"],
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
id: "3",
|
|
586
|
+
name: "payment-service",
|
|
587
|
+
type: "api",
|
|
588
|
+
status: "warning",
|
|
589
|
+
version: "3.1.0",
|
|
590
|
+
instances: 2,
|
|
591
|
+
metrics: {
|
|
592
|
+
requestsPerSecond: 150,
|
|
593
|
+
avgResponseTime: 520,
|
|
594
|
+
p50ResponseTime: 280,
|
|
595
|
+
p95ResponseTime: 1200,
|
|
596
|
+
p99ResponseTime: 2500,
|
|
597
|
+
errorRate: 2.5,
|
|
598
|
+
successRate: 97.5,
|
|
599
|
+
activeConnections: 85,
|
|
600
|
+
cpuUsage: 78,
|
|
601
|
+
memoryUsage: 82,
|
|
602
|
+
},
|
|
603
|
+
tags: ["production", "payment"],
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
id: "4",
|
|
607
|
+
name: "notification-worker",
|
|
608
|
+
type: "worker",
|
|
609
|
+
status: "healthy",
|
|
610
|
+
instances: 5,
|
|
611
|
+
metrics: {
|
|
612
|
+
requestsPerSecond: 500,
|
|
613
|
+
avgResponseTime: 25,
|
|
614
|
+
p50ResponseTime: 18,
|
|
615
|
+
p95ResponseTime: 55,
|
|
616
|
+
p99ResponseTime: 120,
|
|
617
|
+
errorRate: 0.05,
|
|
618
|
+
successRate: 99.95,
|
|
619
|
+
activeConnections: 0,
|
|
620
|
+
cpuUsage: 25,
|
|
621
|
+
memoryUsage: 40,
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
id: "5",
|
|
626
|
+
name: "postgres-primary",
|
|
627
|
+
type: "database",
|
|
628
|
+
status: "healthy",
|
|
629
|
+
instances: 1,
|
|
630
|
+
metrics: {
|
|
631
|
+
requestsPerSecond: 2500,
|
|
632
|
+
avgResponseTime: 8,
|
|
633
|
+
p50ResponseTime: 5,
|
|
634
|
+
p95ResponseTime: 25,
|
|
635
|
+
p99ResponseTime: 80,
|
|
636
|
+
errorRate: 0.01,
|
|
637
|
+
successRate: 99.99,
|
|
638
|
+
activeConnections: 120,
|
|
639
|
+
cpuUsage: 55,
|
|
640
|
+
memoryUsage: 72,
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
id: "6",
|
|
645
|
+
name: "redis-cache",
|
|
646
|
+
type: "cache",
|
|
647
|
+
status: "healthy",
|
|
648
|
+
instances: 3,
|
|
649
|
+
metrics: {
|
|
650
|
+
requestsPerSecond: 15000,
|
|
651
|
+
avgResponseTime: 0.5,
|
|
652
|
+
p50ResponseTime: 0.3,
|
|
653
|
+
p95ResponseTime: 1.2,
|
|
654
|
+
p99ResponseTime: 3,
|
|
655
|
+
errorRate: 0.001,
|
|
656
|
+
successRate: 99.999,
|
|
657
|
+
activeConnections: 500,
|
|
658
|
+
memoryUsage: 45,
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
]
|
|
662
|
+
|
|
663
|
+
export const defaultAPMTransactions: APMTransaction[] = [
|
|
664
|
+
{ id: "1", name: "POST /api/users", service: "user-service", duration: 85, timestamp: new Date(Date.now() - 5000), status: "success" },
|
|
665
|
+
{ id: "2", name: "GET /api/products", service: "api-gateway", duration: 42, timestamp: new Date(Date.now() - 12000), status: "success" },
|
|
666
|
+
{ id: "3", name: "POST /api/payments", service: "payment-service", duration: 1250, timestamp: new Date(Date.now() - 25000), status: "error" },
|
|
667
|
+
{ id: "4", name: "GET /api/orders/:id", service: "api-gateway", duration: 65, timestamp: new Date(Date.now() - 45000), status: "success" },
|
|
668
|
+
{ id: "5", name: "POST /api/notifications", service: "notification-worker", duration: 22, timestamp: new Date(Date.now() - 60000), status: "success" },
|
|
669
|
+
{ id: "6", name: "GET /api/users/:id", service: "user-service", duration: 95, timestamp: new Date(Date.now() - 90000), status: "success" },
|
|
670
|
+
{ id: "7", name: "PUT /api/cart", service: "api-gateway", duration: 180, timestamp: new Date(Date.now() - 120000), status: "success" },
|
|
671
|
+
{ id: "8", name: "POST /api/checkout", service: "payment-service", duration: 3200, timestamp: new Date(Date.now() - 180000), status: "error" },
|
|
672
|
+
]
|