@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,233 @@
1
+ /**
2
+ * Unified formatting utilities for CPU, memory, and other metrics.
3
+ * Use these functions consistently across the app.
4
+ */
5
+
6
+ // =============================================================================
7
+ // CPU Formatting
8
+ // =============================================================================
9
+
10
+ /**
11
+ * Format a cores value to a human-readable string.
12
+ * - >= 1 core: 1 decimal place, no trailing zero (e.g., "14 cores", "3.5 cores")
13
+ * - < 1 core: 2 decimal places (e.g., "0.05 cores")
14
+ */
15
+ function formatCoresValue(cores: number): string {
16
+ if (cores >= 1) {
17
+ // Round to 1 decimal
18
+ const rounded = Math.round(cores * 10) / 10
19
+ // Remove trailing .0
20
+ const formatted = rounded % 1 === 0 ? rounded.toFixed(0) : rounded.toFixed(1)
21
+ return `${formatted} cores`
22
+ }
23
+ if (cores >= 0.01) {
24
+ return `${cores.toFixed(2)} cores`
25
+ }
26
+ return '<0.01 cores'
27
+ }
28
+
29
+ /**
30
+ * Format CPU nanocores to human-readable cores string.
31
+ * Always displays in cores with appropriate decimal places.
32
+ *
33
+ * 1 core = 1000 millicores (m) = 1,000,000,000 nanocores (n)
34
+ *
35
+ * @param nanocores - CPU usage in nanocores (from metrics API)
36
+ * @returns Formatted string like "0.05 cores" or "3.5 cores" or "14 cores"
37
+ */
38
+ export function formatCPUNanocores(nanocores: number): string {
39
+ const cores = nanocores / 1_000_000_000
40
+ return formatCoresValue(cores)
41
+ }
42
+
43
+ /**
44
+ * Parse a Kubernetes CPU string (e.g., "500m", "2", "100n") to nanocores.
45
+ *
46
+ * @param cpuString - K8s CPU string like "500m", "2", "100n"
47
+ * @returns CPU in nanocores
48
+ */
49
+ export function parseCPUToNanocores(cpuString: string): number {
50
+ if (!cpuString) return 0
51
+
52
+ const str = cpuString.trim()
53
+
54
+ // Nanocores suffix
55
+ if (str.endsWith('n')) {
56
+ return parseInt(str.slice(0, -1), 10) || 0
57
+ }
58
+
59
+ // Millicores suffix
60
+ if (str.endsWith('m')) {
61
+ return (parseInt(str.slice(0, -1), 10) || 0) * 1_000_000
62
+ }
63
+
64
+ // Plain number = cores
65
+ const cores = parseFloat(str)
66
+ if (!isNaN(cores)) {
67
+ return cores * 1_000_000_000
68
+ }
69
+
70
+ return 0
71
+ }
72
+
73
+ /**
74
+ * Format a Kubernetes CPU string to human-readable cores.
75
+ *
76
+ * @param cpuString - K8s CPU string like "500m", "2", "100n"
77
+ * @returns Formatted string like "0.50 cores" or "2.00 cores"
78
+ */
79
+ export function formatCPUString(cpuString: string): string {
80
+ return formatCPUNanocores(parseCPUToNanocores(cpuString))
81
+ }
82
+
83
+ // =============================================================================
84
+ // Memory Formatting
85
+ // =============================================================================
86
+
87
+ /**
88
+ * Format bytes to human-readable string (GiB, MiB, KiB).
89
+ *
90
+ * @param bytes - Memory in bytes (from metrics API history)
91
+ * @returns Formatted string like "1.5 GiB" or "256 MiB"
92
+ */
93
+ export function formatMemoryBytes(bytes: number): string {
94
+ if (bytes >= 1024 * 1024 * 1024) {
95
+ const gib = bytes / (1024 * 1024 * 1024)
96
+ return gib >= 10 ? `${gib.toFixed(1)} GiB` : `${gib.toFixed(2)} GiB`
97
+ }
98
+ if (bytes >= 1024 * 1024) {
99
+ return `${Math.round(bytes / (1024 * 1024))} MiB`
100
+ }
101
+ if (bytes >= 1024) {
102
+ return `${Math.round(bytes / 1024)} KiB`
103
+ }
104
+ return `${bytes} B`
105
+ }
106
+
107
+ /**
108
+ * Parse a Kubernetes memory string to bytes.
109
+ * Handles both binary (Ki, Mi, Gi, Ti) and decimal (K, M, G, T) suffixes.
110
+ *
111
+ * @param memString - K8s memory string like "128Mi", "1Gi", "1000000"
112
+ * @returns Memory in bytes
113
+ */
114
+ export function parseMemoryToBytes(memString: string): number {
115
+ if (!memString) return 0
116
+
117
+ const str = memString.trim()
118
+ const match = str.match(/^(\d+(?:\.\d+)?)\s*([A-Za-z]*)$/)
119
+ if (!match) return 0
120
+
121
+ const num = parseFloat(match[1])
122
+ const suffix = match[2]
123
+
124
+ // Binary suffixes (powers of 1024)
125
+ const binarySuffixes: Record<string, number> = {
126
+ 'Ki': 1024,
127
+ 'Mi': 1024 ** 2,
128
+ 'Gi': 1024 ** 3,
129
+ 'Ti': 1024 ** 4,
130
+ }
131
+
132
+ // Decimal suffixes (powers of 1000)
133
+ const decimalSuffixes: Record<string, number> = {
134
+ 'k': 1000,
135
+ 'K': 1000,
136
+ 'M': 1000 ** 2,
137
+ 'G': 1000 ** 3,
138
+ 'T': 1000 ** 4,
139
+ }
140
+
141
+ if (suffix in binarySuffixes) {
142
+ return num * binarySuffixes[suffix]
143
+ }
144
+ if (suffix in decimalSuffixes) {
145
+ return num * decimalSuffixes[suffix]
146
+ }
147
+
148
+ // No suffix = bytes
149
+ return num
150
+ }
151
+
152
+ /**
153
+ * Format a Kubernetes memory string to human-readable form.
154
+ *
155
+ * @param memString - K8s memory string like "128Mi", "1Gi", "153556Ki"
156
+ * @returns Formatted string like "128 MiB" or "1.5 GiB"
157
+ */
158
+ export function formatMemoryString(memString: string): string {
159
+ return formatMemoryBytes(parseMemoryToBytes(memString))
160
+ }
161
+
162
+ // =============================================================================
163
+ // Dashboard Metrics Formatting (different units from metrics-server)
164
+ // =============================================================================
165
+
166
+ /**
167
+ * Format CPU millicores to cores string.
168
+ * Used by dashboard API which returns CPU in millicores.
169
+ *
170
+ * @param millicores - CPU in millicores (from dashboard API)
171
+ * @returns Formatted string like "0.50 cores" or "3.5 cores" or "14 cores"
172
+ */
173
+ export function formatCPUMillicores(millicores: number): string {
174
+ const cores = millicores / 1000
175
+ return formatCoresValue(cores)
176
+ }
177
+
178
+ /**
179
+ * Format memory MiB to human-readable string.
180
+ * Used by dashboard API which returns memory in MiB.
181
+ *
182
+ * @param mib - Memory in MiB (from dashboard API)
183
+ * @returns Formatted string like "4 GiB" or "512 MiB"
184
+ */
185
+ export function formatMemoryMiB(mib: number): string {
186
+ const gib = mib / 1024
187
+ if (gib >= 10) {
188
+ return `${Math.round(gib)} GiB`
189
+ }
190
+ if (gib >= 1) {
191
+ return `${gib.toFixed(1)} GiB`
192
+ }
193
+ return `${Math.round(mib)} MiB`
194
+ }
195
+
196
+ // =============================================================================
197
+ // Combined Resource Formatting
198
+ // =============================================================================
199
+
200
+ /**
201
+ * Format a K8s resources object (requests/limits) to a readable string.
202
+ *
203
+ * @param resources - Object with cpu and/or memory fields
204
+ * @returns Formatted string like "500m CPU, 256Mi RAM" or "0.50 cores, 256 MiB"
205
+ */
206
+ export function formatResourceSpec(resources: { cpu?: string; memory?: string }): string {
207
+ const parts: string[] = []
208
+
209
+ if (resources.cpu) {
210
+ parts.push(`${formatCPUString(resources.cpu)} CPU`)
211
+ }
212
+ if (resources.memory) {
213
+ parts.push(`${formatMemoryString(resources.memory)} RAM`)
214
+ }
215
+
216
+ return parts.join(', ') || '-'
217
+ }
218
+
219
+ // =============================================================================
220
+ // General Byte Formatting
221
+ // =============================================================================
222
+
223
+ /**
224
+ * Format a byte count to a human-readable string (KB, MB, GB, TB).
225
+ * Uses decimal (1024-based) units without the "i" suffix.
226
+ */
227
+ export function formatBytes(bytes: number): string {
228
+ if (bytes === 0) return '0 B'
229
+ const k = 1024
230
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
231
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
232
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
233
+ }
@@ -0,0 +1,10 @@
1
+ export * from './format'
2
+ export * from './badge-colors'
3
+ export * from './resource-icons'
4
+ export * from './navigation'
5
+ export * from './context-name'
6
+ export * from './animation'
7
+ export * from './resource-hierarchy'
8
+ export * from './log-format'
9
+ export * from './download'
10
+ export * from './api-resources'
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Log formatting and display utilities.
3
+ * Shared between LogsViewer and WorkloadLogsViewer components.
4
+ */
5
+
6
+ import type { LogLevel } from '../components/logs/useLogBuffer'
7
+
8
+ /**
9
+ * Format a K8s log timestamp for display.
10
+ * Extracts and formats the time portion (HH:MM:SS).
11
+ */
12
+ export function formatLogTimestamp(ts: string): string {
13
+ const date = new Date(ts)
14
+ if (isNaN(date.getTime())) {
15
+ // Fallback: extract HH:MM:SS from ISO timestamp
16
+ return ts.slice(11, 19) || ts
17
+ }
18
+ return date.toLocaleTimeString('en-US', {
19
+ hour12: false,
20
+ hour: '2-digit',
21
+ minute: '2-digit',
22
+ second: '2-digit',
23
+ })
24
+ }
25
+
26
+ /** Map a detected LogLevel to a Tailwind color class. */
27
+ export function getLevelColor(level: LogLevel): string {
28
+ switch (level) {
29
+ case 'error': return 'text-red-400'
30
+ case 'warn': return 'text-yellow-400'
31
+ case 'debug': return 'text-theme-text-secondary'
32
+ case 'info': return 'text-theme-text-primary'
33
+ default: return 'text-theme-text-primary'
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Highlight search query matches in text with a mark tag.
39
+ * Returns HTML string safe for dangerouslySetInnerHTML.
40
+ */
41
+ export function highlightSearchMatches(
42
+ text: string, query: string, isRegex = false, isCaseSensitive = false,
43
+ ): string {
44
+ if (!query) return escapeHtml(text)
45
+ const escaped = escapeHtml(text)
46
+ const flags = isCaseSensitive ? 'g' : 'gi'
47
+ let pattern: string
48
+ if (isRegex) {
49
+ try {
50
+ // Validate the regex first — if invalid, fall back to literal
51
+ new RegExp(query)
52
+ pattern = query
53
+ } catch {
54
+ pattern = escapeRegExp(escapeHtml(query))
55
+ }
56
+ } else {
57
+ pattern = escapeRegExp(escapeHtml(query))
58
+ }
59
+ const regex = new RegExp(`(${pattern})`, flags)
60
+ return escaped.replace(regex, '<mark class="log-highlight">$1</mark>')
61
+ }
62
+
63
+ /**
64
+ * Escape HTML special characters to prevent XSS.
65
+ */
66
+ export function escapeHtml(text: string): string {
67
+ return text
68
+ .replace(/&/g, '&amp;')
69
+ .replace(/</g, '&lt;')
70
+ .replace(/>/g, '&gt;')
71
+ }
72
+
73
+ /**
74
+ * Strip ANSI escape sequences from text.
75
+ * Covers common CSI sequences (e.g. \x1b[32m, \x1b[0m) found in pod logs.
76
+ * Used for search matching and log level detection on raw log content.
77
+ */
78
+ export function stripAnsi(text: string): string {
79
+ // eslint-disable-next-line no-control-regex
80
+ return text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
81
+ }
82
+
83
+ // Standard 256-color palette (indices 0-255)
84
+ // 0-7: standard colors, 8-15: bright colors, 16-231: 6x6x6 color cube, 232-255: grayscale
85
+ const ANSI_256_COLORS: string[] = (() => {
86
+ const colors: string[] = [
87
+ '#4c4c4c', '#cd3131', '#0dbc79', '#e5e510', '#2472c8', '#bc3fbc', '#11a8cd', '#e5e5e5',
88
+ '#767676', '#f14c4c', '#23d18b', '#f5f543', '#3b8eea', '#d670d6', '#29b8db', '#e5e5e5',
89
+ ]
90
+ // 6x6x6 color cube (indices 16-231)
91
+ const levels = [0, 95, 135, 175, 215, 255]
92
+ for (let r = 0; r < 6; r++) {
93
+ for (let g = 0; g < 6; g++) {
94
+ for (let b = 0; b < 6; b++) {
95
+ colors.push(`#${levels[r].toString(16).padStart(2, '0')}${levels[g].toString(16).padStart(2, '0')}${levels[b].toString(16).padStart(2, '0')}`)
96
+ }
97
+ }
98
+ }
99
+ // Grayscale (indices 232-255)
100
+ for (let i = 0; i < 24; i++) {
101
+ const v = (8 + i * 10).toString(16).padStart(2, '0')
102
+ colors.push(`#${v}${v}${v}`)
103
+ }
104
+ return colors
105
+ })()
106
+
107
+ // ANSI SGR (Select Graphic Rendition) code → CSS style mapping.
108
+ // Colors are chosen to be legible on a dark background (terminal-standard palette).
109
+ const SGR_STYLES: Record<number, string> = {
110
+ 1: 'font-weight:bold',
111
+ 2: 'opacity:0.6',
112
+ 3: 'font-style:italic',
113
+ 4: 'text-decoration:underline',
114
+ // Standard foreground colors (30-37)
115
+ 30: 'color:#4c4c4c',
116
+ 31: 'color:#cd3131',
117
+ 32: 'color:#0dbc79',
118
+ 33: 'color:#e5e510',
119
+ 34: 'color:#2472c8',
120
+ 35: 'color:#bc3fbc',
121
+ 36: 'color:#11a8cd',
122
+ 37: 'color:#e5e5e5',
123
+ // Standard background colors (40-47)
124
+ 40: 'background-color:#4c4c4c',
125
+ 41: 'background-color:#cd3131',
126
+ 42: 'background-color:#0dbc79',
127
+ 43: 'background-color:#e5e510',
128
+ 44: 'background-color:#2472c8',
129
+ 45: 'background-color:#bc3fbc',
130
+ 46: 'background-color:#11a8cd',
131
+ 47: 'background-color:#e5e5e5',
132
+ // Bright foreground colors (90-97)
133
+ 90: 'color:#767676',
134
+ 91: 'color:#f14c4c',
135
+ 92: 'color:#23d18b',
136
+ 93: 'color:#f5f543',
137
+ 94: 'color:#3b8eea',
138
+ 95: 'color:#d670d6',
139
+ 96: 'color:#29b8db',
140
+ 97: 'color:#e5e5e5',
141
+ // Bright background colors (100-107)
142
+ 100: 'background-color:#767676',
143
+ 101: 'background-color:#f14c4c',
144
+ 102: 'background-color:#23d18b',
145
+ 103: 'background-color:#f5f543',
146
+ 104: 'background-color:#3b8eea',
147
+ 105: 'background-color:#d670d6',
148
+ 106: 'background-color:#29b8db',
149
+ 107: 'background-color:#e5e5e5',
150
+ }
151
+
152
+ /**
153
+ * Resolve an array of SGR codes to a CSS style string.
154
+ * Handles 256-color (38;5;N / 48;5;N) sequences.
155
+ */
156
+ function resolveSgrStyles(codes: number[]): string {
157
+ const parts: string[] = []
158
+ let i = 0
159
+ while (i < codes.length) {
160
+ // RGB foreground: 38;2;r;g;b
161
+ if (codes[i] === 38 && codes[i + 1] === 2 && codes[i + 4] !== undefined) {
162
+ const r = codes[i + 2], g = codes[i + 3], b = codes[i + 4]
163
+ parts.push(`color:rgb(${r},${g},${b})`)
164
+ i += 5
165
+ continue
166
+ }
167
+ // RGB background: 48;2;r;g;b
168
+ if (codes[i] === 48 && codes[i + 1] === 2 && codes[i + 4] !== undefined) {
169
+ const r = codes[i + 2], g = codes[i + 3], b = codes[i + 4]
170
+ parts.push(`background-color:rgb(${r},${g},${b})`)
171
+ i += 5
172
+ continue
173
+ }
174
+ // 256-color foreground: 38;5;N
175
+ if (codes[i] === 38 && codes[i + 1] === 5 && codes[i + 2] !== undefined) {
176
+ const colorIdx = codes[i + 2]
177
+ if (colorIdx >= 0 && colorIdx < 256) {
178
+ parts.push(`color:${ANSI_256_COLORS[colorIdx]}`)
179
+ }
180
+ i += 3
181
+ continue
182
+ }
183
+ // 256-color background: 48;5;N
184
+ if (codes[i] === 48 && codes[i + 1] === 5 && codes[i + 2] !== undefined) {
185
+ const colorIdx = codes[i + 2]
186
+ if (colorIdx >= 0 && colorIdx < 256) {
187
+ parts.push(`background-color:${ANSI_256_COLORS[colorIdx]}`)
188
+ }
189
+ i += 3
190
+ continue
191
+ }
192
+ // Standard SGR code
193
+ const style = SGR_STYLES[codes[i]]
194
+ if (style) {
195
+ parts.push(style)
196
+ }
197
+ i++
198
+ }
199
+ return parts.join(';')
200
+ }
201
+
202
+ /**
203
+ * Convert ANSI SGR escape codes in a log line to HTML <span> elements.
204
+ * HTML-escapes the text first so the output is safe for dangerouslySetInnerHTML.
205
+ * Each call is independent — all opened spans are closed before returning.
206
+ * Only SGR sequences (\x1b[...m) are handled; other ANSI sequences are stripped.
207
+ */
208
+ export function ansiToHtml(text: string): string {
209
+ // HTML-escape first: ANSI escape sequences contain no HTML special characters
210
+ // (&, <, >) so escaping won't interfere with the ANSI pattern matching below.
211
+ const escaped = escapeHtml(text)
212
+
213
+ let result = ''
214
+ let openSpans = 0
215
+ // eslint-disable-next-line no-control-regex
216
+ const ansiRe = /\x1b\[([0-9;]*)m/g
217
+ let lastIndex = 0
218
+ let match: RegExpExecArray | null
219
+
220
+ while ((match = ansiRe.exec(escaped)) !== null) {
221
+ result += escaped.slice(lastIndex, match.index)
222
+ lastIndex = match.index + match[0].length
223
+
224
+ const codes = match[1] === '' ? [0] : match[1].split(';').map(Number)
225
+
226
+ const resetIdx = codes.indexOf(0)
227
+ if (resetIdx !== -1) {
228
+ // Close all open spans on reset
229
+ result += '</span>'.repeat(openSpans)
230
+ openSpans = 0
231
+ // Apply any codes that follow the reset in the same sequence (e.g. \x1b[0;32m)
232
+ const afterReset = codes.slice(resetIdx + 1)
233
+ const styles = resolveSgrStyles(afterReset)
234
+ if (styles) {
235
+ result += `<span style="${styles}">`
236
+ openSpans++
237
+ }
238
+ } else {
239
+ const styles = resolveSgrStyles(codes)
240
+ if (styles) {
241
+ result += `<span style="${styles}">`
242
+ openSpans++
243
+ }
244
+ }
245
+ }
246
+
247
+ result += escaped.slice(lastIndex)
248
+ result += '</span>'.repeat(openSpans)
249
+ return result
250
+ }
251
+
252
+ /**
253
+ * Escape special regex characters in a string.
254
+ */
255
+ export function escapeRegExp(text: string): string {
256
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
257
+ }
258
+
259
+ /**
260
+ * Parse a K8s log line to extract timestamp and content.
261
+ * K8s timestamps are in RFC3339Nano format: 2024-01-20T10:30:00.123456789Z content
262
+ */
263
+ export function parseLogLine(line: string): { timestamp: string; content: string } {
264
+ if (line.length > 30 && line[4] === '-' && line[7] === '-' && line[10] === 'T') {
265
+ const spaceIdx = line.indexOf(' ')
266
+ if (spaceIdx > 20 && spaceIdx < 40) {
267
+ return { timestamp: line.slice(0, spaceIdx), content: line.slice(spaceIdx + 1) }
268
+ }
269
+ }
270
+ return { timestamp: '', content: line }
271
+ }
272
+
273
+ /**
274
+ * Parse a log range string (e.g. "500" or "since:300") into tailLines/sinceSeconds params.
275
+ */
276
+ export function parseLogRange(logRange: string): { tailLines?: number; sinceSeconds?: number } {
277
+ if (logRange.startsWith('since:')) {
278
+ return { sinceSeconds: Number(logRange.slice(6)) }
279
+ }
280
+ return { tailLines: Number(logRange) }
281
+ }
282
+
283
+ /**
284
+ * Handle SSE error events from log streams.
285
+ * Parses server-sent error data and logs it, then calls onClose.
286
+ */
287
+ export function handleSSEError(event: Event, prefix: string, onClose: () => void): void {
288
+ const me = event as MessageEvent
289
+ if (me.data) {
290
+ try {
291
+ const data = JSON.parse(me.data)
292
+ console.error(`${prefix}:`, data.error || data.message || me.data)
293
+ } catch {
294
+ console.error(`${prefix}:`, me.data)
295
+ }
296
+ } else {
297
+ console.error(`${prefix} connection error`)
298
+ }
299
+ onClose()
300
+ }
@@ -0,0 +1,119 @@
1
+ import type { SelectedResource, ResourceRef } from '../types/core'
2
+
3
+ /**
4
+ * Canonical callback type for navigating to a resource.
5
+ * All components that trigger resource navigation should use this type.
6
+ */
7
+ export type NavigateToResource = (resource: SelectedResource) => void
8
+
9
+ /**
10
+ * Convert a singular kind (e.g., "Deployment") to plural API resource name (e.g., "deployments").
11
+ * Single source of truth — uses English pluralization rules with a small alias map for
12
+ * abbreviations and special mappings that aren't simple plurals.
13
+ */
14
+ export function kindToPlural(kind: string): string {
15
+ const kindLower = kind.toLowerCase()
16
+
17
+ // Aliases: abbreviations or mappings to a different resource name
18
+ const aliases: Record<string, string> = {
19
+ horizontalpodautoscaler: 'horizontalpodautoscalers',
20
+ pvc: 'persistentvolumeclaims',
21
+ podgroup: 'pods',
22
+ }
23
+ if (aliases[kindLower]) return aliases[kindLower]
24
+
25
+ // English pluralization rules (covers *Class→*classes, *Policy→*policies, *Repository→*repositories, etc.)
26
+ if (kindLower.endsWith('s') || kindLower.endsWith('x') || kindLower.endsWith('ch') || kindLower.endsWith('sh')) {
27
+ return kindLower + 'es'
28
+ }
29
+ if (kindLower.endsWith('y') && !/[aeiou]y$/.test(kindLower)) {
30
+ return kindLower.slice(0, -1) + 'ies'
31
+ }
32
+ return kindLower + 's'
33
+ }
34
+
35
+ /**
36
+ * Convert a plural API resource name (e.g., "deployments") back to singular PascalCase kind (e.g., "Deployment").
37
+ * Inverse of kindToPlural. Converts plural API resource names from URLs back to
38
+ * singular PascalCase form for internal logic (health checks, badge colors, hierarchy matching).
39
+ */
40
+ export function pluralToKind(plural: string): string {
41
+ const lower = plural.toLowerCase()
42
+
43
+ // Explicit reverse mappings for irregular/aliased plurals
44
+ const reverseMap: Record<string, string> = {
45
+ pods: 'Pod',
46
+ services: 'Service',
47
+ deployments: 'Deployment',
48
+ daemonsets: 'DaemonSet',
49
+ statefulsets: 'StatefulSet',
50
+ replicasets: 'ReplicaSet',
51
+ ingresses: 'Ingress',
52
+ gateways: 'Gateway',
53
+ httproutes: 'HTTPRoute',
54
+ grpcroutes: 'GRPCRoute',
55
+ tcproutes: 'TCPRoute',
56
+ tlsroutes: 'TLSRoute',
57
+ configmaps: 'ConfigMap',
58
+ secrets: 'Secret',
59
+ namespaces: 'Namespace',
60
+ events: 'Event',
61
+ nodes: 'Node',
62
+ jobs: 'Job',
63
+ cronjobs: 'CronJob',
64
+ horizontalpodautoscalers: 'HorizontalPodAutoscaler',
65
+ persistentvolumeclaims: 'PersistentVolumeClaim',
66
+ persistentvolumes: 'PersistentVolume',
67
+ storageclasses: 'StorageClass',
68
+ poddisruptionbudgets: 'PodDisruptionBudget',
69
+ rollouts: 'Rollout',
70
+ applications: 'Application',
71
+ kustomizations: 'Kustomization',
72
+ helmreleases: 'HelmRelease',
73
+ gitrepositories: 'GitRepository',
74
+ certificates: 'Certificate',
75
+ roles: 'Role',
76
+ clusterroles: 'ClusterRole',
77
+ rolebindings: 'RoleBinding',
78
+ clusterrolebindings: 'ClusterRoleBinding',
79
+ serviceaccounts: 'ServiceAccount',
80
+ networkpolicies: 'NetworkPolicy',
81
+ verticalpodautoscalers: 'VerticalPodAutoscaler',
82
+ virtualservices: 'VirtualService',
83
+ destinationrules: 'DestinationRule',
84
+ serviceentries: 'ServiceEntry',
85
+ peerauthentications: 'PeerAuthentication',
86
+ authorizationpolicies: 'AuthorizationPolicy',
87
+ }
88
+
89
+ if (reverseMap[lower]) return reverseMap[lower]
90
+
91
+ // If it already looks like a singular PascalCase kind (starts with uppercase), return as-is
92
+ if (plural[0] === plural[0].toUpperCase() && plural[0] !== plural[0].toLowerCase()) {
93
+ return plural
94
+ }
95
+
96
+ // Fallback: basic de-pluralization + capitalize first letter
97
+ let singular = lower
98
+ if (singular.endsWith('ies')) {
99
+ singular = singular.slice(0, -3) + 'y'
100
+ } else if (singular.endsWith('ses') || singular.endsWith('xes') || singular.endsWith('ches') || singular.endsWith('shes')) {
101
+ singular = singular.slice(0, -2)
102
+ } else if (singular.endsWith('s')) {
103
+ singular = singular.slice(0, -1)
104
+ }
105
+ return singular.charAt(0).toUpperCase() + singular.slice(1)
106
+ }
107
+
108
+ /**
109
+ * Convert a ResourceRef (from backend relationships) to a SelectedResource (for navigation).
110
+ * Handles kind singular→plural conversion.
111
+ */
112
+ export function refToSelectedResource(ref: ResourceRef): SelectedResource {
113
+ return {
114
+ kind: kindToPlural(ref.kind),
115
+ namespace: ref.namespace,
116
+ name: ref.name,
117
+ group: ref.group,
118
+ }
119
+ }