@wakastellar/ui 2.1.2 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist/blocks/apm-overview/index.d.ts +58 -0
  2. package/dist/blocks/cicd-builder/index.d.ts +47 -0
  3. package/dist/blocks/cloud-cost-dashboard/index.d.ts +49 -0
  4. package/dist/blocks/container-orchestrator/index.d.ts +63 -0
  5. package/dist/blocks/database-admin/index.d.ts +84 -0
  6. package/dist/blocks/gitops-sync-status/index.d.ts +45 -0
  7. package/dist/blocks/incident-manager/index.d.ts +44 -0
  8. package/dist/blocks/index.d.ts +10 -0
  9. package/dist/blocks/infrastructure-map/index.d.ts +32 -0
  10. package/dist/blocks/on-call-schedule/index.d.ts +43 -0
  11. package/dist/blocks/release-notes/index.d.ts +49 -0
  12. package/dist/components/index.d.ts +34 -0
  13. package/dist/components/waka-ad-banner/index.d.ts +36 -0
  14. package/dist/components/waka-ad-fallback/index.d.ts +33 -0
  15. package/dist/components/waka-ad-inline/index.d.ts +15 -0
  16. package/dist/components/waka-ad-interstitial/index.d.ts +26 -0
  17. package/dist/components/waka-ad-placeholder/index.d.ts +17 -0
  18. package/dist/components/waka-ad-provider/index.d.ts +103 -0
  19. package/dist/components/waka-ad-sidebar/index.d.ts +18 -0
  20. package/dist/components/waka-ad-sticky-footer/index.d.ts +17 -0
  21. package/dist/components/waka-alert-panel/index.d.ts +45 -0
  22. package/dist/components/waka-artifact-list/index.d.ts +32 -0
  23. package/dist/components/waka-build-matrix/index.d.ts +36 -0
  24. package/dist/components/waka-config-comparator/index.d.ts +37 -0
  25. package/dist/components/waka-container-list/index.d.ts +51 -0
  26. package/dist/components/waka-content-recommendation/index.d.ts +23 -0
  27. package/dist/components/waka-database-card/index.d.ts +46 -0
  28. package/dist/components/waka-dependency-tree/index.d.ts +38 -0
  29. package/dist/components/waka-env-var-editor/index.d.ts +30 -0
  30. package/dist/components/waka-feature-flag-row/index.d.ts +45 -0
  31. package/dist/components/waka-kubernetes-overview/index.d.ts +98 -0
  32. package/dist/components/waka-log-viewer/index.d.ts +38 -0
  33. package/dist/components/waka-migration-list/index.d.ts +36 -0
  34. package/dist/components/waka-outstream-video/index.d.ts +24 -0
  35. package/dist/components/waka-pod-card/index.d.ts +73 -0
  36. package/dist/components/waka-query-explain/index.d.ts +48 -0
  37. package/dist/components/waka-secret-card/index.d.ts +43 -0
  38. package/dist/components/waka-security-scan-result/index.d.ts +45 -0
  39. package/dist/components/waka-service-graph/index.d.ts +44 -0
  40. package/dist/components/waka-sponsored-badge/index.d.ts +20 -0
  41. package/dist/components/waka-sponsored-card/index.d.ts +25 -0
  42. package/dist/components/waka-sponsored-feed/index.d.ts +31 -0
  43. package/dist/components/waka-test-report/index.d.ts +60 -0
  44. package/dist/components/waka-trace-viewer/index.d.ts +36 -0
  45. package/dist/components/waka-video-ad/index.d.ts +32 -0
  46. package/dist/components/waka-video-overlay/index.d.ts +26 -0
  47. package/dist/index.cjs.js +251 -200
  48. package/dist/index.d.ts +1 -0
  49. package/dist/index.es.js +47315 -35823
  50. package/dist/utils/security.d.ts +96 -0
  51. package/package.json +4 -4
  52. package/src/blocks/apm-overview/index.tsx +672 -0
  53. package/src/blocks/cicd-builder/index.tsx +738 -0
  54. package/src/blocks/cloud-cost-dashboard/index.tsx +597 -0
  55. package/src/blocks/container-orchestrator/index.tsx +729 -0
  56. package/src/blocks/database-admin/index.tsx +679 -0
  57. package/src/blocks/gitops-sync-status/index.tsx +557 -0
  58. package/src/blocks/incident-manager/index.tsx +586 -0
  59. package/src/blocks/index.ts +119 -0
  60. package/src/blocks/infrastructure-map/index.tsx +638 -0
  61. package/src/blocks/on-call-schedule/index.tsx +615 -0
  62. package/src/blocks/release-notes/index.tsx +643 -0
  63. package/src/blocks/sidebar/index.tsx +6 -6
  64. package/src/components/DataTable/templates/index.tsx +3 -2
  65. package/src/components/index.ts +283 -0
  66. package/src/components/waka-3d-pie-chart/index.tsx +11 -11
  67. package/src/components/waka-achievement-unlock/index.tsx +16 -16
  68. package/src/components/waka-ad-banner/index.tsx +275 -0
  69. package/src/components/waka-ad-fallback/index.tsx +181 -0
  70. package/src/components/waka-ad-inline/index.tsx +103 -0
  71. package/src/components/waka-ad-interstitial/index.tsx +278 -0
  72. package/src/components/waka-ad-placeholder/index.tsx +84 -0
  73. package/src/components/waka-ad-provider/index.tsx +329 -0
  74. package/src/components/waka-ad-sidebar/index.tsx +113 -0
  75. package/src/components/waka-ad-sticky-footer/index.tsx +125 -0
  76. package/src/components/waka-alert-panel/index.tsx +493 -0
  77. package/src/components/waka-artifact-list/index.tsx +416 -0
  78. package/src/components/waka-badge-showcase/index.tsx +12 -11
  79. package/src/components/waka-build-matrix/index.tsx +396 -0
  80. package/src/components/waka-command-bar/index.tsx +2 -1
  81. package/src/components/waka-config-comparator/index.tsx +416 -0
  82. package/src/components/waka-container-list/index.tsx +475 -0
  83. package/src/components/waka-content-recommendation/index.tsx +294 -0
  84. package/src/components/waka-cost-breakdown/index.tsx +10 -10
  85. package/src/components/waka-database-card/index.tsx +473 -0
  86. package/src/components/waka-dependency-tree/index.tsx +542 -0
  87. package/src/components/waka-env-var-editor/index.tsx +417 -0
  88. package/src/components/waka-feature-flag-row/index.tsx +386 -0
  89. package/src/components/waka-funnel-chart/index.tsx +8 -8
  90. package/src/components/waka-health-pulse/index.tsx +6 -6
  91. package/src/components/waka-kubernetes-overview/index.tsx +536 -0
  92. package/src/components/waka-leaderboard/index.tsx +9 -9
  93. package/src/components/waka-log-viewer/index.tsx +386 -0
  94. package/src/components/waka-loot-box/index.tsx +20 -20
  95. package/src/components/waka-migration-list/index.tsx +487 -0
  96. package/src/components/waka-outstream-video/index.tsx +240 -0
  97. package/src/components/waka-player-card/index.tsx +5 -5
  98. package/src/components/waka-pod-card/index.tsx +528 -0
  99. package/src/components/waka-query-explain/index.tsx +657 -0
  100. package/src/components/waka-quota-bar/index.tsx +4 -4
  101. package/src/components/waka-radar-score/index.tsx +10 -10
  102. package/src/components/waka-scratch-card/index.tsx +5 -4
  103. package/src/components/waka-secret-card/index.tsx +371 -0
  104. package/src/components/waka-security-scan-result/index.tsx +473 -0
  105. package/src/components/waka-server-rack/index.tsx +28 -27
  106. package/src/components/waka-service-graph/index.tsx +445 -0
  107. package/src/components/waka-sponsored-badge/index.tsx +97 -0
  108. package/src/components/waka-sponsored-card/index.tsx +275 -0
  109. package/src/components/waka-sponsored-feed/index.tsx +127 -0
  110. package/src/components/waka-spotlight/index.tsx +2 -1
  111. package/src/components/waka-success-explosion/index.tsx +4 -4
  112. package/src/components/waka-test-report/index.tsx +469 -0
  113. package/src/components/waka-trace-viewer/index.tsx +490 -0
  114. package/src/components/waka-video-ad/index.tsx +406 -0
  115. package/src/components/waka-video-overlay/index.tsx +257 -0
  116. package/src/components/waka-xp-bar/index.tsx +13 -13
  117. package/src/styles/base.css +16 -0
  118. package/src/styles/tailwind.preset.js +12 -0
  119. package/src/styles/themes/forest.css +16 -0
  120. package/src/styles/themes/monochrome.css +16 -0
  121. package/src/styles/themes/perpetuity.css +16 -0
  122. package/src/styles/themes/sunset.css +16 -0
  123. package/src/styles/themes/twilight.css +16 -0
@@ -0,0 +1,729 @@
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 { Input } from "../../components/input"
10
+ import { ScrollArea } from "../../components/scroll-area"
11
+ import {
12
+ Select,
13
+ SelectContent,
14
+ SelectItem,
15
+ SelectTrigger,
16
+ SelectValue,
17
+ } from "../../components/select"
18
+ import {
19
+ DropdownMenu,
20
+ DropdownMenuContent,
21
+ DropdownMenuItem,
22
+ DropdownMenuSeparator,
23
+ DropdownMenuTrigger,
24
+ } from "../../components/dropdown-menu"
25
+ import {
26
+ Container,
27
+ Play,
28
+ Pause,
29
+ Square,
30
+ RefreshCw,
31
+ Plus,
32
+ Minus,
33
+ Trash2,
34
+ MoreVertical,
35
+ Search,
36
+ Activity,
37
+ Cpu,
38
+ MemoryStick,
39
+ Network,
40
+ Clock,
41
+ AlertTriangle,
42
+ CheckCircle2,
43
+ XCircle,
44
+ ArrowUp,
45
+ ArrowDown,
46
+ Server,
47
+ Layers,
48
+ Settings,
49
+ ExternalLink,
50
+ Terminal,
51
+ FileText,
52
+ Eye,
53
+ } from "lucide-react"
54
+
55
+ export type ContainerStatus = "running" | "stopped" | "paused" | "restarting" | "creating" | "removing" | "exited" | "dead"
56
+ export type ServiceStatus = "running" | "updating" | "scaled" | "failed" | "pending"
57
+
58
+ export interface ContainerResource {
59
+ cpu: number // percentage
60
+ memory: number // in MB
61
+ memoryLimit: number // in MB
62
+ networkRx: number // bytes/s
63
+ networkTx: number // bytes/s
64
+ }
65
+
66
+ export interface OrchestratorContainer {
67
+ id: string
68
+ name: string
69
+ image: string
70
+ status: ContainerStatus
71
+ createdAt: Date
72
+ startedAt?: Date
73
+ ports?: { host: number; container: number; protocol: "tcp" | "udp" }[]
74
+ resources: ContainerResource
75
+ healthCheck?: "healthy" | "unhealthy" | "starting" | "none"
76
+ restartCount?: number
77
+ node?: string
78
+ }
79
+
80
+ export interface OrchestratorService {
81
+ id: string
82
+ name: string
83
+ image: string
84
+ status: ServiceStatus
85
+ replicas: {
86
+ desired: number
87
+ running: number
88
+ ready: number
89
+ }
90
+ containers: OrchestratorContainer[]
91
+ updateStatus?: {
92
+ state: "updating" | "completed" | "paused" | "rollback"
93
+ progress: number
94
+ message?: string
95
+ }
96
+ ports?: { published: number; target: number }[]
97
+ labels?: Record<string, string>
98
+ createdAt: Date
99
+ updatedAt: Date
100
+ }
101
+
102
+ export interface ContainerOrchestratorProps {
103
+ services: OrchestratorService[]
104
+ onScaleService?: (service: OrchestratorService, replicas: number) => void
105
+ onRestartService?: (service: OrchestratorService) => void
106
+ onStopService?: (service: OrchestratorService) => void
107
+ onDeleteService?: (service: OrchestratorService) => void
108
+ onViewLogs?: (container: OrchestratorContainer) => void
109
+ onExecShell?: (container: OrchestratorContainer) => void
110
+ onRefresh?: () => void
111
+ className?: string
112
+ }
113
+
114
+ const containerStatusConfig: Record<ContainerStatus, { color: string; bgColor: string; label: string }> = {
115
+ running: { color: "text-green-500", bgColor: "bg-green-500", label: "Running" },
116
+ stopped: { color: "text-gray-500", bgColor: "bg-gray-500", label: "Stopped" },
117
+ paused: { color: "text-yellow-500", bgColor: "bg-yellow-500", label: "Paused" },
118
+ restarting: { color: "text-blue-500", bgColor: "bg-blue-500", label: "Restarting" },
119
+ creating: { color: "text-blue-400", bgColor: "bg-blue-400", label: "Creating" },
120
+ removing: { color: "text-orange-500", bgColor: "bg-orange-500", label: "Removing" },
121
+ exited: { color: "text-red-500", bgColor: "bg-red-500", label: "Exited" },
122
+ dead: { color: "text-red-600", bgColor: "bg-red-600", label: "Dead" },
123
+ }
124
+
125
+ const serviceStatusConfig: Record<ServiceStatus, { color: string; bgColor: string; label: string }> = {
126
+ running: { color: "text-green-500", bgColor: "bg-green-500", label: "Running" },
127
+ updating: { color: "text-blue-500", bgColor: "bg-blue-500", label: "Updating" },
128
+ scaled: { color: "text-purple-500", bgColor: "bg-purple-500", label: "Scaling" },
129
+ failed: { color: "text-red-500", bgColor: "bg-red-500", label: "Failed" },
130
+ pending: { color: "text-yellow-500", bgColor: "bg-yellow-500", label: "Pending" },
131
+ }
132
+
133
+ function formatBytes(bytes: number): string {
134
+ if (bytes < 1024) return `${bytes} B`
135
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
136
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
137
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`
138
+ }
139
+
140
+ function formatUptime(startDate?: Date): string {
141
+ if (!startDate) return "N/A"
142
+ const seconds = Math.floor((Date.now() - startDate.getTime()) / 1000)
143
+ if (seconds < 60) return `${seconds}s`
144
+ const minutes = Math.floor(seconds / 60)
145
+ if (minutes < 60) return `${minutes}m`
146
+ const hours = Math.floor(minutes / 60)
147
+ if (hours < 24) return `${hours}h ${minutes % 60}m`
148
+ const days = Math.floor(hours / 24)
149
+ return `${days}d ${hours % 24}h`
150
+ }
151
+
152
+ function ContainerCard({
153
+ container,
154
+ onViewLogs,
155
+ onExecShell,
156
+ }: {
157
+ container: OrchestratorContainer
158
+ onViewLogs?: () => void
159
+ onExecShell?: () => void
160
+ }) {
161
+ const statusConf = containerStatusConfig[container.status]
162
+ const memoryPercent = (container.resources.memory / container.resources.memoryLimit) * 100
163
+
164
+ return (
165
+ <div className={cn(
166
+ "border rounded-lg p-3 transition-all hover:bg-muted/30",
167
+ container.status === "exited" && "border-red-500/30 bg-red-500/5",
168
+ container.healthCheck === "unhealthy" && "border-yellow-500/30"
169
+ )}>
170
+ <div className="flex items-start gap-3">
171
+ <div className="relative">
172
+ <Container className={cn("h-5 w-5", statusConf.color)} />
173
+ {container.healthCheck && container.healthCheck !== "none" && (
174
+ <div className={cn(
175
+ "absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-background",
176
+ container.healthCheck === "healthy" && "bg-green-500",
177
+ container.healthCheck === "unhealthy" && "bg-red-500",
178
+ container.healthCheck === "starting" && "bg-yellow-500"
179
+ )} />
180
+ )}
181
+ </div>
182
+
183
+ <div className="flex-1 min-w-0">
184
+ <div className="flex items-center gap-2">
185
+ <span className="font-mono text-sm truncate">{container.name}</span>
186
+ <Badge variant="outline" className={cn("text-xs", statusConf.color)}>
187
+ {statusConf.label}
188
+ </Badge>
189
+ </div>
190
+ <div className="text-xs text-muted-foreground mt-1 truncate">
191
+ {container.image}
192
+ </div>
193
+
194
+ {/* Resource usage */}
195
+ <div className="grid grid-cols-2 gap-2 mt-2">
196
+ <div>
197
+ <div className="flex items-center justify-between text-xs mb-0.5">
198
+ <span className="text-muted-foreground flex items-center gap-1">
199
+ <Cpu className="h-3 w-3" /> CPU
200
+ </span>
201
+ <span>{container.resources.cpu.toFixed(1)}%</span>
202
+ </div>
203
+ <Progress
204
+ value={container.resources.cpu}
205
+ className={cn("h-1", container.resources.cpu > 80 && "[&>div]:bg-red-500")}
206
+ />
207
+ </div>
208
+ <div>
209
+ <div className="flex items-center justify-between text-xs mb-0.5">
210
+ <span className="text-muted-foreground flex items-center gap-1">
211
+ <MemoryStick className="h-3 w-3" /> MEM
212
+ </span>
213
+ <span>{container.resources.memory}MB</span>
214
+ </div>
215
+ <Progress
216
+ value={memoryPercent}
217
+ className={cn("h-1", memoryPercent > 80 && "[&>div]:bg-red-500")}
218
+ />
219
+ </div>
220
+ </div>
221
+
222
+ {/* Network and uptime */}
223
+ <div className="flex items-center gap-3 mt-2 text-xs text-muted-foreground">
224
+ <span className="flex items-center gap-1">
225
+ <Clock className="h-3 w-3" />
226
+ {formatUptime(container.startedAt)}
227
+ </span>
228
+ <span className="flex items-center gap-1">
229
+ <ArrowDown className="h-3 w-3" />
230
+ {formatBytes(container.resources.networkRx)}/s
231
+ </span>
232
+ <span className="flex items-center gap-1">
233
+ <ArrowUp className="h-3 w-3" />
234
+ {formatBytes(container.resources.networkTx)}/s
235
+ </span>
236
+ {container.restartCount !== undefined && container.restartCount > 0 && (
237
+ <span className="flex items-center gap-1 text-yellow-500">
238
+ <RefreshCw className="h-3 w-3" />
239
+ {container.restartCount} restarts
240
+ </span>
241
+ )}
242
+ </div>
243
+ </div>
244
+
245
+ {/* Actions */}
246
+ <DropdownMenu>
247
+ <DropdownMenuTrigger asChild>
248
+ <Button variant="ghost" size="sm" className="h-8 w-8 p-0">
249
+ <MoreVertical className="h-4 w-4" />
250
+ </Button>
251
+ </DropdownMenuTrigger>
252
+ <DropdownMenuContent align="end">
253
+ {onViewLogs && (
254
+ <DropdownMenuItem onClick={onViewLogs}>
255
+ <FileText className="h-4 w-4 mr-2" />
256
+ View Logs
257
+ </DropdownMenuItem>
258
+ )}
259
+ {onExecShell && (
260
+ <DropdownMenuItem onClick={onExecShell}>
261
+ <Terminal className="h-4 w-4 mr-2" />
262
+ Shell
263
+ </DropdownMenuItem>
264
+ )}
265
+ </DropdownMenuContent>
266
+ </DropdownMenu>
267
+ </div>
268
+ </div>
269
+ )
270
+ }
271
+
272
+ function ServiceCard({
273
+ service,
274
+ onScale,
275
+ onRestart,
276
+ onStop,
277
+ onDelete,
278
+ onViewLogs,
279
+ onExecShell,
280
+ }: {
281
+ service: OrchestratorService
282
+ onScale?: (replicas: number) => void
283
+ onRestart?: () => void
284
+ onStop?: () => void
285
+ onDelete?: () => void
286
+ onViewLogs?: (container: OrchestratorContainer) => void
287
+ onExecShell?: (container: OrchestratorContainer) => void
288
+ }) {
289
+ const [expanded, setExpanded] = React.useState(false)
290
+ const [scaleValue, setScaleValue] = React.useState(service.replicas.desired)
291
+ const statusConf = serviceStatusConfig[service.status]
292
+
293
+ const allHealthy = service.containers.every((c) => c.status === "running")
294
+ const runningPercent = (service.replicas.running / service.replicas.desired) * 100
295
+
296
+ return (
297
+ <Card className={cn(
298
+ service.status === "failed" && "border-red-500/50",
299
+ !allHealthy && "border-yellow-500/30"
300
+ )}>
301
+ <CardContent className="p-4">
302
+ <div className="flex items-start gap-4">
303
+ {/* Service icon */}
304
+ <div className={cn("p-2 rounded-lg", `${statusConf.bgColor}/10`)}>
305
+ <Layers className={cn("h-5 w-5", statusConf.color)} />
306
+ </div>
307
+
308
+ {/* Main info */}
309
+ <div className="flex-1 min-w-0">
310
+ <div className="flex items-center gap-2">
311
+ <h4 className="font-semibold">{service.name}</h4>
312
+ <Badge className={cn("text-white text-xs", statusConf.bgColor)}>
313
+ {statusConf.label}
314
+ </Badge>
315
+ </div>
316
+ <div className="text-sm text-muted-foreground mt-1">
317
+ {service.image}
318
+ </div>
319
+
320
+ {/* Replicas */}
321
+ <div className="mt-3">
322
+ <div className="flex items-center justify-between text-sm mb-1">
323
+ <span className="text-muted-foreground">Replicas</span>
324
+ <span className={cn(
325
+ service.replicas.running < service.replicas.desired && "text-yellow-500"
326
+ )}>
327
+ {service.replicas.running}/{service.replicas.desired} running
328
+ {service.replicas.ready < service.replicas.running && (
329
+ <span className="text-muted-foreground ml-1">
330
+ ({service.replicas.ready} ready)
331
+ </span>
332
+ )}
333
+ </span>
334
+ </div>
335
+ <Progress
336
+ value={runningPercent}
337
+ className={cn(
338
+ "h-2",
339
+ runningPercent < 100 && "[&>div]:bg-yellow-500",
340
+ service.status === "failed" && "[&>div]:bg-red-500"
341
+ )}
342
+ />
343
+ </div>
344
+
345
+ {/* Update status */}
346
+ {service.updateStatus && (
347
+ <div className="mt-3 p-2 bg-muted/50 rounded text-sm">
348
+ <div className="flex items-center justify-between">
349
+ <span className="flex items-center gap-1 text-blue-500">
350
+ <RefreshCw className="h-3 w-3 animate-spin" />
351
+ {service.updateStatus.state}
352
+ </span>
353
+ <span>{service.updateStatus.progress}%</span>
354
+ </div>
355
+ {service.updateStatus.message && (
356
+ <p className="text-xs text-muted-foreground mt-1">
357
+ {service.updateStatus.message}
358
+ </p>
359
+ )}
360
+ <Progress value={service.updateStatus.progress} className="h-1 mt-2" />
361
+ </div>
362
+ )}
363
+
364
+ {/* Ports */}
365
+ {service.ports && service.ports.length > 0 && (
366
+ <div className="flex items-center gap-2 mt-3 text-xs text-muted-foreground">
367
+ <Network className="h-3 w-3" />
368
+ {service.ports.map((port, i) => (
369
+ <span key={i}>
370
+ {port.published}:{port.target}
371
+ {i < service.ports!.length - 1 && ","}
372
+ </span>
373
+ ))}
374
+ </div>
375
+ )}
376
+ </div>
377
+
378
+ {/* Actions */}
379
+ <div className="flex items-center gap-2 shrink-0">
380
+ {/* Scale controls */}
381
+ <div className="flex items-center gap-1 border rounded">
382
+ <Button
383
+ variant="ghost"
384
+ size="sm"
385
+ className="h-8 w-8 p-0"
386
+ onClick={() => {
387
+ const newValue = Math.max(0, scaleValue - 1)
388
+ setScaleValue(newValue)
389
+ onScale?.(newValue)
390
+ }}
391
+ >
392
+ <Minus className="h-4 w-4" />
393
+ </Button>
394
+ <Input
395
+ type="number"
396
+ value={scaleValue}
397
+ onChange={(e) => {
398
+ const value = parseInt(e.target.value) || 0
399
+ setScaleValue(value)
400
+ }}
401
+ onBlur={() => onScale?.(scaleValue)}
402
+ className="w-12 h-8 text-center border-0 p-0"
403
+ min={0}
404
+ />
405
+ <Button
406
+ variant="ghost"
407
+ size="sm"
408
+ className="h-8 w-8 p-0"
409
+ onClick={() => {
410
+ const newValue = scaleValue + 1
411
+ setScaleValue(newValue)
412
+ onScale?.(newValue)
413
+ }}
414
+ >
415
+ <Plus className="h-4 w-4" />
416
+ </Button>
417
+ </div>
418
+
419
+ <DropdownMenu>
420
+ <DropdownMenuTrigger asChild>
421
+ <Button variant="outline" size="sm" className="h-8 w-8 p-0">
422
+ <MoreVertical className="h-4 w-4" />
423
+ </Button>
424
+ </DropdownMenuTrigger>
425
+ <DropdownMenuContent align="end">
426
+ {onRestart && (
427
+ <DropdownMenuItem onClick={onRestart}>
428
+ <RefreshCw className="h-4 w-4 mr-2" />
429
+ Restart
430
+ </DropdownMenuItem>
431
+ )}
432
+ {onStop && (
433
+ <DropdownMenuItem onClick={onStop}>
434
+ <Square className="h-4 w-4 mr-2" />
435
+ Stop
436
+ </DropdownMenuItem>
437
+ )}
438
+ <DropdownMenuSeparator />
439
+ {onDelete && (
440
+ <DropdownMenuItem onClick={onDelete} className="text-destructive">
441
+ <Trash2 className="h-4 w-4 mr-2" />
442
+ Delete
443
+ </DropdownMenuItem>
444
+ )}
445
+ </DropdownMenuContent>
446
+ </DropdownMenu>
447
+
448
+ <Button
449
+ variant="ghost"
450
+ size="sm"
451
+ onClick={() => setExpanded(!expanded)}
452
+ >
453
+ <Eye className="h-4 w-4 mr-1" />
454
+ {expanded ? "Hide" : "Show"} Containers
455
+ </Button>
456
+ </div>
457
+ </div>
458
+
459
+ {/* Containers */}
460
+ {expanded && (
461
+ <div className="mt-4 pt-4 border-t space-y-2">
462
+ {service.containers.length === 0 ? (
463
+ <div className="text-center text-muted-foreground py-4">
464
+ No containers running
465
+ </div>
466
+ ) : (
467
+ service.containers.map((container) => (
468
+ <ContainerCard
469
+ key={container.id}
470
+ container={container}
471
+ onViewLogs={onViewLogs ? () => onViewLogs(container) : undefined}
472
+ onExecShell={onExecShell ? () => onExecShell(container) : undefined}
473
+ />
474
+ ))
475
+ )}
476
+ </div>
477
+ )}
478
+ </CardContent>
479
+ </Card>
480
+ )
481
+ }
482
+
483
+ function ClusterStats({ services }: { services: OrchestratorService[] }) {
484
+ const stats = React.useMemo(() => {
485
+ const totalServices = services.length
486
+ const totalContainers = services.reduce((acc, s) => acc + s.containers.length, 0)
487
+ const runningContainers = services.reduce((acc, s) =>
488
+ acc + s.containers.filter((c) => c.status === "running").length, 0
489
+ )
490
+ const totalCpu = services.reduce((acc, s) =>
491
+ acc + s.containers.reduce((a, c) => a + c.resources.cpu, 0), 0
492
+ )
493
+ const totalMemory = services.reduce((acc, s) =>
494
+ acc + s.containers.reduce((a, c) => a + c.resources.memory, 0), 0
495
+ )
496
+
497
+ return { totalServices, totalContainers, runningContainers, avgCpu: totalCpu / totalContainers, totalMemory }
498
+ }, [services])
499
+
500
+ return (
501
+ <div className="grid grid-cols-5 gap-4">
502
+ <Card>
503
+ <CardContent className="p-4 text-center">
504
+ <div className="text-2xl font-bold">{stats.totalServices}</div>
505
+ <div className="text-sm text-muted-foreground">Services</div>
506
+ </CardContent>
507
+ </Card>
508
+ <Card>
509
+ <CardContent className="p-4 text-center">
510
+ <div className="text-2xl font-bold">{stats.totalContainers}</div>
511
+ <div className="text-sm text-muted-foreground">Containers</div>
512
+ </CardContent>
513
+ </Card>
514
+ <Card>
515
+ <CardContent className="p-4 text-center">
516
+ <div className="text-2xl font-bold text-green-500">{stats.runningContainers}</div>
517
+ <div className="text-sm text-muted-foreground">Running</div>
518
+ </CardContent>
519
+ </Card>
520
+ <Card>
521
+ <CardContent className="p-4 text-center">
522
+ <div className="text-2xl font-bold">{stats.avgCpu.toFixed(1)}%</div>
523
+ <div className="text-sm text-muted-foreground">Avg CPU</div>
524
+ </CardContent>
525
+ </Card>
526
+ <Card>
527
+ <CardContent className="p-4 text-center">
528
+ <div className="text-2xl font-bold">{(stats.totalMemory / 1024).toFixed(1)}GB</div>
529
+ <div className="text-sm text-muted-foreground">Total Memory</div>
530
+ </CardContent>
531
+ </Card>
532
+ </div>
533
+ )
534
+ }
535
+
536
+ export function ContainerOrchestrator({
537
+ services,
538
+ onScaleService,
539
+ onRestartService,
540
+ onStopService,
541
+ onDeleteService,
542
+ onViewLogs,
543
+ onExecShell,
544
+ onRefresh,
545
+ className,
546
+ }: ContainerOrchestratorProps) {
547
+ const [searchQuery, setSearchQuery] = React.useState("")
548
+ const [statusFilter, setStatusFilter] = React.useState<ServiceStatus | "all">("all")
549
+
550
+ const filteredServices = React.useMemo(() => {
551
+ return services.filter((s) => {
552
+ if (searchQuery && !s.name.toLowerCase().includes(searchQuery.toLowerCase())) return false
553
+ if (statusFilter !== "all" && s.status !== statusFilter) return false
554
+ return true
555
+ })
556
+ }, [services, searchQuery, statusFilter])
557
+
558
+ return (
559
+ <div className={cn("space-y-6", className)}>
560
+ {/* Header */}
561
+ <Card>
562
+ <CardHeader>
563
+ <div className="flex items-center justify-between">
564
+ <div className="flex items-center gap-3">
565
+ <Container className="h-6 w-6" />
566
+ <div>
567
+ <CardTitle>Container Orchestrator</CardTitle>
568
+ <p className="text-sm text-muted-foreground">
569
+ Manage and monitor containerized services
570
+ </p>
571
+ </div>
572
+ </div>
573
+
574
+ <div className="flex items-center gap-2">
575
+ <div className="relative">
576
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
577
+ <Input
578
+ placeholder="Search services..."
579
+ value={searchQuery}
580
+ onChange={(e) => setSearchQuery(e.target.value)}
581
+ className="pl-9 w-64"
582
+ />
583
+ </div>
584
+
585
+ <Select value={statusFilter} onValueChange={(v) => setStatusFilter(v as ServiceStatus | "all")}>
586
+ <SelectTrigger className="w-32">
587
+ <SelectValue placeholder="Status" />
588
+ </SelectTrigger>
589
+ <SelectContent>
590
+ <SelectItem value="all">All Status</SelectItem>
591
+ <SelectItem value="running">Running</SelectItem>
592
+ <SelectItem value="updating">Updating</SelectItem>
593
+ <SelectItem value="failed">Failed</SelectItem>
594
+ <SelectItem value="pending">Pending</SelectItem>
595
+ </SelectContent>
596
+ </Select>
597
+
598
+ {onRefresh && (
599
+ <Button variant="outline" onClick={onRefresh}>
600
+ <RefreshCw className="h-4 w-4" />
601
+ </Button>
602
+ )}
603
+ </div>
604
+ </div>
605
+ </CardHeader>
606
+ </Card>
607
+
608
+ {/* Stats */}
609
+ <ClusterStats services={services} />
610
+
611
+ {/* Services */}
612
+ <ScrollArea className="h-[500px]">
613
+ <div className="space-y-4 pr-4">
614
+ {filteredServices.length === 0 ? (
615
+ <div className="flex flex-col items-center justify-center h-48 text-muted-foreground">
616
+ <Container className="h-12 w-12 mb-4" />
617
+ <p className="text-lg font-medium">No services found</p>
618
+ </div>
619
+ ) : (
620
+ filteredServices.map((service) => (
621
+ <ServiceCard
622
+ key={service.id}
623
+ service={service}
624
+ onScale={onScaleService ? (n) => onScaleService(service, n) : undefined}
625
+ onRestart={onRestartService ? () => onRestartService(service) : undefined}
626
+ onStop={onStopService ? () => onStopService(service) : undefined}
627
+ onDelete={onDeleteService ? () => onDeleteService(service) : undefined}
628
+ onViewLogs={onViewLogs}
629
+ onExecShell={onExecShell}
630
+ />
631
+ ))
632
+ )}
633
+ </div>
634
+ </ScrollArea>
635
+ </div>
636
+ )
637
+ }
638
+
639
+ // Default sample data
640
+ function generateContainers(serviceName: string, count: number): OrchestratorContainer[] {
641
+ return Array.from({ length: count }, (_, i) => ({
642
+ id: `${serviceName}-${i + 1}`,
643
+ name: `${serviceName}-${i + 1}`,
644
+ image: `myregistry/${serviceName}:latest`,
645
+ status: "running" as ContainerStatus,
646
+ createdAt: new Date(Date.now() - (i + 1) * 3600000),
647
+ startedAt: new Date(Date.now() - i * 3600000),
648
+ healthCheck: "healthy" as const,
649
+ restartCount: Math.floor(Math.random() * 3),
650
+ node: `node-${Math.floor(Math.random() * 3) + 1}`,
651
+ resources: {
652
+ cpu: Math.random() * 50 + 10,
653
+ memory: Math.floor(Math.random() * 500 + 200),
654
+ memoryLimit: 1024,
655
+ networkRx: Math.floor(Math.random() * 1000000),
656
+ networkTx: Math.floor(Math.random() * 500000),
657
+ },
658
+ }))
659
+ }
660
+
661
+ export const defaultOrchestratorServices: OrchestratorService[] = [
662
+ {
663
+ id: "1",
664
+ name: "api-gateway",
665
+ image: "myregistry/api-gateway:v2.4.1",
666
+ status: "running",
667
+ replicas: { desired: 3, running: 3, ready: 3 },
668
+ containers: generateContainers("api-gateway", 3),
669
+ ports: [{ published: 80, target: 8080 }, { published: 443, target: 8443 }],
670
+ createdAt: new Date(Date.now() - 7 * 24 * 3600000),
671
+ updatedAt: new Date(Date.now() - 2 * 24 * 3600000),
672
+ },
673
+ {
674
+ id: "2",
675
+ name: "user-service",
676
+ image: "myregistry/user-service:v1.8.0",
677
+ status: "updating",
678
+ replicas: { desired: 2, running: 2, ready: 1 },
679
+ containers: generateContainers("user-service", 2),
680
+ updateStatus: { state: "updating", progress: 50, message: "Updating replica 1 of 2" },
681
+ ports: [{ published: 8081, target: 8080 }],
682
+ createdAt: new Date(Date.now() - 14 * 24 * 3600000),
683
+ updatedAt: new Date(),
684
+ },
685
+ {
686
+ id: "3",
687
+ name: "payment-service",
688
+ image: "myregistry/payment-service:v3.1.0",
689
+ status: "running",
690
+ replicas: { desired: 2, running: 2, ready: 2 },
691
+ containers: generateContainers("payment-service", 2),
692
+ ports: [{ published: 8082, target: 8080 }],
693
+ createdAt: new Date(Date.now() - 30 * 24 * 3600000),
694
+ updatedAt: new Date(Date.now() - 5 * 24 * 3600000),
695
+ },
696
+ {
697
+ id: "4",
698
+ name: "notification-worker",
699
+ image: "myregistry/notification-worker:v2.0.0",
700
+ status: "running",
701
+ replicas: { desired: 5, running: 5, ready: 5 },
702
+ containers: generateContainers("notification-worker", 5),
703
+ createdAt: new Date(Date.now() - 21 * 24 * 3600000),
704
+ updatedAt: new Date(Date.now() - 7 * 24 * 3600000),
705
+ },
706
+ {
707
+ id: "5",
708
+ name: "analytics-pipeline",
709
+ image: "myregistry/analytics:v1.2.0",
710
+ status: "failed",
711
+ replicas: { desired: 3, running: 1, ready: 1 },
712
+ containers: [
713
+ ...generateContainers("analytics-pipeline", 1),
714
+ {
715
+ id: "analytics-pipeline-2",
716
+ name: "analytics-pipeline-2",
717
+ image: "myregistry/analytics:v1.2.0",
718
+ status: "exited",
719
+ createdAt: new Date(Date.now() - 2 * 3600000),
720
+ healthCheck: "unhealthy",
721
+ restartCount: 5,
722
+ node: "node-2",
723
+ resources: { cpu: 0, memory: 0, memoryLimit: 1024, networkRx: 0, networkTx: 0 },
724
+ },
725
+ ],
726
+ createdAt: new Date(Date.now() - 10 * 24 * 3600000),
727
+ updatedAt: new Date(Date.now() - 1 * 24 * 3600000),
728
+ },
729
+ ]