@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,638 @@
|
|
|
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 {
|
|
9
|
+
Tooltip,
|
|
10
|
+
TooltipContent,
|
|
11
|
+
TooltipProvider,
|
|
12
|
+
TooltipTrigger,
|
|
13
|
+
} from "../../components/tooltip"
|
|
14
|
+
import {
|
|
15
|
+
Select,
|
|
16
|
+
SelectContent,
|
|
17
|
+
SelectItem,
|
|
18
|
+
SelectTrigger,
|
|
19
|
+
SelectValue,
|
|
20
|
+
} from "../../components/select"
|
|
21
|
+
import {
|
|
22
|
+
Globe,
|
|
23
|
+
Server,
|
|
24
|
+
Database,
|
|
25
|
+
Cloud,
|
|
26
|
+
Shield,
|
|
27
|
+
Activity,
|
|
28
|
+
AlertTriangle,
|
|
29
|
+
CheckCircle2,
|
|
30
|
+
Layers,
|
|
31
|
+
ZoomIn,
|
|
32
|
+
ZoomOut,
|
|
33
|
+
Maximize2,
|
|
34
|
+
RefreshCw,
|
|
35
|
+
Filter,
|
|
36
|
+
HardDrive,
|
|
37
|
+
Cpu,
|
|
38
|
+
Network,
|
|
39
|
+
Lock,
|
|
40
|
+
Box,
|
|
41
|
+
Container,
|
|
42
|
+
} from "lucide-react"
|
|
43
|
+
|
|
44
|
+
export type ResourceType = "region" | "vpc" | "subnet" | "server" | "database" | "loadbalancer" | "cache" | "storage" | "container" | "function" | "cdn" | "firewall"
|
|
45
|
+
export type ResourceStatus = "healthy" | "warning" | "critical" | "unknown" | "maintenance"
|
|
46
|
+
export type CloudProvider = "aws" | "gcp" | "azure" | "custom"
|
|
47
|
+
|
|
48
|
+
export interface InfraResource {
|
|
49
|
+
id: string
|
|
50
|
+
name: string
|
|
51
|
+
type: ResourceType
|
|
52
|
+
status: ResourceStatus
|
|
53
|
+
provider?: CloudProvider
|
|
54
|
+
region?: string
|
|
55
|
+
metadata?: Record<string, string | number>
|
|
56
|
+
children?: InfraResource[]
|
|
57
|
+
connections?: string[] // IDs of connected resources
|
|
58
|
+
metrics?: {
|
|
59
|
+
cpu?: number
|
|
60
|
+
memory?: number
|
|
61
|
+
network?: number
|
|
62
|
+
requests?: number
|
|
63
|
+
latency?: number
|
|
64
|
+
errors?: number
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface InfrastructureMapProps {
|
|
69
|
+
resources: InfraResource[]
|
|
70
|
+
onResourceClick?: (resource: InfraResource) => void
|
|
71
|
+
onRefresh?: () => void
|
|
72
|
+
showConnections?: boolean
|
|
73
|
+
viewMode?: "tree" | "grid" | "topology"
|
|
74
|
+
className?: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const typeConfig: Record<ResourceType, { icon: React.ElementType; color: string; label: string }> = {
|
|
78
|
+
region: { icon: Globe, color: "text-blue-500", label: "Region" },
|
|
79
|
+
vpc: { icon: Network, color: "text-purple-500", label: "VPC" },
|
|
80
|
+
subnet: { icon: Layers, color: "text-indigo-500", label: "Subnet" },
|
|
81
|
+
server: { icon: Server, color: "text-gray-500", label: "Server" },
|
|
82
|
+
database: { icon: Database, color: "text-green-500", label: "Database" },
|
|
83
|
+
loadbalancer: { icon: Activity, color: "text-cyan-500", label: "Load Balancer" },
|
|
84
|
+
cache: { icon: HardDrive, color: "text-red-500", label: "Cache" },
|
|
85
|
+
storage: { icon: HardDrive, color: "text-orange-500", label: "Storage" },
|
|
86
|
+
container: { icon: Container, color: "text-blue-400", label: "Container" },
|
|
87
|
+
function: { icon: Cpu, color: "text-yellow-500", label: "Function" },
|
|
88
|
+
cdn: { icon: Globe, color: "text-teal-500", label: "CDN" },
|
|
89
|
+
firewall: { icon: Shield, color: "text-red-400", label: "Firewall" },
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const statusConfig: Record<ResourceStatus, { color: string; bgColor: string; label: string }> = {
|
|
93
|
+
healthy: { color: "text-green-500", bgColor: "bg-green-500", label: "Healthy" },
|
|
94
|
+
warning: { color: "text-yellow-500", bgColor: "bg-yellow-500", label: "Warning" },
|
|
95
|
+
critical: { color: "text-red-500", bgColor: "bg-red-500", label: "Critical" },
|
|
96
|
+
unknown: { color: "text-gray-500", bgColor: "bg-gray-500", label: "Unknown" },
|
|
97
|
+
maintenance: { color: "text-blue-500", bgColor: "bg-blue-500", label: "Maintenance" },
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const providerConfig: Record<CloudProvider, { label: string; color: string }> = {
|
|
101
|
+
aws: { label: "AWS", color: "text-orange-500" },
|
|
102
|
+
gcp: { label: "GCP", color: "text-blue-500" },
|
|
103
|
+
azure: { label: "Azure", color: "text-cyan-500" },
|
|
104
|
+
custom: { label: "Custom", color: "text-gray-500" },
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function ResourceNode({
|
|
108
|
+
resource,
|
|
109
|
+
depth = 0,
|
|
110
|
+
onClick,
|
|
111
|
+
isExpanded,
|
|
112
|
+
onToggle,
|
|
113
|
+
}: {
|
|
114
|
+
resource: InfraResource
|
|
115
|
+
depth?: number
|
|
116
|
+
onClick?: () => void
|
|
117
|
+
isExpanded?: boolean
|
|
118
|
+
onToggle?: () => void
|
|
119
|
+
}) {
|
|
120
|
+
const typeConf = typeConfig[resource.type]
|
|
121
|
+
const statusConf = statusConfig[resource.status]
|
|
122
|
+
const Icon = typeConf.icon
|
|
123
|
+
const hasChildren = resource.children && resource.children.length > 0
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div className={cn("border-l-2", depth > 0 && "ml-6", statusConf.color.replace("text-", "border-"))}>
|
|
127
|
+
<div
|
|
128
|
+
className={cn(
|
|
129
|
+
"flex items-center gap-3 p-3 cursor-pointer rounded-r-lg transition-all",
|
|
130
|
+
"hover:bg-muted/50",
|
|
131
|
+
resource.status === "critical" && "bg-red-500/5",
|
|
132
|
+
resource.status === "warning" && "bg-yellow-500/5"
|
|
133
|
+
)}
|
|
134
|
+
onClick={onClick}
|
|
135
|
+
>
|
|
136
|
+
{/* Expand toggle */}
|
|
137
|
+
{hasChildren && (
|
|
138
|
+
<button
|
|
139
|
+
className="p-1 hover:bg-muted rounded"
|
|
140
|
+
onClick={(e) => {
|
|
141
|
+
e.stopPropagation()
|
|
142
|
+
onToggle?.()
|
|
143
|
+
}}
|
|
144
|
+
>
|
|
145
|
+
<Layers className={cn("h-4 w-4", isExpanded && "rotate-90")} />
|
|
146
|
+
</button>
|
|
147
|
+
)}
|
|
148
|
+
|
|
149
|
+
{/* Icon */}
|
|
150
|
+
<div className={cn("p-2 rounded-lg bg-muted/50")}>
|
|
151
|
+
<Icon className={cn("h-5 w-5", typeConf.color)} />
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
{/* Info */}
|
|
155
|
+
<div className="flex-1 min-w-0">
|
|
156
|
+
<div className="flex items-center gap-2">
|
|
157
|
+
<span className="font-medium truncate">{resource.name}</span>
|
|
158
|
+
<Badge variant="outline" className="text-xs shrink-0">
|
|
159
|
+
{typeConf.label}
|
|
160
|
+
</Badge>
|
|
161
|
+
{resource.provider && (
|
|
162
|
+
<Badge variant="secondary" className={cn("text-xs shrink-0", providerConfig[resource.provider].color)}>
|
|
163
|
+
{providerConfig[resource.provider].label}
|
|
164
|
+
</Badge>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
{resource.region && (
|
|
168
|
+
<div className="text-xs text-muted-foreground mt-0.5">
|
|
169
|
+
{resource.region}
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{/* Metrics preview */}
|
|
175
|
+
{resource.metrics && (
|
|
176
|
+
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
|
177
|
+
{resource.metrics.cpu !== undefined && (
|
|
178
|
+
<span className={cn(resource.metrics.cpu > 80 && "text-red-500")}>
|
|
179
|
+
CPU: {resource.metrics.cpu}%
|
|
180
|
+
</span>
|
|
181
|
+
)}
|
|
182
|
+
{resource.metrics.memory !== undefined && (
|
|
183
|
+
<span className={cn(resource.metrics.memory > 80 && "text-red-500")}>
|
|
184
|
+
MEM: {resource.metrics.memory}%
|
|
185
|
+
</span>
|
|
186
|
+
)}
|
|
187
|
+
{resource.metrics.latency !== undefined && (
|
|
188
|
+
<span className={cn(resource.metrics.latency > 500 && "text-yellow-500")}>
|
|
189
|
+
{resource.metrics.latency}ms
|
|
190
|
+
</span>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
|
|
195
|
+
{/* Status indicator */}
|
|
196
|
+
<TooltipProvider>
|
|
197
|
+
<Tooltip>
|
|
198
|
+
<TooltipTrigger>
|
|
199
|
+
<div className={cn("w-3 h-3 rounded-full shrink-0", statusConf.bgColor)} />
|
|
200
|
+
</TooltipTrigger>
|
|
201
|
+
<TooltipContent>{statusConf.label}</TooltipContent>
|
|
202
|
+
</Tooltip>
|
|
203
|
+
</TooltipProvider>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function ResourceTree({
|
|
210
|
+
resources,
|
|
211
|
+
depth = 0,
|
|
212
|
+
expandedIds,
|
|
213
|
+
onToggle,
|
|
214
|
+
onResourceClick,
|
|
215
|
+
}: {
|
|
216
|
+
resources: InfraResource[]
|
|
217
|
+
depth?: number
|
|
218
|
+
expandedIds: Set<string>
|
|
219
|
+
onToggle: (id: string) => void
|
|
220
|
+
onResourceClick?: (resource: InfraResource) => void
|
|
221
|
+
}) {
|
|
222
|
+
return (
|
|
223
|
+
<div className={cn(depth > 0 && "ml-4")}>
|
|
224
|
+
{resources.map((resource) => (
|
|
225
|
+
<React.Fragment key={resource.id}>
|
|
226
|
+
<ResourceNode
|
|
227
|
+
resource={resource}
|
|
228
|
+
depth={depth}
|
|
229
|
+
onClick={() => onResourceClick?.(resource)}
|
|
230
|
+
isExpanded={expandedIds.has(resource.id)}
|
|
231
|
+
onToggle={() => onToggle(resource.id)}
|
|
232
|
+
/>
|
|
233
|
+
{expandedIds.has(resource.id) && resource.children && (
|
|
234
|
+
<ResourceTree
|
|
235
|
+
resources={resource.children}
|
|
236
|
+
depth={depth + 1}
|
|
237
|
+
expandedIds={expandedIds}
|
|
238
|
+
onToggle={onToggle}
|
|
239
|
+
onResourceClick={onResourceClick}
|
|
240
|
+
/>
|
|
241
|
+
)}
|
|
242
|
+
</React.Fragment>
|
|
243
|
+
))}
|
|
244
|
+
</div>
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function ResourceGrid({
|
|
249
|
+
resources,
|
|
250
|
+
onResourceClick,
|
|
251
|
+
}: {
|
|
252
|
+
resources: InfraResource[]
|
|
253
|
+
onResourceClick?: (resource: InfraResource) => void
|
|
254
|
+
}) {
|
|
255
|
+
// Flatten resources for grid view
|
|
256
|
+
const flattenResources = (items: InfraResource[]): InfraResource[] => {
|
|
257
|
+
return items.reduce<InfraResource[]>((acc, item) => {
|
|
258
|
+
acc.push(item)
|
|
259
|
+
if (item.children) {
|
|
260
|
+
acc.push(...flattenResources(item.children))
|
|
261
|
+
}
|
|
262
|
+
return acc
|
|
263
|
+
}, [])
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const allResources = flattenResources(resources)
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
|
270
|
+
{allResources.map((resource) => {
|
|
271
|
+
const typeConf = typeConfig[resource.type]
|
|
272
|
+
const statusConf = statusConfig[resource.status]
|
|
273
|
+
const Icon = typeConf.icon
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<Card
|
|
277
|
+
key={resource.id}
|
|
278
|
+
className={cn(
|
|
279
|
+
"cursor-pointer transition-all hover:shadow-md",
|
|
280
|
+
resource.status === "critical" && "border-red-500/50",
|
|
281
|
+
resource.status === "warning" && "border-yellow-500/50"
|
|
282
|
+
)}
|
|
283
|
+
onClick={() => onResourceClick?.(resource)}
|
|
284
|
+
>
|
|
285
|
+
<CardContent className="p-4">
|
|
286
|
+
<div className="flex items-start justify-between">
|
|
287
|
+
<div className={cn("p-2 rounded-lg", `${typeConf.color.replace("text-", "bg-")}/10`)}>
|
|
288
|
+
<Icon className={cn("h-5 w-5", typeConf.color)} />
|
|
289
|
+
</div>
|
|
290
|
+
<div className={cn("w-2 h-2 rounded-full", statusConf.bgColor)} />
|
|
291
|
+
</div>
|
|
292
|
+
<div className="mt-3">
|
|
293
|
+
<div className="font-medium truncate">{resource.name}</div>
|
|
294
|
+
<div className="text-xs text-muted-foreground">{typeConf.label}</div>
|
|
295
|
+
</div>
|
|
296
|
+
{resource.metrics && (
|
|
297
|
+
<div className="flex items-center gap-2 mt-2 text-xs">
|
|
298
|
+
{resource.metrics.cpu !== undefined && (
|
|
299
|
+
<span className={cn(resource.metrics.cpu > 80 && "text-red-500")}>
|
|
300
|
+
{resource.metrics.cpu}%
|
|
301
|
+
</span>
|
|
302
|
+
)}
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
</CardContent>
|
|
306
|
+
</Card>
|
|
307
|
+
)
|
|
308
|
+
})}
|
|
309
|
+
</div>
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function InfraStats({ resources }: { resources: InfraResource[] }) {
|
|
314
|
+
const stats = React.useMemo(() => {
|
|
315
|
+
let total = 0
|
|
316
|
+
let healthy = 0
|
|
317
|
+
let warning = 0
|
|
318
|
+
let critical = 0
|
|
319
|
+
|
|
320
|
+
const count = (items: InfraResource[]) => {
|
|
321
|
+
items.forEach((item) => {
|
|
322
|
+
total++
|
|
323
|
+
if (item.status === "healthy") healthy++
|
|
324
|
+
if (item.status === "warning") warning++
|
|
325
|
+
if (item.status === "critical") critical++
|
|
326
|
+
if (item.children) count(item.children)
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
count(resources)
|
|
330
|
+
|
|
331
|
+
return { total, healthy, warning, critical }
|
|
332
|
+
}, [resources])
|
|
333
|
+
|
|
334
|
+
return (
|
|
335
|
+
<div className="flex items-center gap-6">
|
|
336
|
+
<div className="flex items-center gap-2">
|
|
337
|
+
<Box className="h-4 w-4 text-muted-foreground" />
|
|
338
|
+
<span className="font-medium">{stats.total}</span>
|
|
339
|
+
<span className="text-muted-foreground text-sm">Resources</span>
|
|
340
|
+
</div>
|
|
341
|
+
<div className="flex items-center gap-2">
|
|
342
|
+
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
|
343
|
+
<span className="font-medium text-green-500">{stats.healthy}</span>
|
|
344
|
+
</div>
|
|
345
|
+
<div className="flex items-center gap-2">
|
|
346
|
+
<AlertTriangle className="h-4 w-4 text-yellow-500" />
|
|
347
|
+
<span className="font-medium text-yellow-500">{stats.warning}</span>
|
|
348
|
+
</div>
|
|
349
|
+
<div className="flex items-center gap-2">
|
|
350
|
+
<AlertTriangle className="h-4 w-4 text-red-500" />
|
|
351
|
+
<span className="font-medium text-red-500">{stats.critical}</span>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function InfrastructureMap({
|
|
358
|
+
resources,
|
|
359
|
+
onResourceClick,
|
|
360
|
+
onRefresh,
|
|
361
|
+
showConnections = false,
|
|
362
|
+
viewMode: initialViewMode = "tree",
|
|
363
|
+
className,
|
|
364
|
+
}: InfrastructureMapProps) {
|
|
365
|
+
const [viewMode, setViewMode] = React.useState(initialViewMode)
|
|
366
|
+
const [expandedIds, setExpandedIds] = React.useState<Set<string>>(() => {
|
|
367
|
+
// Expand first level by default
|
|
368
|
+
return new Set(resources.map((r) => r.id))
|
|
369
|
+
})
|
|
370
|
+
const [statusFilter, setStatusFilter] = React.useState<ResourceStatus | "all">("all")
|
|
371
|
+
|
|
372
|
+
const toggleExpand = (id: string) => {
|
|
373
|
+
setExpandedIds((prev) => {
|
|
374
|
+
const next = new Set(prev)
|
|
375
|
+
if (next.has(id)) {
|
|
376
|
+
next.delete(id)
|
|
377
|
+
} else {
|
|
378
|
+
next.add(id)
|
|
379
|
+
}
|
|
380
|
+
return next
|
|
381
|
+
})
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const expandAll = () => {
|
|
385
|
+
const ids = new Set<string>()
|
|
386
|
+
const collectIds = (items: InfraResource[]) => {
|
|
387
|
+
items.forEach((item) => {
|
|
388
|
+
ids.add(item.id)
|
|
389
|
+
if (item.children) collectIds(item.children)
|
|
390
|
+
})
|
|
391
|
+
}
|
|
392
|
+
collectIds(resources)
|
|
393
|
+
setExpandedIds(ids)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const collapseAll = () => {
|
|
397
|
+
setExpandedIds(new Set())
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Filter resources by status
|
|
401
|
+
const filteredResources = React.useMemo(() => {
|
|
402
|
+
if (statusFilter === "all") return resources
|
|
403
|
+
|
|
404
|
+
const filterByStatus = (items: InfraResource[]): InfraResource[] => {
|
|
405
|
+
return items
|
|
406
|
+
.filter((item) => {
|
|
407
|
+
const hasMatchingChild = item.children?.some((c) => {
|
|
408
|
+
const filtered = filterByStatus([c])
|
|
409
|
+
return filtered.length > 0
|
|
410
|
+
})
|
|
411
|
+
return item.status === statusFilter || hasMatchingChild
|
|
412
|
+
})
|
|
413
|
+
.map((item) => ({
|
|
414
|
+
...item,
|
|
415
|
+
children: item.children ? filterByStatus(item.children) : undefined,
|
|
416
|
+
}))
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return filterByStatus(resources)
|
|
420
|
+
}, [resources, statusFilter])
|
|
421
|
+
|
|
422
|
+
return (
|
|
423
|
+
<Card className={className}>
|
|
424
|
+
<CardHeader className="pb-3">
|
|
425
|
+
<div className="flex items-center justify-between">
|
|
426
|
+
<div className="flex items-center gap-3">
|
|
427
|
+
<Cloud className="h-5 w-5" />
|
|
428
|
+
<CardTitle>Infrastructure Map</CardTitle>
|
|
429
|
+
</div>
|
|
430
|
+
<InfraStats resources={resources} />
|
|
431
|
+
</div>
|
|
432
|
+
</CardHeader>
|
|
433
|
+
|
|
434
|
+
<CardContent className="space-y-4">
|
|
435
|
+
{/* Toolbar */}
|
|
436
|
+
<div className="flex items-center justify-between gap-3">
|
|
437
|
+
<div className="flex items-center gap-2">
|
|
438
|
+
<Select value={viewMode} onValueChange={(v) => setViewMode(v as "tree" | "grid")}>
|
|
439
|
+
<SelectTrigger className="w-28">
|
|
440
|
+
<SelectValue />
|
|
441
|
+
</SelectTrigger>
|
|
442
|
+
<SelectContent>
|
|
443
|
+
<SelectItem value="tree">Tree View</SelectItem>
|
|
444
|
+
<SelectItem value="grid">Grid View</SelectItem>
|
|
445
|
+
</SelectContent>
|
|
446
|
+
</Select>
|
|
447
|
+
|
|
448
|
+
<Select value={statusFilter} onValueChange={(v) => setStatusFilter(v as ResourceStatus | "all")}>
|
|
449
|
+
<SelectTrigger className="w-32">
|
|
450
|
+
<Filter className="h-4 w-4 mr-2" />
|
|
451
|
+
<SelectValue placeholder="Status" />
|
|
452
|
+
</SelectTrigger>
|
|
453
|
+
<SelectContent>
|
|
454
|
+
<SelectItem value="all">All Status</SelectItem>
|
|
455
|
+
<SelectItem value="healthy">Healthy</SelectItem>
|
|
456
|
+
<SelectItem value="warning">Warning</SelectItem>
|
|
457
|
+
<SelectItem value="critical">Critical</SelectItem>
|
|
458
|
+
<SelectItem value="maintenance">Maintenance</SelectItem>
|
|
459
|
+
</SelectContent>
|
|
460
|
+
</Select>
|
|
461
|
+
</div>
|
|
462
|
+
|
|
463
|
+
<div className="flex items-center gap-1">
|
|
464
|
+
{viewMode === "tree" && (
|
|
465
|
+
<>
|
|
466
|
+
<Button variant="ghost" size="sm" onClick={expandAll}>
|
|
467
|
+
<Maximize2 className="h-4 w-4 mr-1" />
|
|
468
|
+
Expand
|
|
469
|
+
</Button>
|
|
470
|
+
<Button variant="ghost" size="sm" onClick={collapseAll}>
|
|
471
|
+
Collapse
|
|
472
|
+
</Button>
|
|
473
|
+
</>
|
|
474
|
+
)}
|
|
475
|
+
{onRefresh && (
|
|
476
|
+
<Button variant="ghost" size="sm" onClick={onRefresh}>
|
|
477
|
+
<RefreshCw className="h-4 w-4" />
|
|
478
|
+
</Button>
|
|
479
|
+
)}
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
|
|
483
|
+
{/* Content */}
|
|
484
|
+
<div className="min-h-[400px]">
|
|
485
|
+
{filteredResources.length === 0 ? (
|
|
486
|
+
<div className="flex flex-col items-center justify-center h-64 text-muted-foreground">
|
|
487
|
+
<Cloud className="h-12 w-12 mb-4" />
|
|
488
|
+
<p>No resources found</p>
|
|
489
|
+
</div>
|
|
490
|
+
) : viewMode === "tree" ? (
|
|
491
|
+
<ResourceTree
|
|
492
|
+
resources={filteredResources}
|
|
493
|
+
expandedIds={expandedIds}
|
|
494
|
+
onToggle={toggleExpand}
|
|
495
|
+
onResourceClick={onResourceClick}
|
|
496
|
+
/>
|
|
497
|
+
) : (
|
|
498
|
+
<ResourceGrid
|
|
499
|
+
resources={filteredResources}
|
|
500
|
+
onResourceClick={onResourceClick}
|
|
501
|
+
/>
|
|
502
|
+
)}
|
|
503
|
+
</div>
|
|
504
|
+
</CardContent>
|
|
505
|
+
</Card>
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Default sample data
|
|
510
|
+
export const defaultInfraResources: InfraResource[] = [
|
|
511
|
+
{
|
|
512
|
+
id: "region-us-east-1",
|
|
513
|
+
name: "us-east-1",
|
|
514
|
+
type: "region",
|
|
515
|
+
status: "healthy",
|
|
516
|
+
provider: "aws",
|
|
517
|
+
region: "N. Virginia",
|
|
518
|
+
children: [
|
|
519
|
+
{
|
|
520
|
+
id: "vpc-prod",
|
|
521
|
+
name: "production-vpc",
|
|
522
|
+
type: "vpc",
|
|
523
|
+
status: "healthy",
|
|
524
|
+
provider: "aws",
|
|
525
|
+
metadata: { cidr: "10.0.0.0/16" },
|
|
526
|
+
children: [
|
|
527
|
+
{
|
|
528
|
+
id: "subnet-public-1",
|
|
529
|
+
name: "public-subnet-1a",
|
|
530
|
+
type: "subnet",
|
|
531
|
+
status: "healthy",
|
|
532
|
+
metadata: { az: "us-east-1a", cidr: "10.0.1.0/24" },
|
|
533
|
+
children: [
|
|
534
|
+
{
|
|
535
|
+
id: "alb-1",
|
|
536
|
+
name: "api-load-balancer",
|
|
537
|
+
type: "loadbalancer",
|
|
538
|
+
status: "healthy",
|
|
539
|
+
metrics: { requests: 15000, latency: 45 },
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
id: "nat-1",
|
|
543
|
+
name: "nat-gateway-1a",
|
|
544
|
+
type: "firewall",
|
|
545
|
+
status: "healthy",
|
|
546
|
+
},
|
|
547
|
+
],
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
id: "subnet-private-1",
|
|
551
|
+
name: "private-subnet-1a",
|
|
552
|
+
type: "subnet",
|
|
553
|
+
status: "warning",
|
|
554
|
+
metadata: { az: "us-east-1a", cidr: "10.0.10.0/24" },
|
|
555
|
+
children: [
|
|
556
|
+
{
|
|
557
|
+
id: "ec2-api-1",
|
|
558
|
+
name: "api-server-1",
|
|
559
|
+
type: "server",
|
|
560
|
+
status: "healthy",
|
|
561
|
+
metrics: { cpu: 45, memory: 62 },
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
id: "ec2-api-2",
|
|
565
|
+
name: "api-server-2",
|
|
566
|
+
type: "server",
|
|
567
|
+
status: "warning",
|
|
568
|
+
metrics: { cpu: 85, memory: 78 },
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
id: "ec2-api-3",
|
|
572
|
+
name: "api-server-3",
|
|
573
|
+
type: "server",
|
|
574
|
+
status: "healthy",
|
|
575
|
+
metrics: { cpu: 52, memory: 55 },
|
|
576
|
+
},
|
|
577
|
+
],
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
id: "subnet-data-1",
|
|
581
|
+
name: "data-subnet-1a",
|
|
582
|
+
type: "subnet",
|
|
583
|
+
status: "healthy",
|
|
584
|
+
metadata: { az: "us-east-1a", cidr: "10.0.20.0/24" },
|
|
585
|
+
children: [
|
|
586
|
+
{
|
|
587
|
+
id: "rds-primary",
|
|
588
|
+
name: "postgres-primary",
|
|
589
|
+
type: "database",
|
|
590
|
+
status: "healthy",
|
|
591
|
+
metrics: { cpu: 35, memory: 68 },
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
id: "redis-1",
|
|
595
|
+
name: "redis-cluster",
|
|
596
|
+
type: "cache",
|
|
597
|
+
status: "healthy",
|
|
598
|
+
metrics: { memory: 45 },
|
|
599
|
+
},
|
|
600
|
+
],
|
|
601
|
+
},
|
|
602
|
+
],
|
|
603
|
+
},
|
|
604
|
+
],
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
id: "region-eu-west-1",
|
|
608
|
+
name: "eu-west-1",
|
|
609
|
+
type: "region",
|
|
610
|
+
status: "critical",
|
|
611
|
+
provider: "aws",
|
|
612
|
+
region: "Ireland",
|
|
613
|
+
children: [
|
|
614
|
+
{
|
|
615
|
+
id: "vpc-prod-eu",
|
|
616
|
+
name: "production-vpc-eu",
|
|
617
|
+
type: "vpc",
|
|
618
|
+
status: "critical",
|
|
619
|
+
children: [
|
|
620
|
+
{
|
|
621
|
+
id: "ec2-eu-1",
|
|
622
|
+
name: "api-server-eu-1",
|
|
623
|
+
type: "server",
|
|
624
|
+
status: "critical",
|
|
625
|
+
metrics: { cpu: 98, memory: 95 },
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
id: "rds-eu",
|
|
629
|
+
name: "postgres-replica",
|
|
630
|
+
type: "database",
|
|
631
|
+
status: "warning",
|
|
632
|
+
metrics: { cpu: 75, memory: 82 },
|
|
633
|
+
},
|
|
634
|
+
],
|
|
635
|
+
},
|
|
636
|
+
],
|
|
637
|
+
},
|
|
638
|
+
]
|