@skyhook-io/k8s-ui 1.4.3 → 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.
- package/package.json +31 -8
- package/src/components/dock/LocalTerminalTab.tsx +29 -29
- package/src/components/dock/TerminalTab.tsx +29 -29
- package/src/components/resources/ResourcesView.tsx +139 -139
- package/src/components/resources/renderers/NodeRenderer.tsx +45 -31
- package/src/components/ui/HealthRing.tsx +2 -2
- package/src/components/ui/drawer-components.tsx +108 -6
- package/src/hooks/useKeyboardShortcuts.tsx +19 -22
- package/src/topology.css +6 -0
|
@@ -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-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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="
|
|
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="
|
|
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
|
|
210
|
-
const
|
|
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-
|
|
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 ?
|
|
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">
|
|
@@ -30,6 +30,8 @@ export interface KeyboardShortcut {
|
|
|
30
30
|
handler: (e: KeyboardEvent) => void
|
|
31
31
|
/** Whether this shortcut is currently active */
|
|
32
32
|
enabled?: boolean
|
|
33
|
+
/** Fire even when focus is inside an input/textarea. For global modifier combos (⌘K, Ctrl+Shift+D) that can't collide with typing. */
|
|
34
|
+
allowInInputs?: boolean
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
// Internal registration with stable ID
|
|
@@ -46,28 +48,22 @@ const KeyboardShortcutContext = createContext<KeyboardShortcutContextType | null
|
|
|
46
48
|
|
|
47
49
|
let nextRegistrationId = 0
|
|
48
50
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (e.key === 'Escape') return false
|
|
51
|
+
// Classify suppression level for the currently-focused element.
|
|
52
|
+
// none — no suppression, all shortcuts fire
|
|
53
|
+
// soft — plain text inputs; bypassed by shortcuts with allowInInputs
|
|
54
|
+
// hard — rich editors (Monaco, xterm) that own their own keyboard UX;
|
|
55
|
+
// never bypassed, since Cmd+K / Ctrl+Shift+D have meaning there
|
|
56
|
+
type SuppressionLevel = 'none' | 'soft' | 'hard'
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
function getSuppressionLevel(e: KeyboardEvent): SuppressionLevel {
|
|
59
|
+
const target = e.target as HTMLElement
|
|
60
|
+
if (!target) return 'none'
|
|
61
|
+
if (e.key === 'Escape') return 'none'
|
|
62
|
+
if (target.closest('.monaco-editor') || target.closest('.xterm')) return 'hard'
|
|
58
63
|
const tagName = target.tagName
|
|
59
|
-
if (tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT') return
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (target.isContentEditable) return true
|
|
63
|
-
|
|
64
|
-
// Suppress in Monaco editor
|
|
65
|
-
if (target.closest('.monaco-editor')) return true
|
|
66
|
-
|
|
67
|
-
// Suppress in xterm terminal
|
|
68
|
-
if (target.closest('.xterm')) return true
|
|
69
|
-
|
|
70
|
-
return false
|
|
64
|
+
if (tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT') return 'soft'
|
|
65
|
+
if (target.isContentEditable) return 'soft'
|
|
66
|
+
return 'none'
|
|
71
67
|
}
|
|
72
68
|
|
|
73
69
|
// Parse a key string like "Shift+N", "Cmd+K", "g g" into match criteria
|
|
@@ -158,7 +154,8 @@ export function KeyboardShortcutProvider({ children }: { children: ReactNode })
|
|
|
158
154
|
// Global keydown listener
|
|
159
155
|
useEffect(() => {
|
|
160
156
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
161
|
-
const
|
|
157
|
+
const suppression = getSuppressionLevel(e)
|
|
158
|
+
const suppressed = suppression !== 'none'
|
|
162
159
|
|
|
163
160
|
// Get all enabled shortcuts, sorted by scope priority (highest first)
|
|
164
161
|
const shortcuts = Array.from(shortcutsRef.current.values())
|
|
@@ -215,7 +212,7 @@ export function KeyboardShortcutProvider({ children }: { children: ReactNode })
|
|
|
215
212
|
return
|
|
216
213
|
}
|
|
217
214
|
|
|
218
|
-
if (suppressed) continue
|
|
215
|
+
if (suppressed && !(shortcut.allowInInputs && suppression === 'soft')) continue
|
|
219
216
|
|
|
220
217
|
if (matchesKey(e, matcher, '')) {
|
|
221
218
|
e.preventDefault()
|
package/src/topology.css
ADDED
|
@@ -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";
|