@skyhook-io/k8s-ui 1.0.0
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/package.json +63 -0
- package/src/components/dock/BottomDock.tsx +204 -0
- package/src/components/dock/DockContext.tsx +212 -0
- package/src/components/dock/TerminalTab.tsx +333 -0
- package/src/components/dock/index.ts +3 -0
- package/src/components/gitops/GitOpsActions.tsx +227 -0
- package/src/components/gitops/GitOpsStatusBadge.tsx +221 -0
- package/src/components/gitops/ManagedResourcesList.tsx +210 -0
- package/src/components/gitops/SyncCountdown.tsx +146 -0
- package/src/components/gitops/index.ts +4 -0
- package/src/components/logs/JsonLogLine.tsx +90 -0
- package/src/components/logs/LogCore.tsx +553 -0
- package/src/components/logs/LogToolbarSelects.tsx +74 -0
- package/src/components/logs/LogsViewer.tsx +146 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +263 -0
- package/src/components/logs/index.ts +13 -0
- package/src/components/logs/useLogBuffer.ts +138 -0
- package/src/components/logs/useLogSearch.ts +151 -0
- package/src/components/logs/useLogStream.ts +61 -0
- package/src/components/resources/ResourcesView.tsx +5494 -0
- package/src/components/resources/index.ts +17 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +166 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +233 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +89 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +194 -0
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +79 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +68 -0
- package/src/components/resources/renderers/CertificateRenderer.tsx +209 -0
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +123 -0
- package/src/components/resources/renderers/ChallengeRenderer.tsx +111 -0
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +164 -0
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +140 -0
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +153 -0
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +221 -0
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +124 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +86 -0
- package/src/components/resources/renderers/EventRenderer.tsx +175 -0
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +119 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +172 -0
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +242 -0
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +263 -0
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +68 -0
- package/src/components/resources/renderers/GatewayRenderer.tsx +218 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +476 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +154 -0
- package/src/components/resources/renderers/HPARenderer.tsx +164 -0
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +303 -0
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +167 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +49 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +104 -0
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +175 -0
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +176 -0
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +125 -0
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +84 -0
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +112 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +288 -0
- package/src/components/resources/renderers/JobRenderer.tsx +103 -0
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +157 -0
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +188 -0
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +148 -0
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +90 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +136 -0
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +151 -0
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +70 -0
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +309 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +142 -0
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +232 -0
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +97 -0
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +74 -0
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +138 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +220 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +151 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +352 -0
- package/src/components/resources/renderers/LeaseRenderer.tsx +93 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +208 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +282 -0
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +153 -0
- package/src/components/resources/renderers/OrderRenderer.tsx +108 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +98 -0
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +138 -0
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +126 -0
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +63 -0
- package/src/components/resources/renderers/PodRenderer.tsx +677 -0
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +50 -0
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +62 -0
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +71 -0
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +97 -0
- package/src/components/resources/renderers/RoleRenderer.tsx +192 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +227 -0
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +75 -0
- package/src/components/resources/renderers/SbomReportRenderer.tsx +98 -0
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +121 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +321 -0
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +111 -0
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +81 -0
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +65 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +98 -0
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +195 -0
- package/src/components/resources/renderers/StorageClassRenderer.tsx +82 -0
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +257 -0
- package/src/components/resources/renderers/VPARenderer.tsx +193 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +73 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +193 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +161 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +134 -0
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +36 -0
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +347 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +156 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +202 -0
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +91 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +311 -0
- package/src/components/resources/renderers/argo-cells.tsx +118 -0
- package/src/components/resources/renderers/certmanager-cells.tsx +164 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +161 -0
- package/src/components/resources/renderers/eso-cells.tsx +115 -0
- package/src/components/resources/renderers/flux-cells.tsx +253 -0
- package/src/components/resources/renderers/index.ts +105 -0
- package/src/components/resources/renderers/istio-cells.tsx +210 -0
- package/src/components/resources/renderers/karpenter-cells.tsx +107 -0
- package/src/components/resources/renderers/keda-cells.tsx +94 -0
- package/src/components/resources/renderers/knative-cells.tsx +396 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +85 -0
- package/src/components/resources/renderers/prometheus-cells.tsx +74 -0
- package/src/components/resources/renderers/traefik-cells.tsx +120 -0
- package/src/components/resources/renderers/trivy-cells.tsx +153 -0
- package/src/components/resources/renderers/trivy-shared.tsx +67 -0
- package/src/components/resources/renderers/velero-cells.tsx +164 -0
- package/src/components/resources/resource-utils-argo.ts +155 -0
- package/src/components/resources/resource-utils-certmanager.ts +198 -0
- package/src/components/resources/resource-utils-cnpg.ts +344 -0
- package/src/components/resources/resource-utils-eso.ts +385 -0
- package/src/components/resources/resource-utils-flux.ts +186 -0
- package/src/components/resources/resource-utils-istio.ts +372 -0
- package/src/components/resources/resource-utils-karpenter.ts +150 -0
- package/src/components/resources/resource-utils-keda.ts +203 -0
- package/src/components/resources/resource-utils-knative.ts +218 -0
- package/src/components/resources/resource-utils-kyverno.ts +224 -0
- package/src/components/resources/resource-utils-prometheus.ts +159 -0
- package/src/components/resources/resource-utils-traefik.ts +180 -0
- package/src/components/resources/resource-utils-trivy.ts +146 -0
- package/src/components/resources/resource-utils-velero.ts +293 -0
- package/src/components/resources/resource-utils.ts +1558 -0
- package/src/components/shared/EditableYamlView.tsx +302 -0
- package/src/components/shared/ResourceActionsBar.tsx +924 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +557 -0
- package/src/components/shared/index.ts +3 -0
- package/src/components/timeline/index.ts +1 -0
- package/src/components/timeline/shared.tsx +614 -0
- package/src/components/topology/GroupNode.tsx +198 -0
- package/src/components/topology/K8sResourceNode.tsx +440 -0
- package/src/components/topology/TopologyControls.tsx +62 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +377 -0
- package/src/components/topology/TopologyGraph.tsx +748 -0
- package/src/components/topology/TopologySearch.tsx +304 -0
- package/src/components/topology/index.ts +7 -0
- package/src/components/topology/layout.ts +734 -0
- package/src/components/topology/layout.worker.ts +272 -0
- package/src/components/topology/topology.css +129 -0
- package/src/components/ui/CodeViewer.tsx +451 -0
- package/src/components/ui/ConfirmDialog.tsx +188 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +57 -0
- package/src/components/ui/HealthRing.tsx +72 -0
- package/src/components/ui/MetricsChart.tsx +259 -0
- package/src/components/ui/ResourceBar.tsx +68 -0
- package/src/components/ui/Toast.tsx +225 -0
- package/src/components/ui/Tooltip.tsx +165 -0
- package/src/components/ui/YamlEditor.tsx +280 -0
- package/src/components/ui/drawer-components.tsx +792 -0
- package/src/components/ui/index.ts +10 -0
- package/src/components/workload/ResourceDetailDrawer.tsx +158 -0
- package/src/components/workload/WorkloadView.tsx +1109 -0
- package/src/components/workload/index.ts +2 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useAnimatedUnmount.ts +30 -0
- package/src/hooks/useKeyboardShortcuts.tsx +336 -0
- package/src/hooks/useRefreshAnimation.ts +58 -0
- package/src/index.ts +35 -0
- package/src/types/core.ts +894 -0
- package/src/types/gitops.ts +458 -0
- package/src/types/index.ts +2 -0
- package/src/utils/animation.ts +40 -0
- package/src/utils/api-resources.ts +185 -0
- package/src/utils/badge-colors.ts +399 -0
- package/src/utils/context-name.ts +72 -0
- package/src/utils/download.ts +19 -0
- package/src/utils/format.ts +233 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/log-format.ts +300 -0
- package/src/utils/navigation.ts +119 -0
- package/src/utils/resource-hierarchy.ts +630 -0
- package/src/utils/resource-icons.ts +206 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
import type { Node } from '@xyflow/react'
|
|
2
|
+
import type { TopologyNode, GroupingMode, NodeKind } from '../../types'
|
|
3
|
+
import { NODE_DIMENSIONS } from './K8sResourceNode'
|
|
4
|
+
|
|
5
|
+
// Group padding - space for header + internal spacing (must account for border)
|
|
6
|
+
// Top padding accommodates the header at its largest (when zoomed out)
|
|
7
|
+
// Child nodes are translated up dynamically when zoomed in (see TopologyGraph)
|
|
8
|
+
const GROUP_PADDING = {
|
|
9
|
+
top: 100, // Space for group header at max scale
|
|
10
|
+
left: 30,
|
|
11
|
+
bottom: 36,
|
|
12
|
+
right: 30,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Reduced padding when header is hidden (single namespace view)
|
|
16
|
+
const GROUP_PADDING_NO_HEADER = {
|
|
17
|
+
top: 30,
|
|
18
|
+
left: 30,
|
|
19
|
+
bottom: 30,
|
|
20
|
+
right: 30,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Layout engine abstraction
|
|
25
|
+
//
|
|
26
|
+
// Two execution strategies for ELK layout:
|
|
27
|
+
// 'worker' — Web Worker (Vite/Radar). Keeps the main thread free.
|
|
28
|
+
// 'main-thread' — Inline ELK (webpack/Next.js). Workers can't load the
|
|
29
|
+
// layout.worker.ts file under webpack, so we run ELK
|
|
30
|
+
// directly using elkjs/lib/elk.bundled.js.
|
|
31
|
+
//
|
|
32
|
+
// The active strategy is set once via `setLayoutEngine()` (exported).
|
|
33
|
+
// Defaults to 'worker' — consumers on webpack call setLayoutEngine('main-thread').
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
type LayoutEngine = 'worker' | 'main-thread'
|
|
37
|
+
let layoutEngine: LayoutEngine = 'worker'
|
|
38
|
+
|
|
39
|
+
/** Set the ELK layout execution strategy. Call once at app init. */
|
|
40
|
+
export function setLayoutEngine(engine: LayoutEngine) {
|
|
41
|
+
layoutEngine = engine
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface LayoutResult {
|
|
45
|
+
groupLayouts: Array<{
|
|
46
|
+
groupId: string
|
|
47
|
+
groupKey: string
|
|
48
|
+
width: number
|
|
49
|
+
height: number
|
|
50
|
+
children: Array<{ id: string; x: number; y: number }>
|
|
51
|
+
isCollapsed: boolean
|
|
52
|
+
}>
|
|
53
|
+
ungroupedNodes: Array<{
|
|
54
|
+
id: string
|
|
55
|
+
width: number
|
|
56
|
+
height: number
|
|
57
|
+
}>
|
|
58
|
+
groupPositions: Array<[string, { x: number; y: number }]>
|
|
59
|
+
error?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ELK algorithm options (shared between worker and main-thread engines)
|
|
63
|
+
const elkOptionsGroup = {
|
|
64
|
+
'elk.algorithm': 'layered',
|
|
65
|
+
'elk.direction': 'RIGHT',
|
|
66
|
+
'elk.layered.considerModelOrder.strategy': 'NODES_AND_EDGES',
|
|
67
|
+
'elk.spacing.nodeNode': '40',
|
|
68
|
+
'elk.layered.spacing.nodeNodeBetweenLayers': '85',
|
|
69
|
+
'elk.layered.spacing.edgeNodeBetweenLayers': '25',
|
|
70
|
+
'elk.edgeRouting': 'ORTHOGONAL',
|
|
71
|
+
'elk.layered.nodePlacement.strategy': 'NETWORK_SIMPLEX',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const elkOptionsGroupLayout = {
|
|
75
|
+
'elk.algorithm': 'layered',
|
|
76
|
+
'elk.direction': 'RIGHT',
|
|
77
|
+
'elk.layered.considerModelOrder.strategy': 'NODES_AND_EDGES',
|
|
78
|
+
'elk.spacing.nodeNode': '80',
|
|
79
|
+
'elk.layered.spacing.nodeNodeBetweenLayers': '120',
|
|
80
|
+
'elk.edgeRouting': 'ORTHOGONAL',
|
|
81
|
+
'elk.layered.nodePlacement.strategy': 'NETWORK_SIMPLEX',
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Worker engine
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
let layoutWorker: Worker | null = null
|
|
88
|
+
let requestIdCounter = 0
|
|
89
|
+
const pendingRequests = new Map<number, {
|
|
90
|
+
resolve: (result: LayoutResult) => void
|
|
91
|
+
reject: (error: Error) => void
|
|
92
|
+
}>()
|
|
93
|
+
|
|
94
|
+
function getOrCreateWorker(): Worker {
|
|
95
|
+
if (!layoutWorker) {
|
|
96
|
+
layoutWorker = new Worker(new URL('./layout.worker.ts', import.meta.url), { type: 'module' })
|
|
97
|
+
layoutWorker.onmessage = (e: MessageEvent) => {
|
|
98
|
+
const { requestId, ...result } = e.data
|
|
99
|
+
const pending = pendingRequests.get(requestId)
|
|
100
|
+
if (pending) {
|
|
101
|
+
pendingRequests.delete(requestId)
|
|
102
|
+
if (result.error) {
|
|
103
|
+
pending.reject(new Error(result.error))
|
|
104
|
+
} else {
|
|
105
|
+
pending.resolve(result as LayoutResult)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
layoutWorker.onerror = (e) => {
|
|
110
|
+
console.error('[TopologyLayout] Worker error:', e)
|
|
111
|
+
for (const [, pending] of pendingRequests) {
|
|
112
|
+
pending.reject(new Error('Worker error'))
|
|
113
|
+
}
|
|
114
|
+
pendingRequests.clear()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return layoutWorker
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function runLayoutViaWorker(
|
|
121
|
+
elkGraph: ElkGraph,
|
|
122
|
+
groupingMode: GroupingMode,
|
|
123
|
+
hideGroupHeader: boolean,
|
|
124
|
+
padding: typeof GROUP_PADDING
|
|
125
|
+
): Promise<LayoutResult> {
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const worker = getOrCreateWorker()
|
|
128
|
+
const requestId = ++requestIdCounter
|
|
129
|
+
pendingRequests.set(requestId, { resolve, reject })
|
|
130
|
+
worker.postMessage({ type: 'layout', requestId, elkGraph, groupingMode, hideGroupHeader, padding })
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Main-thread engine — same two-phase ELK layout as layout.worker.ts
|
|
136
|
+
// but runs inline using elkjs/lib/elk.bundled.js (no web-worker dependency).
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
async function runLayoutOnMainThread(
|
|
139
|
+
elkGraph: ElkGraph,
|
|
140
|
+
groupingMode: GroupingMode,
|
|
141
|
+
hideGroupHeader: boolean,
|
|
142
|
+
padding: typeof GROUP_PADDING
|
|
143
|
+
): Promise<LayoutResult> {
|
|
144
|
+
const ELK = (await import('elkjs/lib/elk.bundled.js')).default
|
|
145
|
+
const elk = new ELK()
|
|
146
|
+
|
|
147
|
+
const groupLayouts: LayoutResult['groupLayouts'] = []
|
|
148
|
+
const ungroupedNodes: LayoutResult['ungroupedNodes'] = []
|
|
149
|
+
const groupNodeIds = new Map<string, Set<string>>()
|
|
150
|
+
|
|
151
|
+
// Phase 1: Layout each group independently
|
|
152
|
+
for (const child of elkGraph.children) {
|
|
153
|
+
const isGroup = child.id.startsWith('group-')
|
|
154
|
+
|
|
155
|
+
if (isGroup && child.children && child.children.length > 0) {
|
|
156
|
+
const groupKey = child.id.replace(`group-${groupingMode}-`, '')
|
|
157
|
+
const minWidth = hideGroupHeader ? 300 : Math.max(500, groupKey.length * 16 + 200)
|
|
158
|
+
const nodeIds = new Set(child.children.map(c => c.id))
|
|
159
|
+
groupNodeIds.set(child.id, nodeIds)
|
|
160
|
+
|
|
161
|
+
const intraGroupEdges = elkGraph.edges.filter(e =>
|
|
162
|
+
nodeIds.has(e.sources[0]) && nodeIds.has(e.targets[0])
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
const layoutResult = await elk.layout({
|
|
166
|
+
id: child.id,
|
|
167
|
+
layoutOptions: {
|
|
168
|
+
...elkOptionsGroup,
|
|
169
|
+
'elk.padding': `[left=${padding.left}, top=${padding.top}, right=${padding.right}, bottom=${padding.bottom}]`,
|
|
170
|
+
},
|
|
171
|
+
children: child.children,
|
|
172
|
+
edges: intraGroupEdges,
|
|
173
|
+
}) as any
|
|
174
|
+
|
|
175
|
+
groupLayouts.push({
|
|
176
|
+
groupId: child.id,
|
|
177
|
+
groupKey,
|
|
178
|
+
width: hideGroupHeader
|
|
179
|
+
? (layoutResult.width || 300)
|
|
180
|
+
: Math.max(layoutResult.width || 300, minWidth),
|
|
181
|
+
height: layoutResult.height || 200,
|
|
182
|
+
children: (layoutResult.children || []).map((c: any) => ({
|
|
183
|
+
id: c.id, x: c.x || 0, y: c.y || 0,
|
|
184
|
+
})),
|
|
185
|
+
isCollapsed: false,
|
|
186
|
+
})
|
|
187
|
+
} else if (isGroup) {
|
|
188
|
+
const groupKey = child.id.replace(`group-${groupingMode}-`, '')
|
|
189
|
+
const minWidth = Math.max(400, groupKey.length * 16 + 180)
|
|
190
|
+
groupLayouts.push({
|
|
191
|
+
groupId: child.id, groupKey,
|
|
192
|
+
width: Math.max(child.width || 280, minWidth),
|
|
193
|
+
height: child.height || 90,
|
|
194
|
+
children: [], isCollapsed: true,
|
|
195
|
+
})
|
|
196
|
+
} else {
|
|
197
|
+
ungroupedNodes.push({ id: child.id, width: child.width || 200, height: child.height || 56 })
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Phase 2: Build meta-graph and position groups based on inter-group edges
|
|
202
|
+
const nodeToGroup = new Map<string, string>()
|
|
203
|
+
for (const [groupId, nodeIds] of groupNodeIds) {
|
|
204
|
+
for (const nodeId of nodeIds) nodeToGroup.set(nodeId, groupId)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const interGroupEdges: ElkEdge[] = []
|
|
208
|
+
const seen = new Set<string>()
|
|
209
|
+
for (const edge of elkGraph.edges) {
|
|
210
|
+
const sg = nodeToGroup.get(edge.sources[0])
|
|
211
|
+
const tg = nodeToGroup.get(edge.targets[0])
|
|
212
|
+
if (sg && tg && sg !== tg) {
|
|
213
|
+
const key = `${sg}->${tg}`
|
|
214
|
+
if (!seen.has(key)) { seen.add(key); interGroupEdges.push({ id: `inter-${key}`, sources: [sg], targets: [tg] }) }
|
|
215
|
+
} else if ((!sg && tg) || (sg && !tg)) {
|
|
216
|
+
const s = sg || edge.sources[0], t = tg || edge.targets[0], key = `${s}->${t}`
|
|
217
|
+
if (!seen.has(key)) { seen.add(key); interGroupEdges.push({ id: `inter-${key}`, sources: [s], targets: [t] }) }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const metaResult = await elk.layout({
|
|
222
|
+
id: 'meta-root',
|
|
223
|
+
layoutOptions: elkOptionsGroupLayout,
|
|
224
|
+
children: [
|
|
225
|
+
...groupLayouts.map(g => ({ id: g.groupId, width: g.width, height: g.height })),
|
|
226
|
+
...ungroupedNodes.map(n => ({ id: n.id, width: n.width, height: n.height })),
|
|
227
|
+
],
|
|
228
|
+
edges: interGroupEdges,
|
|
229
|
+
}) as any
|
|
230
|
+
|
|
231
|
+
const groupPositions: Array<[string, { x: number; y: number }]> =
|
|
232
|
+
(metaResult.children || []).map((c: any) => [c.id, { x: c.x || 0, y: c.y || 0 }])
|
|
233
|
+
|
|
234
|
+
return { groupLayouts, ungroupedNodes, groupPositions }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// Dispatcher — routes to the active engine
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
function runLayout(
|
|
241
|
+
elkGraph: ElkGraph,
|
|
242
|
+
groupingMode: GroupingMode,
|
|
243
|
+
hideGroupHeader: boolean,
|
|
244
|
+
padding: typeof GROUP_PADDING
|
|
245
|
+
): Promise<LayoutResult> {
|
|
246
|
+
if (layoutEngine === 'main-thread') {
|
|
247
|
+
return runLayoutOnMainThread(elkGraph, groupingMode, hideGroupHeader, padding)
|
|
248
|
+
}
|
|
249
|
+
return runLayoutViaWorker(elkGraph, groupingMode, hideGroupHeader, padding)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
interface ElkNode {
|
|
253
|
+
id: string
|
|
254
|
+
width?: number
|
|
255
|
+
height?: number
|
|
256
|
+
children?: ElkNode[]
|
|
257
|
+
layoutOptions?: Record<string, string>
|
|
258
|
+
labels?: Array<{ text: string }>
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
interface ElkEdge {
|
|
262
|
+
id: string
|
|
263
|
+
sources: string[]
|
|
264
|
+
targets: string[]
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
interface ElkGraph {
|
|
268
|
+
id: string
|
|
269
|
+
layoutOptions: Record<string, string>
|
|
270
|
+
children: ElkNode[]
|
|
271
|
+
edges: ElkEdge[]
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Get app label from a node (if it has one)
|
|
275
|
+
function getAppLabel(node: TopologyNode): string | null {
|
|
276
|
+
const labels = (node.data.labels as Record<string, string>) || {}
|
|
277
|
+
return labels['app.kubernetes.io/name'] || labels['app'] || labels['app.kubernetes.io/instance'] || null
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Get group key for a node based on grouping mode
|
|
281
|
+
export function getGroupKey(node: TopologyNode, groupingMode: GroupingMode): string | null {
|
|
282
|
+
if (groupingMode === 'none') return null
|
|
283
|
+
|
|
284
|
+
if (groupingMode === 'namespace') {
|
|
285
|
+
return (node.data.namespace as string) || null
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (groupingMode === 'app') {
|
|
289
|
+
return getAppLabel(node)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return null
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Propagate app labels through connected resources and create groups for unlabeled connected components
|
|
296
|
+
// Returns a map of nodeId -> groupName for all nodes that should be grouped
|
|
297
|
+
function propagateAppLabels(
|
|
298
|
+
nodes: TopologyNode[],
|
|
299
|
+
edges: Array<{ id: string; source: string; target: string; type: string }>
|
|
300
|
+
): Map<string, string> {
|
|
301
|
+
const nodeMap = new Map<string, TopologyNode>()
|
|
302
|
+
for (const node of nodes) {
|
|
303
|
+
nodeMap.set(node.id, node)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Build adjacency list (bidirectional for propagation) - only within same namespace
|
|
307
|
+
const connections = new Map<string, Set<string>>()
|
|
308
|
+
for (const node of nodes) {
|
|
309
|
+
connections.set(node.id, new Set())
|
|
310
|
+
}
|
|
311
|
+
for (const edge of edges) {
|
|
312
|
+
const sourceNode = nodeMap.get(edge.source)
|
|
313
|
+
const targetNode = nodeMap.get(edge.target)
|
|
314
|
+
// Only connect nodes in the same namespace
|
|
315
|
+
if (sourceNode && targetNode && sourceNode.data.namespace === targetNode.data.namespace) {
|
|
316
|
+
connections.get(edge.source)?.add(edge.target)
|
|
317
|
+
connections.get(edge.target)?.add(edge.source)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Initial pass: find nodes with explicit app labels
|
|
322
|
+
const nodeGroupLabels = new Map<string, string>()
|
|
323
|
+
for (const node of nodes) {
|
|
324
|
+
const appLabel = getAppLabel(node)
|
|
325
|
+
if (appLabel) {
|
|
326
|
+
nodeGroupLabels.set(node.id, appLabel)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Propagate labels through connections (BFS from labeled nodes)
|
|
331
|
+
let changed = true
|
|
332
|
+
const maxIterations = 10
|
|
333
|
+
let iteration = 0
|
|
334
|
+
|
|
335
|
+
while (changed && iteration < maxIterations) {
|
|
336
|
+
changed = false
|
|
337
|
+
iteration++
|
|
338
|
+
|
|
339
|
+
for (const node of nodes) {
|
|
340
|
+
if (nodeGroupLabels.has(node.id)) continue
|
|
341
|
+
|
|
342
|
+
const connectedNodes = connections.get(node.id) || new Set()
|
|
343
|
+
const connectedLabels = new Set<string>()
|
|
344
|
+
|
|
345
|
+
for (const connectedId of connectedNodes) {
|
|
346
|
+
const connectedLabel = nodeGroupLabels.get(connectedId)
|
|
347
|
+
if (connectedLabel) {
|
|
348
|
+
connectedLabels.add(connectedLabel)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// If exactly one connected label, inherit it
|
|
353
|
+
if (connectedLabels.size === 1) {
|
|
354
|
+
const [inheritedLabel] = connectedLabels
|
|
355
|
+
nodeGroupLabels.set(node.id, inheritedLabel)
|
|
356
|
+
changed = true
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Find connected components among remaining unlabeled nodes
|
|
362
|
+
const unlabeledNodes = nodes.filter(n => !nodeGroupLabels.has(n.id))
|
|
363
|
+
const visited = new Set<string>()
|
|
364
|
+
|
|
365
|
+
for (const startNode of unlabeledNodes) {
|
|
366
|
+
if (visited.has(startNode.id)) continue
|
|
367
|
+
|
|
368
|
+
// BFS to find connected component
|
|
369
|
+
const component: TopologyNode[] = []
|
|
370
|
+
const queue = [startNode.id]
|
|
371
|
+
visited.add(startNode.id)
|
|
372
|
+
|
|
373
|
+
while (queue.length > 0) {
|
|
374
|
+
const nodeId = queue.shift()!
|
|
375
|
+
const node = nodeMap.get(nodeId)
|
|
376
|
+
if (node && !nodeGroupLabels.has(nodeId)) {
|
|
377
|
+
component.push(node)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
for (const connectedId of connections.get(nodeId) || []) {
|
|
381
|
+
if (!visited.has(connectedId) && !nodeGroupLabels.has(connectedId)) {
|
|
382
|
+
visited.add(connectedId)
|
|
383
|
+
queue.push(connectedId)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Create a group for this connected component (only if more than 1 node)
|
|
389
|
+
// Singletons remain ungrouped
|
|
390
|
+
if (component.length > 1) {
|
|
391
|
+
// Name the group after the most "important" node (prefer Deployment, Service, etc.)
|
|
392
|
+
const groupName = pickGroupName(component)
|
|
393
|
+
for (const node of component) {
|
|
394
|
+
nodeGroupLabels.set(node.id, groupName)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return nodeGroupLabels
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Pick a representative name for a connected component group
|
|
403
|
+
function pickGroupName(nodes: TopologyNode[]): string {
|
|
404
|
+
// Priority order for picking the group name
|
|
405
|
+
const kindPriority: Record<string, number> = {
|
|
406
|
+
'Deployment': 1,
|
|
407
|
+
'Rollout': 1,
|
|
408
|
+
'StatefulSet': 2,
|
|
409
|
+
'DaemonSet': 3,
|
|
410
|
+
'CronJob': 4,
|
|
411
|
+
'Job': 5,
|
|
412
|
+
'Service': 6,
|
|
413
|
+
'Gateway': 7,
|
|
414
|
+
'HTTPRoute': 6,
|
|
415
|
+
'GRPCRoute': 6,
|
|
416
|
+
'TCPRoute': 6,
|
|
417
|
+
'TLSRoute': 6,
|
|
418
|
+
'Ingress': 7,
|
|
419
|
+
'ReplicaSet': 8,
|
|
420
|
+
'Pod': 9,
|
|
421
|
+
'PodGroup': 9,
|
|
422
|
+
'ConfigMap': 10,
|
|
423
|
+
'Secret': 10,
|
|
424
|
+
'PersistentVolumeClaim': 10,
|
|
425
|
+
'HorizontalPodAutoscaler': 10,
|
|
426
|
+
'KnativeService': 1,
|
|
427
|
+
'KnativeConfiguration': 3,
|
|
428
|
+
'KnativeRevision': 4,
|
|
429
|
+
'KnativeRoute': 2,
|
|
430
|
+
'Broker': 2,
|
|
431
|
+
'Channel': 2,
|
|
432
|
+
'Trigger': 3,
|
|
433
|
+
'PingSource': 3,
|
|
434
|
+
'ApiServerSource': 3,
|
|
435
|
+
'ContainerSource': 3,
|
|
436
|
+
'SinkBinding': 3,
|
|
437
|
+
'IngressRoute': 1,
|
|
438
|
+
'IngressRouteTCP': 1,
|
|
439
|
+
'IngressRouteUDP': 1,
|
|
440
|
+
'TraefikService': 2,
|
|
441
|
+
'Middleware': 3,
|
|
442
|
+
'MiddlewareTCP': 3,
|
|
443
|
+
'ServersTransport': 4,
|
|
444
|
+
'ServersTransportTCP': 4,
|
|
445
|
+
'TLSOption': 4,
|
|
446
|
+
'TLSStore': 4,
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Sort by priority and pick the first
|
|
450
|
+
const sorted = [...nodes].sort((a, b) => {
|
|
451
|
+
const priorityA = kindPriority[a.kind] || 99
|
|
452
|
+
const priorityB = kindPriority[b.kind] || 99
|
|
453
|
+
return priorityA - priorityB
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
return sorted[0].name
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Build hierarchical ELK graph with groups containing children
|
|
460
|
+
export function buildHierarchicalElkGraph(
|
|
461
|
+
topologyNodes: TopologyNode[],
|
|
462
|
+
edges: Array<{ id: string; source: string; target: string; type: string }>,
|
|
463
|
+
groupingMode: GroupingMode,
|
|
464
|
+
collapsedGroups: Set<string>
|
|
465
|
+
): { elkGraph: ElkGraph; groupMap: Map<string, string[]>; nodeToGroup: Map<string, string> } {
|
|
466
|
+
const groupMap = new Map<string, string[]>()
|
|
467
|
+
const nodeToGroup = new Map<string, string>()
|
|
468
|
+
|
|
469
|
+
// For app grouping, propagate labels through connected resources
|
|
470
|
+
const propagatedAppLabels = groupingMode === 'app'
|
|
471
|
+
? propagateAppLabels(topologyNodes, edges)
|
|
472
|
+
: null
|
|
473
|
+
|
|
474
|
+
// Group nodes by their group key
|
|
475
|
+
for (const node of topologyNodes) {
|
|
476
|
+
let groupKey: string | null = null
|
|
477
|
+
|
|
478
|
+
if (groupingMode === 'namespace') {
|
|
479
|
+
groupKey = (node.data.namespace as string) || null
|
|
480
|
+
} else if (groupingMode === 'app') {
|
|
481
|
+
// Use propagated label if available, otherwise direct label
|
|
482
|
+
groupKey = propagatedAppLabels?.get(node.id) || getAppLabel(node)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (groupKey) {
|
|
486
|
+
const groupId = `group-${groupingMode}-${groupKey}`
|
|
487
|
+
if (!groupMap.has(groupKey)) {
|
|
488
|
+
groupMap.set(groupKey, [])
|
|
489
|
+
}
|
|
490
|
+
groupMap.get(groupKey)!.push(node.id)
|
|
491
|
+
nodeToGroup.set(node.id, groupId)
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const children: ElkNode[] = []
|
|
496
|
+
const processedNodes = new Set<string>()
|
|
497
|
+
|
|
498
|
+
if (groupingMode === 'none') {
|
|
499
|
+
// No grouping - all nodes as direct children of root
|
|
500
|
+
for (const node of topologyNodes) {
|
|
501
|
+
const kind = node.kind as NodeKind
|
|
502
|
+
const dims = NODE_DIMENSIONS[kind] || { width: 200, height: 56 }
|
|
503
|
+
children.push({
|
|
504
|
+
id: node.id,
|
|
505
|
+
width: dims.width,
|
|
506
|
+
height: dims.height,
|
|
507
|
+
})
|
|
508
|
+
}
|
|
509
|
+
} else {
|
|
510
|
+
// Create group nodes with children
|
|
511
|
+
for (const [groupKey, memberIds] of groupMap) {
|
|
512
|
+
const groupId = `group-${groupingMode}-${groupKey}`
|
|
513
|
+
const isCollapsed = collapsedGroups.has(groupId)
|
|
514
|
+
|
|
515
|
+
if (isCollapsed) {
|
|
516
|
+
// Collapsed group is a single node - width based on label length
|
|
517
|
+
const collapsedWidth = Math.max(400, groupKey.length * 16 + 180)
|
|
518
|
+
children.push({
|
|
519
|
+
id: groupId,
|
|
520
|
+
width: collapsedWidth,
|
|
521
|
+
height: 90,
|
|
522
|
+
labels: [{ text: groupKey }],
|
|
523
|
+
})
|
|
524
|
+
} else {
|
|
525
|
+
// Expanded group contains its children
|
|
526
|
+
const groupChildren: ElkNode[] = []
|
|
527
|
+
for (const nodeId of memberIds) {
|
|
528
|
+
const node = topologyNodes.find(n => n.id === nodeId)
|
|
529
|
+
if (node) {
|
|
530
|
+
const kind = node.kind as NodeKind
|
|
531
|
+
const dims = NODE_DIMENSIONS[kind] || { width: 200, height: 56 }
|
|
532
|
+
groupChildren.push({
|
|
533
|
+
id: nodeId,
|
|
534
|
+
width: dims.width,
|
|
535
|
+
height: dims.height,
|
|
536
|
+
})
|
|
537
|
+
processedNodes.add(nodeId)
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Calculate minimum width based on label length (approx 14px per char for text-4xl + padding)
|
|
542
|
+
const minWidth = Math.max(500, groupKey.length * 16 + 200)
|
|
543
|
+
|
|
544
|
+
children.push({
|
|
545
|
+
id: groupId,
|
|
546
|
+
children: groupChildren,
|
|
547
|
+
layoutOptions: {
|
|
548
|
+
'elk.padding': `[left=${GROUP_PADDING.left}, top=${GROUP_PADDING.top}, right=${GROUP_PADDING.right}, bottom=${GROUP_PADDING.bottom}]`,
|
|
549
|
+
'elk.algorithm': 'layered',
|
|
550
|
+
'elk.direction': 'RIGHT',
|
|
551
|
+
'elk.spacing.nodeNode': '40',
|
|
552
|
+
'elk.layered.spacing.nodeNodeBetweenLayers': '85',
|
|
553
|
+
'elk.layered.spacing.edgeNodeBetweenLayers': '25',
|
|
554
|
+
'elk.nodeSize.minimum': `(${minWidth}, 100)`,
|
|
555
|
+
},
|
|
556
|
+
labels: [{ text: groupKey }],
|
|
557
|
+
})
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Mark all members as processed
|
|
561
|
+
for (const nodeId of memberIds) {
|
|
562
|
+
processedNodes.add(nodeId)
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Add ungrouped nodes as direct children
|
|
567
|
+
for (const node of topologyNodes) {
|
|
568
|
+
if (!processedNodes.has(node.id)) {
|
|
569
|
+
const kind = node.kind as NodeKind
|
|
570
|
+
const dims = NODE_DIMENSIONS[kind] || { width: 200, height: 56 }
|
|
571
|
+
children.push({
|
|
572
|
+
id: node.id,
|
|
573
|
+
width: dims.width,
|
|
574
|
+
height: dims.height,
|
|
575
|
+
})
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Build edges, redirecting to groups when collapsed
|
|
581
|
+
const elkEdges: ElkEdge[] = []
|
|
582
|
+
const seenEdges = new Set<string>()
|
|
583
|
+
|
|
584
|
+
for (const edge of edges) {
|
|
585
|
+
let source = edge.source
|
|
586
|
+
let target = edge.target
|
|
587
|
+
|
|
588
|
+
// Redirect edges to collapsed groups
|
|
589
|
+
const sourceGroup = nodeToGroup.get(source)
|
|
590
|
+
if (sourceGroup && collapsedGroups.has(sourceGroup)) {
|
|
591
|
+
source = sourceGroup
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const targetGroup = nodeToGroup.get(target)
|
|
595
|
+
if (targetGroup && collapsedGroups.has(targetGroup)) {
|
|
596
|
+
target = targetGroup
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Skip self-loops
|
|
600
|
+
if (source === target) continue
|
|
601
|
+
|
|
602
|
+
// Skip duplicates
|
|
603
|
+
const edgeKey = `${source}->${target}`
|
|
604
|
+
if (seenEdges.has(edgeKey)) continue
|
|
605
|
+
seenEdges.add(edgeKey)
|
|
606
|
+
|
|
607
|
+
elkEdges.push({
|
|
608
|
+
id: edge.id,
|
|
609
|
+
sources: [source],
|
|
610
|
+
targets: [target],
|
|
611
|
+
})
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return {
|
|
615
|
+
elkGraph: {
|
|
616
|
+
id: 'root',
|
|
617
|
+
layoutOptions: {}, // Root layout options not used - we manually arrange groups
|
|
618
|
+
children,
|
|
619
|
+
edges: elkEdges,
|
|
620
|
+
},
|
|
621
|
+
groupMap,
|
|
622
|
+
nodeToGroup,
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Two-phase layout: first layout groups internally, then position groups based on connections
|
|
627
|
+
// Layout is performed in a Web Worker to avoid blocking the main thread
|
|
628
|
+
export async function applyHierarchicalLayout(
|
|
629
|
+
elkGraph: ElkGraph,
|
|
630
|
+
topologyNodes: TopologyNode[],
|
|
631
|
+
groupMap: Map<string, string[]>,
|
|
632
|
+
groupingMode: GroupingMode,
|
|
633
|
+
_collapsedGroups: Set<string>,
|
|
634
|
+
onToggleCollapse: (groupId: string) => void,
|
|
635
|
+
hideGroupHeader: boolean = false
|
|
636
|
+
): Promise<{ nodes: Node[]; positions: Map<string, { x: number; y: number }>; error?: string }> {
|
|
637
|
+
try {
|
|
638
|
+
const padding = hideGroupHeader ? GROUP_PADDING_NO_HEADER : GROUP_PADDING
|
|
639
|
+
|
|
640
|
+
// Run layout in worker (off main thread)
|
|
641
|
+
const workerResult = await runLayout(elkGraph, groupingMode, hideGroupHeader, padding)
|
|
642
|
+
|
|
643
|
+
if (workerResult.error) {
|
|
644
|
+
return { nodes: [], positions: new Map(), error: workerResult.error }
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Build position map from worker result
|
|
648
|
+
const groupPositions = new Map<string, { x: number; y: number }>(workerResult.groupPositions)
|
|
649
|
+
|
|
650
|
+
// Build ReactFlow nodes using positions from worker
|
|
651
|
+
const nodes: Node[] = []
|
|
652
|
+
const positions = new Map<string, { x: number; y: number }>()
|
|
653
|
+
|
|
654
|
+
for (const group of workerResult.groupLayouts) {
|
|
655
|
+
const pos = groupPositions.get(group.groupId) || { x: 0, y: 0 }
|
|
656
|
+
const memberIds = groupMap.get(group.groupKey) || []
|
|
657
|
+
|
|
658
|
+
positions.set(group.groupId, pos)
|
|
659
|
+
|
|
660
|
+
// Add group node
|
|
661
|
+
nodes.push({
|
|
662
|
+
id: group.groupId,
|
|
663
|
+
type: 'group',
|
|
664
|
+
position: pos,
|
|
665
|
+
data: {
|
|
666
|
+
type: groupingMode,
|
|
667
|
+
name: group.groupKey,
|
|
668
|
+
nodeCount: memberIds.length,
|
|
669
|
+
collapsed: group.isCollapsed,
|
|
670
|
+
onToggleCollapse,
|
|
671
|
+
hideHeader: hideGroupHeader,
|
|
672
|
+
},
|
|
673
|
+
style: {
|
|
674
|
+
width: group.width,
|
|
675
|
+
height: group.height,
|
|
676
|
+
},
|
|
677
|
+
zIndex: -1,
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
// Add child nodes with positions relative to group
|
|
681
|
+
for (const child of group.children) {
|
|
682
|
+
const topoNode = topologyNodes.find(n => n.id === child.id)
|
|
683
|
+
if (topoNode) {
|
|
684
|
+
const absX = pos.x + child.x
|
|
685
|
+
const absY = pos.y + child.y
|
|
686
|
+
positions.set(child.id, { x: absX, y: absY })
|
|
687
|
+
|
|
688
|
+
nodes.push({
|
|
689
|
+
id: child.id,
|
|
690
|
+
type: 'k8sResource',
|
|
691
|
+
position: { x: child.x, y: child.y },
|
|
692
|
+
parentId: group.groupId,
|
|
693
|
+
extent: 'parent',
|
|
694
|
+
data: {
|
|
695
|
+
kind: topoNode.kind,
|
|
696
|
+
name: topoNode.name,
|
|
697
|
+
status: topoNode.status,
|
|
698
|
+
nodeData: topoNode.data,
|
|
699
|
+
selected: false,
|
|
700
|
+
},
|
|
701
|
+
})
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Add ungrouped nodes
|
|
707
|
+
for (const node of workerResult.ungroupedNodes) {
|
|
708
|
+
const pos = groupPositions.get(node.id) || { x: 0, y: 0 }
|
|
709
|
+
const topoNode = topologyNodes.find(n => n.id === node.id)
|
|
710
|
+
if (topoNode) {
|
|
711
|
+
positions.set(node.id, pos)
|
|
712
|
+
|
|
713
|
+
nodes.push({
|
|
714
|
+
id: node.id,
|
|
715
|
+
type: 'k8sResource',
|
|
716
|
+
position: pos,
|
|
717
|
+
data: {
|
|
718
|
+
kind: topoNode.kind,
|
|
719
|
+
name: topoNode.name,
|
|
720
|
+
status: topoNode.status,
|
|
721
|
+
nodeData: topoNode.data,
|
|
722
|
+
selected: false,
|
|
723
|
+
},
|
|
724
|
+
})
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return { nodes, positions }
|
|
729
|
+
} catch (err) {
|
|
730
|
+
const errorMessage = err instanceof Error ? err.message : String(err)
|
|
731
|
+
console.error('ELK hierarchical layout error:', err)
|
|
732
|
+
return { nodes: [], positions: new Map(), error: `Layout failed: ${errorMessage}` }
|
|
733
|
+
}
|
|
734
|
+
}
|