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