@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,1558 @@
1
+ // Utility functions for resource display in tables
2
+
3
+ import { formatCPUString, formatMemoryString, formatBytes } from '../../utils/format'
4
+
5
+ // Import functions from sub-modules used internally by getCellFilterValue
6
+ import { getCertificateStatus, getCertificateRequestStatus, getClusterIssuerStatus, getClusterIssuerType, getOrderState, getChallengeState, getChallengeType } from './resource-utils-certmanager'
7
+ import { getNodePoolStatus, getNodeClaimStatus } from './resource-utils-karpenter'
8
+ import { getScaledObjectStatus, getScaledJobStatus } from './resource-utils-keda'
9
+ import { getGitRepositoryStatus, getOCIRepositoryStatus, getHelmRepositoryStatus, getHelmRepositoryType, getKustomizationStatus, getFluxHelmReleaseStatus, getFluxAlertStatus } from './resource-utils-flux'
10
+ import { getArgoApplicationStatus, getArgoApplicationSetStatus, getArgoApplicationSync, getArgoApplicationHealth, getArgoApplicationProject } from './resource-utils-argo'
11
+ import { getPolicyReportStatus as _getPolicyReportStatus, getKyvernoPolicyStatus as _getKyvernoPolicyStatus } from './resource-utils-kyverno'
12
+ import { getBackupStatus as _getBackupStatus, getRestoreStatus as _getRestoreStatus, getScheduleStatus as _getScheduleStatus, getBSLStatus as _getBSLStatus } from './resource-utils-velero'
13
+ import { getExternalSecretStatus as _getExternalSecretStatus, getClusterExternalSecretStatus as _getClusterExternalSecretStatus, getSecretStoreStatus as _getSecretStoreStatus, getClusterSecretStoreStatus as _getClusterSecretStoreStatus, getSecretStoreProviderType as _getSecretStoreProviderType } from './resource-utils-eso'
14
+
15
+ // ============================================================================
16
+ // STATUS & HEALTH UTILITIES
17
+ // ============================================================================
18
+
19
+ export type HealthLevel = 'healthy' | 'degraded' | 'unhealthy' | 'unknown' | 'neutral'
20
+
21
+ export interface StatusBadge {
22
+ text: string
23
+ color: string
24
+ level: HealthLevel
25
+ }
26
+
27
+ // Color classes for different health levels (theme-aware)
28
+ export const healthColors: Record<HealthLevel, string> = {
29
+ healthy: 'status-healthy',
30
+ degraded: 'status-degraded',
31
+ unhealthy: 'status-unhealthy',
32
+ unknown: 'status-unknown',
33
+ neutral: 'status-neutral',
34
+ }
35
+
36
+ // ============================================================================
37
+ // POD UTILITIES
38
+ // ============================================================================
39
+
40
+ export interface PodProblem {
41
+ severity: 'critical' | 'high' | 'medium'
42
+ message: string
43
+ }
44
+
45
+ export function getPodStatus(pod: any): StatusBadge {
46
+ const phase = pod.status?.phase || 'Unknown'
47
+ const containerStatuses = pod.status?.containerStatuses || []
48
+
49
+ // Check for terminating
50
+ if (pod.metadata?.deletionTimestamp) {
51
+ return { text: 'Terminating', color: healthColors.degraded, level: 'degraded' }
52
+ }
53
+
54
+ // Check container states for issues
55
+ for (const cs of containerStatuses) {
56
+ if (cs.state?.waiting?.reason) {
57
+ const reason = cs.state.waiting.reason
58
+ if (['CrashLoopBackOff', 'ImagePullBackOff', 'ErrImagePull', 'CreateContainerConfigError'].includes(reason)) {
59
+ return { text: reason, color: healthColors.unhealthy, level: 'unhealthy' }
60
+ }
61
+ }
62
+ if (cs.state?.terminated?.reason === 'OOMKilled') {
63
+ return { text: 'OOMKilled', color: healthColors.unhealthy, level: 'unhealthy' }
64
+ }
65
+ }
66
+
67
+ switch (phase) {
68
+ case 'Running':
69
+ // Check if all containers are ready
70
+ const ready = containerStatuses.filter((c: any) => c.ready).length
71
+ const total = containerStatuses.length
72
+ if (total > 0 && ready < total) {
73
+ return { text: `Running (${ready}/${total})`, color: healthColors.degraded, level: 'degraded' }
74
+ }
75
+ return { text: 'Running', color: healthColors.healthy, level: 'healthy' }
76
+ case 'Succeeded':
77
+ return { text: 'Completed', color: healthColors.neutral, level: 'neutral' }
78
+ case 'Pending':
79
+ return { text: 'Pending', color: healthColors.degraded, level: 'degraded' }
80
+ case 'Failed':
81
+ return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
82
+ default:
83
+ return { text: phase, color: healthColors.unknown, level: 'unknown' }
84
+ }
85
+ }
86
+
87
+ export function getPodProblems(pod: any): PodProblem[] {
88
+ const problems: PodProblem[] = []
89
+ const containerStatuses = pod.status?.containerStatuses || []
90
+ const conditions = pod.status?.conditions || []
91
+
92
+ for (const cs of containerStatuses) {
93
+ // Check waiting state
94
+ if (cs.state?.waiting?.reason) {
95
+ const reason = cs.state.waiting.reason
96
+ if (['CrashLoopBackOff', 'ImagePullBackOff', 'ErrImagePull'].includes(reason)) {
97
+ problems.push({ severity: 'critical', message: reason })
98
+ } else if (reason === 'CreateContainerConfigError') {
99
+ problems.push({ severity: 'critical', message: 'Config Error' })
100
+ } else if (reason === 'ContainerCannotRun') {
101
+ problems.push({ severity: 'critical', message: 'Cannot Run' })
102
+ }
103
+ }
104
+ // Check terminated state
105
+ if (cs.state?.terminated?.reason === 'OOMKilled') {
106
+ problems.push({ severity: 'critical', message: 'OOMKilled' })
107
+ }
108
+ // High restart count
109
+ if (cs.restartCount > 5) {
110
+ problems.push({ severity: 'medium', message: `${cs.restartCount} restarts` })
111
+ }
112
+ // Volume mount issues from last state
113
+ const lastMsg = cs.lastState?.terminated?.message?.toLowerCase() || ''
114
+ if (lastMsg.includes('failed to mount') || lastMsg.includes('failedattachvolume')) {
115
+ problems.push({ severity: 'high', message: 'Volume Mount Failed' })
116
+ }
117
+ }
118
+
119
+ // Check conditions
120
+ for (const cond of conditions) {
121
+ if (cond.type === 'PodScheduled' && cond.status === 'False') {
122
+ if (cond.reason === 'Unschedulable') {
123
+ problems.push({ severity: 'high', message: 'Unschedulable' })
124
+ }
125
+ }
126
+ // Readiness/Liveness probe failures
127
+ if (cond.type === 'ContainersReady' && cond.status === 'False') {
128
+ const msg = (cond.message || '').toLowerCase()
129
+ if (msg.includes('readiness')) {
130
+ problems.push({ severity: 'medium', message: 'Readiness Probe Failing' })
131
+ } else if (msg.includes('liveness')) {
132
+ problems.push({ severity: 'high', message: 'Liveness Probe Failing' })
133
+ }
134
+ }
135
+ // IP allocation failures (subnet exhaustion)
136
+ if (cond.type === 'PodReadyToStartContainers' && cond.status === 'False') {
137
+ const msg = (cond.message || '').toLowerCase()
138
+ if (msg.includes('failed to assign an ip') || msg.includes('pod sandbox')) {
139
+ problems.push({ severity: 'critical', message: 'IP Allocation Failed' })
140
+ }
141
+ }
142
+ }
143
+
144
+ // Evicted pods
145
+ if (pod.status?.phase === 'Failed' && pod.status?.reason === 'Evicted') {
146
+ problems.push({ severity: 'high', message: 'Evicted' })
147
+ }
148
+
149
+ // Stuck terminating (zombie pod)
150
+ if (pod.metadata?.deletionTimestamp) {
151
+ const deleteTime = new Date(pod.metadata.deletionTimestamp).getTime()
152
+ const ageSeconds = (Date.now() - deleteTime) / 1000
153
+ if (ageSeconds > 60) {
154
+ problems.push({ severity: 'medium', message: 'Stuck Terminating' })
155
+ }
156
+ }
157
+
158
+ // Not ready (Running but containers not ready)
159
+ const phase = pod.status?.phase
160
+ if (phase === 'Running') {
161
+ const readyContainers = containerStatuses.filter((c: any) => c.ready).length
162
+ const totalContainers = containerStatuses.length
163
+ if (totalContainers > 0 && readyContainers < totalContainers) {
164
+ // Only add if we haven't already flagged a more specific issue
165
+ const hasSpecificIssue = problems.some(p =>
166
+ p.message.includes('Probe') || p.message.includes('CrashLoop') || p.message.includes('OOM')
167
+ )
168
+ if (!hasSpecificIssue) {
169
+ problems.push({ severity: 'medium', message: 'Not Ready' })
170
+ }
171
+ }
172
+ }
173
+
174
+ return problems
175
+ }
176
+
177
+ export function getPodReadiness(pod: any): { ready: number; total: number } {
178
+ const containerStatuses = pod.status?.containerStatuses || []
179
+ const initContainerStatuses = pod.status?.initContainerStatuses || []
180
+
181
+ // Check init containers first
182
+ const initNotComplete = initContainerStatuses.filter((c: any) =>
183
+ !c.state?.terminated || c.state.terminated.exitCode !== 0
184
+ ).length
185
+
186
+ if (initNotComplete > 0 && initContainerStatuses.length > 0) {
187
+ const initComplete = initContainerStatuses.length - initNotComplete
188
+ return { ready: initComplete, total: initContainerStatuses.length }
189
+ }
190
+
191
+ const ready = containerStatuses.filter((c: any) => c.ready).length
192
+ return { ready, total: containerStatuses.length || pod.spec?.containers?.length || 0 }
193
+ }
194
+
195
+ export function getPodRestarts(pod: any): number {
196
+ const containerStatuses = pod.status?.containerStatuses || []
197
+ return containerStatuses.reduce((sum: number, c: any) => sum + (c.restartCount || 0), 0)
198
+ }
199
+
200
+ // ============================================================================
201
+ // WORKLOAD UTILITIES (Deployment, StatefulSet, DaemonSet, ReplicaSet)
202
+ // ============================================================================
203
+
204
+ export function getWorkloadStatus(resource: any, kind: string): StatusBadge {
205
+ const status = resource.status || {}
206
+ const spec = resource.spec || {}
207
+
208
+ if (kind === 'daemonsets') {
209
+ const desired = status.desiredNumberScheduled || 0
210
+ const ready = status.numberReady || 0
211
+ const updated = status.updatedNumberScheduled || 0
212
+
213
+ if (desired === 0) return { text: '0 nodes', color: healthColors.unknown, level: 'unknown' }
214
+ if (ready === desired && updated === desired) {
215
+ return { text: `${ready}/${desired}`, color: healthColors.healthy, level: 'healthy' }
216
+ }
217
+ if (ready > 0) {
218
+ return { text: `${ready}/${desired}`, color: healthColors.degraded, level: 'degraded' }
219
+ }
220
+ return { text: `${ready}/${desired}`, color: healthColors.unhealthy, level: 'unhealthy' }
221
+ }
222
+
223
+ // Deployment, StatefulSet, ReplicaSet
224
+ const desired = spec.replicas ?? status.replicas ?? 0
225
+ const ready = status.readyReplicas || 0
226
+ const updated = status.updatedReplicas || 0
227
+ const available = status.availableReplicas || 0
228
+
229
+ if (desired === 0) {
230
+ return { text: 'Scaled to 0', color: healthColors.neutral, level: 'neutral' }
231
+ }
232
+
233
+ // Check if updating
234
+ if (updated < desired && updated > 0) {
235
+ return { text: `Updating ${updated}/${desired}`, color: healthColors.degraded, level: 'degraded' }
236
+ }
237
+
238
+ if (ready === desired && available === desired) {
239
+ return { text: `${ready}/${desired}`, color: healthColors.healthy, level: 'healthy' }
240
+ }
241
+ if (ready > 0) {
242
+ return { text: `${ready}/${desired}`, color: healthColors.degraded, level: 'degraded' }
243
+ }
244
+ return { text: `${ready}/${desired}`, color: healthColors.unhealthy, level: 'unhealthy' }
245
+ }
246
+
247
+ export function getWorkloadImages(resource: any): string[] {
248
+ const containers = resource.spec?.template?.spec?.containers || []
249
+ return containers.map((c: any) => {
250
+ const image = c.image || ''
251
+ // Extract just image:tag, remove registry prefix
252
+ const parts = image.split('/')
253
+ return parts[parts.length - 1] || image
254
+ })
255
+ }
256
+
257
+ export function getWorkloadConditions(resource: any): { conditions: string[]; hasIssues: boolean } {
258
+ const conditions = resource.status?.conditions || []
259
+ const activeConditions: string[] = []
260
+ let hasIssues = false
261
+
262
+ for (const cond of conditions) {
263
+ if (cond.status === 'True') {
264
+ activeConditions.push(cond.type)
265
+ // Progressing and Available are good, others might indicate issues
266
+ if (!['Progressing', 'Available'].includes(cond.type)) {
267
+ hasIssues = true
268
+ }
269
+ } else if (cond.status === 'False') {
270
+ // Available=False is an issue
271
+ if (cond.type === 'Available') {
272
+ hasIssues = true
273
+ }
274
+ }
275
+ }
276
+
277
+ return { conditions: activeConditions, hasIssues }
278
+ }
279
+
280
+ export function getReplicaSetOwner(rs: any): string | null {
281
+ const ownerRefs = rs.metadata?.ownerReferences || []
282
+ const owner = ownerRefs[0]
283
+ if (owner) {
284
+ return `${owner.kind}/${owner.name}`
285
+ }
286
+ return null
287
+ }
288
+
289
+ export function isReplicaSetActive(rs: any): boolean {
290
+ const replicas = rs.spec?.replicas || 0
291
+ return replicas > 0
292
+ }
293
+
294
+ // ============================================================================
295
+ // SERVICE UTILITIES
296
+ // ============================================================================
297
+
298
+ export function getServiceStatus(service: any): StatusBadge {
299
+ const type = service.spec?.type || 'ClusterIP'
300
+ const color = type === 'LoadBalancer' || type === 'NodePort'
301
+ ? 'status-violet'
302
+ : healthColors.neutral
303
+ return { text: type, color, level: 'neutral' }
304
+ }
305
+
306
+ export function getServicePorts(service: any): string {
307
+ const ports = service.spec?.ports || []
308
+ if (ports.length === 0) return '-'
309
+ if (ports.length <= 2) {
310
+ return ports.map((p: any) => {
311
+ const port = p.port
312
+ const target = p.targetPort !== p.port ? `:${p.targetPort}` : ''
313
+ const proto = p.protocol !== 'TCP' ? `/${p.protocol}` : ''
314
+ return `${port}${target}${proto}`
315
+ }).join(', ')
316
+ }
317
+ return `${ports.length} ports`
318
+ }
319
+
320
+ export function getServiceExternalIP(service: any): string | null {
321
+ const type = service.spec?.type
322
+ if (type === 'LoadBalancer') {
323
+ const ingress = service.status?.loadBalancer?.ingress || []
324
+ // Show all IPs, not just first
325
+ if (ingress.length > 0) {
326
+ const ips = ingress.map((i: any) => i.ip || i.hostname).filter(Boolean)
327
+ if (ips.length > 2) return `${ips[0]} +${ips.length - 1}`
328
+ return ips.join(', ') || null
329
+ }
330
+ return 'Pending'
331
+ }
332
+ if (type === 'NodePort') {
333
+ const ports = service.spec?.ports || []
334
+ const nodePorts = ports.map((p: any) => p.nodePort).filter(Boolean)
335
+ if (nodePorts.length > 0) {
336
+ return `NodePort: ${nodePorts.join(', ')}`
337
+ }
338
+ }
339
+ // Also check spec.externalIPs (can be set on any service type)
340
+ if (service.spec?.externalIPs?.length > 0) {
341
+ const ips = service.spec.externalIPs
342
+ if (ips.length > 2) return `${ips[0]} +${ips.length - 1}`
343
+ return ips.join(', ')
344
+ }
345
+ return null
346
+ }
347
+
348
+ export function getServiceSelector(service: any): string {
349
+ // For ExternalName services, show the external DNS name
350
+ if (service.spec?.type === 'ExternalName') {
351
+ return service.spec.externalName || '-'
352
+ }
353
+ const selector = service.spec?.selector || {}
354
+ const pairs = Object.entries(selector).map(([k, v]) => `${k}=${v}`)
355
+ if (pairs.length === 0) return 'None'
356
+ if (pairs.length <= 2) return pairs.join(', ')
357
+ return `${pairs.slice(0, 2).join(', ')} +${pairs.length - 2}`
358
+ }
359
+
360
+ export function getServiceEndpointsStatus(service: any): { status: string; color: string } {
361
+ const type = service.spec?.type
362
+ if (type === 'ExternalName') {
363
+ return { status: 'External', color: 'status-violet' }
364
+ }
365
+ const selector = service.spec?.selector || {}
366
+ const hasSelector = Object.keys(selector).length > 0
367
+ if (!hasSelector) {
368
+ return { status: 'None', color: 'status-unknown' }
369
+ }
370
+ // If it has a selector, it should have endpoints (we assume active since we can't check endpoints from service alone)
371
+ return { status: 'Active', color: 'status-healthy' }
372
+ }
373
+
374
+ // ============================================================================
375
+ // INGRESS UTILITIES
376
+ // ============================================================================
377
+
378
+ export function getIngressStatus(ingress: any): StatusBadge {
379
+ const lbIngress = ingress.status?.loadBalancer?.ingress || []
380
+ if (lbIngress.length > 0) {
381
+ return { text: 'Active', color: healthColors.healthy, level: 'healthy' }
382
+ }
383
+ return { text: 'Pending', color: healthColors.degraded, level: 'degraded' }
384
+ }
385
+
386
+ export function getIngressHosts(ingress: any): string {
387
+ const rules = ingress.spec?.rules || []
388
+ const hosts = rules.map((r: any) => r.host).filter(Boolean)
389
+ if (hosts.length === 0) return '*'
390
+ if (hosts.length <= 2) return hosts.join(', ')
391
+ return `${hosts[0]} +${hosts.length - 1} more`
392
+ }
393
+
394
+ export function getIngressClass(ingress: any): string | null {
395
+ return ingress.spec?.ingressClassName ||
396
+ ingress.metadata?.annotations?.['kubernetes.io/ingress.class'] ||
397
+ null
398
+ }
399
+
400
+ export function hasIngressTLS(ingress: any): boolean {
401
+ return (ingress.spec?.tls?.length || 0) > 0
402
+ }
403
+
404
+ export function getIngressAddress(ingress: any): string | null {
405
+ const lbIngress = ingress.status?.loadBalancer?.ingress || []
406
+ if (lbIngress.length > 0) {
407
+ return lbIngress[0].ip || lbIngress[0].hostname || null
408
+ }
409
+ return null
410
+ }
411
+
412
+ export function getIngressRules(ingress: any): string {
413
+ const rules = ingress.spec?.rules || []
414
+ if (rules.length === 0) return 'No rules'
415
+
416
+ const formattedRules = rules.map((rule: any) => {
417
+ const host = rule.host || '*'
418
+ const paths = rule.http?.paths || []
419
+ if (paths.length === 0) return host
420
+
421
+ const pathMappings = paths.map((p: any) => {
422
+ const path = p.path || '/'
423
+ // Support both new (service.name) and legacy (serviceName) formats
424
+ const backend = p.backend?.service?.name || (p.backend as any)?.serviceName || 'unknown'
425
+ return `${path}→${backend}`
426
+ })
427
+
428
+ if (pathMappings.length === 1) {
429
+ return `${host}: ${pathMappings[0]}`
430
+ }
431
+ return `${host}: ${pathMappings.join(', ')}`
432
+ })
433
+
434
+ if (formattedRules.length === 1) return formattedRules[0]
435
+ if (formattedRules.length === 2) return formattedRules.join('; ')
436
+ return `${formattedRules[0]}; +${formattedRules.length - 1} more`
437
+ }
438
+
439
+ // ============================================================================
440
+ // CONFIGMAP / SECRET UTILITIES
441
+ // ============================================================================
442
+
443
+ export function getConfigMapKeys(cm: any): { count: number; preview: string } {
444
+ const data = cm.data || {}
445
+ const binaryData = cm.binaryData || {}
446
+ const keys = [...Object.keys(data), ...Object.keys(binaryData)]
447
+ const count = keys.length
448
+
449
+ if (count === 0) return { count: 0, preview: 'Empty' }
450
+ if (count <= 3) return { count, preview: keys.join(', ') }
451
+ return { count, preview: `${keys.slice(0, 2).join(', ')} +${count - 2}` }
452
+ }
453
+
454
+ export function getConfigMapSize(cm: any): string {
455
+ const data = cm.data || {}
456
+ const binaryData = cm.binaryData || {}
457
+
458
+ let totalBytes = 0
459
+ for (const value of Object.values(data)) {
460
+ totalBytes += (value as string).length
461
+ }
462
+ for (const value of Object.values(binaryData)) {
463
+ // Base64 encoded, actual size is ~75% of encoded
464
+ totalBytes += Math.floor((value as string).length * 0.75)
465
+ }
466
+
467
+ return formatBytes(totalBytes)
468
+ }
469
+
470
+ export function getSecretType(secret: any): { type: string; color: string } {
471
+ const type = secret.type || 'Opaque'
472
+ const typeMap: Record<string, { type: string; color: string }> = {
473
+ 'Opaque': { type: 'Opaque', color: 'status-unknown' },
474
+ 'kubernetes.io/tls': { type: 'TLS', color: 'status-neutral' },
475
+ 'kubernetes.io/dockercfg': { type: 'Docker', color: 'status-purple' },
476
+ 'kubernetes.io/dockerconfigjson': { type: 'Docker', color: 'status-purple' },
477
+ 'kubernetes.io/basic-auth': { type: 'Basic Auth', color: 'status-orange' },
478
+ 'kubernetes.io/ssh-auth': { type: 'SSH', color: 'status-cyan' },
479
+ 'kubernetes.io/service-account-token': { type: 'SA Token', color: 'status-healthy' },
480
+ 'bootstrap.kubernetes.io/token': { type: 'Bootstrap', color: 'status-healthy' },
481
+ }
482
+ return typeMap[type] || { type: type.split('/').pop() || type, color: 'status-unknown' }
483
+ }
484
+
485
+ export function getSecretKeyCount(secret: any): number {
486
+ const data = secret.data || {}
487
+ return Object.keys(data).length
488
+ }
489
+
490
+ // ============================================================================
491
+ // JOB / CRONJOB UTILITIES
492
+ // ============================================================================
493
+
494
+ export function getJobStatus(job: any): StatusBadge {
495
+ const status = job.status || {}
496
+ const conditions = status.conditions || []
497
+
498
+ // Check conditions first
499
+ const failedCond = conditions.find((c: any) => c.type === 'Failed' && c.status === 'True')
500
+ if (failedCond) {
501
+ return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
502
+ }
503
+
504
+ const completeCond = conditions.find((c: any) => c.type === 'Complete' && c.status === 'True')
505
+ if (completeCond) {
506
+ return { text: 'Complete', color: healthColors.healthy, level: 'healthy' }
507
+ }
508
+
509
+ if (job.spec?.suspend) {
510
+ return { text: 'Suspended', color: healthColors.degraded, level: 'degraded' }
511
+ }
512
+
513
+ if (status.active > 0) {
514
+ return { text: 'Running', color: healthColors.neutral, level: 'neutral' }
515
+ }
516
+
517
+ return { text: 'Pending', color: healthColors.degraded, level: 'degraded' }
518
+ }
519
+
520
+ export function getJobCompletions(job: any): { succeeded: number; total: number } {
521
+ const completions = job.spec?.completions || 1
522
+ const succeeded = job.status?.succeeded || 0
523
+ return { succeeded, total: completions }
524
+ }
525
+
526
+ export function getJobDuration(job: any): string | null {
527
+ const startTime = job.status?.startTime
528
+ const completionTime = job.status?.completionTime
529
+
530
+ if (!startTime) return null
531
+
532
+ const start = new Date(startTime)
533
+ const end = completionTime ? new Date(completionTime) : new Date()
534
+ const durationMs = end.getTime() - start.getTime()
535
+
536
+ return formatDuration(durationMs)
537
+ }
538
+
539
+ export function getCronJobStatus(cj: any): StatusBadge {
540
+ if (cj.spec?.suspend) {
541
+ return { text: 'Suspended', color: healthColors.degraded, level: 'degraded' }
542
+ }
543
+ const activeJobs = cj.status?.active?.length || 0
544
+ if (activeJobs > 0) {
545
+ return { text: `Active (${activeJobs})`, color: healthColors.neutral, level: 'neutral' }
546
+ }
547
+ return { text: 'Scheduled', color: healthColors.healthy, level: 'healthy' }
548
+ }
549
+
550
+ export function getCronJobSchedule(cj: any): { cron: string; readable: string } {
551
+ const schedule = cj.spec?.schedule || ''
552
+ return { cron: schedule, readable: cronToHuman(schedule) }
553
+ }
554
+
555
+ export function getCronJobLastRun(cj: any): string | null {
556
+ const lastSchedule = cj.status?.lastScheduleTime
557
+ if (!lastSchedule) return null
558
+ return formatAge(lastSchedule)
559
+ }
560
+
561
+ // ============================================================================
562
+ // HPA UTILITIES
563
+ // ============================================================================
564
+
565
+ export function getHPAStatus(hpa: any): StatusBadge {
566
+ const current = hpa.status?.currentReplicas || 0
567
+ const desired = hpa.status?.desiredReplicas || 0
568
+
569
+ if (current === desired) {
570
+ return { text: 'Stable', color: healthColors.healthy, level: 'healthy' }
571
+ }
572
+ if (current < desired) {
573
+ return { text: 'Scaling Up', color: healthColors.degraded, level: 'degraded' }
574
+ }
575
+ return { text: 'Scaling Down', color: healthColors.degraded, level: 'degraded' }
576
+ }
577
+
578
+ export function getHPAReplicas(hpa: any): { current: number; min: number; max: number } {
579
+ return {
580
+ current: hpa.status?.currentReplicas || 0,
581
+ min: hpa.spec?.minReplicas || 1,
582
+ max: hpa.spec?.maxReplicas || 0,
583
+ }
584
+ }
585
+
586
+ export function getHPATarget(hpa: any): string {
587
+ const ref = hpa.spec?.scaleTargetRef
588
+ if (!ref) return '-'
589
+ return `${ref.kind}/${ref.name}`
590
+ }
591
+
592
+ export function getHPAMetrics(hpa: any): { cpu?: number; memory?: number; custom: number } {
593
+ const currentMetrics = hpa.status?.currentMetrics || []
594
+ const result: { cpu?: number; memory?: number; custom: number } = { custom: 0 }
595
+
596
+ for (const metric of currentMetrics) {
597
+ if (metric.type === 'Resource') {
598
+ const current = metric.resource?.current?.averageUtilization
599
+ if (metric.resource?.name === 'cpu' && current !== undefined) {
600
+ result.cpu = current
601
+ } else if (metric.resource?.name === 'memory' && current !== undefined) {
602
+ result.memory = current
603
+ }
604
+ } else {
605
+ result.custom++
606
+ }
607
+ }
608
+
609
+ return result
610
+ }
611
+
612
+ // ============================================================================
613
+ // NODE UTILITIES
614
+ // ============================================================================
615
+
616
+ export function getNodeStatus(node: any): StatusBadge {
617
+ const conditions = node.status?.conditions || []
618
+ const readyCondition = conditions.find((c: any) => c.type === 'Ready')
619
+
620
+ const isReady = readyCondition?.status === 'True'
621
+ const isUnschedulable = node.spec?.unschedulable === true
622
+
623
+ if (isReady && isUnschedulable) {
624
+ return { text: 'Ready,SchedulingDisabled', color: healthColors.degraded, level: 'degraded' }
625
+ }
626
+ if (isReady) {
627
+ return { text: 'Ready', color: healthColors.healthy, level: 'healthy' }
628
+ }
629
+ if (readyCondition?.status === 'False') {
630
+ return { text: 'NotReady', color: healthColors.unhealthy, level: 'unhealthy' }
631
+ }
632
+ return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
633
+ }
634
+
635
+ export function getNodeRoles(node: any): string {
636
+ const labels = node.metadata?.labels || {}
637
+ const roles: string[] = []
638
+
639
+ for (const [key, value] of Object.entries(labels)) {
640
+ if (key.startsWith('node-role.kubernetes.io/')) {
641
+ let role = key.replace('node-role.kubernetes.io/', '')
642
+ // Normalize master to control-plane
643
+ if (role === 'master') role = 'control-plane'
644
+ if (role && value !== 'false') {
645
+ roles.push(role)
646
+ }
647
+ }
648
+ }
649
+
650
+ if (roles.length === 0) return 'worker'
651
+ return roles.join(', ')
652
+ }
653
+
654
+ export interface NodeCondition {
655
+ type: string
656
+ status: 'True' | 'False' | 'Unknown'
657
+ message?: string
658
+ }
659
+
660
+ // Problem conditions that indicate node issues
661
+ const NODE_PROBLEM_CONDITIONS = ['DiskPressure', 'MemoryPressure', 'PIDPressure', 'NetworkUnavailable']
662
+
663
+ export function getNodeConditions(node: any): { problems: string[]; healthy: boolean } {
664
+ const conditions = node.status?.conditions || []
665
+ const problems: string[] = []
666
+
667
+ for (const cond of conditions) {
668
+ // Ready=False is a problem
669
+ if (cond.type === 'Ready' && cond.status !== 'True') {
670
+ problems.push('NotReady')
671
+ }
672
+ // Other conditions are problems when True
673
+ if (NODE_PROBLEM_CONDITIONS.includes(cond.type) && cond.status === 'True') {
674
+ // Format: "DiskPressure" -> "Disk Pressure"
675
+ const formatted = cond.type.replace(/([A-Z])/g, ' $1').trim()
676
+ problems.push(formatted)
677
+ }
678
+ }
679
+
680
+ return { problems, healthy: problems.length === 0 }
681
+ }
682
+
683
+ export function getNodeTaints(node: any): { count: number; text: string } {
684
+ const taints = node.spec?.taints || []
685
+ const count = taints.length
686
+ if (count === 0) return { count: 0, text: 'None' }
687
+ return { count, text: count === 1 ? '1 taint' : `${count} taints` }
688
+ }
689
+
690
+ export function getNodeVersion(node: any): string {
691
+ return node.status?.nodeInfo?.kubeletVersion || '-'
692
+ }
693
+
694
+ // ============================================================================
695
+ // FORMATTING UTILITIES
696
+ // ============================================================================
697
+
698
+ export function formatAge(timestamp: string): string {
699
+ if (!timestamp) return '-'
700
+ const created = new Date(timestamp)
701
+ const now = new Date()
702
+ const diffMs = now.getTime() - created.getTime()
703
+ return formatDuration(diffMs)
704
+ }
705
+
706
+ export function formatDuration(ms: number, detailed: boolean = false): string {
707
+ const seconds = Math.floor(ms / 1000)
708
+ const minutes = Math.floor(seconds / 60)
709
+ const hours = Math.floor(minutes / 60)
710
+ const days = Math.floor(hours / 24)
711
+
712
+ if (detailed) {
713
+ // Detailed format: "2d 5h", "5h 30m", "30m 15s"
714
+ if (days > 0) return `${days}d ${hours % 24}h`
715
+ if (hours > 0) return `${hours}h ${minutes % 60}m`
716
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`
717
+ return `${seconds}s`
718
+ }
719
+
720
+ // Short format: "2d", "5h", "30m"
721
+ if (days > 0) return `${days}d`
722
+ if (hours > 0) return `${hours}h`
723
+ if (minutes > 0) return `${minutes}m`
724
+ if (seconds > 0) return `${seconds}s`
725
+ return '<1s'
726
+ }
727
+
728
+ // formatBytes is available from @skyhook/k8s-ui utils — no re-export needed
729
+
730
+ export function cronToHuman(cron: string): string {
731
+ if (!cron) return '-'
732
+ const parts = cron.split(' ')
733
+ if (parts.length !== 5) return cron
734
+
735
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = parts
736
+
737
+ // Common patterns
738
+ if (minute === '0' && hour === '0' && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
739
+ return 'Daily at midnight'
740
+ }
741
+ if (minute === '0' && hour !== '*' && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
742
+ return `Daily at ${hour}:00`
743
+ }
744
+ if (minute !== '*' && hour === '*' && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
745
+ return `Every hour at :${minute.padStart(2, '0')}`
746
+ }
747
+ if (minute === '*' && hour === '*' && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
748
+ return 'Every minute'
749
+ }
750
+ if (minute.startsWith('*/')) {
751
+ const interval = minute.slice(2)
752
+ return `Every ${interval} minutes`
753
+ }
754
+ if (dayOfWeek === '1-5' || dayOfWeek === 'MON-FRI') {
755
+ return `Weekdays at ${hour}:${minute.padStart(2, '0')}`
756
+ }
757
+
758
+ return cron
759
+ }
760
+
761
+ // ============================================================================
762
+ // PVC UTILITIES
763
+ // ============================================================================
764
+
765
+ export function getPVCStatus(pvc: any): StatusBadge {
766
+ const phase = pvc.status?.phase || 'Unknown'
767
+ switch (phase) {
768
+ case 'Bound':
769
+ return { text: 'Bound', color: healthColors.healthy, level: 'healthy' }
770
+ case 'Pending':
771
+ return { text: 'Pending', color: healthColors.degraded, level: 'degraded' }
772
+ case 'Lost':
773
+ return { text: 'Lost', color: healthColors.unhealthy, level: 'unhealthy' }
774
+ default:
775
+ return { text: phase, color: healthColors.unknown, level: 'unknown' }
776
+ }
777
+ }
778
+
779
+ export function getPVCCapacity(pvc: any): string {
780
+ return pvc.status?.capacity?.storage || pvc.spec?.resources?.requests?.storage || '-'
781
+ }
782
+
783
+ export function getPVCAccessModes(pvc: any): string {
784
+ const modes = pvc.status?.accessModes || pvc.spec?.accessModes || []
785
+ const shortModes = modes.map((m: string) => {
786
+ switch (m) {
787
+ case 'ReadWriteOnce': return 'RWO'
788
+ case 'ReadOnlyMany': return 'ROX'
789
+ case 'ReadWriteMany': return 'RWX'
790
+ case 'ReadWriteOncePod': return 'RWOP'
791
+ default: return m
792
+ }
793
+ })
794
+ return shortModes.join(', ') || '-'
795
+ }
796
+
797
+ // ============================================================================
798
+ // ROLLOUT UTILITIES (Argo Rollouts CRD)
799
+ // ============================================================================
800
+
801
+ export function getRolloutStatus(rollout: any): StatusBadge {
802
+ const phase = rollout.status?.phase || 'Unknown'
803
+ switch (phase) {
804
+ case 'Healthy':
805
+ return { text: 'Healthy', color: healthColors.healthy, level: 'healthy' }
806
+ case 'Paused':
807
+ return { text: 'Paused', color: healthColors.degraded, level: 'degraded' }
808
+ case 'Progressing':
809
+ return { text: 'Progressing', color: healthColors.degraded, level: 'degraded' }
810
+ case 'Degraded':
811
+ return { text: 'Degraded', color: healthColors.unhealthy, level: 'unhealthy' }
812
+ default:
813
+ return { text: phase, color: healthColors.unknown, level: 'unknown' }
814
+ }
815
+ }
816
+
817
+ export function getRolloutStrategy(rollout: any): string {
818
+ if (rollout.spec?.strategy?.canary) return 'Canary'
819
+ if (rollout.spec?.strategy?.blueGreen) return 'BlueGreen'
820
+ return 'Unknown'
821
+ }
822
+
823
+ export function getRolloutReady(rollout: any): string {
824
+ const ready = rollout.status?.availableReplicas || 0
825
+ const desired = rollout.spec?.replicas || 0
826
+ return `${ready}/${desired}`
827
+ }
828
+
829
+ export function getRolloutStep(rollout: any): string | null {
830
+ const steps = rollout.spec?.strategy?.canary?.steps || []
831
+ const currentIndex = rollout.status?.currentStepIndex
832
+ if (steps.length === 0 || currentIndex === undefined) return null
833
+ return `${currentIndex}/${steps.length}`
834
+ }
835
+
836
+ // ============================================================================
837
+ // WORKFLOW UTILITIES (Argo Workflows CRD)
838
+ // ============================================================================
839
+
840
+ export function getWorkflowStatus(workflow: any): StatusBadge {
841
+ const phase = workflow.status?.phase || 'Unknown'
842
+ switch (phase) {
843
+ case 'Succeeded':
844
+ return { text: 'Succeeded', color: healthColors.healthy, level: 'healthy' }
845
+ case 'Running':
846
+ return { text: 'Running', color: healthColors.degraded, level: 'degraded' }
847
+ case 'Failed':
848
+ return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
849
+ case 'Error':
850
+ return { text: 'Error', color: healthColors.unhealthy, level: 'unhealthy' }
851
+ case 'Pending':
852
+ return { text: 'Pending', color: healthColors.degraded, level: 'degraded' }
853
+ default:
854
+ return { text: phase, color: healthColors.unknown, level: 'unknown' }
855
+ }
856
+ }
857
+
858
+ export function getWorkflowDuration(workflow: any): string | null {
859
+ const startedAt = workflow.status?.startedAt
860
+ const finishedAt = workflow.status?.finishedAt
861
+ if (!startedAt) return null
862
+ const start = new Date(startedAt)
863
+ const end = finishedAt ? new Date(finishedAt) : new Date()
864
+ return formatDuration(end.getTime() - start.getTime())
865
+ }
866
+
867
+ export function getWorkflowProgress(workflow: any): string | null {
868
+ const nodes = workflow.status?.nodes
869
+ if (!nodes) return null
870
+ const nodeList = Object.values(nodes) as any[]
871
+ const podNodes = nodeList.filter((n: any) => n.type === 'Pod')
872
+ if (podNodes.length === 0) return null
873
+ const succeeded = podNodes.filter((n: any) => n.phase === 'Succeeded').length
874
+ return `${succeeded}/${podNodes.length}`
875
+ }
876
+
877
+ export function getWorkflowTemplate(workflow: any): string | null {
878
+ return workflow.spec?.workflowTemplateRef?.name || null
879
+ }
880
+
881
+ // ============================================================================
882
+ // CERT-MANAGER UTILITIES — re-exported from resource-utils-certmanager.ts
883
+ // ============================================================================
884
+ export {
885
+ getCertificateStatus,
886
+ getCertificateDomains,
887
+ getCertificateIssuer,
888
+ getCertificateExpiry,
889
+ getCertificateRequestStatus,
890
+ getCertificateRequestIssuer,
891
+ getCertificateRequestApproved,
892
+ getClusterIssuerStatus,
893
+ getClusterIssuerType,
894
+ getIssuerStatus,
895
+ getIssuerType,
896
+ getOrderState,
897
+ getOrderDomains,
898
+ getOrderIssuer,
899
+ getChallengeState,
900
+ getChallengeType,
901
+ getChallengeDomain,
902
+ getChallengePresented,
903
+ } from './resource-utils-certmanager'
904
+
905
+ // ============================================================================
906
+ // FLUXCD UTILITIES — re-exported from resource-utils-flux.ts
907
+ // ============================================================================
908
+ export {
909
+ getFluxResourceStatus,
910
+ getGitRepositoryStatus,
911
+ getOCIRepositoryStatus,
912
+ getHelmRepositoryStatus,
913
+ getFluxAlertStatus,
914
+ getKustomizationStatus,
915
+ getFluxHelmReleaseStatus,
916
+ getGitRepositoryUrl,
917
+ getGitRepositoryRef,
918
+ getGitRepositoryRevision,
919
+ getOCIRepositoryUrl,
920
+ getOCIRepositoryRef,
921
+ getOCIRepositoryRevision,
922
+ getHelmRepositoryUrl,
923
+ getHelmRepositoryType,
924
+ getKustomizationSource,
925
+ getKustomizationPath,
926
+ getKustomizationInventory,
927
+ getFluxHelmReleaseChart,
928
+ getFluxHelmReleaseVersion,
929
+ getFluxHelmReleaseRevision,
930
+ getFluxAlertProvider,
931
+ getFluxAlertEventCount,
932
+ } from './resource-utils-flux'
933
+
934
+ // ============================================================================
935
+ // ARGOCD UTILITIES — re-exported from resource-utils-argo.ts
936
+ // ============================================================================
937
+ export {
938
+ getArgoApplicationStatus,
939
+ getArgoApplicationProject,
940
+ getArgoApplicationSync,
941
+ getArgoApplicationHealth,
942
+ getArgoApplicationRepo,
943
+ getArgoApplicationSetGenerators,
944
+ getArgoApplicationSetTemplate,
945
+ getArgoApplicationSetAppCount,
946
+ getArgoApplicationSetStatus,
947
+ getArgoAppProjectDescription,
948
+ getArgoAppProjectDestinations,
949
+ getArgoAppProjectSources,
950
+ } from './resource-utils-argo'
951
+
952
+ // ============================================================================
953
+ // PERSISTENT VOLUME UTILITIES
954
+ // ============================================================================
955
+
956
+ export function getPVStatus(pv: any): StatusBadge {
957
+ const phase = pv.status?.phase
958
+ switch (phase) {
959
+ case 'Bound':
960
+ return { text: 'Bound', color: healthColors.healthy, level: 'healthy' }
961
+ case 'Available':
962
+ return { text: 'Available', color: healthColors.healthy, level: 'healthy' }
963
+ case 'Released':
964
+ return { text: 'Released', color: healthColors.degraded, level: 'degraded' }
965
+ case 'Failed':
966
+ return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
967
+ default:
968
+ return { text: phase || 'Unknown', color: healthColors.unknown, level: 'unknown' }
969
+ }
970
+ }
971
+
972
+ export function getPVAccessModes(pv: any): string {
973
+ const shorthand: Record<string, string> = {
974
+ ReadWriteOnce: 'RWO', ReadOnlyMany: 'ROX', ReadWriteMany: 'RWX', ReadWriteOncePod: 'RWOP',
975
+ }
976
+ const modes = pv.spec?.accessModes || []
977
+ return modes.map((m: string) => shorthand[m] || m).join(', ') || '-'
978
+ }
979
+
980
+ export function getPVClaim(pv: any): string {
981
+ const ref = pv.spec?.claimRef
982
+ if (!ref) return '-'
983
+ return ref.namespace ? `${ref.namespace}/${ref.name}` : ref.name || '-'
984
+ }
985
+
986
+ // ============================================================================
987
+ // STORAGE CLASS UTILITIES
988
+ // ============================================================================
989
+
990
+ export function getStorageClassProvisioner(sc: any): string {
991
+ return sc.provisioner || '-'
992
+ }
993
+
994
+ export function getStorageClassReclaimPolicy(sc: any): string {
995
+ return sc.reclaimPolicy || '-'
996
+ }
997
+
998
+ export function getStorageClassBindingMode(sc: any): string {
999
+ const mode = sc.volumeBindingMode
1000
+ if (mode === 'WaitForFirstConsumer') return 'WaitForConsumer'
1001
+ return mode || '-'
1002
+ }
1003
+
1004
+ export function getStorageClassExpansion(sc: any): string {
1005
+ return sc.allowVolumeExpansion ? 'Yes' : 'No'
1006
+ }
1007
+
1008
+
1009
+
1010
+ // ============================================================================
1011
+ // GATEWAY UTILITIES (Gateway API)
1012
+ // ============================================================================
1013
+
1014
+ export function getGatewayStatus(gw: any): StatusBadge {
1015
+ const conditions = gw.status?.conditions || []
1016
+ const programmed = conditions.find((c: any) => c.type === 'Programmed')
1017
+ const accepted = conditions.find((c: any) => c.type === 'Accepted')
1018
+ if (programmed?.status === 'True') return { text: 'Programmed', color: healthColors.healthy, level: 'healthy' }
1019
+ if (accepted?.status === 'True') return { text: 'Accepted', color: healthColors.degraded, level: 'degraded' }
1020
+ if (accepted?.status === 'False') return { text: 'Not Accepted', color: healthColors.unhealthy, level: 'unhealthy' }
1021
+ return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
1022
+ }
1023
+
1024
+ export function getGatewayClass(gw: any): string {
1025
+ return gw.spec?.gatewayClassName || '-'
1026
+ }
1027
+
1028
+ export function getGatewayListeners(gw: any): string {
1029
+ const listeners = gw.spec?.listeners || []
1030
+ if (listeners.length === 0) return '-'
1031
+ return listeners.map((l: any) => `${l.protocol}:${l.port}`).join(', ')
1032
+ }
1033
+
1034
+ export function getGatewayAttachedRoutes(gw: any): number {
1035
+ const statusListeners = gw.status?.listeners || []
1036
+ return statusListeners.reduce((sum: number, l: any) => sum + (l.attachedRoutes ?? 0), 0)
1037
+ }
1038
+
1039
+ export function getGatewayAddresses(gw: any): string {
1040
+ const addrs = gw.status?.addresses || gw.spec?.addresses || []
1041
+ return addrs.map((a: any) => a.value).join(', ') || '-'
1042
+ }
1043
+
1044
+ // ============================================================================
1045
+ // GATEWAYCLASS UTILITIES (Gateway API)
1046
+ // ============================================================================
1047
+
1048
+ export function getGatewayClassStatus(gc: any): StatusBadge {
1049
+ const conditions = gc.status?.conditions || []
1050
+ const accepted = conditions.find((c: any) => c.type === 'Accepted')
1051
+ if (accepted?.status === 'True') return { text: 'Accepted', color: healthColors.healthy, level: 'healthy' }
1052
+ if (accepted?.status === 'False') return { text: accepted.reason || 'Not Accepted', color: healthColors.unhealthy, level: 'unhealthy' }
1053
+ return { text: 'Pending', color: healthColors.degraded, level: 'degraded' }
1054
+ }
1055
+
1056
+ export function getGatewayClassController(gc: any): string {
1057
+ return gc.spec?.controllerName || '-'
1058
+ }
1059
+
1060
+ export function getGatewayClassDescription(gc: any): string {
1061
+ return gc.spec?.description || '-'
1062
+ }
1063
+
1064
+ // ============================================================================
1065
+ // GATEWAY API ROUTE UTILITIES (shared by HTTPRoute, GRPCRoute, TCPRoute, TLSRoute)
1066
+ // ============================================================================
1067
+
1068
+ // All Gateway API route types share the same status/parents/rules/hostnames structure
1069
+ export function getRouteStatus(route: any): StatusBadge {
1070
+ const parents = route.status?.parents || []
1071
+ if (parents.length === 0) return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
1072
+ const allAccepted = parents.every((p: any) =>
1073
+ (p.conditions || []).some((c: any) => c.type === 'Accepted' && c.status === 'True')
1074
+ )
1075
+ const anyRejected = parents.some((p: any) =>
1076
+ (p.conditions || []).some((c: any) => c.type === 'Accepted' && c.status === 'False')
1077
+ )
1078
+ if (allAccepted) return { text: 'Accepted', color: healthColors.healthy, level: 'healthy' }
1079
+ if (anyRejected) return { text: 'Not Accepted', color: healthColors.unhealthy, level: 'unhealthy' }
1080
+ return { text: 'Pending', color: healthColors.degraded, level: 'degraded' }
1081
+ }
1082
+
1083
+ export function getRouteParents(route: any): string {
1084
+ const refs = route.spec?.parentRefs || []
1085
+ return refs.map((r: any) => r.name).join(', ') || '-'
1086
+ }
1087
+
1088
+ export function getRouteHostnames(route: any): string {
1089
+ const hostnames = route.spec?.hostnames || []
1090
+ return hostnames.join(', ') || 'Any'
1091
+ }
1092
+
1093
+ export function getRouteRulesCount(route: any): number {
1094
+ return (route.spec?.rules || []).length
1095
+ }
1096
+
1097
+ export function getRouteBackends(route: any): string {
1098
+ const rules = route.spec?.rules || []
1099
+ // Collect unique backend name:port pairs across all rules
1100
+ const seen = new Set<string>()
1101
+ for (const rule of rules) {
1102
+ for (const ref of rule.backendRefs || []) {
1103
+ const key = ref.port ? `${ref.name}:${ref.port}` : ref.name
1104
+ seen.add(key)
1105
+ }
1106
+ }
1107
+ if (seen.size === 0) return '-'
1108
+ return Array.from(seen).join(', ')
1109
+ }
1110
+
1111
+ // Legacy aliases for HTTPRoute (used by HTTPRouteRenderer)
1112
+ export const getHTTPRouteStatus = getRouteStatus
1113
+ export const getHTTPRouteParents = getRouteParents
1114
+ export const getHTTPRouteHostnames = getRouteHostnames
1115
+ export const getHTTPRouteRulesCount = getRouteRulesCount
1116
+
1117
+ // ============================================================================
1118
+ // SEALED SECRET UTILITIES (Bitnami)
1119
+ // ============================================================================
1120
+
1121
+ export function getSealedSecretStatus(ss: any): StatusBadge {
1122
+ const conditions = ss.status?.conditions || []
1123
+ const synced = conditions.find((c: any) => c.type === 'Synced')
1124
+ if (synced?.status === 'True') return { text: 'Synced', color: healthColors.healthy, level: 'healthy' }
1125
+ if (synced?.status === 'False') return { text: 'Not Synced', color: healthColors.unhealthy, level: 'unhealthy' }
1126
+ return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
1127
+ }
1128
+
1129
+ export function getSealedSecretKeyCount(ss: any): number {
1130
+ return Object.keys(ss.spec?.encryptedData || {}).length
1131
+ }
1132
+
1133
+ // ============================================================================
1134
+ // WORKFLOW TEMPLATE UTILITIES (Argo)
1135
+ // ============================================================================
1136
+
1137
+ export function getWorkflowTemplateCount(wt: any): number {
1138
+ return (wt.spec?.templates || []).length
1139
+ }
1140
+
1141
+ export function getWorkflowTemplateEntrypoint(wt: any): string {
1142
+ return wt.spec?.entrypoint || '-'
1143
+ }
1144
+
1145
+ // ============================================================================
1146
+ // NETWORK POLICY UTILITIES
1147
+ // ============================================================================
1148
+
1149
+ export function getNetworkPolicyTypes(np: any): string {
1150
+ const types = np.spec?.policyTypes || []
1151
+ return types.join(', ') || '-'
1152
+ }
1153
+
1154
+ export function getNetworkPolicyRuleCount(np: any): { ingress: number; egress: number } {
1155
+ return {
1156
+ ingress: (np.spec?.ingress || []).length,
1157
+ egress: (np.spec?.egress || []).length,
1158
+ }
1159
+ }
1160
+
1161
+ export function getNetworkPolicySelector(np: any): string {
1162
+ const labels = np.spec?.podSelector?.matchLabels
1163
+ if (!labels || Object.keys(labels).length === 0) return 'All pods'
1164
+ return Object.entries(labels).map(([k, v]) => `${k}=${v}`).join(', ')
1165
+ }
1166
+
1167
+ // ============================================================================
1168
+ // POD DISRUPTION BUDGET UTILITIES
1169
+ // ============================================================================
1170
+
1171
+ export function getPDBStatus(pdb: any): StatusBadge {
1172
+ const status = pdb.status || {}
1173
+ const allowed = status.disruptionsAllowed
1174
+ const healthy = status.currentHealthy || 0
1175
+ const desired = status.desiredHealthy || 0
1176
+
1177
+ if (healthy < desired) return { text: 'Unhealthy', color: healthColors.unhealthy, level: 'unhealthy' }
1178
+ if (allowed === 0 && (status.expectedPods || 0) > 0) return { text: 'Blocked', color: healthColors.degraded, level: 'degraded' }
1179
+ if (allowed > 0) return { text: 'OK', color: healthColors.healthy, level: 'healthy' }
1180
+ return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
1181
+ }
1182
+
1183
+ export function getPDBBudget(pdb: any): string {
1184
+ const spec = pdb.spec || {}
1185
+ if (spec.minAvailable !== undefined) return `min: ${spec.minAvailable}`
1186
+ if (spec.maxUnavailable !== undefined) return `max unavail: ${spec.maxUnavailable}`
1187
+ return '-'
1188
+ }
1189
+
1190
+ export function getPDBHealthy(pdb: any): string {
1191
+ const status = pdb.status || {}
1192
+ return `${status.currentHealthy || 0}/${status.expectedPods || 0}`
1193
+ }
1194
+
1195
+ export function getPDBAllowed(pdb: any): number {
1196
+ return pdb.status?.disruptionsAllowed ?? 0
1197
+ }
1198
+
1199
+ // ============================================================================
1200
+ // SERVICE ACCOUNT UTILITIES
1201
+ // ============================================================================
1202
+
1203
+ export function getServiceAccountSecretCount(sa: any): number {
1204
+ return (sa.secrets || []).length
1205
+ }
1206
+
1207
+ export function getServiceAccountAutomount(sa: any): string {
1208
+ return sa.automountServiceAccountToken === false ? 'No' : 'Yes'
1209
+ }
1210
+
1211
+ // ============================================================================
1212
+ // ROLE / CLUSTER ROLE UTILITIES
1213
+ // ============================================================================
1214
+
1215
+ export function getRoleRuleCount(role: any): number {
1216
+ return (role.rules || []).length
1217
+ }
1218
+
1219
+ // ============================================================================
1220
+ // ROLE BINDING / CLUSTER ROLE BINDING UTILITIES
1221
+ // ============================================================================
1222
+
1223
+ export function getRoleBindingRole(rb: any): string {
1224
+ const ref = rb.roleRef
1225
+ if (!ref) return '-'
1226
+ return ref.name || '-'
1227
+ }
1228
+
1229
+ export function getRoleBindingSubjectCount(rb: any): number {
1230
+ return (rb.subjects || []).length
1231
+ }
1232
+
1233
+ // ============================================================================
1234
+ // WORKLOAD PROBLEM DETECTION (for table row indicators)
1235
+ // ============================================================================
1236
+
1237
+ export function getWorkloadProblems(resource: any, kind: string): string[] {
1238
+ const problems: string[] = []
1239
+ const status = resource.status || {}
1240
+ const spec = resource.spec || {}
1241
+
1242
+ if (kind === 'daemonsets') {
1243
+ const ready = status.numberReady || 0
1244
+ const desired = status.desiredNumberScheduled || 0
1245
+ if (desired > 0 && ready < desired) {
1246
+ problems.push(`${desired - ready} pods not ready`)
1247
+ }
1248
+ } else {
1249
+ const ready = status.readyReplicas || 0
1250
+ const desired = spec.replicas ?? 0
1251
+ if (desired > 0 && ready < desired) {
1252
+ problems.push(`${desired - ready} replicas not ready`)
1253
+ }
1254
+ }
1255
+
1256
+ const conditions = status.conditions || []
1257
+ for (const cond of conditions) {
1258
+ if (cond.status === 'True' && cond.type === 'ReplicaFailure') {
1259
+ problems.push('ReplicaFailure')
1260
+ }
1261
+ if (cond.status === 'False' && cond.type === 'Available') {
1262
+ problems.push('Unavailable')
1263
+ }
1264
+ }
1265
+
1266
+ return problems
1267
+ }
1268
+
1269
+ // ============================================================================
1270
+ // FORMATTING UTILITIES
1271
+ // ============================================================================
1272
+
1273
+ export function truncate(str: string, length: number): string {
1274
+ if (!str || str.length <= length) return str
1275
+ return str.slice(0, length - 1) + '…'
1276
+ }
1277
+
1278
+ export function formatResources(resources: any): string {
1279
+ const parts: string[] = []
1280
+ if (resources.cpu) {
1281
+ parts.push(`CPU: ${formatCPUString(resources.cpu)}`)
1282
+ }
1283
+ if (resources.memory) {
1284
+ parts.push(`Mem: ${formatMemoryString(resources.memory)}`)
1285
+ }
1286
+ return parts.join(', ') || '-'
1287
+ }
1288
+
1289
+ // ============================================================================
1290
+ // GENERIC COLUMN FILTER VALUE EXTRACTION
1291
+ // ============================================================================
1292
+
1293
+ /**
1294
+ * Extracts a string filter value for a given column key and resource kind.
1295
+ * Used by the generic column filter system to match resources against filter values.
1296
+ * Reuses existing utility functions for kind-specific columns.
1297
+ */
1298
+ // Parse column filters from URL `filters` param (format: "col:val1,val2|col2:val3")
1299
+ // Uses `|` as pair separator between columns, `,` between values within a column.
1300
+ // Multi-select: each column key maps to an array of selected values.
1301
+ export function parseColumnFilters(filtersParam: string | null): Record<string, string[]> {
1302
+ if (!filtersParam) return {}
1303
+ const filters: Record<string, string[]> = {}
1304
+ for (const pair of filtersParam.split('|')) {
1305
+ const colonIdx = pair.indexOf(':')
1306
+ if (colonIdx > 0) {
1307
+ const key = pair.slice(0, colonIdx).trim()
1308
+ const valStr = pair.slice(colonIdx + 1).trim()
1309
+ if (key && valStr) {
1310
+ filters[key] = valStr.split(',').map(v => {
1311
+ try { return decodeURIComponent(v.trim()) } catch { return v.trim() }
1312
+ }).filter(Boolean)
1313
+ }
1314
+ }
1315
+ }
1316
+ return filters
1317
+ }
1318
+
1319
+ // Serialize column filters to URL param format
1320
+ // Values are URI-encoded so commas inside values (e.g. "Ready,SchedulingDisabled") survive the round-trip.
1321
+ export function serializeColumnFilters(filters: Record<string, string[]>): string {
1322
+ const result = Object.entries(filters)
1323
+ .filter(([, v]) => v.length > 0)
1324
+ .map(([k, vals]) => `${k}:${vals.map(v => encodeURIComponent(v)).join(',')}`)
1325
+ .join('|')
1326
+ return result
1327
+ }
1328
+
1329
+ export function getCellFilterValue(resource: any, column: string, kind: string): string {
1330
+ try {
1331
+ const kindLower = kind.toLowerCase()
1332
+
1333
+ switch (column) {
1334
+ case 'type':
1335
+ if (kindLower === 'secrets' || kindLower === 'sealedsecrets') return getSecretType(resource).type
1336
+ if (kindLower === 'services') return resource.spec?.type || ''
1337
+ if (kindLower === 'events') return resource.type || ''
1338
+ if (kindLower === 'helmrepositories') return getHelmRepositoryType(resource)
1339
+ return resource.spec?.type || resource.type || ''
1340
+ case 'status':
1341
+ if (kindLower === 'pods') return getPodStatus(resource).text
1342
+ if (['deployments', 'statefulsets', 'daemonsets', 'replicasets'].includes(kindLower)) {
1343
+ const status = getWorkloadStatus(resource, kindLower)
1344
+ if (status.text === 'Scaled to 0') return 'Scaled to 0'
1345
+ if (status.level === 'healthy') return 'Healthy'
1346
+ if (status.level === 'degraded') return 'Degraded'
1347
+ if (status.level === 'unhealthy') return 'Unhealthy'
1348
+ return 'Unknown'
1349
+ }
1350
+ if (kindLower === 'nodes') return getNodeStatus(resource).text
1351
+ if (kindLower === 'jobs') return getJobStatus(resource).text
1352
+ if (kindLower === 'cronjobs') return getCronJobStatus(resource).text
1353
+ if (kindLower === 'certificates') return getCertificateStatus(resource).text
1354
+ if (kindLower === 'certificaterequests') return getCertificateRequestStatus(resource).text
1355
+ if (kindLower === 'clusterissuers' || kindLower === 'issuers') return getClusterIssuerStatus(resource).text
1356
+ if (kindLower === 'persistentvolumeclaims') return getPVCStatus(resource).text
1357
+ if (kindLower === 'persistentvolumes') return getPVStatus(resource).text
1358
+ if (kindLower === 'rollouts') return getRolloutStatus(resource).text
1359
+ if (kindLower === 'workflows') return getWorkflowStatus(resource).text
1360
+ if (kindLower === 'hpas' || kindLower === 'horizontalpodautoscalers') return getHPAStatus(resource).text
1361
+ if (kindLower === 'gateways') return getGatewayStatus(resource).text
1362
+ if (kindLower === 'gatewayclasses') return getGatewayClassStatus(resource).text
1363
+ if (['httproutes', 'grpcroutes', 'tcproutes', 'tlsroutes'].includes(kindLower)) return getRouteStatus(resource).text
1364
+ if (kindLower === 'sealedsecrets') return getSealedSecretStatus(resource).text
1365
+ if (kindLower === 'poddisruptionbudgets') return getPDBStatus(resource).text
1366
+ if (kindLower === 'applications') return getArgoApplicationStatus(resource).text
1367
+ if (kindLower === 'applicationsets') return getArgoApplicationSetStatus(resource).text
1368
+ if (kindLower === 'gitrepositories') return getGitRepositoryStatus(resource).text
1369
+ if (kindLower === 'ocirepositories') return getOCIRepositoryStatus(resource).text
1370
+ if (kindLower === 'helmrepositories') return getHelmRepositoryStatus(resource).text
1371
+ if (kindLower === 'kustomizations') return getKustomizationStatus(resource).text
1372
+ if (kindLower === 'helmreleases') return getFluxHelmReleaseStatus(resource).text
1373
+ if (kindLower === 'alerts') return getFluxAlertStatus(resource).text
1374
+ if (kindLower === 'nodepools') return getNodePoolStatus(resource).text
1375
+ if (kindLower === 'nodeclaims') return getNodeClaimStatus(resource).text
1376
+ if (kindLower === 'scaledobjects') return getScaledObjectStatus(resource).text
1377
+ if (kindLower === 'scaledjobs') return getScaledJobStatus(resource).text
1378
+ if (kindLower === 'policyreports' || kindLower === 'clusterpolicyreports') return _getPolicyReportStatus(resource).text
1379
+ if (kindLower === 'kyvernopolicies' || kindLower === 'clusterpolicies') return _getKyvernoPolicyStatus(resource).text
1380
+ if (kindLower === 'backups') return _getBackupStatus(resource).text
1381
+ if (kindLower === 'restores') return _getRestoreStatus(resource).text
1382
+ if (kindLower === 'schedules') return _getScheduleStatus(resource).text
1383
+ if (kindLower === 'backupstoragelocations') return _getBSLStatus(resource).text
1384
+ if (kindLower === 'externalsecrets') return _getExternalSecretStatus(resource).text
1385
+ if (kindLower === 'clusterexternalsecrets') return _getClusterExternalSecretStatus(resource).text
1386
+ if (kindLower === 'secretstores') return _getSecretStoreStatus(resource).text
1387
+ if (kindLower === 'clustersecretstores') return _getClusterSecretStoreStatus(resource).text
1388
+ // Generic CRDs: try status.phase, then Ready condition
1389
+ if (resource.status?.phase) return resource.status.phase
1390
+ {
1391
+ const conditions = resource.status?.conditions || []
1392
+ const ready = conditions.find((c: any) => c.type === 'Ready')
1393
+ if (ready?.status === 'True') return 'Ready'
1394
+ if (ready?.status === 'False') return 'Not Ready'
1395
+ }
1396
+ return ''
1397
+ case 'state':
1398
+ if (kindLower === 'orders') return getOrderState(resource).text
1399
+ if (kindLower === 'challenges') return getChallengeState(resource).text
1400
+ return resource.status?.state || ''
1401
+ case 'challengeType':
1402
+ return getChallengeType(resource)
1403
+ case 'issuerType':
1404
+ return getClusterIssuerType(resource)
1405
+ case 'class':
1406
+ return resource.spec?.ingressClassName || resource.spec?.gatewayClassName || ''
1407
+ case 'roles':
1408
+ return getNodeRoles(resource)
1409
+ case 'strategy':
1410
+ return getRolloutStrategy(resource)
1411
+ case 'storageClass':
1412
+ return resource.spec?.storageClassName || ''
1413
+ case 'reclaimPolicy':
1414
+ return resource.reclaimPolicy || resource.spec?.persistentVolumeReclaimPolicy || ''
1415
+ case 'bindingMode':
1416
+ return getStorageClassBindingMode(resource)
1417
+ case 'expansion':
1418
+ return getStorageClassExpansion(resource)
1419
+ case 'automount':
1420
+ return getServiceAccountAutomount(resource)
1421
+ case 'policyTypes':
1422
+ return getNetworkPolicyTypes(resource)
1423
+ case 'node':
1424
+ return resource.spec?.nodeName || ''
1425
+ case 'version':
1426
+ if (kindLower === 'nodes') return getNodeVersion(resource)
1427
+ return resource.spec?.version || ''
1428
+ case 'health':
1429
+ if (kindLower === 'applications') return getArgoApplicationHealth(resource).status
1430
+ return resource.status?.health?.status || ''
1431
+ case 'sync':
1432
+ if (kindLower === 'applications') return getArgoApplicationSync(resource).status
1433
+ return resource.status?.sync?.status || ''
1434
+ case 'project':
1435
+ if (kindLower === 'applications') return getArgoApplicationProject(resource)
1436
+ return resource.spec?.project || ''
1437
+ case 'provider':
1438
+ if (kindLower === 'secretstores' || kindLower === 'clustersecretstores') return _getSecretStoreProviderType(resource)
1439
+ return resource.spec?.provider || ''
1440
+ }
1441
+
1442
+ // Fallback: try common paths
1443
+ const val = resource.status?.[column] ?? resource.spec?.[column] ?? resource[column]
1444
+ if (val === undefined || val === null) return ''
1445
+ if (typeof val === 'boolean') return val ? 'Yes' : 'No'
1446
+ if (typeof val === 'string') return val
1447
+ if (typeof val === 'number') return String(val)
1448
+ if (typeof val === 'object') return '' // Skip objects/arrays — not filterable
1449
+ return String(val)
1450
+ } catch (err) {
1451
+ console.warn(`[getCellFilterValue] Failed for kind=${kind} column=${column}:`, err)
1452
+ return ''
1453
+ }
1454
+ }
1455
+
1456
+ // ============================================================================
1457
+ // TRIVY OPERATOR UTILITIES — re-exported from resource-utils-trivy.ts
1458
+ // ============================================================================
1459
+ export {
1460
+ getVulnerabilityReportSummary,
1461
+ getVulnerabilityReportStatus,
1462
+ getVulnerabilityReportImage,
1463
+ getVulnerabilityReportContainer,
1464
+ getConfigAuditReportSummary,
1465
+ getConfigAuditReportStatus,
1466
+ getExposedSecretReportSummary,
1467
+ getExposedSecretReportContainer,
1468
+ getExposedSecretReportImage,
1469
+ getExposedSecretReportStatus,
1470
+ getRbacAssessmentReportSummary,
1471
+ getRbacAssessmentReportStatus,
1472
+ getClusterComplianceReportStatus,
1473
+ getSbomReportStatus,
1474
+ getSbomReportContainer,
1475
+ } from './resource-utils-trivy'
1476
+ export type { VulnerabilitySummary, ConfigAuditSummary } from './resource-utils-trivy'
1477
+
1478
+ // ============================================================================
1479
+ // KARPENTER UTILITIES — re-exported from resource-utils-karpenter.ts
1480
+ // ============================================================================
1481
+ export {
1482
+ getNodePoolStatus,
1483
+ getNodePoolNodeClassRef,
1484
+ getNodePoolLimits,
1485
+ getNodePoolDisruptionPolicy,
1486
+ getNodePoolRequirements,
1487
+ getNodePoolWeight,
1488
+ getNodeClaimStatus,
1489
+ getNodeClaimInstanceType,
1490
+ getNodeClaimNodeName,
1491
+ getNodeClaimCapacity,
1492
+ getNodeClaimNodePoolRef,
1493
+ } from './resource-utils-karpenter'
1494
+
1495
+ // ============================================================================
1496
+ // KEDA UTILITIES — re-exported from resource-utils-keda.ts
1497
+ // ============================================================================
1498
+ export {
1499
+ getScaledObjectStatus,
1500
+ getScaledObjectTarget,
1501
+ getScaledObjectTargetKind,
1502
+ getScaledObjectTargetName,
1503
+ getScaledObjectReplicas,
1504
+ getScaledObjectTriggers,
1505
+ getScaledObjectTriggerCount,
1506
+ getScaledObjectHpaName,
1507
+ getScaledObjectLastActiveTime,
1508
+ getScaledObjectPollingInterval,
1509
+ getScaledObjectCooldownPeriod,
1510
+ getScaledJobStatus,
1511
+ getScaledJobTarget,
1512
+ getScaledJobStrategy,
1513
+ getScaledJobTriggerCount,
1514
+ getScaledJobTriggers,
1515
+ } from './resource-utils-keda'
1516
+
1517
+ // ============================================================================
1518
+ // KYVERNO / POLICY REPORT UTILITIES — re-exported from resource-utils-kyverno.ts
1519
+ // ============================================================================
1520
+ export {
1521
+ getPolicyReportStatus,
1522
+ getPolicyReportSummary,
1523
+ getPolicyReportResults,
1524
+ getPolicyReportResultCount,
1525
+ getPolicyReportScope,
1526
+ getPolicyReportSource,
1527
+ getClusterPolicyReportStatus,
1528
+ getKyvernoPolicyStatus,
1529
+ getKyvernoPolicyAction,
1530
+ getKyvernoPolicyRuleCount,
1531
+ getKyvernoPolicyRuleTypes,
1532
+ getKyvernoPolicyRules,
1533
+ getKyvernoPolicyBackground,
1534
+ getKyvernoPolicyRuleCountByType,
1535
+ getClusterPolicyStatus,
1536
+ getClusterPolicyAction,
1537
+ getClusterPolicyRuleCount,
1538
+ } from './resource-utils-kyverno'
1539
+
1540
+ // ============================================================================
1541
+ // EXTERNAL SECRETS OPERATOR UTILITIES — re-exported from resource-utils-eso.ts
1542
+ // ============================================================================
1543
+ export {
1544
+ getExternalSecretStatus,
1545
+ getExternalSecretStore,
1546
+ getExternalSecretRefreshInterval,
1547
+ getExternalSecretSecretCount,
1548
+ getExternalSecretLastSync,
1549
+ getExternalSecretTargetName,
1550
+ getExternalSecretProvider,
1551
+ getClusterExternalSecretStatus,
1552
+ getClusterExternalSecretNamespaceCount,
1553
+ getClusterExternalSecretFailedCount,
1554
+ getSecretStoreStatus,
1555
+ getSecretStoreProviderType,
1556
+ getSecretStoreProviderKey,
1557
+ getClusterSecretStoreStatus,
1558
+ } from './resource-utils-eso'