@skyhook-io/k8s-ui 1.5.0 → 1.5.1

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.
@@ -106,37 +106,51 @@ export function NodeRenderer({ data, relationships, onViewPods, metrics, metrics
106
106
 
107
107
  {/* Capacity */}
108
108
  <Section title="Capacity" icon={HardDrive}>
109
- <div className="space-y-1">
110
- <div className="grid grid-cols-4 gap-2 text-xs text-theme-text-tertiary font-medium mb-2">
111
- <span>Resource</span>
112
- <span>Capacity</span>
113
- <span>Allocatable</span>
114
- <span>In Use</span>
115
- </div>
116
- <div className="grid grid-cols-4 gap-2 text-sm">
117
- <span className="text-theme-text-secondary">CPU</span>
118
- <span className="text-theme-text-primary">{capacity.cpu || '-'}</span>
119
- <span className="text-theme-text-primary">{allocatable.cpu || '-'}</span>
120
- <span className="text-theme-text-primary font-medium">{metrics?.usage?.cpu || '-'}</span>
121
- </div>
122
- <div className="grid grid-cols-4 gap-2 text-sm">
123
- <span className="text-theme-text-secondary">Memory</span>
124
- <span className="text-theme-text-primary">{formatMemory(capacity.memory)}</span>
125
- <span className="text-theme-text-primary">{formatMemory(allocatable.memory)}</span>
126
- <span className="text-theme-text-primary font-medium">{metrics?.usage?.memory ? formatMemory(metrics.usage.memory) : '-'}</span>
127
- </div>
128
- <div className="grid grid-cols-4 gap-2 text-sm">
129
- <span className="text-theme-text-secondary">Pods</span>
130
- <span className="text-theme-text-primary">{capacity.pods || '-'}</span>
131
- <span className="text-theme-text-primary">{allocatable.pods || '-'}</span>
132
- <span className="text-theme-text-primary font-medium">{relationships?.pods?.length ?? '-'}</span>
133
- </div>
134
- <div className="grid grid-cols-4 gap-2 text-sm">
135
- <span className="text-theme-text-secondary">Ephemeral Storage</span>
136
- <span className="text-theme-text-primary">{formatStorage(capacity['ephemeral-storage'])}</span>
137
- <span className="text-theme-text-primary">{formatStorage(allocatable['ephemeral-storage'])}</span>
138
- <span className="text-theme-text-primary">-</span>
139
- </div>
109
+ <div className="space-y-2">
110
+ {[
111
+ {
112
+ label: 'CPU',
113
+ capacity: capacity.cpu,
114
+ allocatable: allocatable.cpu,
115
+ inUse: metrics?.usage?.cpu,
116
+ },
117
+ {
118
+ label: 'Memory',
119
+ capacity: formatMemory(capacity.memory),
120
+ allocatable: formatMemory(allocatable.memory),
121
+ inUse: metrics?.usage?.memory ? formatMemory(metrics.usage.memory) : undefined,
122
+ },
123
+ {
124
+ label: 'Pods',
125
+ capacity: capacity.pods,
126
+ allocatable: allocatable.pods,
127
+ inUse: relationships?.pods?.length,
128
+ },
129
+ {
130
+ label: 'Ephemeral Storage',
131
+ capacity: formatStorage(capacity['ephemeral-storage']),
132
+ allocatable: formatStorage(allocatable['ephemeral-storage']),
133
+ inUse: undefined,
134
+ },
135
+ ].map((row) => (
136
+ <div key={row.label} className="card-inner">
137
+ <div className="text-xs font-medium text-theme-text-secondary mb-1">{row.label}</div>
138
+ <div className="space-y-0.5 text-xs">
139
+ <div className="flex items-baseline justify-between gap-2">
140
+ <span className="text-theme-text-tertiary">Capacity</span>
141
+ <span className="text-theme-text-primary tabular-nums">{row.capacity ?? '-'}</span>
142
+ </div>
143
+ <div className="flex items-baseline justify-between gap-2">
144
+ <span className="text-theme-text-tertiary">Allocatable</span>
145
+ <span className="text-theme-text-primary tabular-nums">{row.allocatable ?? '-'}</span>
146
+ </div>
147
+ <div className="flex items-baseline justify-between gap-2">
148
+ <span className="text-theme-text-tertiary">In Use</span>
149
+ <span className="text-theme-text-primary font-medium tabular-nums">{row.inUse ?? '-'}</span>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ ))}
140
154
  </div>
141
155
  {onViewPods && (
142
156
  <div className="mt-3 pt-3 border-t border-theme-border">
@@ -35,7 +35,7 @@ export function HealthRing({ segments, size = 48, strokeWidth = 5, label }: Heal
35
35
  className="text-theme-border"
36
36
  />
37
37
  {label && (
38
- <text x="50" y="48" textAnchor="middle" dominantBaseline="central" className="fill-theme-text-tertiary text-[22px] font-semibold font-mono">
38
+ <text x="50" y="48" textAnchor="middle" dominantBaseline="central" fill="currentColor" className="text-theme-text-tertiary text-[22px] font-semibold font-mono">
39
39
  0
40
40
  </text>
41
41
  )}
@@ -98,7 +98,7 @@ export function HealthRing({ segments, size = 48, strokeWidth = 5, label }: Heal
98
98
  </g>
99
99
  {/* Center label — nudged up slightly for visual centering in 270° arc */}
100
100
  {label && (
101
- <text x="50" y="48" textAnchor="middle" dominantBaseline="central" className="fill-theme-text-primary text-[22px] font-semibold font-mono">
101
+ <text x="50" y="48" textAnchor="middle" dominantBaseline="central" fill="currentColor" className="text-theme-text-primary text-[22px] font-semibold font-mono">
102
102
  {label}
103
103
  </text>
104
104
  )}
@@ -1,5 +1,5 @@
1
1
  import { useState } from 'react'
2
- import { ChevronRight, Copy, Check, Tag, AlertTriangle, CheckCircle, ExternalLink, Layers } from 'lucide-react'
2
+ import { ChevronRight, Copy, Check, Tag, AlertTriangle, CheckCircle, ExternalLink, Layers, X, Minus } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
4
  import { formatAge, formatDuration } from '../resources/resource-utils'
5
5
  import { Tooltip } from './Tooltip'
@@ -182,6 +182,106 @@ export function Property({ label, value, copyable, onCopy, copied }: PropertyPro
182
182
  // COMMON SECTIONS
183
183
  // ============================================================================
184
184
 
185
+ // Condition types where status=True means a problem and status=False means healthy.
186
+ //
187
+ // K8s has no canonical API metadata for polarity — SIG-API Machinery explicitly
188
+ // declined to add it (https://gateway-api.sigs.k8s.io/geps/gep-1364/), so every
189
+ // consumer has to maintain a list. This one is assembled from:
190
+ // - Core K8s (kubelet node conditions, Pod, workload controllers)
191
+ // - Node Problem Detector default configs (kubernetes/node-problem-detector)
192
+ // - GKE's NPD plugin set (observed in-cluster)
193
+ // - ArgoCD Application conditions (argoproj/argo-cd)
194
+ // - FluxCD meta conditions (fluxcd/pkg/apis/meta)
195
+ // - Conventional Operator-pattern conditions (Degraded, OutOfSync)
196
+ // Patterns only cover two NPD naming families whose convention is
197
+ // strict ("Deprecated*" for deprecation checks, "Frequent*" for restart
198
+ // counters) — everything else is explicit to avoid guessing wrong.
199
+ const NEGATIVE_POLARITY_TYPES = new Set([
200
+ // Core K8s — Node (kubelet)
201
+ 'MemoryPressure',
202
+ 'DiskPressure',
203
+ 'PIDPressure',
204
+ 'NetworkUnavailable',
205
+ // Core K8s — Pod
206
+ 'DisruptionTarget',
207
+ // Core K8s — workloads
208
+ 'ReplicaFailure',
209
+ // NPD — upstream default configs
210
+ 'KernelDeadlock',
211
+ 'CperHardwareErrorFatal',
212
+ 'XfsShutdown',
213
+ 'CorruptDockerOverlay2',
214
+ 'ReadonlyFilesystem',
215
+ 'ContainerRuntimeUnhealthy',
216
+ 'KubeletUnhealthy',
217
+ // NPD — GKE extensions (observed)
218
+ 'ReadOnlyRootFileSystem',
219
+ 'ResourceExhausted',
220
+ 'Swap',
221
+ // NPD — AKS extensions (learn.microsoft.com/azure/aks/node-problem-detector)
222
+ 'FilesystemCorruptionProblem',
223
+ 'KubeletProblem',
224
+ 'ContainerRuntimeProblem',
225
+ 'VMEventScheduled',
226
+ 'GPUMissing',
227
+ 'NVLinkStatusInactive',
228
+ 'XIDErrors',
229
+ 'IBLinkFlapping',
230
+ 'GPUClockThrottling',
231
+ 'UnhealthyNvidiaDevicePlugin',
232
+ // ArgoCD Application
233
+ 'DeletionError',
234
+ 'InvalidSpecError',
235
+ 'ComparisonError',
236
+ 'SyncError',
237
+ 'UnknownError',
238
+ 'SharedResourceWarning',
239
+ 'RepeatedResourceWarning',
240
+ 'OrphanedResourceWarning',
241
+ 'ExcludedResourceWarning',
242
+ 'OutOfSync',
243
+ // ArgoCD ApplicationSet
244
+ 'ErrorOccurred',
245
+ // FluxCD (meta)
246
+ 'Stalled',
247
+ // Gateway API (sigs.k8s.io/gateway-api v1)
248
+ 'Conflicted',
249
+ 'OverlappingTLSConfig',
250
+ 'PartiallyInvalid',
251
+ 'InsecureFrontendValidationMode',
252
+ // cert-manager (CertificateRequest)
253
+ 'Denied',
254
+ 'InvalidRequest',
255
+ // Karpenter NodeClaim
256
+ 'Drifted',
257
+ // VPA
258
+ 'ConfigUnsupported',
259
+ 'LowConfidence',
260
+ // KEDA ScaledObject
261
+ 'Fallback',
262
+ // Operator-pattern CRDs (widespread)
263
+ 'Degraded',
264
+ 'Failed',
265
+ ])
266
+
267
+ const NEGATIVE_POLARITY_PATTERNS: RegExp[] = [
268
+ // NPD deprecation-check family (e.g. DeprecatedAuthsFieldInContainerdConfiguration)
269
+ /^Deprecated/,
270
+ // NPD restart-counter family (e.g. FrequentKubeletRestart, FrequentUnregisterNetDevice)
271
+ /^Frequent/,
272
+ ]
273
+
274
+ function isInvertedPolarityCondition(type: string | undefined): boolean {
275
+ if (!type) return false
276
+ if (NEGATIVE_POLARITY_TYPES.has(type)) return true
277
+ return NEGATIVE_POLARITY_PATTERNS.some((re) => re.test(type))
278
+ }
279
+
280
+ function isConditionHealthy(cond: { type?: string; status?: string }): boolean {
281
+ const inverted = isInvertedPolarityCondition(cond.type)
282
+ return inverted ? cond.status === 'False' : cond.status === 'True'
283
+ }
284
+
185
285
  export function ConditionsSection({ conditions }: { conditions?: any[] }) {
186
286
  if (!conditions || conditions.length === 0) return null
187
287
 
@@ -193,7 +293,7 @@ export function ConditionsSection({ conditions }: { conditions?: any[] }) {
193
293
  return (a.type || '').localeCompare(b.type || '')
194
294
  })
195
295
 
196
- const failCount = sorted.filter((c: any) => c.status === 'False').length
296
+ const failCount = sorted.filter((c: any) => (c.status === 'True' || c.status === 'False') && !isConditionHealthy(c)).length
197
297
 
198
298
  return (
199
299
  <Section
@@ -206,8 +306,8 @@ export function ConditionsSection({ conditions }: { conditions?: any[] }) {
206
306
 
207
307
  <div className="space-y-0.5">
208
308
  {sorted.map((cond: any) => {
209
- const isOk = cond.status === 'True'
210
- const isUnknown = cond.status === 'Unknown'
309
+ const isUnknown = cond.status !== 'True' && cond.status !== 'False'
310
+ const isOk = !isUnknown && isConditionHealthy(cond)
211
311
  const isFail = !isOk && !isUnknown
212
312
  return (
213
313
  <div key={cond.type} className={clsx(
@@ -220,12 +320,14 @@ export function ConditionsSection({ conditions }: { conditions?: any[] }) {
220
320
  </div>
221
321
  {/* Timeline dot */}
222
322
  <span className={clsx(
223
- 'w-[13px] h-[13px] rounded-full flex items-center justify-center text-[8px] shrink-0 mt-[3px] z-10 ring-2 ring-theme-surface',
323
+ 'w-3 h-3 rounded-full flex items-center justify-center shrink-0 mt-1 z-10 ring-2 ring-theme-surface',
224
324
  isOk ? 'bg-emerald-500/20 text-emerald-500 dark:bg-emerald-500/30'
225
325
  : isUnknown ? 'bg-gray-400/20 text-gray-400 dark:bg-gray-400/30'
226
326
  : 'bg-red-500/25 text-red-500 dark:bg-red-500/35'
227
327
  )}>
228
- {isOk ? '✓' : isUnknown ? '?' : '✗'}
328
+ {isOk ? <Check className="w-2 h-2" strokeWidth={4} />
329
+ : isUnknown ? <Minus className="w-2 h-2" strokeWidth={4} />
330
+ : <X className="w-2 h-2" strokeWidth={4} />}
229
331
  </span>
230
332
  {/* Content */}
231
333
  <div className="min-w-0 flex-1 pl-2">
@@ -0,0 +1,6 @@
1
+ /* Top-level re-export of topology.css so both npm-exports resolution and
2
+ * source-alias resolution (in the radar monorepo's Vite config) land on
3
+ * the same stylesheet. Keeps external consumers' @import paths simple
4
+ * (@skyhook-io/k8s-ui/topology.css) without forcing them to know the
5
+ * components/ subdirectory layout. */
6
+ @import "./components/topology/topology.css";