@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.
Files changed (193) hide show
  1. package/package.json +63 -0
  2. package/src/components/dock/BottomDock.tsx +204 -0
  3. package/src/components/dock/DockContext.tsx +212 -0
  4. package/src/components/dock/TerminalTab.tsx +333 -0
  5. package/src/components/dock/index.ts +3 -0
  6. package/src/components/gitops/GitOpsActions.tsx +227 -0
  7. package/src/components/gitops/GitOpsStatusBadge.tsx +221 -0
  8. package/src/components/gitops/ManagedResourcesList.tsx +210 -0
  9. package/src/components/gitops/SyncCountdown.tsx +146 -0
  10. package/src/components/gitops/index.ts +4 -0
  11. package/src/components/logs/JsonLogLine.tsx +90 -0
  12. package/src/components/logs/LogCore.tsx +553 -0
  13. package/src/components/logs/LogToolbarSelects.tsx +74 -0
  14. package/src/components/logs/LogsViewer.tsx +146 -0
  15. package/src/components/logs/WorkloadLogsViewer.tsx +263 -0
  16. package/src/components/logs/index.ts +13 -0
  17. package/src/components/logs/useLogBuffer.ts +138 -0
  18. package/src/components/logs/useLogSearch.ts +151 -0
  19. package/src/components/logs/useLogStream.ts +61 -0
  20. package/src/components/resources/ResourcesView.tsx +5494 -0
  21. package/src/components/resources/index.ts +17 -0
  22. package/src/components/resources/renderers/AlertRenderer.tsx +166 -0
  23. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +233 -0
  24. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +89 -0
  25. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +194 -0
  26. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +79 -0
  27. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +68 -0
  28. package/src/components/resources/renderers/CertificateRenderer.tsx +209 -0
  29. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +123 -0
  30. package/src/components/resources/renderers/ChallengeRenderer.tsx +111 -0
  31. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +164 -0
  32. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +140 -0
  33. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +153 -0
  34. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +221 -0
  35. package/src/components/resources/renderers/ConfigMapRenderer.tsx +124 -0
  36. package/src/components/resources/renderers/CronJobRenderer.tsx +86 -0
  37. package/src/components/resources/renderers/EventRenderer.tsx +175 -0
  38. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +119 -0
  39. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +172 -0
  40. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +242 -0
  41. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +263 -0
  42. package/src/components/resources/renderers/GatewayClassRenderer.tsx +68 -0
  43. package/src/components/resources/renderers/GatewayRenderer.tsx +218 -0
  44. package/src/components/resources/renderers/GenericRenderer.tsx +476 -0
  45. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +154 -0
  46. package/src/components/resources/renderers/HPARenderer.tsx +164 -0
  47. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +303 -0
  48. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +167 -0
  49. package/src/components/resources/renderers/IngressClassRenderer.tsx +49 -0
  50. package/src/components/resources/renderers/IngressRenderer.tsx +104 -0
  51. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +175 -0
  52. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +176 -0
  53. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +125 -0
  54. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +84 -0
  55. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +112 -0
  56. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +288 -0
  57. package/src/components/resources/renderers/JobRenderer.tsx +103 -0
  58. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +157 -0
  59. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +188 -0
  60. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +148 -0
  61. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +90 -0
  62. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +136 -0
  63. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +151 -0
  64. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +70 -0
  65. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +309 -0
  66. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +142 -0
  67. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +232 -0
  68. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +97 -0
  69. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +74 -0
  70. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +138 -0
  71. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +220 -0
  72. package/src/components/resources/renderers/KustomizationRenderer.tsx +151 -0
  73. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +352 -0
  74. package/src/components/resources/renderers/LeaseRenderer.tsx +93 -0
  75. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +208 -0
  76. package/src/components/resources/renderers/NodeRenderer.tsx +282 -0
  77. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +153 -0
  78. package/src/components/resources/renderers/OrderRenderer.tsx +108 -0
  79. package/src/components/resources/renderers/PVCRenderer.tsx +98 -0
  80. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +138 -0
  81. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +126 -0
  82. package/src/components/resources/renderers/PodMonitorRenderer.tsx +63 -0
  83. package/src/components/resources/renderers/PodRenderer.tsx +677 -0
  84. package/src/components/resources/renderers/PriorityClassRenderer.tsx +50 -0
  85. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +62 -0
  86. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +71 -0
  87. package/src/components/resources/renderers/RoleBindingRenderer.tsx +97 -0
  88. package/src/components/resources/renderers/RoleRenderer.tsx +192 -0
  89. package/src/components/resources/renderers/RolloutRenderer.tsx +227 -0
  90. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +75 -0
  91. package/src/components/resources/renderers/SbomReportRenderer.tsx +98 -0
  92. package/src/components/resources/renderers/SealedSecretRenderer.tsx +121 -0
  93. package/src/components/resources/renderers/SecretRenderer.tsx +321 -0
  94. package/src/components/resources/renderers/SecretStoreRenderer.tsx +111 -0
  95. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +81 -0
  96. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +65 -0
  97. package/src/components/resources/renderers/ServiceRenderer.tsx +98 -0
  98. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +195 -0
  99. package/src/components/resources/renderers/StorageClassRenderer.tsx +82 -0
  100. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +257 -0
  101. package/src/components/resources/renderers/VPARenderer.tsx +193 -0
  102. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +73 -0
  103. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +193 -0
  104. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +161 -0
  105. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +134 -0
  106. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +36 -0
  107. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +347 -0
  108. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +156 -0
  109. package/src/components/resources/renderers/WorkflowRenderer.tsx +202 -0
  110. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +91 -0
  111. package/src/components/resources/renderers/WorkloadRenderer.tsx +311 -0
  112. package/src/components/resources/renderers/argo-cells.tsx +118 -0
  113. package/src/components/resources/renderers/certmanager-cells.tsx +164 -0
  114. package/src/components/resources/renderers/cnpg-cells.tsx +161 -0
  115. package/src/components/resources/renderers/eso-cells.tsx +115 -0
  116. package/src/components/resources/renderers/flux-cells.tsx +253 -0
  117. package/src/components/resources/renderers/index.ts +105 -0
  118. package/src/components/resources/renderers/istio-cells.tsx +210 -0
  119. package/src/components/resources/renderers/karpenter-cells.tsx +107 -0
  120. package/src/components/resources/renderers/keda-cells.tsx +94 -0
  121. package/src/components/resources/renderers/knative-cells.tsx +396 -0
  122. package/src/components/resources/renderers/kyverno-cells.tsx +85 -0
  123. package/src/components/resources/renderers/prometheus-cells.tsx +74 -0
  124. package/src/components/resources/renderers/traefik-cells.tsx +120 -0
  125. package/src/components/resources/renderers/trivy-cells.tsx +153 -0
  126. package/src/components/resources/renderers/trivy-shared.tsx +67 -0
  127. package/src/components/resources/renderers/velero-cells.tsx +164 -0
  128. package/src/components/resources/resource-utils-argo.ts +155 -0
  129. package/src/components/resources/resource-utils-certmanager.ts +198 -0
  130. package/src/components/resources/resource-utils-cnpg.ts +344 -0
  131. package/src/components/resources/resource-utils-eso.ts +385 -0
  132. package/src/components/resources/resource-utils-flux.ts +186 -0
  133. package/src/components/resources/resource-utils-istio.ts +372 -0
  134. package/src/components/resources/resource-utils-karpenter.ts +150 -0
  135. package/src/components/resources/resource-utils-keda.ts +203 -0
  136. package/src/components/resources/resource-utils-knative.ts +218 -0
  137. package/src/components/resources/resource-utils-kyverno.ts +224 -0
  138. package/src/components/resources/resource-utils-prometheus.ts +159 -0
  139. package/src/components/resources/resource-utils-traefik.ts +180 -0
  140. package/src/components/resources/resource-utils-trivy.ts +146 -0
  141. package/src/components/resources/resource-utils-velero.ts +293 -0
  142. package/src/components/resources/resource-utils.ts +1558 -0
  143. package/src/components/shared/EditableYamlView.tsx +302 -0
  144. package/src/components/shared/ResourceActionsBar.tsx +924 -0
  145. package/src/components/shared/ResourceRendererDispatch.tsx +557 -0
  146. package/src/components/shared/index.ts +3 -0
  147. package/src/components/timeline/index.ts +1 -0
  148. package/src/components/timeline/shared.tsx +614 -0
  149. package/src/components/topology/GroupNode.tsx +198 -0
  150. package/src/components/topology/K8sResourceNode.tsx +440 -0
  151. package/src/components/topology/TopologyControls.tsx +62 -0
  152. package/src/components/topology/TopologyFilterSidebar.tsx +377 -0
  153. package/src/components/topology/TopologyGraph.tsx +748 -0
  154. package/src/components/topology/TopologySearch.tsx +304 -0
  155. package/src/components/topology/index.ts +7 -0
  156. package/src/components/topology/layout.ts +734 -0
  157. package/src/components/topology/layout.worker.ts +272 -0
  158. package/src/components/topology/topology.css +129 -0
  159. package/src/components/ui/CodeViewer.tsx +451 -0
  160. package/src/components/ui/ConfirmDialog.tsx +188 -0
  161. package/src/components/ui/ForceDeleteConfirmDialog.tsx +57 -0
  162. package/src/components/ui/HealthRing.tsx +72 -0
  163. package/src/components/ui/MetricsChart.tsx +259 -0
  164. package/src/components/ui/ResourceBar.tsx +68 -0
  165. package/src/components/ui/Toast.tsx +225 -0
  166. package/src/components/ui/Tooltip.tsx +165 -0
  167. package/src/components/ui/YamlEditor.tsx +280 -0
  168. package/src/components/ui/drawer-components.tsx +792 -0
  169. package/src/components/ui/index.ts +10 -0
  170. package/src/components/workload/ResourceDetailDrawer.tsx +158 -0
  171. package/src/components/workload/WorkloadView.tsx +1109 -0
  172. package/src/components/workload/index.ts +2 -0
  173. package/src/hooks/index.ts +9 -0
  174. package/src/hooks/useAnimatedUnmount.ts +30 -0
  175. package/src/hooks/useKeyboardShortcuts.tsx +336 -0
  176. package/src/hooks/useRefreshAnimation.ts +58 -0
  177. package/src/index.ts +35 -0
  178. package/src/types/core.ts +894 -0
  179. package/src/types/gitops.ts +458 -0
  180. package/src/types/index.ts +2 -0
  181. package/src/utils/animation.ts +40 -0
  182. package/src/utils/api-resources.ts +185 -0
  183. package/src/utils/badge-colors.ts +399 -0
  184. package/src/utils/context-name.ts +72 -0
  185. package/src/utils/download.ts +19 -0
  186. package/src/utils/format.ts +233 -0
  187. package/src/utils/index.ts +10 -0
  188. package/src/utils/log-format.ts +300 -0
  189. package/src/utils/navigation.ts +119 -0
  190. package/src/utils/resource-hierarchy.ts +630 -0
  191. package/src/utils/resource-icons.ts +206 -0
  192. package/src/vite-env.d.ts +1 -0
  193. package/tsconfig.json +24 -0
@@ -0,0 +1,630 @@
1
+ /**
2
+ * Shared resource hierarchy building logic.
3
+ *
4
+ * This module provides utilities for building hierarchical resource lanes from timeline events.
5
+ * It's used by both TimelineSwimlanes (for the main timeline view) and WorkloadView
6
+ * (for showing related events in the detail view).
7
+ *
8
+ * The hierarchy is built from:
9
+ * 1. Owner references (event.owner) - most reliable for Deployment→RS→Pod chains
10
+ * 2. Topology edges (Service→Deployment via 'exposes', Ingress→Service and Gateway→Route→Service via 'routes-to', etc.)
11
+ * 3. App label grouping (app.kubernetes.io/name or app label)
12
+ */
13
+
14
+ import type { TimelineEvent, Topology } from '../types/core'
15
+ import { isWorkloadKind } from '../types/core'
16
+
17
+ /**
18
+ * Resource lane representing a single resource and its timeline events.
19
+ * Can have child lanes for related resources (e.g., Deployment has ReplicaSet and Pod children).
20
+ */
21
+ export interface ResourceLane {
22
+ id: string
23
+ kind: string
24
+ namespace: string
25
+ name: string
26
+ events: TimelineEvent[]
27
+ isWorkload: boolean
28
+ children?: ResourceLane[]
29
+ childEventCount?: number
30
+ allEventsSorted?: TimelineEvent[]
31
+ }
32
+
33
+ /**
34
+ * Options for building resource hierarchy.
35
+ */
36
+ export interface HierarchyOptions {
37
+ events: TimelineEvent[]
38
+ topology?: Topology
39
+ /** If provided, returns only the hierarchy rooted at this resource */
40
+ rootResource?: { kind: string; namespace: string; name: string }
41
+ /** Whether to group by app.kubernetes.io/name or app label (default: true) */
42
+ groupByApp?: boolean
43
+ }
44
+
45
+ /**
46
+ * Event reasons that indicate problems even if eventType is "Normal"
47
+ * Comprehensive list based on Kubernetes source code and documentation
48
+ */
49
+ const PROBLEMATIC_REASONS = new Set([
50
+ // Container state issues
51
+ 'BackOff', 'CrashLoopBackOff', 'Failed', 'Error',
52
+ 'OOMKilling', 'OOMKilled',
53
+ 'CreateContainerConfigError', 'CreateContainerError', 'RunContainerError',
54
+ 'InvalidImageName', 'ErrImagePull', 'ImagePullBackOff',
55
+ 'ContainerStatusUnknown',
56
+
57
+ // Pod scheduling/lifecycle issues
58
+ 'FailedScheduling', 'FailedMount', 'FailedAttachVolume',
59
+ 'FailedCreate', 'FailedDelete', 'Unhealthy', 'Killing', 'Evicted',
60
+ 'FailedSync', 'FailedValidation',
61
+ 'FailedPreStopHook', 'FailedPostStartHook',
62
+ 'HostPortConflict', 'InsufficientMemory', 'InsufficientCPU',
63
+
64
+ // Node conditions
65
+ 'NodeNotReady', 'NetworkNotReady', 'KubeletNotReady',
66
+ 'MemoryPressure', 'DiskPressure', 'PIDPressure',
67
+ 'NodeStatusUnknown',
68
+
69
+ // Deployment/workload issues
70
+ 'ProgressDeadlineExceeded', 'ReplicaFailure',
71
+ 'MinimumReplicasUnavailable',
72
+
73
+ // HPA issues
74
+ 'FailedGetScale', 'FailedRescale', 'FailedUpdateScale',
75
+ 'FailedGetResourceMetric', 'FailedComputeMetricsReplicas',
76
+
77
+ // PVC/storage issues
78
+ 'ProvisioningFailed', 'FailedBinding', 'VolumeFailedDelete',
79
+
80
+ // Job issues
81
+ 'DeadlineExceeded', 'BackoffLimitExceeded',
82
+ ])
83
+
84
+ /**
85
+ * Check if an event is problematic (warning or error condition)
86
+ */
87
+ export function isProblematicEvent(event: TimelineEvent): boolean {
88
+ if (event.eventType === 'Warning') return true
89
+ if (event.reason && PROBLEMATIC_REASONS.has(event.reason)) return true
90
+ return false
91
+ }
92
+
93
+ /**
94
+ * Convert topology node ID to lane ID format.
95
+ * Node IDs are formatted as: kind/namespace/name (e.g., "pod/default/nginx-abc123")
96
+ * Lane IDs are formatted as: Kind/namespace/name (e.g., "Pod/default/nginx-abc123")
97
+ */
98
+ function nodeIdToLaneId(nodeId: string): string | null {
99
+ const parts = nodeId.split('/')
100
+ if (parts.length < 3) return null
101
+ const kind = parts[0]
102
+ const namespace = parts[1]
103
+ const name = parts[2]
104
+ // Maps lowercase topology node IDs to PascalCase kind names used in timeline lane IDs.
105
+ const kindMap: Record<string, string> = {
106
+ pod: 'Pod', service: 'Service', deployment: 'Deployment',
107
+ replicaset: 'ReplicaSet', statefulset: 'StatefulSet', daemonset: 'DaemonSet',
108
+ ingress: 'Ingress', gateway: 'Gateway', httproute: 'HTTPRoute',
109
+ grpcroute: 'GRPCRoute', tcproute: 'TCPRoute', tlsroute: 'TLSRoute',
110
+ configmap: 'ConfigMap', secret: 'Secret',
111
+ persistentvolumeclaim: 'PersistentVolumeClaim',
112
+ persistentvolume: 'PersistentVolume', storageclass: 'StorageClass',
113
+ job: 'Job', cronjob: 'CronJob',
114
+ horizontalpodautoscaler: 'HorizontalPodAutoscaler',
115
+ verticalpodautoscaler: 'VerticalPodAutoscaler',
116
+ poddisruptionbudget: 'PodDisruptionBudget',
117
+ podgroup: 'PodGroup', rollout: 'Rollout', namespace: 'Namespace',
118
+ node: 'Node',
119
+ application: 'Application', applicationset: 'ApplicationSet', appproject: 'AppProject',
120
+ kustomization: 'Kustomization',
121
+ helmrelease: 'HelmRelease', helmrepository: 'HelmRepository',
122
+ helmchart: 'HelmChart', gitrepository: 'GitRepository',
123
+ ocirepository: 'OCIRepository', certificate: 'Certificate',
124
+ // Istio
125
+ virtualservice: 'VirtualService', destinationrule: 'DestinationRule',
126
+ istiogateway: 'IstioGateway', serviceentry: 'ServiceEntry',
127
+ peerauthentication: 'PeerAuthentication', authorizationpolicy: 'AuthorizationPolicy',
128
+ // KEDA
129
+ scaledobject: 'ScaledObject', scaledjob: 'ScaledJob',
130
+ // Karpenter
131
+ nodepool: 'NodePool', nodeclaim: 'NodeClaim',
132
+ // cert-manager
133
+ issuer: 'Issuer', clusterissuer: 'ClusterIssuer',
134
+ // Knative
135
+ knativeservice: 'KnativeService', knativeconfiguration: 'KnativeConfiguration',
136
+ knativerevision: 'KnativeRevision', knativeroute: 'KnativeRoute',
137
+ broker: 'Broker', trigger: 'Trigger', channel: 'Channel',
138
+ pingsource: 'PingSource', apiserversource: 'ApiServerSource',
139
+ containersource: 'ContainerSource', sinkbinding: 'SinkBinding',
140
+ // Traefik
141
+ ingressroute: 'IngressRoute', ingressroutetcp: 'IngressRouteTCP',
142
+ ingressrouteudp: 'IngressRouteUDP', middleware: 'Middleware',
143
+ middlewaretcp: 'MiddlewareTCP', traefikservice: 'TraefikService',
144
+ serverstransport: 'ServersTransport', serverstransporttcp: 'ServersTransportTCP',
145
+ tlsoption: 'TLSOption', tlsstore: 'TLSStore',
146
+ }
147
+ return `${kindMap[kind] || kind}/${namespace}/${name}`
148
+ }
149
+
150
+ /**
151
+ * Sort events for rendering (important events render on top).
152
+ * Priority: updates (0) < adds (1) < deletes (2) < problematic/warnings (3)
153
+ * Lower priority renders first (behind), higher renders last (on top)
154
+ */
155
+ function sortEventsForRendering(events: TimelineEvent[]): TimelineEvent[] {
156
+ return [...events].sort((a, b) => {
157
+ const getPriority = (e: TimelineEvent) => {
158
+ if (isProblematicEvent(e)) return 3
159
+ if (e.eventType === 'delete') return 2
160
+ if (e.eventType === 'add') return 1
161
+ return 0 // updates and others
162
+ }
163
+ return getPriority(a) - getPriority(b)
164
+ })
165
+ }
166
+
167
+ /**
168
+ * Build hierarchical resource lanes from timeline events.
169
+ *
170
+ * This function groups events by resource, establishes parent-child relationships,
171
+ * and returns a flat list of top-level lanes (each with nested children).
172
+ *
173
+ * @param options - Configuration options
174
+ * @returns Array of top-level resource lanes with nested children
175
+ */
176
+ export function buildResourceHierarchy(options: HierarchyOptions): ResourceLane[] {
177
+ const { events, topology, rootResource, groupByApp = true } = options
178
+ const laneMap = new Map<string, ResourceLane>()
179
+
180
+ // Track events that should be attached to their owner instead of their own lane
181
+ const eventsToAttach: { event: TimelineEvent; ownerLaneId: string }[] = []
182
+
183
+ // First pass: create lanes from events (but not for Events with owners)
184
+ for (const event of events) {
185
+ // K8s Events with an owner (involvedObject) should attach to that resource, not get their own lane
186
+ if (event.kind === 'Event' && event.owner) {
187
+ const ownerLaneId = `${event.owner.kind}/${event.namespace}/${event.owner.name}`
188
+ eventsToAttach.push({ event, ownerLaneId })
189
+ continue
190
+ }
191
+
192
+ const laneId = `${event.kind}/${event.namespace}/${event.name}`
193
+ if (!laneMap.has(laneId)) {
194
+ laneMap.set(laneId, {
195
+ id: laneId,
196
+ kind: event.kind,
197
+ namespace: event.namespace,
198
+ name: event.name,
199
+ events: [],
200
+ isWorkload: isWorkloadKind(event.kind),
201
+ children: [],
202
+ childEventCount: 0,
203
+ })
204
+ }
205
+ laneMap.get(laneId)!.events.push(event)
206
+ }
207
+
208
+ // Attach K8s Events to their owner lanes
209
+ for (const { event, ownerLaneId } of eventsToAttach) {
210
+ if (laneMap.has(ownerLaneId)) {
211
+ // Owner exists, attach event to it
212
+ laneMap.get(ownerLaneId)!.events.push(event)
213
+ } else {
214
+ // Owner doesn't exist yet, create lane for it
215
+ const parts = ownerLaneId.split('/')
216
+ laneMap.set(ownerLaneId, {
217
+ id: ownerLaneId,
218
+ kind: parts[0],
219
+ namespace: parts[1],
220
+ name: parts.slice(2).join('/'),
221
+ events: [event],
222
+ isWorkload: isWorkloadKind(parts[0]),
223
+ children: [],
224
+ childEventCount: 0,
225
+ })
226
+ }
227
+ }
228
+
229
+ // Build parent map from BOTH owner references AND topology edges
230
+ const laneParent = new Map<string, string>() // childLaneId -> parentLaneId
231
+
232
+ // Source 1: Owner references from events (most reliable for Deployment→RS→Pod)
233
+ for (const [laneId, lane] of laneMap) {
234
+ const eventWithOwner = lane.events.find(e => e.owner)
235
+ if (eventWithOwner?.owner) {
236
+ const ownerLaneId = `${eventWithOwner.owner.kind}/${lane.namespace}/${eventWithOwner.owner.name}`
237
+
238
+ // Create parent lane if it doesn't exist (parent may have no events)
239
+ if (!laneMap.has(ownerLaneId)) {
240
+ laneMap.set(ownerLaneId, {
241
+ id: ownerLaneId,
242
+ kind: eventWithOwner.owner.kind,
243
+ namespace: lane.namespace,
244
+ name: eventWithOwner.owner.name,
245
+ events: [],
246
+ isWorkload: isWorkloadKind(eventWithOwner.owner.kind),
247
+ children: [],
248
+ childEventCount: 0,
249
+ })
250
+ }
251
+ laneParent.set(laneId, ownerLaneId)
252
+ }
253
+ }
254
+
255
+ // Source 2: Topology edges (for Service→Deployment, Ingress→Service, ConfigMap→Deployment)
256
+ if (topology?.edges) {
257
+ for (const edge of topology.edges) {
258
+ const sourceLaneId = nodeIdToLaneId(edge.source)
259
+ const targetLaneId = nodeIdToLaneId(edge.target)
260
+ if (!sourceLaneId || !targetLaneId) continue
261
+
262
+ // manages: Deployment→RS→Pod (already covered by owner refs, skip)
263
+ if (edge.type === 'manages') continue
264
+
265
+ // At least one side must have events
266
+ const sourceExists = laneMap.has(sourceLaneId)
267
+ const targetExists = laneMap.has(targetLaneId)
268
+ if (!sourceExists && !targetExists) continue
269
+
270
+ // exposes: Service→Deployment (Service is parent of Deployment)
271
+ if (edge.type === 'exposes') {
272
+ if (!laneParent.has(targetLaneId) && targetExists) {
273
+ if (!sourceExists) {
274
+ const parts = sourceLaneId.split('/')
275
+ laneMap.set(sourceLaneId, {
276
+ id: sourceLaneId,
277
+ kind: parts[0],
278
+ namespace: parts[1],
279
+ name: parts.slice(2).join('/'),
280
+ events: [],
281
+ isWorkload: isWorkloadKind(parts[0]),
282
+ children: [],
283
+ childEventCount: 0,
284
+ })
285
+ }
286
+ laneParent.set(targetLaneId, sourceLaneId)
287
+ }
288
+ }
289
+
290
+ // routes-to has two cases:
291
+ // 1. Ingress→Service: Service should be parent (representative)
292
+ // 2. Service→Pod/PodGroup: Service should be parent (normal hierarchy)
293
+ if (edge.type === 'routes-to') {
294
+ const sourceKind = sourceLaneId.split('/')[0]
295
+ const targetKind = targetLaneId.split('/')[0]
296
+
297
+ // Gateway→Route: Gateway is parent of Route
298
+ if (sourceKind === 'Gateway' && (targetKind === 'HTTPRoute' || targetKind === 'GRPCRoute' || targetKind === 'TCPRoute' || targetKind === 'TLSRoute')) {
299
+ if (!laneParent.has(targetLaneId) && targetExists) {
300
+ if (!sourceExists) {
301
+ const parts = sourceLaneId.split('/')
302
+ laneMap.set(sourceLaneId, {
303
+ id: sourceLaneId,
304
+ kind: parts[0],
305
+ namespace: parts[1],
306
+ name: parts.slice(2).join('/'),
307
+ events: [],
308
+ isWorkload: isWorkloadKind(parts[0]),
309
+ children: [],
310
+ childEventCount: 0,
311
+ })
312
+ }
313
+ laneParent.set(targetLaneId, sourceLaneId)
314
+ }
315
+ }
316
+ // Route→Service: reverse (Service is representative, like Ingress)
317
+ else if ((sourceKind === 'HTTPRoute' || sourceKind === 'GRPCRoute' || sourceKind === 'TCPRoute' || sourceKind === 'TLSRoute') && targetKind === 'Service') {
318
+ if (!laneParent.has(sourceLaneId) && sourceExists) {
319
+ if (!targetExists) {
320
+ const parts = targetLaneId.split('/')
321
+ laneMap.set(targetLaneId, {
322
+ id: targetLaneId,
323
+ kind: parts[0],
324
+ namespace: parts[1],
325
+ name: parts.slice(2).join('/'),
326
+ events: [],
327
+ isWorkload: isWorkloadKind(parts[0]),
328
+ children: [],
329
+ childEventCount: 0,
330
+ })
331
+ }
332
+ laneParent.set(sourceLaneId, targetLaneId)
333
+ }
334
+ }
335
+ // Ingress→Service: reverse relationship (Service is representative)
336
+ else if (sourceKind === 'Ingress' && targetKind === 'Service') {
337
+ if (!laneParent.has(sourceLaneId) && sourceExists) {
338
+ if (!targetExists) {
339
+ const parts = targetLaneId.split('/')
340
+ laneMap.set(targetLaneId, {
341
+ id: targetLaneId,
342
+ kind: parts[0],
343
+ namespace: parts[1],
344
+ name: parts.slice(2).join('/'),
345
+ events: [],
346
+ isWorkload: isWorkloadKind(parts[0]),
347
+ children: [],
348
+ childEventCount: 0,
349
+ })
350
+ }
351
+ laneParent.set(sourceLaneId, targetLaneId)
352
+ }
353
+ }
354
+ // Service→Pod/PodGroup: normal direction (Service is parent)
355
+ else if (sourceKind === 'Service') {
356
+ if (!laneParent.has(targetLaneId) && targetExists) {
357
+ if (!sourceExists) {
358
+ const parts = sourceLaneId.split('/')
359
+ laneMap.set(sourceLaneId, {
360
+ id: sourceLaneId,
361
+ kind: parts[0],
362
+ namespace: parts[1],
363
+ name: parts.slice(2).join('/'),
364
+ events: [],
365
+ isWorkload: isWorkloadKind(parts[0]),
366
+ children: [],
367
+ childEventCount: 0,
368
+ })
369
+ }
370
+ laneParent.set(targetLaneId, sourceLaneId)
371
+ }
372
+ }
373
+ }
374
+
375
+ // configures/uses/protects: ConfigMap→Deployment, HPA→Deployment, PDB→Deployment (target is parent)
376
+ if (edge.type === 'configures' || edge.type === 'uses' || edge.type === 'protects') {
377
+ if (!laneParent.has(sourceLaneId) && sourceExists) {
378
+ if (!targetExists) {
379
+ const parts = targetLaneId.split('/')
380
+ laneMap.set(targetLaneId, {
381
+ id: targetLaneId,
382
+ kind: parts[0],
383
+ namespace: parts[1],
384
+ name: parts.slice(2).join('/'),
385
+ events: [],
386
+ isWorkload: isWorkloadKind(parts[0]),
387
+ children: [],
388
+ childEventCount: 0,
389
+ })
390
+ }
391
+ laneParent.set(sourceLaneId, targetLaneId)
392
+ }
393
+ }
394
+ }
395
+ }
396
+
397
+ // Source 3: App label grouping (optional)
398
+ if (groupByApp && topology?.nodes) {
399
+ const laneAppLabels = new Map<string, string>()
400
+ for (const node of topology.nodes) {
401
+ const laneId = nodeIdToLaneId(node.id)
402
+ if (!laneId) continue
403
+ const labels = node.data?.labels as Record<string, string> | undefined
404
+ const appLabel = labels?.['app.kubernetes.io/name'] || labels?.['app']
405
+ if (appLabel) {
406
+ laneAppLabels.set(laneId, appLabel)
407
+ }
408
+ }
409
+
410
+ // Only include primary resource kinds in app label grouping
411
+ const appLabelEligibleKinds = new Set([
412
+ 'Service', 'Deployment', 'Rollout', 'StatefulSet', 'DaemonSet',
413
+ 'Job', 'CronJob', 'Ingress', 'Gateway', 'HTTPRoute', 'GRPCRoute',
414
+ 'TCPRoute', 'TLSRoute', 'ConfigMap', 'Secret',
415
+ 'Application', 'Kustomization', 'HelmRelease', 'GitRepository',
416
+ 'Workflow', 'CronWorkflow',
417
+ 'KnativeService', 'KnativeConfiguration', 'KnativeRevision', 'KnativeRoute',
418
+ 'Broker', 'Trigger',
419
+ 'IngressRoute', 'IngressRouteTCP', 'IngressRouteUDP',
420
+ ])
421
+
422
+ // Group lanes by app label
423
+ const appGroups = new Map<string, string[]>()
424
+ for (const [laneId, lane] of laneMap) {
425
+ if (laneParent.has(laneId)) continue
426
+ if (!appLabelEligibleKinds.has(lane.kind)) continue
427
+ const appLabel = laneAppLabels.get(laneId)
428
+ if (!appLabel) continue
429
+ if (!appGroups.has(appLabel)) {
430
+ appGroups.set(appLabel, [])
431
+ }
432
+ appGroups.get(appLabel)!.push(laneId)
433
+ }
434
+
435
+ // For each app group with multiple members, pick the best parent
436
+ for (const [, laneIds] of appGroups) {
437
+ if (laneIds.length < 2) continue
438
+
439
+ const kindPriority: Record<string, number> = {
440
+ Service: 1, Ingress: 2, Gateway: 2,
441
+ HTTPRoute: 2, GRPCRoute: 2, TCPRoute: 2, TLSRoute: 2,
442
+ Deployment: 3, Rollout: 3, StatefulSet: 3, DaemonSet: 3,
443
+ Job: 4, CronJob: 4,
444
+ ConfigMap: 5, Secret: 5,
445
+ ReplicaSet: 6, Pod: 7,
446
+ KnativeService: 1, KnativeRoute: 2, Broker: 2, Channel: 2,
447
+ KnativeConfiguration: 3, KnativeRevision: 4, Trigger: 3,
448
+ PingSource: 3, ApiServerSource: 3, ContainerSource: 3, SinkBinding: 3,
449
+ IngressRoute: 2, IngressRouteTCP: 2, IngressRouteUDP: 2,
450
+ TraefikService: 3, Middleware: 4, MiddlewareTCP: 4,
451
+ }
452
+
453
+ const sorted = [...laneIds].sort((a, b) => {
454
+ const aLane = laneMap.get(a)!
455
+ const bLane = laneMap.get(b)!
456
+ const aPriority = kindPriority[aLane.kind] || 10
457
+ const bPriority = kindPriority[bLane.kind] || 10
458
+ return aPriority - bPriority
459
+ })
460
+
461
+ const parentLaneId = sorted[0]
462
+ for (let i = 1; i < sorted.length; i++) {
463
+ const childLaneId = sorted[i]
464
+ if (!laneParent.has(childLaneId)) {
465
+ laneParent.set(childLaneId, parentLaneId)
466
+ }
467
+ }
468
+ }
469
+ }
470
+
471
+ // Walk up parent chain to find root
472
+ const findRoot = (laneId: string, visited = new Set<string>()): string => {
473
+ if (visited.has(laneId)) return laneId
474
+ visited.add(laneId)
475
+ const parentId = laneParent.get(laneId)
476
+ if (parentId && laneMap.has(parentId)) {
477
+ return findRoot(parentId, visited)
478
+ }
479
+ return laneId
480
+ }
481
+
482
+ // Second pass: group children under their root
483
+ const topLevelLanes: ResourceLane[] = []
484
+ const childLaneIds = new Set<string>()
485
+
486
+ for (const [laneId] of laneMap) {
487
+ if (!laneParent.has(laneId)) continue
488
+ const rootId = findRoot(laneId)
489
+ if (rootId !== laneId && laneMap.has(rootId)) {
490
+ const root = laneMap.get(rootId)!
491
+ const child = laneMap.get(laneId)!
492
+ root.children!.push(child)
493
+ root.childEventCount = (root.childEventCount || 0) + child.events.length
494
+ childLaneIds.add(laneId)
495
+ }
496
+ }
497
+
498
+ // Collect top-level lanes (not children of anyone)
499
+ for (const [laneId, lane] of laneMap) {
500
+ if (!childLaneIds.has(laneId)) {
501
+ // Sort children by kind priority then by latest event
502
+ if (lane.children && lane.children.length > 0) {
503
+ const kindPriority: Record<string, number> = {
504
+ Service: 1, Gateway: 1, HTTPRoute: 2, GRPCRoute: 2, TCPRoute: 2, TLSRoute: 2,
505
+ Deployment: 2, Rollout: 2, StatefulSet: 2, DaemonSet: 2,
506
+ Job: 3, CronJob: 3,
507
+ ReplicaSet: 3, Pod: 4, ConfigMap: 5, Secret: 5,
508
+ KnativeService: 1, KnativeRoute: 2, Broker: 1, Channel: 1,
509
+ KnativeConfiguration: 2, KnativeRevision: 3, Trigger: 2,
510
+ PingSource: 3, ApiServerSource: 3, ContainerSource: 3, SinkBinding: 3,
511
+ IngressRoute: 1, IngressRouteTCP: 1, IngressRouteUDP: 1,
512
+ TraefikService: 2, Middleware: 3, MiddlewareTCP: 3,
513
+ }
514
+ lane.children.sort((a, b) => {
515
+ const aPriority = kindPriority[a.kind] || 10
516
+ const bPriority = kindPriority[b.kind] || 10
517
+ if (aPriority !== bPriority) return aPriority - bPriority
518
+ const aLatest = a.events.length > 0 ? Math.max(...a.events.map(e => new Date(e.timestamp).getTime())) : 0
519
+ const bLatest = b.events.length > 0 ? Math.max(...b.events.map(e => new Date(e.timestamp).getTime())) : 0
520
+ return bLatest - aLatest
521
+ })
522
+ }
523
+
524
+ // Pre-compute sorted events (own + children) for efficient rendering
525
+ // Deduplicate by event ID
526
+ const allEvents = [...lane.events, ...(lane.children?.flatMap(c => c.events) || [])]
527
+ const uniqueEvents = Array.from(new Map(allEvents.map(e => [e.id, e])).values())
528
+ lane.allEventsSorted = sortEventsForRendering(uniqueEvents)
529
+
530
+ topLevelLanes.push(lane)
531
+ }
532
+ }
533
+
534
+ // If rootResource is specified, filter to only include lanes related to that resource
535
+ if (rootResource) {
536
+ const rootLaneId = `${rootResource.kind}/${rootResource.namespace}/${rootResource.name}`
537
+ const rootLane = topLevelLanes.find(l => l.id === rootLaneId)
538
+
539
+ if (rootLane) {
540
+ // Return the root lane with all its children
541
+ return [rootLane]
542
+ }
543
+
544
+ // If root resource is a child of another lane, find its parent and return that
545
+ for (const lane of topLevelLanes) {
546
+ if (lane.children?.some(c => c.id === rootLaneId)) {
547
+ return [lane]
548
+ }
549
+ // Also check if root is the parent and we're looking at the hierarchy from the parent perspective
550
+ }
551
+
552
+ // If not found, check if it's a child somewhere in the hierarchy and return its parent
553
+ const childLane = laneMap.get(rootLaneId)
554
+ if (childLane) {
555
+ const parentId = laneParent.get(rootLaneId)
556
+ if (parentId) {
557
+ const parentLane = topLevelLanes.find(l => l.id === parentId)
558
+ if (parentLane) {
559
+ return [parentLane]
560
+ }
561
+ // Walk up to find the top-level ancestor
562
+ const rootAncestorId = findRoot(rootLaneId)
563
+ const rootAncestor = topLevelLanes.find(l => l.id === rootAncestorId)
564
+ if (rootAncestor) {
565
+ return [rootAncestor]
566
+ }
567
+ }
568
+ // No parent found, return as a standalone lane
569
+ const allEvents = [...childLane.events, ...(childLane.children?.flatMap(c => c.events) || [])]
570
+ const uniqueEvents = Array.from(new Map(allEvents.map(e => [e.id, e])).values())
571
+ childLane.allEventsSorted = sortEventsForRendering(uniqueEvents)
572
+ return [childLane]
573
+ }
574
+
575
+ // Resource not found in hierarchy - create a placeholder lane
576
+ // This ensures the detail view always has something to show
577
+ const placeholderLane: ResourceLane = {
578
+ id: rootLaneId,
579
+ kind: rootResource.kind,
580
+ namespace: rootResource.namespace,
581
+ name: rootResource.name,
582
+ events: [],
583
+ isWorkload: isWorkloadKind(rootResource.kind),
584
+ children: [],
585
+ childEventCount: 0,
586
+ allEventsSorted: [],
587
+ }
588
+ return [placeholderLane]
589
+ }
590
+
591
+ return topLevelLanes
592
+ }
593
+
594
+ /**
595
+ * Get all events from a hierarchy, flattened and sorted by timestamp (newest first).
596
+ */
597
+ export function getAllEventsFromHierarchy(lanes: ResourceLane[]): TimelineEvent[] {
598
+ const allEvents: TimelineEvent[] = []
599
+
600
+ for (const lane of lanes) {
601
+ allEvents.push(...lane.events)
602
+ if (lane.children) {
603
+ for (const child of lane.children) {
604
+ allEvents.push(...child.events)
605
+ }
606
+ }
607
+ }
608
+
609
+ // Deduplicate by event ID
610
+ const uniqueEvents = Array.from(new Map(allEvents.map(e => [e.id, e])).values())
611
+
612
+ // Sort by timestamp (newest first)
613
+ return uniqueEvents.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
614
+ }
615
+
616
+ /**
617
+ * Count total events in a hierarchy.
618
+ */
619
+ export function countEventsInHierarchy(lanes: ResourceLane[]): number {
620
+ let count = 0
621
+ for (const lane of lanes) {
622
+ count += lane.events.length
623
+ if (lane.children) {
624
+ for (const child of lane.children) {
625
+ count += child.events.length
626
+ }
627
+ }
628
+ }
629
+ return count
630
+ }