@skyhook-io/k8s-ui 1.8.5 → 1.8.6
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 +1 -1
- package/src/components/resources/get-pod-phase-display.test.ts +115 -3
- package/src/components/resources/renderers/CronJobRenderer.tsx +14 -7
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +4 -3
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +9 -15
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +3 -11
- package/src/components/resources/renderers/GatewayRenderer.tsx +11 -35
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +10 -16
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -6
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +24 -28
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +19 -26
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +20 -19
- package/src/components/resources/renderers/JobRenderer.tsx +6 -5
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +9 -11
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +6 -12
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +11 -10
- package/src/components/resources/renderers/TraefikMiddlewareRenderer.tsx +185 -0
- package/src/components/resources/renderers/TraefikServersTransportRenderer.tsx +84 -0
- package/src/components/resources/renderers/TraefikServiceRenderer.tsx +118 -0
- package/src/components/resources/renderers/TraefikTLSOptionRenderer.tsx +62 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +1 -1
- package/src/components/resources/renderers/badge-no-handrolled.test.tsx +66 -0
- package/src/components/resources/renderers/contour-cells.tsx +6 -5
- package/src/components/resources/renderers/index.ts +4 -0
- package/src/components/resources/renderers/istio-cells.tsx +16 -25
- package/src/components/resources/resource-utils-cnpg.ts +1 -1
- package/src/components/resources/resource-utils-keda.ts +12 -6
- package/src/components/resources/resource-utils.ts +172 -57
- package/src/components/shared/ResourceRendererDispatch.tsx +10 -0
- package/src/components/topology/TopologyControls.tsx +41 -9
- package/src/components/ui/Badge.tsx +66 -3
- package/src/components/ui/ClusterName.tsx +12 -4
- package/src/components/ui/PaneLoader.tsx +6 -1
- package/src/types/core.ts +37 -0
- package/src/utils/asset-url.ts +13 -0
|
@@ -112,41 +112,122 @@ export function podMatchesProblemCategory(problems: PodProblem[], restarts: numb
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// A transient lifecycle state (plain Pending, Terminating, awaiting an address)
|
|
116
|
+
// is not a fault while it's young — it becomes one only once it's stuck past
|
|
117
|
+
// these windows, mirroring the backend health thresholds (ClassifyPodHealth:
|
|
118
|
+
// pending 5m; detect.go: terminating 10m, LB/PVC pending 5m). Two things escalate
|
|
119
|
+
// immediately, regardless of age, because they're definitive failures the backend
|
|
120
|
+
// also flags at once: fatal *container* states (CrashLoopBackOff, ImagePull,
|
|
121
|
+
// InvalidImageName, OOMKilled, …) and Unschedulable (the scheduler tried and
|
|
122
|
+
// could not place the pod).
|
|
123
|
+
const PENDING_STUCK_MINUTES = 5
|
|
124
|
+
const TERMINATING_STUCK_MINUTES = 10
|
|
125
|
+
|
|
126
|
+
// minutesSince returns minutes elapsed since an ISO timestamp, or 0 when it's
|
|
127
|
+
// missing/invalid — so "unknown age" is treated as not-yet-stuck (benign).
|
|
128
|
+
function minutesSince(timestamp?: string): number {
|
|
129
|
+
if (!timestamp) return 0
|
|
130
|
+
const t = new Date(timestamp).getTime()
|
|
131
|
+
if (Number.isNaN(t)) return 0
|
|
132
|
+
return (Date.now() - t) / 60000
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function podUnschedulable(pod: any): boolean {
|
|
136
|
+
const conds = pod?.status?.conditions || []
|
|
137
|
+
return conds.some(
|
|
138
|
+
(c: any) => c.type === 'PodScheduled' && c.status === 'False' && c.reason === 'Unschedulable'
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// A container that has terminated successfully (exit 0) is done, not "not ready".
|
|
143
|
+
// A completing Job pod (Running phase, container Completed, Ready=false) must not
|
|
144
|
+
// read as degraded just because ready<total — this mirrors ClassifyPodHealth, so
|
|
145
|
+
// the badge agrees with the timeline/Problems verdict.
|
|
146
|
+
function containerSettledOk(cs: any): boolean {
|
|
147
|
+
return cs?.ready === true || cs?.state?.terminated?.exitCode === 0
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Hard-failure waiting reasons that won't self-resolve — mirrors the backend's
|
|
151
|
+
// isFatalWaitingReason. These escalate immediately regardless of pod age or a
|
|
152
|
+
// Terminating overlay (a bad image / config error is a real failure now, not a
|
|
153
|
+
// benign young-Pending pod).
|
|
154
|
+
const FATAL_WAITING_REASONS = new Set([
|
|
155
|
+
'CrashLoopBackOff', 'ImagePullBackOff', 'ErrImagePull', 'InvalidImageName',
|
|
156
|
+
'ImageInspectError', 'CreateContainerConfigError', 'CreateContainerError', 'RunContainerError',
|
|
157
|
+
])
|
|
158
|
+
|
|
159
|
+
// firstFatalContainer returns the first init- or main-container in a hard-failure
|
|
160
|
+
// state (or null). Init containers are walked first: when init is failing the pod
|
|
161
|
+
// stays Pending and main ContainerStatuses haven't populated yet, so checking main
|
|
162
|
+
// alone would miss it and fall through to a benign "Pending".
|
|
163
|
+
function firstFatalContainer(pod: any): { name: string; reason: string } | null {
|
|
164
|
+
const init = pod?.status?.initContainerStatuses || []
|
|
165
|
+
const main = pod?.status?.containerStatuses || []
|
|
166
|
+
for (const cs of [...init, ...main]) {
|
|
167
|
+
const w = cs?.state?.waiting?.reason
|
|
168
|
+
if (w && FATAL_WAITING_REASONS.has(w)) return { name: cs?.name, reason: w }
|
|
169
|
+
if (cs?.state?.terminated?.reason === 'OOMKilled') return { name: cs?.name, reason: 'OOMKilled' }
|
|
170
|
+
}
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
173
|
+
|
|
115
174
|
export function getPodStatus(pod: any): StatusBadge {
|
|
116
175
|
const phase = pod.status?.phase || 'Unknown'
|
|
117
176
|
const containerStatuses = pod.status?.containerStatuses || []
|
|
118
177
|
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
178
|
+
// Fatal init/main container states win over the Pending grace AND a Terminating
|
|
179
|
+
// overlay — a crash-loop, bad image, or config error is a real failure now,
|
|
180
|
+
// whatever the pod's age or deletion state. Skip for Succeeded: a terminal
|
|
181
|
+
// success must not flip unhealthy on a sidecar that OOMed before the main
|
|
182
|
+
// container finished (matches getPodPhaseDisplay + ClassifyPodHealth).
|
|
183
|
+
if (phase !== 'Succeeded') {
|
|
184
|
+
const fatal = firstFatalContainer(pod)
|
|
185
|
+
if (fatal) {
|
|
186
|
+
return { text: fatal.reason, color: healthColors.unhealthy, level: 'unhealthy' }
|
|
187
|
+
}
|
|
122
188
|
}
|
|
123
189
|
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
190
|
+
// A terminal failure stays unhealthy even while the pod is being deleted — the
|
|
191
|
+
// Terminating overlay below must not mask a Failed pod (the Problems panel and
|
|
192
|
+
// dashboard report it as an error regardless).
|
|
193
|
+
if (phase === 'Failed') {
|
|
194
|
+
return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Terminating: neutral while gracefully shutting down, degraded once stuck
|
|
198
|
+
// (a wedged finalizer / preStop hook holding the pod open).
|
|
199
|
+
if (pod.metadata?.deletionTimestamp) {
|
|
200
|
+
if (minutesSince(pod.metadata.deletionTimestamp) >= TERMINATING_STUCK_MINUTES) {
|
|
201
|
+
return { text: 'Terminating', color: healthColors.degraded, level: 'degraded' }
|
|
134
202
|
}
|
|
203
|
+
return { text: 'Terminating', color: healthColors.neutral, level: 'neutral' }
|
|
135
204
|
}
|
|
136
205
|
|
|
137
206
|
switch (phase) {
|
|
138
|
-
case 'Running':
|
|
139
|
-
//
|
|
207
|
+
case 'Running': {
|
|
208
|
+
// Degrade only on containers that are neither ready nor successfully done —
|
|
209
|
+
// a completed container (exit 0) on a Running pod is finishing, not a fault.
|
|
140
210
|
const ready = containerStatuses.filter((c: any) => c.ready).length
|
|
141
211
|
const total = containerStatuses.length
|
|
142
|
-
|
|
212
|
+
const unsettled = containerStatuses.filter((c: any) => !containerSettledOk(c)).length
|
|
213
|
+
if (unsettled > 0) {
|
|
143
214
|
return { text: `Running (${ready}/${total})`, color: healthColors.degraded, level: 'degraded' }
|
|
144
215
|
}
|
|
145
216
|
return { text: 'Running', color: healthColors.healthy, level: 'healthy' }
|
|
217
|
+
}
|
|
146
218
|
case 'Succeeded':
|
|
147
219
|
return { text: 'Completed', color: healthColors.neutral, level: 'neutral' }
|
|
148
220
|
case 'Pending':
|
|
149
|
-
|
|
221
|
+
// Unschedulable = the scheduler tried and failed to place this pod; the
|
|
222
|
+
// backend flags it immediately (severity ramps with duration), so the badge
|
|
223
|
+
// does too. Plain Pending (not yet placed) keeps the young-grace window.
|
|
224
|
+
if (podUnschedulable(pod)) {
|
|
225
|
+
return { text: 'Unschedulable', color: healthColors.degraded, level: 'degraded' }
|
|
226
|
+
}
|
|
227
|
+
if (minutesSince(pod.metadata?.creationTimestamp) >= PENDING_STUCK_MINUTES) {
|
|
228
|
+
return { text: 'Pending', color: healthColors.degraded, level: 'degraded' }
|
|
229
|
+
}
|
|
230
|
+
return { text: 'Pending', color: healthColors.neutral, level: 'neutral' }
|
|
150
231
|
case 'Failed':
|
|
151
232
|
return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
152
233
|
default:
|
|
@@ -181,49 +262,51 @@ export function getPodPhaseDisplay(pod: any): PodPhaseDisplay {
|
|
|
181
262
|
0
|
|
182
263
|
)
|
|
183
264
|
|
|
265
|
+
// Container-state failures take precedence over phase AND over a Terminating
|
|
266
|
+
// overlay: a CrashLoopBackOff pod can still report phase: Running, an init
|
|
267
|
+
// failure keeps the pod Pending, and a pod crashing while being deleted is
|
|
268
|
+
// still a failure worth surfacing. Skip for Succeeded — a Job pod whose sidecar
|
|
269
|
+
// was OOMKilled before the main container completed should not read unhealthy
|
|
270
|
+
// after terminal success.
|
|
271
|
+
if (phase !== 'Succeeded') {
|
|
272
|
+
const fatal = firstFatalContainer(pod)
|
|
273
|
+
if (fatal) {
|
|
274
|
+
return {
|
|
275
|
+
phase,
|
|
276
|
+
text: `${phase} — ${fatal.reason}`,
|
|
277
|
+
level: 'unhealthy',
|
|
278
|
+
hint: fatal.reason === 'OOMKilled'
|
|
279
|
+
? `Container "${fatal.name}" was OOMKilled.`
|
|
280
|
+
: `Container "${fatal.name}" is stuck in ${fatal.reason}.`,
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// A terminal failure stays unhealthy even while the pod is being deleted — don't
|
|
286
|
+
// let the Terminating overlay mask a Failed pod.
|
|
287
|
+
if (phase === 'Failed') {
|
|
288
|
+
return { phase, text: 'Failed', level: 'unhealthy' }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Terminating: neutral while gracefully shutting down, degraded once stuck.
|
|
184
292
|
if (pod?.metadata?.deletionTimestamp) {
|
|
293
|
+
const stuck = minutesSince(pod.metadata.deletionTimestamp) >= TERMINATING_STUCK_MINUTES
|
|
185
294
|
return {
|
|
186
295
|
phase,
|
|
187
296
|
text: `${phase} — Terminating`,
|
|
188
|
-
level: 'degraded',
|
|
189
|
-
hint:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
// Container-state failures take precedence over phase: a CrashLoopBackOff
|
|
194
|
-
// pod can still report phase: Running. Skip for Succeeded — a Job pod whose
|
|
195
|
-
// sidecar was OOMKilled before the main container completed should not be
|
|
196
|
-
// shown as unhealthy after the pod has reached terminal success.
|
|
197
|
-
if (phase !== 'Succeeded') {
|
|
198
|
-
for (const cs of containerStatuses) {
|
|
199
|
-
const waitingReason = cs?.state?.waiting?.reason
|
|
200
|
-
if (
|
|
201
|
-
waitingReason === 'CrashLoopBackOff' ||
|
|
202
|
-
waitingReason === 'ImagePullBackOff' ||
|
|
203
|
-
waitingReason === 'ErrImagePull' ||
|
|
204
|
-
waitingReason === 'CreateContainerConfigError'
|
|
205
|
-
) {
|
|
206
|
-
return {
|
|
207
|
-
phase,
|
|
208
|
-
text: `${phase} — ${waitingReason}`,
|
|
209
|
-
level: 'unhealthy',
|
|
210
|
-
hint: `Container "${cs.name}" is stuck in ${waitingReason}.`,
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
if (cs?.state?.terminated?.reason === 'OOMKilled') {
|
|
214
|
-
return {
|
|
215
|
-
phase,
|
|
216
|
-
text: `${phase} — OOMKilled`,
|
|
217
|
-
level: 'unhealthy',
|
|
218
|
-
hint: `Container "${cs.name}" was OOMKilled.`,
|
|
219
|
-
}
|
|
220
|
-
}
|
|
297
|
+
level: stuck ? 'degraded' : 'neutral',
|
|
298
|
+
hint: stuck
|
|
299
|
+
? 'Pod has been terminating for a while — a finalizer or preStop hook may be wedged.'
|
|
300
|
+
: 'Pod has a deletionTimestamp set; awaiting graceful termination.',
|
|
221
301
|
}
|
|
222
302
|
}
|
|
223
303
|
|
|
224
304
|
switch (phase) {
|
|
225
305
|
case 'Running': {
|
|
226
|
-
|
|
306
|
+
// A successfully-completed container (exit 0) is settled, not "not ready" —
|
|
307
|
+
// don't degrade a completing pod over it (matches getPodStatus / timeline).
|
|
308
|
+
const unsettled = containerStatuses.filter((c) => !containerSettledOk(c)).length
|
|
309
|
+
const notReady = totalContainers > 0 && unsettled > 0
|
|
227
310
|
const cycling = restartTotal > RESTART_CYCLING_THRESHOLD
|
|
228
311
|
if (notReady && cycling) {
|
|
229
312
|
return {
|
|
@@ -254,7 +337,26 @@ export function getPodPhaseDisplay(pod: any): PodPhaseDisplay {
|
|
|
254
337
|
case 'Succeeded':
|
|
255
338
|
return { phase, text: 'Completed', level: 'neutral' }
|
|
256
339
|
case 'Pending':
|
|
257
|
-
|
|
340
|
+
// Unschedulable = the scheduler tried and failed; surface it immediately
|
|
341
|
+
// (the backend does, with severity ramping by duration). Plain Pending
|
|
342
|
+
// (not yet placed) keeps the young-grace window.
|
|
343
|
+
if (podUnschedulable(pod)) {
|
|
344
|
+
return {
|
|
345
|
+
phase,
|
|
346
|
+
text: `${phase} — Unschedulable`,
|
|
347
|
+
level: 'degraded',
|
|
348
|
+
hint: 'No node can accept this pod (insufficient resources, taints, affinity, or quota).',
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (minutesSince(pod?.metadata?.creationTimestamp) >= PENDING_STUCK_MINUTES) {
|
|
352
|
+
return {
|
|
353
|
+
phase,
|
|
354
|
+
text: 'Pending',
|
|
355
|
+
level: 'degraded',
|
|
356
|
+
hint: 'Pod has been Pending for several minutes — check scheduling and image pulls.',
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return { phase, text: 'Pending', level: 'neutral' }
|
|
258
360
|
case 'Failed':
|
|
259
361
|
return { phase, text: 'Failed', level: 'unhealthy' }
|
|
260
362
|
default:
|
|
@@ -778,7 +880,12 @@ export function getIngressStatus(ingress: any): StatusBadge {
|
|
|
778
880
|
if (lbIngress.length > 0) {
|
|
779
881
|
return { text: 'Active', color: healthColors.healthy, level: 'healthy' }
|
|
780
882
|
}
|
|
781
|
-
|
|
883
|
+
// Awaiting an external address is normal right after creation; only flag it
|
|
884
|
+
// once the ingress/LB controller has had time and still hasn't assigned one.
|
|
885
|
+
if (minutesSince(ingress.metadata?.creationTimestamp) >= PENDING_STUCK_MINUTES) {
|
|
886
|
+
return { text: 'Pending', color: healthColors.degraded, level: 'degraded' }
|
|
887
|
+
}
|
|
888
|
+
return { text: 'Pending', color: healthColors.neutral, level: 'neutral' }
|
|
782
889
|
}
|
|
783
890
|
|
|
784
891
|
export function getIngressHosts(ingress: any): string {
|
|
@@ -905,7 +1012,7 @@ export function getJobStatus(job: any): StatusBadge {
|
|
|
905
1012
|
}
|
|
906
1013
|
|
|
907
1014
|
if (job.spec?.suspend) {
|
|
908
|
-
return { text: 'Suspended', color: healthColors.
|
|
1015
|
+
return { text: 'Suspended', color: healthColors.neutral, level: 'neutral' }
|
|
909
1016
|
}
|
|
910
1017
|
|
|
911
1018
|
if (status.active > 0) {
|
|
@@ -936,7 +1043,7 @@ export function getJobDuration(job: any): string | null {
|
|
|
936
1043
|
|
|
937
1044
|
export function getCronJobStatus(cj: any): StatusBadge {
|
|
938
1045
|
if (cj.spec?.suspend) {
|
|
939
|
-
return { text: 'Suspended', color: healthColors.
|
|
1046
|
+
return { text: 'Suspended', color: healthColors.neutral, level: 'neutral' }
|
|
940
1047
|
}
|
|
941
1048
|
const activeJobs = cj.status?.active?.length || 0
|
|
942
1049
|
if (activeJobs > 0) {
|
|
@@ -1010,6 +1117,9 @@ export function getNodeStatus(node: any): StatusBadge {
|
|
|
1010
1117
|
const isUnschedulable = node.spec?.unschedulable === true
|
|
1011
1118
|
|
|
1012
1119
|
if (isReady && isUnschedulable) {
|
|
1120
|
+
// Cordon is intentional but consequential — it's lost scheduling capacity, and
|
|
1121
|
+
// a forgotten cordon strands nodes — so it stays on the warning axis (matching
|
|
1122
|
+
// the backend Cordoned issue), unlike no-op intentional states (suspended/idle).
|
|
1013
1123
|
return { text: 'Ready,SchedulingDisabled', color: healthColors.degraded, level: 'degraded' }
|
|
1014
1124
|
}
|
|
1015
1125
|
if (isReady) {
|
|
@@ -1161,7 +1271,12 @@ export function getPVCStatus(pvc: any): StatusBadge {
|
|
|
1161
1271
|
case 'Bound':
|
|
1162
1272
|
return { text: 'Bound', color: healthColors.healthy, level: 'healthy' }
|
|
1163
1273
|
case 'Pending':
|
|
1164
|
-
|
|
1274
|
+
// Pending is benign here: a WaitForFirstConsumer claim stays Pending by
|
|
1275
|
+
// design until a pod needs it (could be forever for a scaled-to-zero
|
|
1276
|
+
// workload). We can't see the StorageClass binding mode from the PVC alone,
|
|
1277
|
+
// so age can't distinguish that from genuinely-stuck — the Problems panel,
|
|
1278
|
+
// which has that context, owns the stuck-PVC alarm.
|
|
1279
|
+
return { text: 'Pending', color: healthColors.neutral, level: 'neutral' }
|
|
1165
1280
|
case 'Lost':
|
|
1166
1281
|
return { text: 'Lost', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
1167
1282
|
default:
|
|
@@ -1236,7 +1351,7 @@ export function getWorkflowStatus(workflow: any): StatusBadge {
|
|
|
1236
1351
|
case 'Succeeded':
|
|
1237
1352
|
return { text: 'Succeeded', color: healthColors.healthy, level: 'healthy' }
|
|
1238
1353
|
case 'Running':
|
|
1239
|
-
return { text: 'Running', color: healthColors.
|
|
1354
|
+
return { text: 'Running', color: healthColors.neutral, level: 'neutral' }
|
|
1240
1355
|
case 'Failed':
|
|
1241
1356
|
return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
1242
1357
|
case 'Error':
|
|
@@ -181,6 +181,10 @@ import {
|
|
|
181
181
|
RuntimeClassRenderer,
|
|
182
182
|
LeaseRenderer,
|
|
183
183
|
TraefikIngressRouteRenderer,
|
|
184
|
+
TraefikMiddlewareRenderer,
|
|
185
|
+
TraefikServiceRenderer,
|
|
186
|
+
TraefikTLSOptionRenderer,
|
|
187
|
+
TraefikServersTransportRenderer,
|
|
184
188
|
ContourHTTPProxyRenderer,
|
|
185
189
|
CAPIClusterRenderer,
|
|
186
190
|
CAPIMachineRenderer,
|
|
@@ -348,6 +352,8 @@ const KNOWN_KINDS = new Set([
|
|
|
348
352
|
'channels', 'inmemorychannels', 'subscriptions', 'sequences', 'parallels',
|
|
349
353
|
'knativeingresses', 'knativecertificates', 'serverlessservices', 'domainmappings',
|
|
350
354
|
'ingressroutes', 'ingressroutetcps', 'ingressrouteudps',
|
|
355
|
+
'middlewares', 'middlewaretcps', 'traefikservices',
|
|
356
|
+
'serverstransports', 'serverstransporttcps', 'tlsoptions',
|
|
351
357
|
'httpproxies',
|
|
352
358
|
'machinedeployments', 'machines', 'machinesets', 'machinepools',
|
|
353
359
|
'kubeadmcontrolplanes', 'clusterclasses', 'machinehealthchecks',
|
|
@@ -665,6 +671,10 @@ export function ResourceRendererDispatch({
|
|
|
665
671
|
|
|
666
672
|
{/* Traefik */}
|
|
667
673
|
{(kind === 'ingressroutes' || kind === 'ingressroutetcps' || kind === 'ingressrouteudps') && <TraefikIngressRouteRenderer data={data} onNavigate={onNavigate} />}
|
|
674
|
+
{(kind === 'middlewares' || kind === 'middlewaretcps') && <TraefikMiddlewareRenderer data={data} onNavigate={onNavigate} />}
|
|
675
|
+
{kind === 'traefikservices' && <TraefikServiceRenderer data={data} onNavigate={onNavigate} />}
|
|
676
|
+
{(kind === 'serverstransports' || kind === 'serverstransporttcps') && <TraefikServersTransportRenderer data={data} onNavigate={onNavigate} />}
|
|
677
|
+
{kind === 'tlsoptions' && <TraefikTLSOptionRenderer data={data} />}
|
|
668
678
|
|
|
669
679
|
{/* Contour */}
|
|
670
680
|
{kind === 'httpproxies' && <ContourHTTPProxyRenderer data={data} onNavigate={onNavigate} />}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FolderTree, ShieldCheck } from 'lucide-react'
|
|
2
2
|
import type { TopologyMode, GroupingMode } from '../../types/core'
|
|
3
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
3
4
|
|
|
4
5
|
interface TopologyControlsProps {
|
|
5
6
|
viewMode: TopologyMode
|
|
@@ -11,6 +12,12 @@ interface TopologyControlsProps {
|
|
|
11
12
|
onShowPolicyEffectChange?: (show: boolean) => void
|
|
12
13
|
/** Show the "Fleet" button (CAPI cluster management view) */
|
|
13
14
|
showFleetMode?: boolean
|
|
15
|
+
/**
|
|
16
|
+
* Navigate to the observed-traffic view. When provided, the "Network Flow"
|
|
17
|
+
* tooltip offers a link to it — disambiguating the config-derived flow graph
|
|
18
|
+
* here from the live, observed Traffic view. Omitted by hosts without one.
|
|
19
|
+
*/
|
|
20
|
+
onNavigateToTraffic?: () => void
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
export function TopologyControls({
|
|
@@ -22,6 +29,7 @@ export function TopologyControls({
|
|
|
22
29
|
showPolicyEffect = false,
|
|
23
30
|
onShowPolicyEffectChange,
|
|
24
31
|
showFleetMode = false,
|
|
32
|
+
onNavigateToTraffic,
|
|
25
33
|
}: TopologyControlsProps) {
|
|
26
34
|
return (
|
|
27
35
|
<div className="absolute top-4 right-4 z-10 flex items-center gap-2">
|
|
@@ -69,16 +77,40 @@ export function TopologyControls({
|
|
|
69
77
|
>
|
|
70
78
|
Resources
|
|
71
79
|
</button>
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
<Tooltip
|
|
81
|
+
position="bottom"
|
|
82
|
+
content={
|
|
83
|
+
<div className="space-y-1.5 text-left">
|
|
84
|
+
<p className="text-theme-text-secondary">
|
|
85
|
+
How requests <em>should</em> route, derived from Ingress, Services and
|
|
86
|
+
routing CRDs (Traefik, Gateway API, Istio…) — not observed packets.
|
|
87
|
+
</p>
|
|
88
|
+
{onNavigateToTraffic && (
|
|
89
|
+
<p className="text-theme-text-tertiary">
|
|
90
|
+
Looking for observed, measured traffic?{' '}
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
onClick={onNavigateToTraffic}
|
|
94
|
+
className="text-skyhook-400 hover:text-skyhook-300 underline underline-offset-2"
|
|
95
|
+
>
|
|
96
|
+
Open Live Traffic →
|
|
97
|
+
</button>
|
|
98
|
+
</p>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
}
|
|
79
102
|
>
|
|
80
|
-
|
|
81
|
-
|
|
103
|
+
<button
|
|
104
|
+
onClick={() => onViewModeChange('traffic')}
|
|
105
|
+
className={`px-2.5 py-1 text-xs rounded-md transition-colors whitespace-nowrap ${
|
|
106
|
+
viewMode === 'traffic'
|
|
107
|
+
? 'bg-skyhook-600 text-white'
|
|
108
|
+
: 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated'
|
|
109
|
+
}`}
|
|
110
|
+
>
|
|
111
|
+
Network Flow <span className="opacity-70">(config)</span>
|
|
112
|
+
</button>
|
|
113
|
+
</Tooltip>
|
|
82
114
|
{showFleetMode && (
|
|
83
115
|
<button
|
|
84
116
|
onClick={() => onViewModeChange('fleet')}
|
|
@@ -9,11 +9,31 @@ import type { ReactNode } from 'react'
|
|
|
9
9
|
export type BadgeSeverity = 'success' | 'warning' | 'alert' | 'error' | 'info' | 'neutral'
|
|
10
10
|
export type BadgeSize = 'sm' | 'default'
|
|
11
11
|
|
|
12
|
+
// Intent tokens beyond severity/kind. Named by PURPOSE, not hue: two tokens may
|
|
13
|
+
// share a color value today yet stay separate names so a future retune moves
|
|
14
|
+
// exactly the right ones. See DESIGN.md "Badge decision tree".
|
|
15
|
+
//
|
|
16
|
+
// - protocol: transport/scheme tags (HTTP/TCP/UDP/…). Its OWN family because it
|
|
17
|
+
// is a recurring, meaningful axis and must NOT collide with severity hues
|
|
18
|
+
// (e.g. a green "HTTPS" must not read as "success").
|
|
19
|
+
// - note: neutral attention/FYI markers (cross-namespace, wildcard, default,
|
|
20
|
+
// immutable). Distinct from severity-info — it flags "noteworthy", not a status.
|
|
21
|
+
// - accent1/2/3: LOCAL categorical distinction (rw/ro, spot/on-demand,
|
|
22
|
+
// control-plane/worker) where the hue carries no cross-screen meaning — just
|
|
23
|
+
// "tell these sibling options apart". The only place sharing is intended.
|
|
24
|
+
// - structural: ports/paths/hosts/weights/names — neutral data fragments, not a
|
|
25
|
+
// status. Its own name so it can diverge from severity-neutral later.
|
|
26
|
+
export type BadgeTone = 'note' | 'accent1' | 'accent2' | 'accent3' | 'structural'
|
|
27
|
+
|
|
12
28
|
interface BadgeProps {
|
|
13
29
|
/** Severity-based coloring (status badges) */
|
|
14
30
|
severity?: BadgeSeverity
|
|
15
31
|
/** K8s resource kind coloring (kind badges) */
|
|
16
32
|
kind?: string
|
|
33
|
+
/** Transport protocol/scheme tag (http/https/tls/tcp/udp/grpc/h2) */
|
|
34
|
+
protocol?: string
|
|
35
|
+
/** Categorical/accent intent tone (see BadgeTone) */
|
|
36
|
+
tone?: BadgeTone
|
|
17
37
|
/** Explicit color class override (bypasses severity/kind lookup) */
|
|
18
38
|
colorClass?: string
|
|
19
39
|
/** Size variant */
|
|
@@ -135,12 +155,49 @@ const KIND: Record<string, string> = {
|
|
|
135
155
|
|
|
136
156
|
const DEFAULT_KIND_COLOR = 'bg-fuchsia-50 text-fuchsia-700 border-fuchsia-200 dark:bg-fuchsia-950/40 dark:text-fuchsia-400 dark:border-fuchsia-800/40'
|
|
137
157
|
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// PROTOCOL COLORS — transport/scheme tags. Deliberately avoid severity hues
|
|
160
|
+
// (emerald/amber/orange/red/sky) so a protocol never reads as a status.
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
const PROTOCOL: Record<string, string> = {
|
|
163
|
+
http: 'bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-950/50 dark:text-blue-400 dark:border-blue-700/40',
|
|
164
|
+
http2: 'bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-950/50 dark:text-blue-400 dark:border-blue-700/40',
|
|
165
|
+
h2: 'bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-950/50 dark:text-blue-400 dark:border-blue-700/40',
|
|
166
|
+
h2c: 'bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-950/50 dark:text-blue-400 dark:border-blue-700/40',
|
|
167
|
+
https: 'bg-teal-100 text-teal-800 border-teal-300 dark:bg-teal-950/50 dark:text-teal-400 dark:border-teal-700/40',
|
|
168
|
+
tls: 'bg-teal-100 text-teal-800 border-teal-300 dark:bg-teal-950/50 dark:text-teal-400 dark:border-teal-700/40',
|
|
169
|
+
tcp: 'bg-indigo-100 text-indigo-700 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-400 dark:border-indigo-700/40',
|
|
170
|
+
udp: 'bg-violet-100 text-violet-700 border-violet-300 dark:bg-violet-950/50 dark:text-violet-400 dark:border-violet-700/40',
|
|
171
|
+
grpc: 'bg-fuchsia-100 text-fuchsia-700 border-fuchsia-300 dark:bg-fuchsia-950/50 dark:text-fuchsia-400 dark:border-fuchsia-700/40',
|
|
172
|
+
'grpc-web': 'bg-fuchsia-100 text-fuchsia-700 border-fuchsia-300 dark:bg-fuchsia-950/50 dark:text-fuchsia-400 dark:border-fuchsia-700/40',
|
|
173
|
+
}
|
|
174
|
+
const DEFAULT_PROTOCOL_COLOR = 'bg-slate-100 text-slate-700 border-slate-300 dark:bg-slate-950/50 dark:text-slate-400 dark:border-slate-700/40'
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// TONE COLORS — note / local-categorical accents / structural.
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
const TONE: Record<BadgeTone, string> = {
|
|
180
|
+
// attention/FYI — distinct from severity so it doesn't read as warning/info
|
|
181
|
+
note: 'bg-violet-100 text-violet-700 border-violet-300 dark:bg-violet-950/50 dark:text-violet-400 dark:border-violet-700/40',
|
|
182
|
+
// three visually-distinct accents for local "tell siblings apart" use
|
|
183
|
+
accent1: 'bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-950/50 dark:text-blue-400 dark:border-blue-700/40',
|
|
184
|
+
accent2: 'bg-fuchsia-100 text-fuchsia-700 border-fuchsia-300 dark:bg-fuchsia-950/50 dark:text-fuchsia-400 dark:border-fuchsia-700/40',
|
|
185
|
+
accent3: 'bg-teal-100 text-teal-800 border-teal-300 dark:bg-teal-950/50 dark:text-teal-400 dark:border-teal-700/40',
|
|
186
|
+
// neutral data fragment (ports/paths/hosts/names)
|
|
187
|
+
structural: 'bg-theme-elevated text-theme-text-secondary border-theme-border',
|
|
188
|
+
}
|
|
189
|
+
|
|
138
190
|
// Structure classes
|
|
139
191
|
const SIZE_CLASSES: Record<BadgeSize, string> = {
|
|
140
192
|
default: 'badge',
|
|
141
193
|
sm: 'badge-sm',
|
|
142
194
|
}
|
|
143
195
|
|
|
196
|
+
/** Resolve a protocol/scheme label (case-insensitive) to its color class. */
|
|
197
|
+
export function getProtocolColorClass(protocol: string): string {
|
|
198
|
+
return PROTOCOL[protocol.toLowerCase().trim()] ?? DEFAULT_PROTOCOL_COLOR
|
|
199
|
+
}
|
|
200
|
+
|
|
144
201
|
/** Resolve kind color with fuzzy matching for plural/lowercase forms */
|
|
145
202
|
export function getKindColorClass(kind: string): string {
|
|
146
203
|
// Direct match
|
|
@@ -185,8 +242,14 @@ export function getSeverityColorClass(severity: BadgeSeverity): string {
|
|
|
185
242
|
* Badge component — the ONE source of truth for badge rendering.
|
|
186
243
|
* Use severity for status badges, kind for resource type badges, or colorClass for custom.
|
|
187
244
|
*/
|
|
188
|
-
export function Badge({ severity, kind, colorClass, size = 'default', className, onClick, title, children }: BadgeProps) {
|
|
189
|
-
const color =
|
|
245
|
+
export function Badge({ severity, kind, protocol, tone, colorClass, size = 'default', className, onClick, title, children }: BadgeProps) {
|
|
246
|
+
const color =
|
|
247
|
+
colorClass ??
|
|
248
|
+
(severity ? SEVERITY[severity]
|
|
249
|
+
: protocol ? getProtocolColorClass(protocol)
|
|
250
|
+
: tone ? TONE[tone]
|
|
251
|
+
: kind ? getKindColorClass(kind)
|
|
252
|
+
: '')
|
|
190
253
|
const cls = clsx(SIZE_CLASSES[size], color, className)
|
|
191
254
|
|
|
192
255
|
if (onClick) {
|
|
@@ -196,4 +259,4 @@ export function Badge({ severity, kind, colorClass, size = 'default', className,
|
|
|
196
259
|
}
|
|
197
260
|
|
|
198
261
|
// Re-export the raw color maps for backwards compat (used by badge-colors.ts consumers)
|
|
199
|
-
export { SEVERITY as BADGE_SEVERITY_COLORS, KIND as BADGE_KIND_COLORS }
|
|
262
|
+
export { SEVERITY as BADGE_SEVERITY_COLORS, KIND as BADGE_KIND_COLORS, PROTOCOL as BADGE_PROTOCOL_COLORS, TONE as BADGE_TONE_COLORS }
|
|
@@ -3,10 +3,18 @@ import { MiddleEllipsis } from './MiddleEllipsis'
|
|
|
3
3
|
import { Tooltip } from './Tooltip'
|
|
4
4
|
import { parseContextName } from '../../utils/context-name'
|
|
5
5
|
import type { ParsedContextName } from '../../utils/context-name'
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
6
|
+
import { assetUrl } from '../../utils/asset-url'
|
|
7
|
+
import awsLogoAsset from './provider-logos/aws.png'
|
|
8
|
+
import awsLogoDarkAsset from './provider-logos/aws-dark.png'
|
|
9
|
+
import gcpLogoAsset from './provider-logos/gcp.png'
|
|
10
|
+
import azureLogoAsset from './provider-logos/azure.svg'
|
|
11
|
+
|
|
12
|
+
// assetUrl normalizes the bundler-specific asset-import type (string under Vite,
|
|
13
|
+
// StaticImageData under webpack/Next) to a URL string usable in `<img src>`.
|
|
14
|
+
const awsLogo = assetUrl(awsLogoAsset)
|
|
15
|
+
const awsLogoDark = assetUrl(awsLogoDarkAsset)
|
|
16
|
+
const gcpLogo = assetUrl(gcpLogoAsset)
|
|
17
|
+
const azureLogo = assetUrl(azureLogoAsset)
|
|
10
18
|
|
|
11
19
|
// ClusterName renders a kubectl context string with the meaningful
|
|
12
20
|
// cluster identity surfaced as primary text and provider/region pushed
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { assetUrl } from '../../utils/asset-url'
|
|
2
|
+
import radarLoadingIconAsset from '../../assets/radar/radar-icon-loading.svg'
|
|
3
|
+
|
|
4
|
+
// assetUrl normalizes the bundler-specific asset-import type (string under Vite,
|
|
5
|
+
// StaticImageData under webpack/Next) to a URL string usable in `<img src>`.
|
|
6
|
+
const radarLoadingIcon = assetUrl(radarLoadingIconAsset)
|
|
2
7
|
|
|
3
8
|
// PaneLoader — center-of-pane loading state. Animated radar icon stacked
|
|
4
9
|
// above a label so swapping the label across the loading chain doesn't
|
package/src/types/core.ts
CHANGED
|
@@ -554,6 +554,8 @@ export interface HelmRelease {
|
|
|
554
554
|
status: string
|
|
555
555
|
revision: number
|
|
556
556
|
updated: string // ISO date string
|
|
557
|
+
lastOperation?: HelmOperation
|
|
558
|
+
operations?: HelmOperation[]
|
|
557
559
|
// Health summary from owned resources
|
|
558
560
|
resourceHealth?: 'healthy' | 'degraded' | 'unhealthy' | 'unknown'
|
|
559
561
|
healthIssue?: string // Primary issue if unhealthy (e.g., "OOMKilled")
|
|
@@ -574,6 +576,27 @@ export interface HelmRevision {
|
|
|
574
576
|
updated: string // ISO date string
|
|
575
577
|
}
|
|
576
578
|
|
|
579
|
+
export type HelmOperationKind = 'release_failed' | 'upgrade_failed' | 'upgrade_rolled_back' | 'rollback' | 'pending'
|
|
580
|
+
export type HelmOperationStatus = 'failed' | 'rolled_back' | 'completed' | 'stuck_pending'
|
|
581
|
+
export type HelmOperationConfidence = 'high' | 'medium' | 'low'
|
|
582
|
+
export type HelmOperationSource = 'helm_status' | 'helm_history'
|
|
583
|
+
|
|
584
|
+
export interface HelmOperation {
|
|
585
|
+
kind: HelmOperationKind
|
|
586
|
+
status: HelmOperationStatus
|
|
587
|
+
source: HelmOperationSource
|
|
588
|
+
confidence: HelmOperationConfidence
|
|
589
|
+
message: string
|
|
590
|
+
evidence?: string
|
|
591
|
+
failureDescription?: string
|
|
592
|
+
revision?: number
|
|
593
|
+
failedRevision?: number
|
|
594
|
+
rollbackRevision?: number
|
|
595
|
+
targetRevision?: number
|
|
596
|
+
pendingStatus?: string
|
|
597
|
+
updated?: string
|
|
598
|
+
}
|
|
599
|
+
|
|
577
600
|
export interface HelmReleaseDetail {
|
|
578
601
|
name: string
|
|
579
602
|
namespace: string
|
|
@@ -589,9 +612,14 @@ export interface HelmReleaseDetail {
|
|
|
589
612
|
notes: string
|
|
590
613
|
history: HelmRevision[]
|
|
591
614
|
resources: HelmOwnedResource[]
|
|
615
|
+
resourceHealth?: 'healthy' | 'degraded' | 'unhealthy' | 'unknown'
|
|
616
|
+
healthIssue?: string
|
|
617
|
+
healthSummary?: string
|
|
592
618
|
hooks?: HelmHook[]
|
|
593
619
|
readme?: string
|
|
594
620
|
dependencies?: ChartDependency[]
|
|
621
|
+
lastOperation?: HelmOperation
|
|
622
|
+
operations?: HelmOperation[]
|
|
595
623
|
// When set, this release was installed by Flux's helm-controller — see
|
|
596
624
|
// HelmRelease.managedByFluxHelmRelease for context. Format: "namespace/name".
|
|
597
625
|
managedByFluxHelmRelease?: string
|
|
@@ -649,7 +677,16 @@ export interface UpgradeInfo {
|
|
|
649
677
|
latestVersion?: string
|
|
650
678
|
updateAvailable: boolean
|
|
651
679
|
repositoryName?: string
|
|
680
|
+
// 'repository' for classic HTTP-repo matches, 'oci' when discovered via a
|
|
681
|
+
// registered OCI chart source. Absent when the source couldn't be determined.
|
|
682
|
+
sourceType?: 'repository' | 'oci'
|
|
683
|
+
// oci:// chart reference an OCI-sourced upgrade lives at (display only).
|
|
684
|
+
chartRef?: string
|
|
652
685
|
error?: string
|
|
686
|
+
// True only when the error is a genuinely untracked source (registering a
|
|
687
|
+
// chart source could fix it) — NOT for repo-side errors like a stale index or
|
|
688
|
+
// classic ambiguity. Gates the "track source" affordance.
|
|
689
|
+
untracked?: boolean
|
|
653
690
|
}
|
|
654
691
|
|
|
655
692
|
// Batch upgrade info keyed by "storageNamespace/name".
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// k8s-ui ships TS source, so each consumer's bundler types asset imports
|
|
2
|
+
// differently: Vite resolves `import x from './x.png'` to a URL string, while
|
|
3
|
+
// webpack/Next resolves it to a StaticImageData object. Reading `.src` off the
|
|
4
|
+
// object (vs Vite's bare string) yields the bundler's own emitted URL, which is
|
|
5
|
+
// the one that's correct in both client and SSR output — unlike
|
|
6
|
+
// `new URL(asset, import.meta.url)`, which can produce a `file://` URL under SSR.
|
|
7
|
+
// The structural `{ src: string }` type matches Next's StaticImageData without
|
|
8
|
+
// depending on the Next-only type, so this also type-checks under Vite.
|
|
9
|
+
export type AssetImport = string | { src: string }
|
|
10
|
+
|
|
11
|
+
export function assetUrl(asset: AssetImport): string {
|
|
12
|
+
return typeof asset === 'string' ? asset : asset.src
|
|
13
|
+
}
|