@skyhook-io/k8s-ui 1.5.0 → 1.5.2
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 +2 -2
- package/src/components/dock/LocalTerminalTab.tsx +29 -29
- package/src/components/dock/TerminalTab.tsx +29 -29
- package/src/components/logs/LogCore.tsx +310 -56
- package/src/components/logs/StructuredLogLine.tsx +98 -15
- package/src/components/logs/useLogSearch.ts +5 -0
- package/src/components/resources/ResourcesView.tsx +139 -139
- package/src/components/resources/renderers/NodeRenderer.tsx +45 -31
- package/src/components/resources/resource-utils-argo.ts +9 -2
- package/src/components/ui/HealthRing.tsx +2 -2
- package/src/components/ui/drawer-components.tsx +108 -6
- package/src/topology.css +6 -0
- package/src/utils/log-format.ts +77 -12
|
@@ -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">
|
|
@@ -12,9 +12,16 @@ export function getArgoApplicationStatus(app: any): StatusBadge {
|
|
|
12
12
|
const sync = app.status?.sync?.status
|
|
13
13
|
const opPhase = app.status?.operationState?.phase
|
|
14
14
|
|
|
15
|
-
// Check for suspended (no automated sync policy)
|
|
15
|
+
// Check for suspended (no automated sync policy). Honor both the current
|
|
16
|
+
// "radarhq.io/suspended-prune" annotation and the legacy "skyhook.io/..."
|
|
17
|
+
// key still present on Applications suspended by older Radar builds.
|
|
18
|
+
// The annotation value stores the prior prune state as "true"/"false" for
|
|
19
|
+
// restore on resume — both strings are truthy in JS, which is intentional:
|
|
20
|
+
// the *presence* of the annotation is what signals suspended, not its value.
|
|
16
21
|
const hasAutomatedSync = !!app.spec?.syncPolicy?.automated
|
|
17
|
-
|
|
22
|
+
const annotations = app.metadata?.annotations
|
|
23
|
+
const suspendedByRadar = annotations?.['radarhq.io/suspended-prune'] || annotations?.['skyhook.io/suspended-prune']
|
|
24
|
+
if (health === 'Suspended' || (!hasAutomatedSync && suspendedByRadar)) {
|
|
18
25
|
return { text: 'Suspended', color: healthColors.degraded, level: 'degraded' }
|
|
19
26
|
}
|
|
20
27
|
|
|
@@ -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">
|
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";
|
package/src/utils/log-format.ts
CHANGED
|
@@ -5,22 +5,87 @@
|
|
|
5
5
|
|
|
6
6
|
import type { LogLevel } from '../components/logs/useLogBuffer'
|
|
7
7
|
|
|
8
|
+
export type TimestampFormat =
|
|
9
|
+
| 'time-local'
|
|
10
|
+
| 'time-utc'
|
|
11
|
+
| 'iso-local'
|
|
12
|
+
| 'iso-utc'
|
|
13
|
+
| 'relative'
|
|
14
|
+
| 'epoch'
|
|
15
|
+
|
|
16
|
+
export const TIMESTAMP_FORMAT_LABELS: Record<TimestampFormat, string> = {
|
|
17
|
+
'time-local': 'Time (local)',
|
|
18
|
+
'time-utc': 'Time (UTC)',
|
|
19
|
+
'iso-local': 'ISO (local)',
|
|
20
|
+
'iso-utc': 'ISO (UTC)',
|
|
21
|
+
'relative': 'Relative',
|
|
22
|
+
'epoch': 'Epoch',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function pad2(n: number): string {
|
|
26
|
+
return n < 10 ? '0' + n : String(n)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatIsoLocal(date: Date): string {
|
|
30
|
+
const offset = -date.getTimezoneOffset()
|
|
31
|
+
const sign = offset >= 0 ? '+' : '-'
|
|
32
|
+
const abs = Math.abs(offset)
|
|
33
|
+
return (
|
|
34
|
+
date.getFullYear() +
|
|
35
|
+
'-' + pad2(date.getMonth() + 1) +
|
|
36
|
+
'-' + pad2(date.getDate()) +
|
|
37
|
+
'T' + pad2(date.getHours()) +
|
|
38
|
+
':' + pad2(date.getMinutes()) +
|
|
39
|
+
':' + pad2(date.getSeconds()) +
|
|
40
|
+
sign + pad2(Math.floor(abs / 60)) + ':' + pad2(abs % 60)
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatRelative(date: Date, now: number): string {
|
|
45
|
+
const diffSec = Math.round((now - date.getTime()) / 1000)
|
|
46
|
+
const abs = Math.abs(diffSec)
|
|
47
|
+
const suffix = diffSec >= 0 ? ' ago' : ' from now'
|
|
48
|
+
if (abs < 2) return 'just now'
|
|
49
|
+
if (abs < 60) return `${abs}s${suffix}`
|
|
50
|
+
if (abs < 3600) return `${Math.floor(abs / 60)}m${suffix}`
|
|
51
|
+
if (abs < 86400) return `${Math.floor(abs / 3600)}h${suffix}`
|
|
52
|
+
return `${Math.floor(abs / 86400)}d${suffix}`
|
|
53
|
+
}
|
|
54
|
+
|
|
8
55
|
/**
|
|
9
56
|
* Format a K8s log timestamp for display.
|
|
10
|
-
*
|
|
57
|
+
* Supports multiple display formats and UTC/local time zones.
|
|
58
|
+
* `now` is accepted for testability; defaults to Date.now().
|
|
11
59
|
*/
|
|
12
|
-
export function formatLogTimestamp(
|
|
60
|
+
export function formatLogTimestamp(
|
|
61
|
+
ts: string,
|
|
62
|
+
format: TimestampFormat = 'time-local',
|
|
63
|
+
now?: number,
|
|
64
|
+
): string {
|
|
13
65
|
const date = new Date(ts)
|
|
14
66
|
if (isNaN(date.getTime())) {
|
|
15
|
-
// Fallback: extract HH:MM:SS from ISO timestamp
|
|
16
67
|
return ts.slice(11, 19) || ts
|
|
17
68
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
69
|
+
switch (format) {
|
|
70
|
+
case 'time-utc':
|
|
71
|
+
return date.toISOString().slice(11, 19)
|
|
72
|
+
case 'iso-local':
|
|
73
|
+
return formatIsoLocal(date)
|
|
74
|
+
case 'iso-utc':
|
|
75
|
+
return date.toISOString()
|
|
76
|
+
case 'relative':
|
|
77
|
+
return formatRelative(date, now ?? Date.now())
|
|
78
|
+
case 'epoch':
|
|
79
|
+
return String(Math.floor(date.getTime() / 1000))
|
|
80
|
+
case 'time-local':
|
|
81
|
+
default:
|
|
82
|
+
return date.toLocaleTimeString('en-US', {
|
|
83
|
+
hour12: false,
|
|
84
|
+
hour: '2-digit',
|
|
85
|
+
minute: '2-digit',
|
|
86
|
+
second: '2-digit',
|
|
87
|
+
})
|
|
88
|
+
}
|
|
24
89
|
}
|
|
25
90
|
|
|
26
91
|
/** Map a detected LogLevel to a Tailwind color class. */
|
|
@@ -294,9 +359,9 @@ export function parseLogRange(logRange: string): { tailLines?: number; sinceSeco
|
|
|
294
359
|
// Syntax highlight colors shared between JSON and logfmt rendering
|
|
295
360
|
export const SYNTAX_COLOR_KEY = '#7cacf8'
|
|
296
361
|
export const SYNTAX_COLOR_STRING = '#73c991'
|
|
297
|
-
const SYNTAX_COLOR_NUMBER = '#e5c07b'
|
|
298
|
-
const SYNTAX_COLOR_BOOLEAN = '#c678dd'
|
|
299
|
-
const SYNTAX_COLOR_NULL = '#808080'
|
|
362
|
+
export const SYNTAX_COLOR_NUMBER = '#e5c07b'
|
|
363
|
+
export const SYNTAX_COLOR_BOOLEAN = '#c678dd'
|
|
364
|
+
export const SYNTAX_COLOR_NULL = '#808080'
|
|
300
365
|
|
|
301
366
|
/**
|
|
302
367
|
* Syntax-highlight a pretty-printed JSON string for HTML display.
|