@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,445 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../utils/cn"
|
|
5
|
+
import { Badge } from "../badge"
|
|
6
|
+
import { Button } from "../button"
|
|
7
|
+
import {
|
|
8
|
+
Tooltip,
|
|
9
|
+
TooltipContent,
|
|
10
|
+
TooltipProvider,
|
|
11
|
+
TooltipTrigger,
|
|
12
|
+
} from "../tooltip"
|
|
13
|
+
import {
|
|
14
|
+
Network,
|
|
15
|
+
Server,
|
|
16
|
+
Database,
|
|
17
|
+
Globe,
|
|
18
|
+
MessageSquare,
|
|
19
|
+
Zap,
|
|
20
|
+
RefreshCw,
|
|
21
|
+
ZoomIn,
|
|
22
|
+
ZoomOut,
|
|
23
|
+
Maximize2,
|
|
24
|
+
CheckCircle2,
|
|
25
|
+
XCircle,
|
|
26
|
+
AlertTriangle,
|
|
27
|
+
} from "lucide-react"
|
|
28
|
+
|
|
29
|
+
export type ServiceHealth = "healthy" | "degraded" | "unhealthy" | "unknown"
|
|
30
|
+
export type ServiceType = "api" | "database" | "cache" | "queue" | "gateway" | "external"
|
|
31
|
+
|
|
32
|
+
export interface ServiceNode {
|
|
33
|
+
id: string
|
|
34
|
+
name: string
|
|
35
|
+
type: ServiceType
|
|
36
|
+
health: ServiceHealth
|
|
37
|
+
namespace?: string
|
|
38
|
+
version?: string
|
|
39
|
+
replicas?: number
|
|
40
|
+
requestsPerSec?: number
|
|
41
|
+
errorRate?: number
|
|
42
|
+
latencyP50?: number
|
|
43
|
+
latencyP99?: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ServiceConnection {
|
|
47
|
+
source: string
|
|
48
|
+
target: string
|
|
49
|
+
protocol?: string
|
|
50
|
+
requestsPerSec?: number
|
|
51
|
+
errorRate?: number
|
|
52
|
+
latency?: number
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface WakaServiceGraphProps {
|
|
56
|
+
/** List of services */
|
|
57
|
+
services: ServiceNode[]
|
|
58
|
+
/** List of connections between services */
|
|
59
|
+
connections: ServiceConnection[]
|
|
60
|
+
/** Selected service ID */
|
|
61
|
+
selectedService?: string
|
|
62
|
+
/** Callback when clicking on a service */
|
|
63
|
+
onServiceClick?: (service: ServiceNode) => void
|
|
64
|
+
/** Callback when clicking on a connection */
|
|
65
|
+
onConnectionClick?: (connection: ServiceConnection) => void
|
|
66
|
+
/** Callback when refreshing */
|
|
67
|
+
onRefresh?: () => void
|
|
68
|
+
/** Show metrics on nodes */
|
|
69
|
+
showMetrics?: boolean
|
|
70
|
+
/** Custom class name */
|
|
71
|
+
className?: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const typeConfig: Record<ServiceType, { icon: React.ElementType; color: string }> = {
|
|
75
|
+
api: { icon: Server, color: "bg-blue-500" },
|
|
76
|
+
database: { icon: Database, color: "bg-purple-500" },
|
|
77
|
+
cache: { icon: Zap, color: "bg-yellow-500" },
|
|
78
|
+
queue: { icon: MessageSquare, color: "bg-orange-500" },
|
|
79
|
+
gateway: { icon: Globe, color: "bg-green-500" },
|
|
80
|
+
external: { icon: Network, color: "bg-gray-500" },
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const healthConfig: Record<ServiceHealth, { color: string; borderColor: string; icon: React.ElementType }> = {
|
|
84
|
+
healthy: { color: "text-green-500", borderColor: "border-green-500", icon: CheckCircle2 },
|
|
85
|
+
degraded: { color: "text-yellow-500", borderColor: "border-yellow-500", icon: AlertTriangle },
|
|
86
|
+
unhealthy: { color: "text-red-500", borderColor: "border-red-500", icon: XCircle },
|
|
87
|
+
unknown: { color: "text-gray-500", borderColor: "border-gray-500", icon: AlertTriangle },
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function ServiceNodeComponent({
|
|
91
|
+
service,
|
|
92
|
+
isSelected,
|
|
93
|
+
onClick,
|
|
94
|
+
showMetrics,
|
|
95
|
+
position,
|
|
96
|
+
}: {
|
|
97
|
+
service: ServiceNode
|
|
98
|
+
isSelected: boolean
|
|
99
|
+
onClick?: () => void
|
|
100
|
+
showMetrics: boolean
|
|
101
|
+
position: { x: number; y: number }
|
|
102
|
+
}) {
|
|
103
|
+
const typeConf = typeConfig[service.type]
|
|
104
|
+
const healthConf = healthConfig[service.health]
|
|
105
|
+
const TypeIcon = typeConf.icon
|
|
106
|
+
const HealthIcon = healthConf.icon
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<TooltipProvider>
|
|
110
|
+
<Tooltip>
|
|
111
|
+
<TooltipTrigger asChild>
|
|
112
|
+
<div
|
|
113
|
+
className={cn(
|
|
114
|
+
"absolute transform -translate-x-1/2 -translate-y-1/2 cursor-pointer",
|
|
115
|
+
"transition-all duration-200 hover:scale-110"
|
|
116
|
+
)}
|
|
117
|
+
style={{ left: position.x, top: position.y }}
|
|
118
|
+
onClick={onClick}
|
|
119
|
+
>
|
|
120
|
+
<div
|
|
121
|
+
className={cn(
|
|
122
|
+
"relative bg-background border-2 rounded-xl p-3 shadow-lg",
|
|
123
|
+
healthConf.borderColor,
|
|
124
|
+
isSelected && "ring-2 ring-primary ring-offset-2"
|
|
125
|
+
)}
|
|
126
|
+
>
|
|
127
|
+
{/* Health indicator */}
|
|
128
|
+
<div className={cn("absolute -top-2 -right-2", healthConf.color)}>
|
|
129
|
+
<HealthIcon className="h-4 w-4" />
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{/* Icon */}
|
|
133
|
+
<div className={cn("w-10 h-10 rounded-lg flex items-center justify-center text-white", typeConf.color)}>
|
|
134
|
+
<TypeIcon className="h-5 w-5" />
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{/* Name */}
|
|
138
|
+
<div className="mt-2 text-center">
|
|
139
|
+
<div className="font-medium text-sm truncate max-w-[100px]">{service.name}</div>
|
|
140
|
+
{service.namespace && (
|
|
141
|
+
<div className="text-xs text-muted-foreground">{service.namespace}</div>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{/* Metrics */}
|
|
146
|
+
{showMetrics && (
|
|
147
|
+
<div className="mt-2 text-xs text-center space-y-0.5">
|
|
148
|
+
{service.requestsPerSec !== undefined && (
|
|
149
|
+
<div className="text-muted-foreground">{service.requestsPerSec} req/s</div>
|
|
150
|
+
)}
|
|
151
|
+
{service.errorRate !== undefined && service.errorRate > 0 && (
|
|
152
|
+
<div className="text-red-500">{(service.errorRate * 100).toFixed(1)}% errors</div>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</TooltipTrigger>
|
|
159
|
+
<TooltipContent side="right" className="max-w-xs">
|
|
160
|
+
<div className="space-y-2">
|
|
161
|
+
<div className="font-semibold">{service.name}</div>
|
|
162
|
+
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
|
|
163
|
+
<span className="text-muted-foreground">Type:</span>
|
|
164
|
+
<span className="capitalize">{service.type}</span>
|
|
165
|
+
<span className="text-muted-foreground">Health:</span>
|
|
166
|
+
<span className={healthConf.color}>{service.health}</span>
|
|
167
|
+
{service.version && (
|
|
168
|
+
<>
|
|
169
|
+
<span className="text-muted-foreground">Version:</span>
|
|
170
|
+
<span>{service.version}</span>
|
|
171
|
+
</>
|
|
172
|
+
)}
|
|
173
|
+
{service.replicas !== undefined && (
|
|
174
|
+
<>
|
|
175
|
+
<span className="text-muted-foreground">Replicas:</span>
|
|
176
|
+
<span>{service.replicas}</span>
|
|
177
|
+
</>
|
|
178
|
+
)}
|
|
179
|
+
{service.latencyP50 !== undefined && (
|
|
180
|
+
<>
|
|
181
|
+
<span className="text-muted-foreground">Latency P50:</span>
|
|
182
|
+
<span>{service.latencyP50}ms</span>
|
|
183
|
+
</>
|
|
184
|
+
)}
|
|
185
|
+
{service.latencyP99 !== undefined && (
|
|
186
|
+
<>
|
|
187
|
+
<span className="text-muted-foreground">Latency P99:</span>
|
|
188
|
+
<span>{service.latencyP99}ms</span>
|
|
189
|
+
</>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</TooltipContent>
|
|
194
|
+
</Tooltip>
|
|
195
|
+
</TooltipProvider>
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function ConnectionLine({
|
|
200
|
+
start,
|
|
201
|
+
end,
|
|
202
|
+
connection,
|
|
203
|
+
onClick,
|
|
204
|
+
}: {
|
|
205
|
+
start: { x: number; y: number }
|
|
206
|
+
end: { x: number; y: number }
|
|
207
|
+
connection: ServiceConnection
|
|
208
|
+
onClick?: () => void
|
|
209
|
+
}) {
|
|
210
|
+
// Calculate control points for curved line
|
|
211
|
+
const midX = (start.x + end.x) / 2
|
|
212
|
+
const midY = (start.y + end.y) / 2
|
|
213
|
+
const dx = end.x - start.x
|
|
214
|
+
const dy = end.y - start.y
|
|
215
|
+
const dist = Math.sqrt(dx * dx + dy * dy)
|
|
216
|
+
const curvature = 0.2
|
|
217
|
+
|
|
218
|
+
// Perpendicular offset for curve
|
|
219
|
+
const px = -dy / dist * curvature * dist
|
|
220
|
+
const py = dx / dist * curvature * dist
|
|
221
|
+
|
|
222
|
+
const controlX = midX + px
|
|
223
|
+
const controlY = midY + py
|
|
224
|
+
|
|
225
|
+
const path = `M ${start.x} ${start.y} Q ${controlX} ${controlY} ${end.x} ${end.y}`
|
|
226
|
+
|
|
227
|
+
// Determine line color based on error rate
|
|
228
|
+
const errorRate = connection.errorRate || 0
|
|
229
|
+
const lineColor = errorRate > 0.05 ? "#ef4444" : errorRate > 0.01 ? "#eab308" : "#6b7280"
|
|
230
|
+
const lineWidth = Math.min(Math.max((connection.requestsPerSec || 1) / 100, 1), 4)
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<g className="cursor-pointer hover:opacity-70" onClick={onClick}>
|
|
234
|
+
{/* Main line */}
|
|
235
|
+
<path
|
|
236
|
+
d={path}
|
|
237
|
+
fill="none"
|
|
238
|
+
stroke={lineColor}
|
|
239
|
+
strokeWidth={lineWidth}
|
|
240
|
+
strokeOpacity={0.6}
|
|
241
|
+
markerEnd="url(#arrowhead)"
|
|
242
|
+
/>
|
|
243
|
+
|
|
244
|
+
{/* Label */}
|
|
245
|
+
{connection.requestsPerSec !== undefined && (
|
|
246
|
+
<text
|
|
247
|
+
x={controlX}
|
|
248
|
+
y={controlY}
|
|
249
|
+
textAnchor="middle"
|
|
250
|
+
dy={-8}
|
|
251
|
+
className="fill-muted-foreground text-xs"
|
|
252
|
+
>
|
|
253
|
+
{connection.requestsPerSec} req/s
|
|
254
|
+
</text>
|
|
255
|
+
)}
|
|
256
|
+
</g>
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function WakaServiceGraph({
|
|
261
|
+
services,
|
|
262
|
+
connections,
|
|
263
|
+
selectedService,
|
|
264
|
+
onServiceClick,
|
|
265
|
+
onConnectionClick,
|
|
266
|
+
onRefresh,
|
|
267
|
+
showMetrics = true,
|
|
268
|
+
className,
|
|
269
|
+
}: WakaServiceGraphProps) {
|
|
270
|
+
const [zoom, setZoom] = React.useState(1)
|
|
271
|
+
const containerRef = React.useRef<HTMLDivElement>(null)
|
|
272
|
+
|
|
273
|
+
// Calculate positions for services in a force-directed layout (simplified)
|
|
274
|
+
const positions = React.useMemo(() => {
|
|
275
|
+
const pos: Record<string, { x: number; y: number }> = {}
|
|
276
|
+
const width = 600
|
|
277
|
+
const height = 400
|
|
278
|
+
const centerX = width / 2
|
|
279
|
+
const centerY = height / 2
|
|
280
|
+
|
|
281
|
+
// Simple circular layout
|
|
282
|
+
const angleStep = (2 * Math.PI) / services.length
|
|
283
|
+
const radius = Math.min(width, height) * 0.35
|
|
284
|
+
|
|
285
|
+
services.forEach((service, i) => {
|
|
286
|
+
const angle = i * angleStep - Math.PI / 2
|
|
287
|
+
pos[service.id] = {
|
|
288
|
+
x: centerX + radius * Math.cos(angle),
|
|
289
|
+
y: centerY + radius * Math.sin(angle),
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
return pos
|
|
294
|
+
}, [services])
|
|
295
|
+
|
|
296
|
+
// Count services by health
|
|
297
|
+
const healthCounts = React.useMemo(() => {
|
|
298
|
+
return services.reduce((acc, s) => {
|
|
299
|
+
acc[s.health] = (acc[s.health] || 0) + 1
|
|
300
|
+
return acc
|
|
301
|
+
}, {} as Record<ServiceHealth, number>)
|
|
302
|
+
}, [services])
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
<div className={cn("flex flex-col border rounded-lg bg-background", className)}>
|
|
306
|
+
{/* Header */}
|
|
307
|
+
<div className="flex items-center justify-between gap-4 p-3 border-b">
|
|
308
|
+
<div className="flex items-center gap-3">
|
|
309
|
+
<Network className="h-5 w-5" />
|
|
310
|
+
<h3 className="font-semibold">Service Mesh</h3>
|
|
311
|
+
<Badge variant="secondary">{services.length} services</Badge>
|
|
312
|
+
|
|
313
|
+
<div className="hidden md:flex items-center gap-1">
|
|
314
|
+
{healthCounts.healthy > 0 && (
|
|
315
|
+
<Badge className="bg-green-500 text-xs">{healthCounts.healthy} healthy</Badge>
|
|
316
|
+
)}
|
|
317
|
+
{healthCounts.degraded > 0 && (
|
|
318
|
+
<Badge className="bg-yellow-500 text-xs">{healthCounts.degraded} degraded</Badge>
|
|
319
|
+
)}
|
|
320
|
+
{healthCounts.unhealthy > 0 && (
|
|
321
|
+
<Badge className="bg-red-500 text-xs">{healthCounts.unhealthy} unhealthy</Badge>
|
|
322
|
+
)}
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<div className="flex items-center gap-1">
|
|
327
|
+
<Button variant="ghost" size="sm" onClick={() => setZoom((z) => Math.max(0.5, z - 0.1))}>
|
|
328
|
+
<ZoomOut className="h-4 w-4" />
|
|
329
|
+
</Button>
|
|
330
|
+
<span className="text-xs text-muted-foreground w-12 text-center">{(zoom * 100).toFixed(0)}%</span>
|
|
331
|
+
<Button variant="ghost" size="sm" onClick={() => setZoom((z) => Math.min(2, z + 0.1))}>
|
|
332
|
+
<ZoomIn className="h-4 w-4" />
|
|
333
|
+
</Button>
|
|
334
|
+
<Button variant="ghost" size="sm" onClick={() => setZoom(1)}>
|
|
335
|
+
<Maximize2 className="h-4 w-4" />
|
|
336
|
+
</Button>
|
|
337
|
+
{onRefresh && (
|
|
338
|
+
<Button variant="ghost" size="sm" onClick={onRefresh}>
|
|
339
|
+
<RefreshCw className="h-4 w-4" />
|
|
340
|
+
</Button>
|
|
341
|
+
)}
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
{/* Legend */}
|
|
346
|
+
<div className="flex items-center gap-4 px-3 py-2 border-b bg-muted/30 text-xs flex-wrap">
|
|
347
|
+
{Object.entries(typeConfig).map(([type, config]) => {
|
|
348
|
+
const Icon = config.icon
|
|
349
|
+
return (
|
|
350
|
+
<div key={type} className="flex items-center gap-1.5">
|
|
351
|
+
<div className={cn("w-3 h-3 rounded", config.color)} />
|
|
352
|
+
<span className="capitalize">{type}</span>
|
|
353
|
+
</div>
|
|
354
|
+
)
|
|
355
|
+
})}
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
{/* Graph */}
|
|
359
|
+
<div
|
|
360
|
+
ref={containerRef}
|
|
361
|
+
className="flex-1 relative overflow-hidden min-h-[400px]"
|
|
362
|
+
style={{ cursor: "grab" }}
|
|
363
|
+
>
|
|
364
|
+
<div
|
|
365
|
+
className="absolute inset-0 transition-transform duration-200"
|
|
366
|
+
style={{
|
|
367
|
+
transform: `scale(${zoom})`,
|
|
368
|
+
transformOrigin: "center center",
|
|
369
|
+
}}
|
|
370
|
+
>
|
|
371
|
+
{/* SVG for connections */}
|
|
372
|
+
<svg className="absolute inset-0 w-full h-full pointer-events-none">
|
|
373
|
+
<defs>
|
|
374
|
+
<marker
|
|
375
|
+
id="arrowhead"
|
|
376
|
+
markerWidth="10"
|
|
377
|
+
markerHeight="7"
|
|
378
|
+
refX="9"
|
|
379
|
+
refY="3.5"
|
|
380
|
+
orient="auto"
|
|
381
|
+
>
|
|
382
|
+
<polygon
|
|
383
|
+
points="0 0, 10 3.5, 0 7"
|
|
384
|
+
fill="#6b7280"
|
|
385
|
+
/>
|
|
386
|
+
</marker>
|
|
387
|
+
</defs>
|
|
388
|
+
|
|
389
|
+
{connections.map((connection, i) => {
|
|
390
|
+
const startPos = positions[connection.source]
|
|
391
|
+
const endPos = positions[connection.target]
|
|
392
|
+
if (!startPos || !endPos) return null
|
|
393
|
+
|
|
394
|
+
return (
|
|
395
|
+
<ConnectionLine
|
|
396
|
+
key={i}
|
|
397
|
+
start={startPos}
|
|
398
|
+
end={endPos}
|
|
399
|
+
connection={connection}
|
|
400
|
+
onClick={() => onConnectionClick?.(connection)}
|
|
401
|
+
/>
|
|
402
|
+
)
|
|
403
|
+
})}
|
|
404
|
+
</svg>
|
|
405
|
+
|
|
406
|
+
{/* Service nodes */}
|
|
407
|
+
{services.map((service) => (
|
|
408
|
+
<ServiceNodeComponent
|
|
409
|
+
key={service.id}
|
|
410
|
+
service={service}
|
|
411
|
+
isSelected={selectedService === service.id}
|
|
412
|
+
onClick={() => onServiceClick?.(service)}
|
|
413
|
+
showMetrics={showMetrics}
|
|
414
|
+
position={positions[service.id]}
|
|
415
|
+
/>
|
|
416
|
+
))}
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Default sample data for demo
|
|
424
|
+
export const defaultServices: ServiceNode[] = [
|
|
425
|
+
{ id: "gateway", name: "API Gateway", type: "gateway", health: "healthy", namespace: "ingress", version: "v2.1.0", replicas: 3, requestsPerSec: 1250, errorRate: 0.001 },
|
|
426
|
+
{ id: "auth", name: "Auth Service", type: "api", health: "healthy", namespace: "auth", version: "v1.5.2", replicas: 2, requestsPerSec: 450, errorRate: 0.002 },
|
|
427
|
+
{ id: "users", name: "User Service", type: "api", health: "healthy", namespace: "users", version: "v3.0.1", replicas: 3, requestsPerSec: 320, errorRate: 0.001 },
|
|
428
|
+
{ id: "orders", name: "Order Service", type: "api", health: "degraded", namespace: "commerce", version: "v2.8.0", replicas: 4, requestsPerSec: 180, errorRate: 0.05 },
|
|
429
|
+
{ id: "postgres", name: "PostgreSQL", type: "database", health: "healthy", version: "15.2", replicas: 1 },
|
|
430
|
+
{ id: "redis", name: "Redis Cache", type: "cache", health: "healthy", version: "7.0", replicas: 3 },
|
|
431
|
+
{ id: "rabbitmq", name: "RabbitMQ", type: "queue", health: "healthy", version: "3.12", replicas: 3 },
|
|
432
|
+
{ id: "stripe", name: "Stripe API", type: "external", health: "healthy" },
|
|
433
|
+
]
|
|
434
|
+
|
|
435
|
+
export const defaultConnections: ServiceConnection[] = [
|
|
436
|
+
{ source: "gateway", target: "auth", protocol: "gRPC", requestsPerSec: 450, errorRate: 0.001 },
|
|
437
|
+
{ source: "gateway", target: "users", protocol: "gRPC", requestsPerSec: 320, errorRate: 0.001 },
|
|
438
|
+
{ source: "gateway", target: "orders", protocol: "gRPC", requestsPerSec: 180, errorRate: 0.03 },
|
|
439
|
+
{ source: "auth", target: "postgres", protocol: "PostgreSQL", requestsPerSec: 200 },
|
|
440
|
+
{ source: "auth", target: "redis", protocol: "Redis", requestsPerSec: 800 },
|
|
441
|
+
{ source: "users", target: "postgres", protocol: "PostgreSQL", requestsPerSec: 150 },
|
|
442
|
+
{ source: "orders", target: "postgres", protocol: "PostgreSQL", requestsPerSec: 100 },
|
|
443
|
+
{ source: "orders", target: "rabbitmq", protocol: "AMQP", requestsPerSec: 50 },
|
|
444
|
+
{ source: "orders", target: "stripe", protocol: "HTTPS", requestsPerSec: 30 },
|
|
445
|
+
]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../utils/cn"
|
|
5
|
+
|
|
6
|
+
export type SponsoredBadgeVariant = "default" | "subtle" | "outline" | "dark"
|
|
7
|
+
export type SponsoredBadgeSize = "sm" | "md" | "lg"
|
|
8
|
+
|
|
9
|
+
export interface WakaSponsoredBadgeProps {
|
|
10
|
+
/** Sponsor name (optional) */
|
|
11
|
+
sponsor?: string
|
|
12
|
+
/** Badge variant */
|
|
13
|
+
variant?: SponsoredBadgeVariant
|
|
14
|
+
/** Badge size */
|
|
15
|
+
size?: SponsoredBadgeSize
|
|
16
|
+
/** Show info icon */
|
|
17
|
+
showIcon?: boolean
|
|
18
|
+
/** Custom label text */
|
|
19
|
+
label?: string
|
|
20
|
+
/** Custom class name */
|
|
21
|
+
className?: string
|
|
22
|
+
/** Callback when info is clicked */
|
|
23
|
+
onInfoClick?: () => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function WakaSponsoredBadge({
|
|
27
|
+
sponsor,
|
|
28
|
+
variant = "default",
|
|
29
|
+
size = "sm",
|
|
30
|
+
showIcon = true,
|
|
31
|
+
label = "Sponsored",
|
|
32
|
+
className,
|
|
33
|
+
onInfoClick,
|
|
34
|
+
}: WakaSponsoredBadgeProps) {
|
|
35
|
+
const variantClasses: Record<SponsoredBadgeVariant, string> = {
|
|
36
|
+
default: "bg-muted text-muted-foreground border-transparent",
|
|
37
|
+
subtle: "bg-black/50 text-white border-transparent backdrop-blur-sm",
|
|
38
|
+
outline: "bg-transparent text-muted-foreground border-muted-foreground/30",
|
|
39
|
+
dark: "bg-black/70 text-white border-transparent",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const sizeClasses: Record<SponsoredBadgeSize, string> = {
|
|
43
|
+
sm: "text-[10px] px-1.5 py-0.5 gap-1",
|
|
44
|
+
md: "text-xs px-2 py-1 gap-1.5",
|
|
45
|
+
lg: "text-sm px-2.5 py-1 gap-2",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const iconSizes: Record<SponsoredBadgeSize, string> = {
|
|
49
|
+
sm: "h-2.5 w-2.5",
|
|
50
|
+
md: "h-3 w-3",
|
|
51
|
+
lg: "h-3.5 w-3.5",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<span
|
|
56
|
+
className={cn(
|
|
57
|
+
"inline-flex items-center rounded border font-medium",
|
|
58
|
+
variantClasses[variant],
|
|
59
|
+
sizeClasses[size],
|
|
60
|
+
className
|
|
61
|
+
)}
|
|
62
|
+
>
|
|
63
|
+
<span className="whitespace-nowrap">
|
|
64
|
+
{sponsor ? `${label} by ${sponsor}` : label}
|
|
65
|
+
</span>
|
|
66
|
+
|
|
67
|
+
{showIcon && (
|
|
68
|
+
<button
|
|
69
|
+
onClick={(e) => {
|
|
70
|
+
e.preventDefault()
|
|
71
|
+
e.stopPropagation()
|
|
72
|
+
onInfoClick?.()
|
|
73
|
+
}}
|
|
74
|
+
className={cn(
|
|
75
|
+
"rounded-full hover:opacity-70 transition-opacity focus:outline-none focus:ring-1 focus:ring-current",
|
|
76
|
+
iconSizes[size]
|
|
77
|
+
)}
|
|
78
|
+
aria-label="Why am I seeing this ad?"
|
|
79
|
+
>
|
|
80
|
+
<svg
|
|
81
|
+
viewBox="0 0 24 24"
|
|
82
|
+
fill="none"
|
|
83
|
+
stroke="currentColor"
|
|
84
|
+
strokeWidth={2}
|
|
85
|
+
className="w-full h-full"
|
|
86
|
+
>
|
|
87
|
+
<circle cx="12" cy="12" r="10" />
|
|
88
|
+
<path d="M12 16v-4" />
|
|
89
|
+
<path d="M12 8h.01" />
|
|
90
|
+
</svg>
|
|
91
|
+
</button>
|
|
92
|
+
)}
|
|
93
|
+
</span>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default WakaSponsoredBadge
|