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