@skyhook-io/k8s-ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +63 -0
- package/src/components/dock/BottomDock.tsx +204 -0
- package/src/components/dock/DockContext.tsx +212 -0
- package/src/components/dock/TerminalTab.tsx +333 -0
- package/src/components/dock/index.ts +3 -0
- package/src/components/gitops/GitOpsActions.tsx +227 -0
- package/src/components/gitops/GitOpsStatusBadge.tsx +221 -0
- package/src/components/gitops/ManagedResourcesList.tsx +210 -0
- package/src/components/gitops/SyncCountdown.tsx +146 -0
- package/src/components/gitops/index.ts +4 -0
- package/src/components/logs/JsonLogLine.tsx +90 -0
- package/src/components/logs/LogCore.tsx +553 -0
- package/src/components/logs/LogToolbarSelects.tsx +74 -0
- package/src/components/logs/LogsViewer.tsx +146 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +263 -0
- package/src/components/logs/index.ts +13 -0
- package/src/components/logs/useLogBuffer.ts +138 -0
- package/src/components/logs/useLogSearch.ts +151 -0
- package/src/components/logs/useLogStream.ts +61 -0
- package/src/components/resources/ResourcesView.tsx +5494 -0
- package/src/components/resources/index.ts +17 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +166 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +233 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +89 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +194 -0
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +79 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +68 -0
- package/src/components/resources/renderers/CertificateRenderer.tsx +209 -0
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +123 -0
- package/src/components/resources/renderers/ChallengeRenderer.tsx +111 -0
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +164 -0
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +140 -0
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +153 -0
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +221 -0
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +124 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +86 -0
- package/src/components/resources/renderers/EventRenderer.tsx +175 -0
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +119 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +172 -0
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +242 -0
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +263 -0
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +68 -0
- package/src/components/resources/renderers/GatewayRenderer.tsx +218 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +476 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +154 -0
- package/src/components/resources/renderers/HPARenderer.tsx +164 -0
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +303 -0
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +167 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +49 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +104 -0
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +175 -0
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +176 -0
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +125 -0
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +84 -0
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +112 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +288 -0
- package/src/components/resources/renderers/JobRenderer.tsx +103 -0
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +157 -0
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +188 -0
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +148 -0
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +90 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +136 -0
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +151 -0
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +70 -0
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +309 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +142 -0
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +232 -0
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +97 -0
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +74 -0
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +138 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +220 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +151 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +352 -0
- package/src/components/resources/renderers/LeaseRenderer.tsx +93 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +208 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +282 -0
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +153 -0
- package/src/components/resources/renderers/OrderRenderer.tsx +108 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +98 -0
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +138 -0
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +126 -0
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +63 -0
- package/src/components/resources/renderers/PodRenderer.tsx +677 -0
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +50 -0
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +62 -0
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +71 -0
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +97 -0
- package/src/components/resources/renderers/RoleRenderer.tsx +192 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +227 -0
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +75 -0
- package/src/components/resources/renderers/SbomReportRenderer.tsx +98 -0
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +121 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +321 -0
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +111 -0
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +81 -0
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +65 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +98 -0
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +195 -0
- package/src/components/resources/renderers/StorageClassRenderer.tsx +82 -0
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +257 -0
- package/src/components/resources/renderers/VPARenderer.tsx +193 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +73 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +193 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +161 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +134 -0
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +36 -0
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +347 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +156 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +202 -0
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +91 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +311 -0
- package/src/components/resources/renderers/argo-cells.tsx +118 -0
- package/src/components/resources/renderers/certmanager-cells.tsx +164 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +161 -0
- package/src/components/resources/renderers/eso-cells.tsx +115 -0
- package/src/components/resources/renderers/flux-cells.tsx +253 -0
- package/src/components/resources/renderers/index.ts +105 -0
- package/src/components/resources/renderers/istio-cells.tsx +210 -0
- package/src/components/resources/renderers/karpenter-cells.tsx +107 -0
- package/src/components/resources/renderers/keda-cells.tsx +94 -0
- package/src/components/resources/renderers/knative-cells.tsx +396 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +85 -0
- package/src/components/resources/renderers/prometheus-cells.tsx +74 -0
- package/src/components/resources/renderers/traefik-cells.tsx +120 -0
- package/src/components/resources/renderers/trivy-cells.tsx +153 -0
- package/src/components/resources/renderers/trivy-shared.tsx +67 -0
- package/src/components/resources/renderers/velero-cells.tsx +164 -0
- package/src/components/resources/resource-utils-argo.ts +155 -0
- package/src/components/resources/resource-utils-certmanager.ts +198 -0
- package/src/components/resources/resource-utils-cnpg.ts +344 -0
- package/src/components/resources/resource-utils-eso.ts +385 -0
- package/src/components/resources/resource-utils-flux.ts +186 -0
- package/src/components/resources/resource-utils-istio.ts +372 -0
- package/src/components/resources/resource-utils-karpenter.ts +150 -0
- package/src/components/resources/resource-utils-keda.ts +203 -0
- package/src/components/resources/resource-utils-knative.ts +218 -0
- package/src/components/resources/resource-utils-kyverno.ts +224 -0
- package/src/components/resources/resource-utils-prometheus.ts +159 -0
- package/src/components/resources/resource-utils-traefik.ts +180 -0
- package/src/components/resources/resource-utils-trivy.ts +146 -0
- package/src/components/resources/resource-utils-velero.ts +293 -0
- package/src/components/resources/resource-utils.ts +1558 -0
- package/src/components/shared/EditableYamlView.tsx +302 -0
- package/src/components/shared/ResourceActionsBar.tsx +924 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +557 -0
- package/src/components/shared/index.ts +3 -0
- package/src/components/timeline/index.ts +1 -0
- package/src/components/timeline/shared.tsx +614 -0
- package/src/components/topology/GroupNode.tsx +198 -0
- package/src/components/topology/K8sResourceNode.tsx +440 -0
- package/src/components/topology/TopologyControls.tsx +62 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +377 -0
- package/src/components/topology/TopologyGraph.tsx +748 -0
- package/src/components/topology/TopologySearch.tsx +304 -0
- package/src/components/topology/index.ts +7 -0
- package/src/components/topology/layout.ts +734 -0
- package/src/components/topology/layout.worker.ts +272 -0
- package/src/components/topology/topology.css +129 -0
- package/src/components/ui/CodeViewer.tsx +451 -0
- package/src/components/ui/ConfirmDialog.tsx +188 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +57 -0
- package/src/components/ui/HealthRing.tsx +72 -0
- package/src/components/ui/MetricsChart.tsx +259 -0
- package/src/components/ui/ResourceBar.tsx +68 -0
- package/src/components/ui/Toast.tsx +225 -0
- package/src/components/ui/Tooltip.tsx +165 -0
- package/src/components/ui/YamlEditor.tsx +280 -0
- package/src/components/ui/drawer-components.tsx +792 -0
- package/src/components/ui/index.ts +10 -0
- package/src/components/workload/ResourceDetailDrawer.tsx +158 -0
- package/src/components/workload/WorkloadView.tsx +1109 -0
- package/src/components/workload/index.ts +2 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useAnimatedUnmount.ts +30 -0
- package/src/hooks/useKeyboardShortcuts.tsx +336 -0
- package/src/hooks/useRefreshAnimation.ts +58 -0
- package/src/index.ts +35 -0
- package/src/types/core.ts +894 -0
- package/src/types/gitops.ts +458 -0
- package/src/types/index.ts +2 -0
- package/src/utils/animation.ts +40 -0
- package/src/utils/api-resources.ts +185 -0
- package/src/utils/badge-colors.ts +399 -0
- package/src/utils/context-name.ts +72 -0
- package/src/utils/download.ts +19 -0
- package/src/utils/format.ts +233 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/log-format.ts +300 -0
- package/src/utils/navigation.ts +119 -0
- package/src/utils/resource-hierarchy.ts +630 -0
- package/src/utils/resource-icons.ts +206 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,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'
|