@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,614 @@
1
+ /**
2
+ * Shared timeline components and utilities.
3
+ *
4
+ * These components are used by both TimelineSwimlanes.tsx and the EventsTab in WorkloadView.
5
+ * They ensure consistent behavior for time formatting, zoom controls, legends, and markers.
6
+ */
7
+
8
+ import { clsx } from 'clsx'
9
+ import { ZoomIn, ZoomOut } from 'lucide-react'
10
+ import type { TimelineEvent } from '../../types/core'
11
+ import { isChangeEvent, isHistoricalEvent } from '../../types/core'
12
+ import { isProblematicEvent } from '../../utils/resource-hierarchy'
13
+
14
+ // ============================================================================
15
+ // Constants
16
+ // ============================================================================
17
+
18
+ /**
19
+ * Available zoom levels in hours.
20
+ * These control how much time is visible on the timeline.
21
+ */
22
+ export const ZOOM_LEVELS = [0.5, 1, 2, 6, 12, 24, 48, 168] as const
23
+ export type ZoomLevel = (typeof ZOOM_LEVELS)[number]
24
+
25
+ // ============================================================================
26
+ // Time Formatting Utilities
27
+ // ============================================================================
28
+
29
+ /**
30
+ * Format a timestamp for display on the time axis.
31
+ * Shows time only for today, date + time for other days.
32
+ */
33
+ export function formatAxisTime(date: Date): string {
34
+ const now = new Date()
35
+ const isToday = date.toDateString() === now.toDateString()
36
+ const time = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
37
+
38
+ if (isToday) {
39
+ return time
40
+ }
41
+
42
+ const month = date.toLocaleDateString([], { month: 'short', day: 'numeric' })
43
+ return `${month} ${time}`
44
+ }
45
+
46
+ /**
47
+ * Format a timestamp for full display (tooltips, detail panels).
48
+ */
49
+ export function formatFullTime(date: Date): string {
50
+ return date.toLocaleString()
51
+ }
52
+
53
+ /**
54
+ * Format a relative time string (e.g., "5m ago", "2h ago").
55
+ */
56
+ export function formatRelativeTime(timestamp: string): string {
57
+ const diff = Date.now() - new Date(timestamp).getTime()
58
+ const mins = Math.floor(diff / 60000)
59
+ if (mins < 1) return 'just now'
60
+ if (mins < 60) return `${mins}m ago`
61
+ const hours = Math.floor(mins / 60)
62
+ if (hours < 24) return `${hours}h ago`
63
+ return `${Math.floor(hours / 24)}d ago`
64
+ }
65
+
66
+ /**
67
+ * Format zoom level for display (e.g., "1h", "6h", "24h", "7d").
68
+ */
69
+ export function formatZoomLevel(hours: number): string {
70
+ if (hours < 1) return `${Math.round(hours * 60)}m`
71
+ if (hours < 24) return `${hours}h`
72
+ return `${Math.round(hours / 24)}d`
73
+ }
74
+
75
+ // ============================================================================
76
+ // Shared Components
77
+ // ============================================================================
78
+
79
+ /**
80
+ * Legend item with hover tooltip explaining what a color means.
81
+ */
82
+ interface LegendItemProps {
83
+ color: string
84
+ label: string
85
+ description: string
86
+ dashed?: boolean
87
+ }
88
+
89
+ export function LegendItem({ color, label, description, dashed }: LegendItemProps) {
90
+ return (
91
+ <span className="relative flex items-center gap-1 group cursor-help">
92
+ <span className={clsx(
93
+ 'w-2 h-2 rounded-full',
94
+ dashed ? 'border border-dashed border-current bg-transparent' : color
95
+ )} />
96
+ <span>{label}</span>
97
+ <span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 text-xs bg-theme-base text-theme-text-primary rounded whitespace-nowrap opacity-0 group-hover:opacity-100 pointer-events-none z-50 transition-opacity duration-75 shadow-lg border border-theme-border-light">
98
+ {description}
99
+ </span>
100
+ </span>
101
+ )
102
+ }
103
+
104
+ /**
105
+ * Legend for event dot markers (used in both swimlanes and detail view).
106
+ */
107
+ export function EventDotLegend() {
108
+ return (
109
+ <div className="flex items-center gap-4 text-xs text-theme-text-tertiary">
110
+ <LegendItem color="bg-green-500" label="Created" description="Resource was created" />
111
+ <LegendItem color="bg-blue-500" label="Modified" description="Resource was modified" />
112
+ <LegendItem color="bg-red-500" label="Deleted" description="Resource was deleted" />
113
+ <LegendItem color="bg-amber-500" label="Warning" description="Warning event or error condition" />
114
+ <LegendItem color="" label="Historical" description="Reconstructed from metadata" dashed />
115
+ </div>
116
+ )
117
+ }
118
+
119
+ /**
120
+ * Legend for health span bars (colored bars showing health state over time).
121
+ */
122
+ export function HealthSpanLegend() {
123
+ return (
124
+ <div className="flex items-center gap-4 text-xs text-theme-text-tertiary">
125
+ <span className="flex items-center gap-1">
126
+ <span className="w-4 h-2 rounded-sm bg-green-500/60" />
127
+ <span>Healthy</span>
128
+ </span>
129
+ <span className="flex items-center gap-1">
130
+ <span className="w-4 h-2 rounded-sm bg-blue-500/60" />
131
+ <span>Rolling</span>
132
+ </span>
133
+ <span className="flex items-center gap-1">
134
+ <span className="w-4 h-2 rounded-sm bg-amber-500/60" />
135
+ <span>Degraded</span>
136
+ </span>
137
+ <span className="flex items-center gap-1">
138
+ <span className="w-4 h-2 rounded-sm bg-red-500/60" />
139
+ <span>Unhealthy</span>
140
+ </span>
141
+ </div>
142
+ )
143
+ }
144
+
145
+ /**
146
+ * Zoom controls for the timeline (zoom in/out buttons + current level display).
147
+ */
148
+ interface ZoomControlsProps {
149
+ zoom: ZoomLevel
150
+ onZoomIn: () => void
151
+ onZoomOut: () => void
152
+ canZoomIn: boolean
153
+ canZoomOut: boolean
154
+ }
155
+
156
+ export function ZoomControls({ zoom, onZoomIn, onZoomOut, canZoomIn, canZoomOut }: ZoomControlsProps) {
157
+ return (
158
+ <div className="flex items-center gap-1 text-theme-text-tertiary">
159
+ <button
160
+ onClick={onZoomOut}
161
+ disabled={!canZoomOut}
162
+ className="p-1.5 hover:bg-theme-elevated rounded disabled:opacity-30"
163
+ title="Zoom out (show more time)"
164
+ >
165
+ <ZoomOut className="w-4 h-4" />
166
+ </button>
167
+ <span className="text-xs min-w-[3ch] text-center">{formatZoomLevel(zoom)}</span>
168
+ <button
169
+ onClick={onZoomIn}
170
+ disabled={!canZoomIn}
171
+ className="p-1.5 hover:bg-theme-elevated rounded disabled:opacity-30"
172
+ title="Zoom in (show less time)"
173
+ >
174
+ <ZoomIn className="w-4 h-4" />
175
+ </button>
176
+ </div>
177
+ )
178
+ }
179
+
180
+ /**
181
+ * Quick zoom level selector (5m, 30m, 1h, 6h, 24h buttons).
182
+ */
183
+ interface QuickZoomSelectorProps {
184
+ zoom: ZoomLevel
185
+ onZoomChange: (level: ZoomLevel) => void
186
+ }
187
+
188
+ export function QuickZoomSelector({ zoom, onZoomChange }: QuickZoomSelectorProps) {
189
+ const quickLevels: ZoomLevel[] = [0.5, 1, 6, 24]
190
+ const labels: Record<number, string> = { 0.5: '30m', 1: '1h', 6: '6h', 24: '24h' }
191
+
192
+ return (
193
+ <div className="flex items-center gap-0.5 bg-theme-elevated rounded-lg p-0.5">
194
+ {quickLevels.map(level => (
195
+ <button
196
+ key={level}
197
+ onClick={() => onZoomChange(level)}
198
+ className={clsx(
199
+ 'px-2 py-1 text-xs rounded transition-colors',
200
+ zoom === level
201
+ ? 'bg-theme-surface text-theme-text-primary shadow-sm'
202
+ : 'text-theme-text-tertiary hover:text-theme-text-secondary'
203
+ )}
204
+ >
205
+ {labels[level]}
206
+ </button>
207
+ ))}
208
+ </div>
209
+ )
210
+ }
211
+
212
+ /**
213
+ * Event marker dot for timeline visualization.
214
+ */
215
+ interface EventMarkerProps {
216
+ event: TimelineEvent
217
+ x: number
218
+ selected?: boolean
219
+ onClick: () => void
220
+ dimmed?: boolean
221
+ small?: boolean
222
+ }
223
+
224
+ export function EventMarker({ event, x, selected, onClick, dimmed, small }: EventMarkerProps) {
225
+ const isChange = isChangeEvent(event)
226
+ const isProblematic = isProblematicEvent(event)
227
+ const isHistorical = isHistoricalEvent(event)
228
+
229
+ const getMarkerStyle = () => {
230
+ if (isHistorical) {
231
+ if (isProblematic) {
232
+ return 'bg-amber-500/20 border-2 border-dashed border-amber-500/60'
233
+ }
234
+ if (isChange) {
235
+ switch (event.eventType) {
236
+ case 'add':
237
+ return 'bg-green-500/20 border-2 border-dashed border-green-500/60'
238
+ case 'delete':
239
+ return 'bg-red-500/20 border-2 border-dashed border-red-500/60'
240
+ case 'update':
241
+ return 'bg-blue-500/20 border-2 border-dashed border-blue-500/60'
242
+ }
243
+ }
244
+ return 'bg-theme-hover/30 border-2 border-dashed border-theme-border-light'
245
+ }
246
+
247
+ const opacity = dimmed ? '/50' : ''
248
+ if (isProblematic) {
249
+ return `bg-amber-500${opacity}`
250
+ }
251
+ if (isChange) {
252
+ switch (event.eventType) {
253
+ case 'add':
254
+ return `bg-green-500${opacity}`
255
+ case 'delete':
256
+ return `bg-red-500${opacity}`
257
+ case 'update':
258
+ return `bg-blue-500${opacity}`
259
+ }
260
+ }
261
+ return `bg-theme-text-tertiary${opacity}`
262
+ }
263
+
264
+ const getOperationLabel = () => {
265
+ if (isProblematic) {
266
+ return `⚠ ${event.reason || 'Warning'}`
267
+ }
268
+ if (isChange) {
269
+ switch (event.eventType) {
270
+ case 'add': return '● Created'
271
+ case 'delete': return '● Deleted'
272
+ case 'update': return '● Modified'
273
+ default: return '● Changed'
274
+ }
275
+ }
276
+ if (event.reason) {
277
+ return `● ${event.reason}`
278
+ }
279
+ return '● Event'
280
+ }
281
+
282
+ const tooltipLines: string[] = []
283
+ tooltipLines.push(getOperationLabel())
284
+ if (event.message) {
285
+ const msg = event.message.length > 60 ? event.message.slice(0, 60) + '...' : event.message
286
+ tooltipLines.push(msg)
287
+ }
288
+ tooltipLines.push(formatRelativeTime(event.timestamp))
289
+ if (isHistorical) tooltipLines.push('(from metadata)')
290
+
291
+ const tooltipText = tooltipLines.join(' · ')
292
+
293
+ return (
294
+ <button
295
+ className={clsx(
296
+ 'absolute top-1/2 -translate-y-1/2 -translate-x-1/2 rounded-full transition-all group',
297
+ small ? 'w-2.5 h-2.5' : 'w-3 h-3',
298
+ getMarkerStyle(),
299
+ selected ? 'ring-2 ring-white ring-offset-2 ring-offset-theme-base scale-150' : 'hover:scale-125',
300
+ dimmed ? 'z-5' : isHistorical ? 'z-5' : 'z-10'
301
+ )}
302
+ style={{ left: `${x}%` }}
303
+ onClick={(e) => {
304
+ e.stopPropagation()
305
+ onClick()
306
+ }}
307
+ >
308
+ <span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 text-xs bg-theme-base text-theme-text-primary rounded whitespace-nowrap opacity-0 group-hover:opacity-100 pointer-events-none z-50 transition-opacity duration-75">
309
+ {tooltipText}
310
+ </span>
311
+ </button>
312
+ )
313
+ }
314
+
315
+ /**
316
+ * "Now" marker line that shows the current time on the timeline.
317
+ */
318
+ interface NowMarkerProps {
319
+ /** X position as percentage (0-100) */
320
+ x: number
321
+ /** Label to show next to the line */
322
+ label?: string
323
+ /** Additional CSS classes for positioning */
324
+ className?: string
325
+ }
326
+
327
+ export function NowMarker({ x, label = 'now', className }: NowMarkerProps) {
328
+ if (x < 0 || x > 100) return null
329
+
330
+ return (
331
+ <div
332
+ className={clsx(
333
+ 'absolute top-0 bottom-0 w-0.5 bg-purple-500/50 z-10 pointer-events-none',
334
+ className
335
+ )}
336
+ style={{ left: `${x}%` }}
337
+ >
338
+ <span className="absolute -top-4 left-1/2 -translate-x-1/2 text-xs text-purple-500 font-medium">
339
+ {label}
340
+ </span>
341
+ </div>
342
+ )
343
+ }
344
+
345
+ /**
346
+ * Time axis with tick marks and labels.
347
+ */
348
+ interface TimeAxisProps {
349
+ startTime: number
350
+ endTime: number
351
+ tickCount?: number
352
+ /** CSS class for label column width (e.g., "w-64") */
353
+ labelColumnClass?: string
354
+ }
355
+
356
+ export function TimeAxis({ startTime, endTime, tickCount = 8, labelColumnClass = 'w-64' }: TimeAxisProps) {
357
+ const windowMs = endTime - startTime
358
+ const ticks = Array.from({ length: tickCount + 1 }, (_, i) => {
359
+ const t = startTime + (windowMs * i) / tickCount
360
+ return { time: t, label: formatAxisTime(new Date(t)) }
361
+ })
362
+
363
+ const timeToX = (ts: number) => ((ts - startTime) / windowMs) * 100
364
+
365
+ return (
366
+ <div className="flex">
367
+ <div className={clsx('shrink-0 bg-theme-surface/50 border-r border-theme-border', labelColumnClass)} />
368
+ <div className="flex-1 relative h-6 bg-theme-elevated/30">
369
+ {ticks.map((tick, i) => {
370
+ const x = timeToX(tick.time)
371
+ return (
372
+ <div
373
+ key={i}
374
+ className="absolute top-0 flex flex-col items-center"
375
+ style={{ left: `${x}%`, transform: 'translateX(-50%)' }}
376
+ >
377
+ <div className="h-2 w-px bg-theme-border" />
378
+ <span className="text-xs text-theme-text-tertiary">{tick.label}</span>
379
+ </div>
380
+ )
381
+ })}
382
+ </div>
383
+ </div>
384
+ )
385
+ }
386
+
387
+ /**
388
+ * Health span bar showing a health state over a time range.
389
+ */
390
+ interface HealthSpanProps {
391
+ health: 'healthy' | 'degraded' | 'unhealthy' | string
392
+ left: number // percentage
393
+ width: number // percentage
394
+ title?: string
395
+ /** Show indicator that resource was created before visible window */
396
+ createdBefore?: Date
397
+ }
398
+
399
+ export function HealthSpan({ health, left, width, title, createdBefore }: HealthSpanProps) {
400
+ if (width <= 0) return null
401
+
402
+ const getHealthColor = () => {
403
+ switch (health) {
404
+ case 'healthy':
405
+ return 'bg-green-500/60'
406
+ case 'rolling':
407
+ // Blue/purple for expected degradation during rollout
408
+ return 'bg-blue-500/60'
409
+ case 'degraded':
410
+ // Amber for unexpected degradation
411
+ return 'bg-amber-500/60'
412
+ case 'unhealthy':
413
+ return 'bg-red-500/60'
414
+ default:
415
+ // Unknown or other states
416
+ return 'bg-gray-400/40'
417
+ }
418
+ }
419
+
420
+ return (
421
+ <div
422
+ className={clsx(
423
+ 'absolute top-1 bottom-1 rounded-sm group',
424
+ getHealthColor()
425
+ )}
426
+ style={{ left: `${left}%`, width: `${width}%` }}
427
+ title={title}
428
+ >
429
+ {createdBefore && left === 0 && (
430
+ <span className="absolute left-0.5 top-1/2 -translate-y-1/2 text-[9px] text-black/70 dark:text-black/60 whitespace-nowrap pointer-events-none group-hover:text-black/90 dark:group-hover:text-black/80">
431
+ ← {formatCreatedBefore(createdBefore)}
432
+ </span>
433
+ )}
434
+ </div>
435
+ )
436
+ }
437
+
438
+ /**
439
+ * Format the "created before" date for display.
440
+ * Shows relative time for recent dates, or short date for older ones.
441
+ */
442
+ function formatCreatedBefore(date: Date): string {
443
+ const now = Date.now()
444
+ const diffMs = now - date.getTime()
445
+ const diffHours = diffMs / (1000 * 60 * 60)
446
+ const diffDays = diffHours / 24
447
+
448
+ if (diffDays < 1) {
449
+ return `${Math.round(diffHours)}h ago`
450
+ } else if (diffDays < 7) {
451
+ return `${Math.round(diffDays)}d ago`
452
+ } else {
453
+ return date.toLocaleDateString([], { month: 'short', day: 'numeric' })
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Calculate visible time range from zoom level and optional pan offset.
459
+ */
460
+ export function calculateTimeRange(
461
+ zoom: ZoomLevel,
462
+ now: number,
463
+ panOffset: number = 0
464
+ ): { start: number; end: number; windowMs: number } {
465
+ const windowMs = zoom * 60 * 60 * 1000
466
+ const end = now - panOffset
467
+ const start = end - windowMs
468
+ return { start, end, windowMs }
469
+ }
470
+
471
+ /**
472
+ * Convert a timestamp to X position (0-100) within a time range.
473
+ */
474
+ export function timeToX(timestamp: number, startTime: number, windowMs: number): number {
475
+ return ((timestamp - startTime) / windowMs) * 100
476
+ }
477
+
478
+ /**
479
+ * Result of building health spans, including metadata about resource creation.
480
+ */
481
+ export interface HealthSpanResult {
482
+ spans: { start: number; end: number; health: string }[]
483
+ /** When the resource was actually created (from K8s metadata) */
484
+ createdAt?: number
485
+ /** True if resource was created before the visible window */
486
+ createdBeforeWindow: boolean
487
+ }
488
+
489
+ /**
490
+ * Check if an event indicates a rollout is in progress (expected degradation).
491
+ * Rollout signals include:
492
+ * - Diff summary contains "updated:" (indicating replica count or image changes)
493
+ * - Diff summary mentions image changes
494
+ * - Event is for Deployment/StatefulSet/DaemonSet with degraded health
495
+ */
496
+ function isRolloutEvent(event: TimelineEvent): boolean {
497
+ // Only workload kinds can have rollouts
498
+ const rolloutKinds = new Set(['Deployment', 'StatefulSet', 'DaemonSet', 'Rollout', 'ReplicaSet'])
499
+ if (!rolloutKinds.has(event.kind)) return false
500
+
501
+ // Check diff summary for rollout signals
502
+ if (event.diff?.summary) {
503
+ const summary = event.diff.summary.toLowerCase()
504
+ // "updated:" typically indicates replica count changes during rollout
505
+ if (summary.includes('updated:')) return true
506
+ // Image changes trigger rollouts
507
+ if (summary.includes('image(') || summary.includes('image:')) return true
508
+ // Spec template changes often indicate rollout
509
+ if (summary.includes('template')) return true
510
+ }
511
+
512
+ return false
513
+ }
514
+
515
+ /**
516
+ * Determine the effective health state for an event, distinguishing between
517
+ * expected rollout degradation (shown as 'rolling') and unexpected degradation.
518
+ */
519
+ function getEffectiveHealthState(event: TimelineEvent): string {
520
+ // Get the base health state
521
+ const baseHealth = event.healthState || (isProblematicEvent(event) ? 'unhealthy' : 'healthy')
522
+
523
+ // If degraded, check if it's a rollout (expected degradation)
524
+ if (baseHealth === 'degraded' && isRolloutEvent(event)) {
525
+ return 'rolling'
526
+ }
527
+
528
+ return baseHealth
529
+ }
530
+
531
+ /**
532
+ * Build health spans from events for visualization.
533
+ * Uses the resource's createdAt timestamp (from K8s metadata) to determine when it existed.
534
+ * Returns metadata about creation time for rendering indicators.
535
+ *
536
+ * @param events - Change events (informer events) for health state transitions
537
+ * @param allEvents - All events including K8s Events, used to extract createdAt
538
+ */
539
+ export function buildHealthSpans(
540
+ events: TimelineEvent[],
541
+ startTime: number,
542
+ now: number,
543
+ allEvents?: TimelineEvent[]
544
+ ): HealthSpanResult {
545
+ // Use allEvents if provided to get createdAt from K8s Events too
546
+ const eventsForMetadata = allEvents ?? events
547
+
548
+ if (eventsForMetadata.length === 0) {
549
+ return { spans: [], createdBeforeWindow: false }
550
+ }
551
+
552
+ const sorted = [...events].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
553
+ const allSorted = [...eventsForMetadata].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
554
+
555
+ // Get the resource's actual creation time from K8s metadata (createdAt field)
556
+ // Check all events (including K8s Events) for createdAt
557
+ const firstEventWithCreatedAt = allSorted.find(e => e.createdAt)
558
+ const createdAtTimestamp = firstEventWithCreatedAt?.createdAt
559
+ ? new Date(firstEventWithCreatedAt.createdAt).getTime()
560
+ : undefined
561
+
562
+ // Check for delete event to know when resource stopped existing
563
+ const deleteEvent = sorted.find(e => e.eventType === 'delete')
564
+
565
+ // Determine when the resource "existed"
566
+ // - If we have createdAt, use it
567
+ // - Otherwise, assume it existed before the time window
568
+ const existsFrom = createdAtTimestamp ?? startTime
569
+ const createdBeforeWindow = createdAtTimestamp ? createdAtTimestamp < startTime : false
570
+
571
+ // If deleted, it stops existing at that point
572
+ const existsUntil = deleteEvent ? new Date(deleteEvent.timestamp).getTime() : now
573
+
574
+ const spans: { start: number; end: number; health: string }[] = []
575
+ let currentHealth = 'unknown'
576
+ let spanStart = Math.max(existsFrom, startTime)
577
+
578
+ for (const evt of sorted) {
579
+ const ts = new Date(evt.timestamp).getTime()
580
+ if (ts < existsFrom) continue // Resource didn't exist yet
581
+ if (ts > existsUntil) continue // Resource was deleted
582
+
583
+ // Use getEffectiveHealthState to distinguish rollouts from unexpected degradation
584
+ const newHealth = getEffectiveHealthState(evt)
585
+
586
+ if (ts < startTime) {
587
+ // Pre-window event: track health state but don't create a span yet
588
+ currentHealth = newHealth
589
+ continue
590
+ }
591
+
592
+ if (newHealth !== currentHealth && currentHealth !== 'unknown') {
593
+ spans.push({ start: spanStart, end: ts, health: currentHealth })
594
+ spanStart = ts
595
+ }
596
+ currentHealth = newHealth
597
+ }
598
+
599
+ // Close final span (only up to when resource existed)
600
+ if (currentHealth !== 'unknown') {
601
+ spans.push({ start: spanStart, end: Math.min(existsUntil, now), health: currentHealth })
602
+ }
603
+
604
+ // If no health spans but we know the resource exists (has createdAt), show a default "healthy" bar
605
+ // This handles resources like Services that don't have explicit health tracking
606
+ if (spans.length === 0 && createdAtTimestamp && !deleteEvent) {
607
+ const effectiveStart = Math.max(existsFrom, startTime)
608
+ if (effectiveStart < now) {
609
+ spans.push({ start: effectiveStart, end: now, health: 'healthy' })
610
+ }
611
+ }
612
+
613
+ return { spans, createdAt: createdAtTimestamp, createdBeforeWindow }
614
+ }