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