@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,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
|
+
]
|