@skyhook-io/k8s-ui 1.14.0 → 1.14.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.
Files changed (48) hide show
  1. package/package.json +1 -1
  2. package/src/components/gitops/GitOpsTableView.tsx +168 -11
  3. package/src/components/gitops/applicationset-row.test.ts +214 -0
  4. package/src/components/gitops/detail-helpers.test.ts +54 -0
  5. package/src/components/gitops/detail-helpers.ts +13 -3
  6. package/src/components/gitops/index.ts +1 -0
  7. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +7 -0
  8. package/src/components/issues/IssuesView.tsx +1 -1
  9. package/src/components/issues/types.ts +2 -0
  10. package/src/components/resources/ResourcesView.tsx +382 -141
  11. package/src/components/resources/column-header-width.test.ts +75 -0
  12. package/src/components/resources/column-settings-merge.test.ts +104 -0
  13. package/src/components/resources/istio-gateway-filter.test.ts +5 -3
  14. package/src/components/resources/istio-policy-accessors.test.ts +193 -0
  15. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +11 -24
  16. package/src/components/resources/renderers/eso-cells.tsx +0 -5
  17. package/src/components/resources/renderers/istio-cells.tsx +36 -26
  18. package/src/components/resources/renderers/kueue-cells.tsx +0 -9
  19. package/src/components/resources/renderers/kyverno-cells.tsx +1 -1
  20. package/src/components/resources/renderers/prometheus-cells.tsx +3 -3
  21. package/src/components/resources/renderers/ray-cells.tsx +12 -4
  22. package/src/components/resources/renderers/volcano-cells.tsx +0 -9
  23. package/src/components/resources/resource-utils-cnpg.ts +5 -0
  24. package/src/components/resources/resource-utils-eso.test.ts +56 -0
  25. package/src/components/resources/resource-utils-eso.ts +37 -5
  26. package/src/components/resources/resource-utils-istio.ts +152 -44
  27. package/src/components/resources/resource-utils-kyverno.ts +3 -6
  28. package/src/components/resources/resource-utils-prometheus.ts +51 -23
  29. package/src/components/resources/resource-utils-ray.test.ts +36 -0
  30. package/src/components/resources/resource-utils-ray.ts +6 -0
  31. package/src/components/resources/resource-utils.ts +166 -18
  32. package/src/components/resources/rules-column-width.test.ts +42 -0
  33. package/src/components/resources/status-accessors.test.ts +363 -0
  34. package/src/components/resources/status-overclaim.test.ts +158 -0
  35. package/src/components/shared/ResourceRendererDispatch.test.tsx +75 -0
  36. package/src/components/shared/ResourceRendererDispatch.tsx +24 -6
  37. package/src/components/timeline/DiffViewer.test.tsx +26 -0
  38. package/src/components/timeline/DiffViewer.tsx +16 -12
  39. package/src/components/topology/GroupNode.tsx +6 -2
  40. package/src/components/topology/TopologyGraph.tsx +7 -2
  41. package/src/components/topology/topology.css +4 -1
  42. package/src/components/ui/Collapse.tsx +18 -3
  43. package/src/components/ui/ConfirmDialog.test.tsx +37 -0
  44. package/src/components/ui/ConfirmDialog.tsx +3 -1
  45. package/src/components/ui/RowActionMenu.tsx +4 -0
  46. package/src/components/ui/drawer-components.tsx +5 -3
  47. package/src/components/workload/WorkloadView.tsx +11 -3
  48. package/src/types/gitops.ts +51 -0
@@ -110,6 +110,7 @@ import {
110
110
  getGatewayClassController,
111
111
  getGatewayClassDescription,
112
112
  getRouteStatus,
113
+ getRouteStatusReason,
113
114
  getRouteParents,
114
115
  getRouteHostnames,
115
116
  getRouteBackends,
@@ -227,6 +228,12 @@ export const SKIP_FILTER_COLUMNS = new Set([
227
228
  'lastUpdated', 'chart', 'events', 'repo',
228
229
  'generators', 'applications', 'destinations', 'sources', 'budget', 'healthy', 'allowed',
229
230
  'secrets', 'subjects', 'role', 'entrypoint', 'templates',
231
+ // Trivy and Kyverno report counts. These look filterable but cannot be:
232
+ // getCellFilterValue has no case for them and its fallback probes
233
+ // status[key]/spec[key], while the counts live under report.summary /
234
+ // status.summary. Listing them here stops the header reserving room for a
235
+ // button that can never render.
236
+ 'critical', 'high', 'medium', 'low', 'pass', 'fail', 'warn', 'error', 'skip',
230
237
  ])
231
238
 
232
239
  // Namespace/node cardinality scales with cluster size, not kind enum size;
@@ -240,11 +247,10 @@ export function isColumnFilterableByDistinctCount(colKey: string, distinctCount:
240
247
  }
241
248
 
242
249
  // Column definitions per resource kind
243
- interface Column {
250
+ export interface Column {
244
251
  key: string
245
252
  label: string
246
253
  width?: string
247
- hideOnMobile?: boolean
248
254
  tooltip?: string // Explanation of what this column means
249
255
  defaultVisible?: boolean // false = hidden by default, shown via column picker
250
256
  defaultWidth?: number // default width in px (used for resizable columns)
@@ -321,7 +327,179 @@ const COMPARE_COLUMN_STYLE: React.CSSProperties = {
321
327
  maxWidth: COMPARE_COLUMN_WIDTH,
322
328
  }
323
329
 
324
- function getColumnMinWidth(col: Column): number {
330
+ // Built-in sortable keys. Hoisted so getColumnMinWidth can reserve room for the
331
+ // sort icon without the header render and the width math drifting apart.
332
+ const BUILTIN_SORTABLE_COLUMN_KEYS = new Set([
333
+ 'name', 'namespace', 'age', 'status', 'ready', 'restarts', 'type', 'version',
334
+ 'desired', 'available', 'upToDate', 'lastSeen', 'count', 'reason', 'object',
335
+ 'cpu', 'memory', 'containers',
336
+ ])
337
+
338
+ // The header row is px-4 with a `truncate` label, under table-layout:fixed with
339
+ // explicit <col> widths — so a column narrower than its own heading clips to
340
+ // "ADDRESS T…" and stays clipped at every viewport size, because only the Name
341
+ // column flexes. Deriving a floor from the label keeps a declared w-* from
342
+ // being narrower than the word it has to show.
343
+ const HEADER_PADDING_PX = 32 // px-4 on both sides
344
+ const HEADER_SORT_AFFORDANCE_PX = 20 // gap-1 + the w-3.5 chevron / ArrowUpDown
345
+ const HEADER_FILTER_AFFORDANCE_PX = 20 // gap-1 + the p-0.5 filter button
346
+ // Deliberately an upper bound on the rendered header font (DM Sans 500, 12px,
347
+ // uppercase, tracking-wide): measured per-character widths across the curated
348
+ // labels ranged 7.2–8.1px, so 8.2 never under-reserves. Over-reserving costs
349
+ // space the flexible Name column has to spare; under-reserving clips again.
350
+ // Printer and host-extra columns take their label from vendor CRD text, which
351
+ // can be long or non-Latin. Cap the floor so one such label can't size a column
352
+ // off the screen; past this a truncated heading (with its tooltip) is better.
353
+ const HEADER_FLOOR_MAX_PX = 320
354
+
355
+ // Must match the rendered header: text-xs (12px) / font-medium (500) / uppercase
356
+ // / tracking-wide, in the app font. measureText does not apply letter-spacing,
357
+ // so it is added per gap.
358
+ const HEADER_FONT = "500 12px 'DM Sans Variable', 'DM Sans', system-ui, sans-serif"
359
+ const HEADER_LETTER_SPACING_PX = 0.3 // tracking-wide = 0.025em at 12px
360
+ // Fallback only — used where there is no canvas (SSR, jsdom). Deliberately an
361
+ // upper bound: measured per-character widths across the curated labels ranged
362
+ // 7.2–8.1px, so this never under-reserves for Latin text.
363
+ const HEADER_CHAR_PX_FALLBACK = 8.2
364
+ const HEADER_SPACE_PX_FALLBACK = 4
365
+
366
+ let headerMeasureCtx: CanvasRenderingContext2D | null | undefined
367
+ const headerLabelWidthCache = new Map<string, number>()
368
+
369
+ // DM Sans is loaded with font-display:swap, so the first tables can render — and
370
+ // be measured — while the browser is still painting the fallback face. Those
371
+ // measurements would otherwise be cached against the wrong glyph metrics for the
372
+ // rest of the session: a wider real face clips a label whose tooltip never
373
+ // activates, a narrower one leaves the table permanently too wide. Drop the
374
+ // cache when the real font arrives and let subscribers recompute.
375
+ let headerFontEpoch = 0
376
+ const headerFontListeners = new Set<() => void>()
377
+ let headerFontWatchStarted = false
378
+
379
+ function watchHeaderFont(): void {
380
+ if (headerFontWatchStarted) return
381
+ headerFontWatchStarted = true
382
+ const fonts = typeof document === 'undefined' ? undefined : (document as Document & { fonts?: FontFaceSet }).fonts
383
+ if (!fonts?.ready) return
384
+ void fonts.ready.then(() => {
385
+ headerLabelWidthCache.clear()
386
+ headerMeasureCtx = undefined // re-created so ctx.font resolves against the loaded face
387
+ headerFontEpoch++
388
+ for (const notify of headerFontListeners) notify()
389
+ })
390
+ }
391
+
392
+ /**
393
+ * Re-renders once the web font has loaded, so column widths measured against the
394
+ * fallback face are recomputed against the real one.
395
+ */
396
+ export function useHeaderFontEpoch(): number {
397
+ const [epoch, setEpoch] = useState(headerFontEpoch)
398
+ useEffect(() => {
399
+ const notify = () => setEpoch(headerFontEpoch)
400
+ headerFontListeners.add(notify)
401
+ notify() // the font may have loaded between render and effect
402
+ return () => { headerFontListeners.delete(notify) }
403
+ }, [])
404
+ return epoch
405
+ }
406
+
407
+ /**
408
+ * Width of a header label as the browser will actually draw it.
409
+ *
410
+ * Measured rather than estimated because the labels are not a closed set: an
411
+ * uncurated CRD's printer columns take their names from the vendor's own
412
+ * additionalPrinterColumns, so they can be any length, any script, and a
413
+ * per-character constant calibrated on curated ASCII silently stops holding.
414
+ */
415
+ function headerLabelWidth(label: string): number {
416
+ watchHeaderFont()
417
+ const cached = headerLabelWidthCache.get(label)
418
+ if (cached !== undefined) return cached
419
+ const upper = label.toUpperCase() // the header renders uppercase
420
+ let width: number
421
+ if (headerMeasureCtx === undefined) {
422
+ headerMeasureCtx = typeof document === 'undefined'
423
+ ? null
424
+ : document.createElement('canvas').getContext('2d')
425
+ if (headerMeasureCtx) headerMeasureCtx.font = HEADER_FONT
426
+ }
427
+ if (headerMeasureCtx) {
428
+ width = headerMeasureCtx.measureText(upper).width
429
+ + HEADER_LETTER_SPACING_PX * Math.max(0, upper.length - 1)
430
+ } else {
431
+ width = 0
432
+ for (const ch of upper) width += ch === ' ' ? HEADER_SPACE_PX_FALLBACK : HEADER_CHAR_PX_FALLBACK
433
+ }
434
+ headerLabelWidthCache.set(label, width)
435
+ return width
436
+ }
437
+
438
+ /**
439
+ * Header label that carries its own full text in a tooltip when it truncates.
440
+ *
441
+ * The width floor in getColumnMinWidth keeps a heading from being clipped by its
442
+ * own column, but three cases still truncate: a label past the floor cap, a
443
+ * column the user dragged narrower, and the sort/filter controls sharing the
444
+ * row — whose width depends on their state (an active filter renders padding,
445
+ * an icon and a count). Their rendered width isn't knowable from the column
446
+ * definition, so this reads the element's actual overflow rather than computing
447
+ * it.
448
+ *
449
+ * The Tooltip wrapper stays mounted whether or not the label truncates: swapping
450
+ * it in on overflow would remount the measured span, stranding the
451
+ * ResizeObserver on the detached node so the state could never flip back. It
452
+ * also needs min-w-0 — the wrapper is the flex item in the header row, and an
453
+ * inline-flex box won't shrink below its content unless told to, which would
454
+ * push the sort and filter controls out of the cell.
455
+ */
456
+ function HeaderLabel({ label, className }: { label: string; className?: string }) {
457
+ const ref = useRef<HTMLSpanElement>(null)
458
+ const [truncated, setTruncated] = useState(false)
459
+ useEffect(() => {
460
+ const el = ref.current
461
+ if (!el) return
462
+ const check = () => setTruncated(el.scrollWidth > el.clientWidth + 0.5)
463
+ check()
464
+ if (typeof ResizeObserver === 'undefined') return
465
+ const ro = new ResizeObserver(check)
466
+ ro.observe(el)
467
+ return () => { ro.disconnect() }
468
+ }, [label])
469
+ return (
470
+ <Tooltip content={label} disabled={!truncated} preserveWrapperWhenDisabled wrapperClassName="min-w-0">
471
+ <span ref={ref} className={clsx('truncate', className)}>{label}</span>
472
+ </Tooltip>
473
+ )
474
+ }
475
+
476
+ /**
477
+ * Whether a column's header renders a sort control, which takes width beside the
478
+ * label. Shared by the header render and the width math so the two can't
479
+ * disagree: printer and custom columns are sortable through their own
480
+ * getSortValue even though their keys aren't in the built-in list.
481
+ */
482
+ export function isColumnSortable(col: Column, extras?: Map<string, ExtraColumn>): boolean {
483
+ return BUILTIN_SORTABLE_COLUMN_KEYS.has(col.key) || !!extras?.get(col.key)?.getSortValue
484
+ }
485
+
486
+ export function getColumnMinWidth(col: Column, extras?: Map<string, ExtraColumn>): number {
487
+ const declared = declaredColumnWidth(col)
488
+ // A column-filter button also sits in this row, but whether one renders
489
+ // depends on the distinct values in the data (isColumnFilterableByDistinctCount),
490
+ // which isn't knowable here — those columns stay ~20px tighter than ideal.
491
+ // Both controls are reserved statically. The filter button only renders once
492
+ // the data has a filterable spread of values (isColumnFilterableByDistinctCount),
493
+ // but sizing the column from that would make it change width as rows load or
494
+ // are filtered — the jumping-column problem fixed layout exists to avoid. So
495
+ // reserve for any column that could ever carry one, and keep the width stable.
496
+ const affordance = (isColumnSortable(col, extras) ? HEADER_SORT_AFFORDANCE_PX : 0)
497
+ + (SKIP_FILTER_COLUMNS.has(col.key) ? 0 : HEADER_FILTER_AFFORDANCE_PX)
498
+ const headerFloor = HEADER_PADDING_PX + headerLabelWidth(col.label) + affordance
499
+ return Math.max(declared, Math.min(Math.ceil(headerFloor), HEADER_FLOOR_MAX_PX))
500
+ }
501
+
502
+ function declaredColumnWidth(col: Column): number {
325
503
  if (col.minWidth) return col.minWidth
326
504
  if (!col.width) return 200 // Name column (no width class) gets wider minimum
327
505
  const match = col.width.match(/(?:min-)?w-(\d+)/)
@@ -372,7 +550,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
372
550
  { key: 'restarts', label: 'Restarts', width: 'w-28' },
373
551
  { key: 'gpu', label: 'GPU', width: 'w-20', tooltip: 'Effective GPU request (requests, falling back to limits) summed across containers', defaultVisible: false },
374
552
  { key: 'podIP', label: 'Pod IP', width: 'w-32', defaultVisible: false },
375
- { key: 'node', label: 'Node', width: 'w-44', hideOnMobile: true },
553
+ { key: 'node', label: 'Node', width: 'w-44' },
376
554
  { key: 'age', label: 'Age', width: 'w-24' },
377
555
  ],
378
556
  deployments: [
@@ -380,20 +558,20 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
380
558
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
381
559
  { key: 'status', label: 'Status', width: 'w-40' },
382
560
  { key: 'ready', label: 'Ready', width: 'w-24', tooltip: 'Ready pods / Desired replicas' },
383
- { key: 'upToDate', label: 'Up-to-date', width: 'w-32', hideOnMobile: true, tooltip: 'Number of pods running the current pod template' },
384
- { key: 'available', label: 'Available', width: 'w-28', hideOnMobile: true, tooltip: 'Number of pods available (ready for minReadySeconds)' },
385
- { key: 'images', label: 'Images', width: 'w-48', hideOnMobile: true },
561
+ { key: 'upToDate', label: 'Up-to-date', width: 'w-32', tooltip: 'Number of pods running the current pod template' },
562
+ { key: 'available', label: 'Available', width: 'w-28', tooltip: 'Number of pods available (ready for minReadySeconds)' },
563
+ { key: 'images', label: 'Images', width: 'w-48' },
386
564
  { key: 'age', label: 'Age', width: 'w-24' },
387
565
  ],
388
566
  daemonsets: [
389
567
  { key: 'name', label: 'Name' },
390
568
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
391
569
  { key: 'status', label: 'Status', width: 'w-40' },
392
- { key: 'desired', label: 'Desired', width: 'w-20', tooltip: 'Number of nodes that should run the daemon pod (based on node selector)' },
570
+ { key: 'desired', label: 'Desired', width: 'w-20', tooltip: 'Nodes eligible for this daemon after node selectors, required node affinity and taints/tolerations are applied' },
393
571
  { key: 'ready', label: 'Ready', width: 'w-20', tooltip: 'Number of pods that are ready (passing readiness probes)' },
394
- { key: 'upToDate', label: 'Up-to-date', width: 'w-32', hideOnMobile: true, tooltip: 'Number of pods running the current pod template spec' },
395
- { key: 'available', label: 'Available', width: 'w-28', hideOnMobile: true, tooltip: 'Number of pods available (ready for minReadySeconds duration)' },
396
- { key: 'images', label: 'Images', width: 'w-48', hideOnMobile: true },
572
+ { key: 'upToDate', label: 'Up-to-date', width: 'w-32', tooltip: 'Number of pods running the current pod template spec' },
573
+ { key: 'available', label: 'Available', width: 'w-28', tooltip: 'Number of pods available (ready for minReadySeconds duration)' },
574
+ { key: 'images', label: 'Images', width: 'w-48' },
397
575
  { key: 'age', label: 'Age', width: 'w-24' },
398
576
  ],
399
577
  statefulsets: [
@@ -401,8 +579,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
401
579
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
402
580
  { key: 'status', label: 'Status', width: 'w-40' },
403
581
  { key: 'ready', label: 'Ready', width: 'w-24', tooltip: 'Ready pods / Desired replicas' },
404
- { key: 'upToDate', label: 'Up-to-date', width: 'w-32', hideOnMobile: true, tooltip: 'Number of pods running the current pod template' },
405
- { key: 'images', label: 'Images', width: 'w-48', hideOnMobile: true },
582
+ { key: 'upToDate', label: 'Up-to-date', width: 'w-32', tooltip: 'Pods running the current pod template; a rolling-update partition or OnDelete strategy can intentionally leave pods on an older template' },
583
+ { key: 'images', label: 'Images', width: 'w-48' },
406
584
  { key: 'age', label: 'Age', width: 'w-24' },
407
585
  ],
408
586
  replicasets: [
@@ -410,17 +588,17 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
410
588
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
411
589
  { key: 'ready', label: 'Ready', width: 'w-24' },
412
590
  { key: 'owner', label: 'Owner', width: 'w-48' },
413
- { key: 'status', label: 'Status', width: 'w-24', hideOnMobile: true },
591
+ { key: 'status', label: 'Status', width: 'w-24', tooltip: 'Active means desired replicas are greater than zero; Old means desired replicas are zero, regardless of Deployment revision' },
414
592
  { key: 'age', label: 'Age', width: 'w-24' },
415
593
  ],
416
594
  services: [
417
595
  { key: 'name', label: 'Name' },
418
596
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
419
597
  { key: 'type', label: 'Type', width: 'w-28' },
420
- { key: 'selector', label: 'Selector', width: 'w-48', hideOnMobile: true },
598
+ { key: 'selector', label: 'Selector', width: 'w-48' },
421
599
  { key: 'endpoints', label: 'Endpoints', width: 'w-24' },
422
600
  { key: 'ports', label: 'Ports', width: 'w-40' },
423
- { key: 'externalIP', label: 'External', width: 'w-40', hideOnMobile: true },
601
+ { key: 'externalIP', label: 'External', width: 'w-40' },
424
602
  { key: 'age', label: 'Age', width: 'w-24' },
425
603
  ],
426
604
  endpointslices: [
@@ -430,7 +608,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
430
608
  { key: 'addressType', label: 'Address Type', width: 'w-28' },
431
609
  { key: 'endpoints', label: 'Endpoints', width: 'w-32' },
432
610
  { key: 'addresses', label: 'Addresses', width: 'w-24' },
433
- { key: 'ports', label: 'Ports', width: 'w-40', hideOnMobile: true },
611
+ { key: 'ports', label: 'Ports', width: 'w-40' },
434
612
  { key: 'age', label: 'Age', width: 'w-24' },
435
613
  ],
436
614
  ingresses: [
@@ -438,21 +616,21 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
438
616
  { key: 'namespace', label: 'Namespace', width: 'w-36 shrink-0' },
439
617
  { key: 'class', label: 'Class', width: 'w-24 shrink-0' },
440
618
  { key: 'hosts', label: 'Hosts', width: 'min-w-48' },
441
- { key: 'rules', label: 'Rules', width: 'min-w-56', hideOnMobile: true },
619
+ { key: 'rules', label: 'Rules', width: 'min-w-56' },
442
620
  { key: 'tls', label: 'TLS', width: 'w-14 shrink-0' },
443
- { key: 'address', label: 'Address', width: 'min-w-32', hideOnMobile: true },
621
+ { key: 'address', label: 'Address', width: 'min-w-32' },
444
622
  { key: 'age', label: 'Age', width: 'w-24 shrink-0' },
445
623
  ],
446
624
  nodes: [
447
625
  { key: 'name', label: 'Name' },
448
626
  { key: 'status', label: 'Status', width: 'w-44' },
449
- { key: 'roles', label: 'Roles', width: 'w-28' },
627
+ { key: 'roles', label: 'Roles', width: 'w-28', tooltip: 'Node roles from node-role.kubernetes.io/* labels. A node with no role label shows as worker.' },
450
628
  { key: 'cpu', label: 'CPU', width: 'w-40', tooltip: 'Current CPU usage / allocatable' },
451
629
  { key: 'memory', label: 'Memory', width: 'w-40', tooltip: 'Current memory usage / allocatable' },
452
630
  { key: 'pods', label: 'Pods', width: 'w-28', tooltip: 'Pods running / allocatable' },
453
631
  { key: 'gpu', label: 'GPUs', width: 'w-20', tooltip: 'Allocatable GPUs', defaultVisible: false },
454
- { key: 'conditions', label: 'Conditions', width: 'w-40', hideOnMobile: true },
455
- { key: 'taints', label: 'Taints', width: 'w-24', hideOnMobile: true },
632
+ { key: 'conditions', label: 'Conditions', width: 'w-40' },
633
+ { key: 'taints', label: 'Taints', width: 'w-24' },
456
634
  { key: 'version', label: 'Version', width: 'w-28' },
457
635
  { key: 'age', label: 'Age', width: 'w-24' },
458
636
  ],
@@ -460,7 +638,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
460
638
  { key: 'name', label: 'Name' },
461
639
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
462
640
  { key: 'keys', label: 'Keys', width: 'w-48' },
463
- { key: 'size', label: 'Size', width: 'w-24' },
641
+ { key: 'size', label: 'Size', width: 'w-24', tooltip: 'Estimated size of data values, including decoded binary data. Kubernetes rejects ConfigMaps whose combined data exceeds 1 MiB.' },
464
642
  { key: 'age', label: 'Age', width: 'w-24' },
465
643
  ],
466
644
  secrets: [
@@ -468,7 +646,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
468
646
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
469
647
  { key: 'type', label: 'Type', width: 'w-28' },
470
648
  { key: 'keys', label: 'Keys', width: 'w-20' },
471
- { key: 'expires', label: 'Expires', width: 'w-24', tooltip: 'Certificate expiry for TLS secrets' },
649
+ { key: 'expires', label: 'Expires', width: 'w-24', tooltip: "Days until the certificate in a TLS Secret expires. Blank for Secrets carrying no readable TLS certificate; ! means the expiry lookup itself failed." },
472
650
  { key: 'age', label: 'Age', width: 'w-24' },
473
651
  ],
474
652
  jobs: [
@@ -476,15 +654,15 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
476
654
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
477
655
  { key: 'status', label: 'Status', width: 'w-28' },
478
656
  { key: 'completions', label: 'Completions', width: 'w-32' },
479
- { key: 'duration', label: 'Duration', width: 'w-24', hideOnMobile: true },
657
+ { key: 'duration', label: 'Duration', width: 'w-24' },
480
658
  { key: 'age', label: 'Age', width: 'w-24' },
481
659
  ],
482
660
  cronjobs: [
483
661
  { key: 'name', label: 'Name' },
484
662
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
485
- { key: 'schedule', label: 'Schedule', width: 'w-40' },
663
+ { key: 'schedule', label: 'Schedule', width: 'w-40', tooltip: "Cron schedule; fires in spec.timeZone when set, otherwise the controller's zone" },
486
664
  { key: 'status', label: 'Status', width: 'w-28' },
487
- { key: 'lastRun', label: 'Last Run', width: 'w-28', hideOnMobile: true },
665
+ { key: 'lastRun', label: 'Last Run', width: 'w-28' },
488
666
  { key: 'age', label: 'Age', width: 'w-24' },
489
667
  ],
490
668
  hpas: [
@@ -492,7 +670,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
492
670
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
493
671
  { key: 'target', label: 'Target', width: 'w-48' },
494
672
  { key: 'replicas', label: 'Replicas', width: 'w-32' },
495
- { key: 'metrics', label: 'Metrics', width: 'w-36', hideOnMobile: true },
673
+ { key: 'metrics', label: 'Metrics', width: 'w-36' },
496
674
  { key: 'status', label: 'Status', width: 'w-28' },
497
675
  { key: 'age', label: 'Age', width: 'w-24' },
498
676
  ],
@@ -501,18 +679,18 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
501
679
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
502
680
  { key: 'target', label: 'Target', width: 'w-48' },
503
681
  { key: 'replicas', label: 'Replicas', width: 'w-32' },
504
- { key: 'metrics', label: 'Metrics', width: 'w-36', hideOnMobile: true },
682
+ { key: 'metrics', label: 'Metrics', width: 'w-36' },
505
683
  { key: 'status', label: 'Status', width: 'w-28' },
506
684
  { key: 'age', label: 'Age', width: 'w-24' },
507
685
  ],
508
686
  persistentvolumeclaims: [
509
687
  { key: 'name', label: 'Name' },
510
688
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
511
- { key: 'status', label: 'Status', width: 'w-24' },
689
+ { key: 'status', label: 'Status', width: 'w-24', tooltip: 'With WaitForFirstConsumer, Pending can mean no pod has triggered binding yet. If a pod is waiting, check PVC events.' },
512
690
  { key: 'capacity', label: 'Capacity', width: 'w-24' },
513
- { key: 'storageClass', label: 'Storage Class', width: 'w-40', hideOnMobile: true },
514
- { key: 'accessModes', label: 'Access', width: 'w-20', tooltip: 'Access modes: RWO=ReadWriteOnce, RWX=ReadWriteMany, ROX=ReadOnlyMany' },
515
- { key: 'volume', label: 'Volume', width: 'w-48', hideOnMobile: true },
691
+ { key: 'storageClass', label: 'Storage Class', width: 'w-40' },
692
+ { key: 'accessModes', label: 'Access', width: 'w-20', tooltip: 'RWO=ReadWriteOnce (one node, possibly several pods), ROX=ReadOnlyMany (read-only on many nodes), RWX=ReadWriteMany (read-write on many nodes), RWOP=ReadWriteOncePod (one pod only)' },
693
+ { key: 'volume', label: 'Volume', width: 'w-48' },
516
694
  { key: 'age', label: 'Age', width: 'w-24' },
517
695
  ],
518
696
  rollouts: [
@@ -521,8 +699,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
521
699
  { key: 'status', label: 'Status', width: 'w-32' },
522
700
  { key: 'ready', label: 'Ready', width: 'w-24', tooltip: 'Available / Desired replicas' },
523
701
  { key: 'strategy', label: 'Strategy', width: 'w-24' },
524
- { key: 'step', label: 'Step', width: 'w-20', hideOnMobile: true, tooltip: 'Current canary step / Total steps' },
525
- { key: 'images', label: 'Images', width: 'w-48', hideOnMobile: true },
702
+ { key: 'step', label: 'Step', width: 'w-20', tooltip: 'Current canary step / Total steps' },
703
+ { key: 'images', label: 'Images', width: 'w-48' },
526
704
  { key: 'age', label: 'Age', width: 'w-24' },
527
705
  ],
528
706
  analysisruns: [
@@ -530,7 +708,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
530
708
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
531
709
  { key: 'status', label: 'Phase', width: 'w-28' },
532
710
  { key: 'trigger', label: 'Trigger', width: 'w-32', tooltip: 'Which part of the Rollout started this run' },
533
- { key: 'metrics', label: 'Metrics', width: 'w-40', hideOnMobile: true, tooltip: 'Per-metric verdicts' },
711
+ { key: 'metrics', label: 'Metrics', width: 'w-40', tooltip: 'Per-metric verdicts' },
534
712
  { key: 'age', label: 'Age', width: 'w-24' },
535
713
  ],
536
714
  workflows: [
@@ -538,8 +716,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
538
716
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
539
717
  { key: 'status', label: 'Phase', width: 'w-28' },
540
718
  { key: 'duration', label: 'Duration', width: 'w-24' },
541
- { key: 'progress', label: 'Progress', width: 'w-24', hideOnMobile: true, tooltip: 'Succeeded steps / Total steps' },
542
- { key: 'template', label: 'Template', width: 'w-40', hideOnMobile: true },
719
+ { key: 'progress', label: 'Progress', width: 'w-24', tooltip: 'Succeeded steps / Total steps' },
720
+ { key: 'template', label: 'Template', width: 'w-40' },
543
721
  { key: 'age', label: 'Age', width: 'w-24' },
544
722
  ],
545
723
  cronworkflows: [
@@ -547,8 +725,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
547
725
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
548
726
  { key: 'schedule', label: 'Schedule', width: 'w-40' },
549
727
  { key: 'status', label: 'Status', width: 'w-28' },
550
- { key: 'lastRun', label: 'Last Run', width: 'w-28', hideOnMobile: true },
551
- { key: 'template', label: 'Template', width: 'w-40', hideOnMobile: true },
728
+ { key: 'lastRun', label: 'Last Run', width: 'w-28' },
729
+ { key: 'template', label: 'Template', width: 'w-40' },
552
730
  { key: 'age', label: 'Age', width: 'w-24' },
553
731
  ],
554
732
  certificates: [
@@ -556,25 +734,25 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
556
734
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
557
735
  { key: 'status', label: 'Ready', width: 'w-24' },
558
736
  { key: 'domains', label: 'Domains', width: 'w-56' },
559
- { key: 'issuer', label: 'Issuer', width: 'w-36', hideOnMobile: true },
737
+ { key: 'issuer', label: 'Issuer', width: 'w-36', tooltip: "Issuer name alone may be ambiguous. Check the certificate's issuer reference for its kind and API group." },
560
738
  { key: 'expires', label: 'Expires', width: 'w-24', tooltip: 'Days until certificate expires' },
561
739
  { key: 'age', label: 'Age', width: 'w-24' },
562
740
  ],
563
741
  persistentvolumes: [
564
742
  { key: 'name', label: 'Name' },
565
- { key: 'status', label: 'Status', width: 'w-24' },
743
+ { key: 'status', label: 'Status', width: 'w-24', tooltip: "Released means the claim is gone but the volume still holds the previous claimant's data, so it will not be re-bound — check Reclaim before reusing it." },
566
744
  { key: 'capacity', label: 'Capacity', width: 'w-24' },
567
- { key: 'accessModes', label: 'Access', width: 'w-20', tooltip: 'RWO=ReadWriteOnce, ROX=ReadOnlyMany, RWX=ReadWriteMany' },
568
- { key: 'reclaimPolicy', label: 'Reclaim', width: 'w-20' },
569
- { key: 'storageClass', label: 'Storage Class', width: 'w-40', hideOnMobile: true },
570
- { key: 'claim', label: 'Claim', width: 'w-48', hideOnMobile: true },
745
+ { key: 'accessModes', label: 'Access', width: 'w-20', tooltip: 'RWO=ReadWriteOnce (one node, possibly several pods), ROX=ReadOnlyMany (read-only on many nodes), RWX=ReadWriteMany (read-write on many nodes), RWOP=ReadWriteOncePod (one pod only)' },
746
+ { key: 'reclaimPolicy', label: 'Reclaim', width: 'w-20', tooltip: 'When the claim is deleted, Delete removes the backing storage and its data; Retain leaves it for manual reclamation.' },
747
+ { key: 'storageClass', label: 'Storage Class', width: 'w-40' },
748
+ { key: 'claim', label: 'Claim', width: 'w-48' },
571
749
  { key: 'age', label: 'Age', width: 'w-24' },
572
750
  ],
573
751
  storageclasses: [
574
752
  { key: 'name', label: 'Name' },
575
753
  { key: 'provisioner', label: 'Provisioner', width: 'w-48' },
576
- { key: 'reclaimPolicy', label: 'Reclaim', width: 'w-20' },
577
- { key: 'bindingMode', label: 'Binding Mode', width: 'w-36' },
754
+ { key: 'reclaimPolicy', label: 'Reclaim', width: 'w-20', tooltip: 'Default policy for volumes this class provisions — Delete removes the backing storage when the claim goes away; Retain leaves it for manual reclamation.' },
755
+ { key: 'bindingMode', label: 'Binding Mode', width: 'w-36', tooltip: "When the volume is provisioned — WaitForFirstConsumer waits for a pod so the disk lands in the right zone; Immediate provisions before the pod's placement is known, which can leave the pod unschedulable." },
578
756
  { key: 'expansion', label: 'Expansion', width: 'w-24', tooltip: 'Whether volumes can be expanded after creation' },
579
757
  { key: 'age', label: 'Age', width: 'w-24' },
580
758
  ],
@@ -602,38 +780,40 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
602
780
  orders: [
603
781
  { key: 'name', label: 'Name' },
604
782
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
605
- { key: 'state', label: 'State', width: 'w-24' },
783
+ { key: 'state', label: 'State', width: 'w-24', tooltip: 'Valid means ACME issuance finished; it does not confirm the TLS Secret is ready. If Pending persists, inspect the challenges and the order reason.' },
606
784
  { key: 'domains', label: 'Domains', width: 'w-48' },
607
- { key: 'issuer', label: 'Issuer', width: 'w-36', hideOnMobile: true },
785
+ { key: 'issuer', label: 'Issuer', width: 'w-36' },
608
786
  { key: 'age', label: 'Age', width: 'w-24' },
609
787
  ],
610
788
  challenges: [
611
789
  { key: 'name', label: 'Name' },
612
790
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
613
791
  { key: 'challengeType', label: 'Type', width: 'w-20' },
614
- { key: 'state', label: 'State', width: 'w-24' },
792
+ { key: 'state', label: 'State', width: 'w-24', tooltip: 'Valid means domain authorization succeeded. If Pending persists, inspect the reason for scheduling, solver, or propagation problems.' },
615
793
  { key: 'domain', label: 'Domain', width: 'w-48' },
616
- { key: 'presented', label: 'Presented', width: 'w-24', hideOnMobile: true },
794
+ { key: 'presented', label: 'Presented', width: 'w-24', tooltip: 'While Pending, Yes means the solver presented the token, not that it is reachable. Check the reason for propagation failures; No can also mean queued.' },
617
795
  { key: 'age', label: 'Age', width: 'w-24' },
618
796
  ],
619
797
  gateways: [
620
798
  { key: 'name', label: 'Name' },
621
799
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
622
- { key: 'status', label: 'Status', width: 'w-28' },
800
+ { key: 'status', label: 'Status', width: 'w-28', tooltip: "Programmed = the controller reports configuration should take effect soon; traffic has not been tested. Accepted = the controller accepted this Gateway but has not reported Programmed. If it stays Accepted, check the Gateway's conditions and addresses." },
623
801
  { key: 'class', label: 'Class', width: 'w-36' },
624
802
  { key: 'listeners', label: 'Listeners', width: 'w-40', tooltip: 'Protocol:Port for each listener' },
625
- { key: 'routes', label: 'Routes', width: 'w-20', tooltip: 'Total attached routes across all listeners' },
626
- { key: 'addresses', label: 'Addresses', width: 'w-48', hideOnMobile: true },
803
+ // attachedRoutes is reported per listener, so a route attached to several
804
+ // listeners is counted once for each — the total is not a route count.
805
+ { key: 'routes', label: 'Routes', width: 'w-20', tooltip: "Sum of route counts reported by listeners; one route can count more than once. If 0 is unexpected, check the routes' Gateway names, listener permissions, and controller status." },
806
+ { key: 'addresses', label: 'Addresses', width: 'w-48' },
627
807
  { key: 'age', label: 'Age', width: 'w-24' },
628
808
  ],
629
809
  httproutes: [
630
810
  { key: 'name', label: 'Name' },
631
811
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
632
- { key: 'status', label: 'Status', width: 'w-28' },
812
+ { key: 'status', label: 'Status', width: 'w-28', tooltip: 'Accepted = controllers accepted this route and confirmed it can use the backends and other objects it names. Traffic has not been tested. Degraded or Not Accepted: open the route to see which Gateway or backend failed. Pending or Unknown: check the named Gateways or Services and their controllers.' },
633
813
  { key: 'hostnames', label: 'Hostnames', width: 'w-48' },
634
814
  { key: 'parents', label: 'Gateways', width: 'w-36' },
635
815
  { key: 'backends', label: 'Backends', width: 'w-48', tooltip: 'Backend services receiving traffic' },
636
- { key: 'rules', label: 'Rules', width: 'w-16', hideOnMobile: true },
816
+ { key: 'rules', label: 'Rules', width: 'w-20' },
637
817
  { key: 'age', label: 'Age', width: 'w-24' },
638
818
  ],
639
819
  istiogateways: [
@@ -641,13 +821,13 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
641
821
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
642
822
  { key: 'status', label: 'Status', width: 'w-28' },
643
823
  { key: 'servers', label: 'Servers', width: 'w-24' },
644
- { key: 'selector', label: 'Selector', width: 'w-56' },
824
+ { key: 'selector', label: 'Selector', width: 'w-56', tooltip: "Selects gateway workloads that receive this configuration. An omitted selector applies to all workloads in Istio's selection scope; it does not mean none." },
645
825
  { key: 'age', label: 'Age', width: 'w-24' },
646
826
  ],
647
827
  gatewayclasses: [
648
828
  { key: 'name', label: 'Name' },
649
829
  { key: 'controller', label: 'Controller', width: 'w-64', tooltip: 'Gateway controller implementation (spec.controllerName)' },
650
- { key: 'description', label: 'Description', width: 'w-64', hideOnMobile: true },
830
+ { key: 'description', label: 'Description', width: 'w-64' },
651
831
  { key: 'status', label: 'Status', width: 'w-28' },
652
832
  { key: 'age', label: 'Age', width: 'w-24' },
653
833
  ],
@@ -656,18 +836,17 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
656
836
  { key: 'status', label: 'Status', width: 'w-24' },
657
837
  { key: 'nodeClass', label: 'Node Class', width: 'w-36' },
658
838
  { key: 'limits', label: 'Limits', width: 'w-36', tooltip: 'CPU and memory limits' },
659
- { key: 'disruption', label: 'Disruption', width: 'w-40', hideOnMobile: true },
839
+ { key: 'disruption', label: 'Disruption', width: 'w-40' },
660
840
  { key: 'age', label: 'Age', width: 'w-24' },
661
841
  ],
662
842
  nodeclaims: [
663
843
  { key: 'name', label: 'Name' },
664
- { key: 'namespace', label: 'Namespace', width: 'w-36' },
665
844
  { key: 'status', label: 'Status', width: 'w-24' },
666
845
  { key: 'instanceType', label: 'Instance Type', width: 'w-32' },
667
846
  { key: 'capacityType', label: 'Capacity', width: 'w-24', tooltip: 'Spot or On-Demand' },
668
- { key: 'zone', label: 'Zone', width: 'w-28', hideOnMobile: true },
847
+ { key: 'zone', label: 'Zone', width: 'w-28' },
669
848
  { key: 'nodePool', label: 'Node Pool', width: 'w-32' },
670
- { key: 'nodeName', label: 'Node', width: 'w-40', hideOnMobile: true },
849
+ { key: 'nodeName', label: 'Node', width: 'w-40' },
671
850
  { key: 'age', label: 'Age', width: 'w-24' },
672
851
  ],
673
852
  ec2nodeclasses: [
@@ -691,7 +870,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
691
870
  { key: 'name', label: 'Name' },
692
871
  { key: 'namespace', label: 'Namespace', width: 'w-36' },
693
872
  { key: 'status', label: 'Status', width: 'w-28', tooltip: 'Allocated + reserved, allocated-but-unreserved, or pending' },
694
- { key: 'deviceClass', label: 'Device Class', width: 'w-44' },
873
+ { key: 'deviceClass', label: 'Device Class', width: 'w-44', tooltip: 'DeviceClasses whose selectors constrain eligible devices. A request may list alternative classes; this column does not show which alternative was allocated.' },
695
874
  { key: 'allocated', label: 'Allocated Driver', width: 'w-44' },
696
875
  { key: 'reservedFor', label: 'Reserved For', width: 'w-44' },
697
876
  { key: 'age', label: 'Age', width: 'w-24' },
@@ -704,15 +883,17 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
704
883
  ],
705
884
  deviceclasses: [
706
885
  { key: 'name', label: 'Name' },
707
- { key: 'selectors', label: 'Selectors', width: 'w-28', tooltip: 'CEL device selector count' },
886
+ // Selectors are ANDed, so an empty list restricts nothing rather than
887
+ // matching nothing — the reading most people reach for first.
888
+ { key: 'selectors', label: 'Selectors', width: 'w-28', tooltip: 'Devices must satisfy every selector. Zero selectors means this class imposes no selector restriction; claim selectors and other allocation requirements still apply.' },
708
889
  { key: 'age', label: 'Age', width: 'w-24' },
709
890
  ],
710
891
  resourceslices: [
711
892
  { key: 'name', label: 'Name' },
712
893
  { key: 'driver', label: 'Driver', width: 'w-44' },
713
- { key: 'pool', label: 'Pool', width: 'w-36' },
714
- { key: 'node', label: 'Node', width: 'w-44' },
715
- { key: 'devices', label: 'Devices', width: 'w-24' },
894
+ { key: 'pool', label: 'Pool', width: 'w-36', tooltip: "Pools are identified by driver and pool name. The scheduler needs a complete set of slices at the pool's highest generation before allocating from it." },
895
+ { key: 'node', label: 'Node', width: 'w-44', tooltip: "Node providing this slice's devices. A dash means node access is described by a node selector, all-nodes access, or per-device node selection." },
896
+ { key: 'devices', label: 'Devices', width: 'w-24', tooltip: 'Devices published in this slice, including devices already allocated. This is not a count of free devices or the whole pool.' },
716
897
  { key: 'age', label: 'Age', width: 'w-24' },
717
898
  ],
718
899
  nvidiaclusterpolicies: [
@@ -758,7 +939,6 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
758
939
  ],
759
940
  resourceflavors: [
760
941
  { key: 'name', label: 'Name' },
761
- { key: 'status', label: 'Status', width: 'w-24' },
762
942
  { key: 'nodeLabels', label: 'Node Labels', width: 'w-28', tooltip: 'Node label selector count' },
763
943
  { key: 'taints', label: 'Taints', width: 'w-24' },
764
944
  { key: 'age', label: 'Age', width: 'w-24' },
@@ -808,7 +988,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
808
988
  { key: 'namespace', label: 'Namespace', width: 'w-36' },
809
989
  { key: 'status', label: 'Status', width: 'w-24' },
810
990
  { key: 'schedule', label: 'Schedule', width: 'w-32' },
811
- { key: 'suspend', label: 'Suspend', width: 'w-20' },
991
+ { key: 'timeZone', label: 'Time Zone', width: 'w-36', tooltip: 'IANA zone the schedule is read in; unset follows the local zone of the KubeRay operator pod' },
812
992
  { key: 'lastSchedule', label: 'Last Schedule', width: 'w-28' },
813
993
  { key: 'age', label: 'Age', width: 'w-24' },
814
994
  ],
@@ -935,7 +1115,6 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
935
1115
  jobtemplates: [
936
1116
  { key: 'name', label: 'Name' },
937
1117
  { key: 'namespace', label: 'Namespace', width: 'w-36' },
938
- { key: 'status', label: 'Status', width: 'w-24' },
939
1118
  { key: 'tasks', label: 'Tasks', width: 'w-20' },
940
1119
  { key: 'age', label: 'Age', width: 'w-24' },
941
1120
  ],
@@ -1042,7 +1221,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1042
1221
  { key: 'namespace', label: 'Namespace', width: 'w-36' },
1043
1222
  { key: 'status', label: 'Status', width: 'w-24' },
1044
1223
  { key: 'target', label: 'Job Target', width: 'w-48' },
1045
- { key: 'strategy', label: 'Strategy', width: 'w-28' },
1224
+ { key: 'strategy', label: 'Strategy', width: 'w-28', tooltip: 'Controls how queue length and running or pending Jobs determine how many new Jobs KEDA creates' },
1046
1225
  { key: 'triggerTypes', label: 'Trigger Types', width: 'w-40' },
1047
1226
  { key: 'age', label: 'Age', width: 'w-24' },
1048
1227
  ],
@@ -1064,7 +1243,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1064
1243
  servicemonitors: [
1065
1244
  { key: 'name', label: 'Name' },
1066
1245
  { key: 'namespace', label: 'Namespace', width: 'w-36' },
1067
- { key: 'status', label: 'Status', width: 'w-24' },
1246
+ { key: 'status', label: 'Status', width: 'w-36' },
1068
1247
  { key: 'endpoints', label: 'Endpoints', width: 'w-20', tooltip: 'Number of scrape endpoints' },
1069
1248
  { key: 'jobLabel', label: 'Job Label', width: 'w-32' },
1070
1249
  { key: 'selector', label: 'Selector', width: 'w-48' },
@@ -1073,7 +1252,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1073
1252
  prometheusrules: [
1074
1253
  { key: 'name', label: 'Name' },
1075
1254
  { key: 'namespace', label: 'Namespace', width: 'w-36' },
1076
- { key: 'status', label: 'Status', width: 'w-24' },
1255
+ { key: 'status', label: 'Status', width: 'w-36' },
1077
1256
  { key: 'groups', label: 'Groups', width: 'w-20' },
1078
1257
  { key: 'rules', label: 'Rules', width: 'w-20', tooltip: 'Total alert + recording rules' },
1079
1258
  { key: 'age', label: 'Age', width: 'w-24' },
@@ -1081,7 +1260,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1081
1260
  podmonitors: [
1082
1261
  { key: 'name', label: 'Name' },
1083
1262
  { key: 'namespace', label: 'Namespace', width: 'w-36' },
1084
- { key: 'status', label: 'Status', width: 'w-24' },
1263
+ { key: 'status', label: 'Status', width: 'w-36' },
1085
1264
  { key: 'endpoints', label: 'Endpoints', width: 'w-20', tooltip: 'Number of pod metrics endpoints' },
1086
1265
  { key: 'selector', label: 'Selector', width: 'w-48' },
1087
1266
  { key: 'age', label: 'Age', width: 'w-24' },
@@ -1117,14 +1296,14 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1117
1296
  { key: 'namespace', label: 'Namespace', width: 'w-36' },
1118
1297
  { key: 'status', label: 'Status', width: 'w-24' },
1119
1298
  { key: 'action', label: 'Action', width: 'w-36', tooltip: 'Enforcement at admission — Enforce blocks, Audit reports; Background only or Inactive when admission is disabled' },
1120
- { key: 'rules', label: 'Rules', width: 'w-16' },
1299
+ { key: 'rules', label: 'Rules', width: 'w-20' },
1121
1300
  { key: 'age', label: 'Age', width: 'w-24' },
1122
1301
  ],
1123
1302
  clusterpolicies: [
1124
1303
  { key: 'name', label: 'Name' },
1125
1304
  { key: 'status', label: 'Status', width: 'w-24' },
1126
1305
  { key: 'action', label: 'Action', width: 'w-36', tooltip: 'Enforcement at admission — Enforce blocks, Audit reports; Background only or Inactive when admission is disabled' },
1127
- { key: 'rules', label: 'Rules', width: 'w-16' },
1306
+ { key: 'rules', label: 'Rules', width: 'w-20' },
1128
1307
  { key: 'age', label: 'Age', width: 'w-24' },
1129
1308
  ],
1130
1309
  // Kyverno modern CEL family (policies.kyverno.io). "Enforcement" is the
@@ -1243,30 +1422,30 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1243
1422
  grpcroutes: [
1244
1423
  { key: 'name', label: 'Name' },
1245
1424
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
1246
- { key: 'status', label: 'Status', width: 'w-28' },
1247
- { key: 'hostnames', label: 'Hostnames', width: 'w-48' },
1425
+ { key: 'status', label: 'Status', width: 'w-28', tooltip: 'Accepted = controllers accepted this route and confirmed it can use the backends and other objects it names. Traffic has not been tested. Degraded or Not Accepted: open the route to see which Gateway or backend failed. Pending or Unknown: check the named Gateways or Services and their controllers.' },
1426
+ { key: 'hostnames', label: 'Hostnames', width: 'w-48', tooltip: "gRPC hostnames this route matches. Any means the route adds no hostname restriction; the Gateway listener and the route's other matching rules still apply." },
1248
1427
  { key: 'parents', label: 'Gateways', width: 'w-36' },
1249
1428
  { key: 'backends', label: 'Backends', width: 'w-48', tooltip: 'Backend services receiving traffic' },
1250
- { key: 'rules', label: 'Rules', width: 'w-16', hideOnMobile: true },
1429
+ { key: 'rules', label: 'Rules', width: 'w-20' },
1251
1430
  { key: 'age', label: 'Age', width: 'w-24' },
1252
1431
  ],
1253
1432
  tcproutes: [
1254
1433
  { key: 'name', label: 'Name' },
1255
1434
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
1256
- { key: 'status', label: 'Status', width: 'w-28' },
1435
+ { key: 'status', label: 'Status', width: 'w-28', tooltip: 'Accepted = controllers accepted this route and confirmed it can use the backends and other objects it names. Traffic has not been tested. Degraded or Not Accepted: open the route to see which Gateway or backend failed. Pending or Unknown: check the named Gateways or Services and their controllers.' },
1257
1436
  { key: 'parents', label: 'Gateways', width: 'w-36' },
1258
1437
  { key: 'backends', label: 'Backends', width: 'w-48', tooltip: 'Backend services receiving traffic' },
1259
- { key: 'rules', label: 'Rules', width: 'w-16', hideOnMobile: true },
1438
+ { key: 'rules', label: 'Rules', width: 'w-20' },
1260
1439
  { key: 'age', label: 'Age', width: 'w-24' },
1261
1440
  ],
1262
1441
  tlsroutes: [
1263
1442
  { key: 'name', label: 'Name' },
1264
1443
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
1265
- { key: 'status', label: 'Status', width: 'w-28' },
1266
- { key: 'hostnames', label: 'Hostnames', width: 'w-48', tooltip: 'SNI hostnames for TLS routing' },
1444
+ { key: 'status', label: 'Status', width: 'w-28', tooltip: 'Accepted = controllers accepted this route and confirmed it can use the backends and other objects it names. Traffic has not been tested. Degraded or Not Accepted: open the route to see which Gateway or backend failed. Pending or Unknown: check the named Gateways or Services and their controllers.' },
1445
+ { key: 'hostnames', label: 'Hostnames', width: 'w-48', tooltip: 'TLS SNI names requested by this route. Any means no route-level hostname restriction; the Gateway listener still applies.' },
1267
1446
  { key: 'parents', label: 'Gateways', width: 'w-36' },
1268
1447
  { key: 'backends', label: 'Backends', width: 'w-48', tooltip: 'Backend services receiving traffic' },
1269
- { key: 'rules', label: 'Rules', width: 'w-16', hideOnMobile: true },
1448
+ { key: 'rules', label: 'Rules', width: 'w-20' },
1270
1449
  { key: 'age', label: 'Age', width: 'w-24' },
1271
1450
  ],
1272
1451
  sealedsecrets: [
@@ -1274,7 +1453,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1274
1453
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
1275
1454
  { key: 'status', label: 'Synced', width: 'w-24' },
1276
1455
  { key: 'keys', label: 'Keys', width: 'w-20' },
1277
- { key: 'type', label: 'Type', width: 'w-36', hideOnMobile: true },
1456
+ { key: 'type', label: 'Type', width: 'w-36' },
1278
1457
  { key: 'age', label: 'Age', width: 'w-24' },
1279
1458
  ],
1280
1459
  workflowtemplates: [
@@ -1295,14 +1474,13 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1295
1474
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
1296
1475
  { key: 'policyTypes', label: 'Types', width: 'w-28' },
1297
1476
  { key: 'selector', label: 'Pod Selector', width: 'w-48' },
1298
- { key: 'rules', label: 'Rules', width: 'w-24', tooltip: 'Ingress / Egress rule count' },
1477
+ { key: 'rules', label: 'Rules', width: 'w-24', tooltip: 'Ingress / Egress allow-rule counts. For a direction listed in Types, 0 means this policy allows none; other policies can still allow traffic.' },
1299
1478
  { key: 'age', label: 'Age', width: 'w-24' },
1300
1479
  ],
1301
1480
  caliconetworkpolicies: [
1302
1481
  { key: 'name', label: 'Name' },
1303
1482
  { key: 'namespace', label: 'Namespace', width: 'w-36' },
1304
1483
  { key: 'selector', label: 'Selector', width: 'w-48' },
1305
- { key: 'namespaceSelector', label: 'Namespace Selector', width: 'w-48', defaultVisible: false },
1306
1484
  { key: 'serviceAccountSelector', label: 'Service Account Selector', width: 'w-48', defaultVisible: false },
1307
1485
  { key: 'tier', label: 'Tier', width: 'w-28' },
1308
1486
  { key: 'order', label: 'Order', width: 'w-24' },
@@ -1325,7 +1503,6 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1325
1503
  { key: 'name', label: 'Name' },
1326
1504
  { key: 'namespace', label: 'Namespace', width: 'w-36' },
1327
1505
  { key: 'selector', label: 'Selector', width: 'w-48' },
1328
- { key: 'namespaceSelector', label: 'Namespace Selector', width: 'w-48', defaultVisible: false },
1329
1506
  { key: 'serviceAccountSelector', label: 'Service Account Selector', width: 'w-48', defaultVisible: false },
1330
1507
  { key: 'tier', label: 'Tier', width: 'w-28' },
1331
1508
  { key: 'order', label: 'Order', width: 'w-24' },
@@ -1384,7 +1561,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1384
1561
  { key: 'name', label: 'Name' },
1385
1562
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
1386
1563
  { key: 'status', label: 'Status', width: 'w-24' },
1387
- { key: 'budget', label: 'Budget', width: 'w-36' },
1564
+ { key: 'budget', label: 'Budget', width: 'w-36', tooltip: 'min keeps at least this many pods available; max unavail permits at most this many unavailable during voluntary evictions; percentages use the expected pod count' },
1388
1565
  { key: 'healthy', label: 'Healthy', width: 'w-24' },
1389
1566
  { key: 'allowed', label: 'Allowed', width: 'w-24', tooltip: 'Number of disruptions currently allowed' },
1390
1567
  { key: 'age', label: 'Age', width: 'w-24' },
@@ -1392,7 +1569,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1392
1569
  serviceaccounts: [
1393
1570
  { key: 'name', label: 'Name' },
1394
1571
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
1395
- { key: 'automount', label: 'Automount', width: 'w-32', tooltip: 'Whether token is automatically mounted in pods' },
1572
+ { key: 'automount', label: 'Automount', width: 'w-32', tooltip: 'Default automatic API-token mounting for pods using this ServiceAccount. A pod can override this setting. Disable it for workloads that do not need Kubernetes API credentials.' },
1396
1573
  { key: 'secrets', label: 'Secrets', width: 'w-24' },
1397
1574
  { key: 'age', label: 'Age', width: 'w-24' },
1398
1575
  ],
@@ -1430,15 +1607,15 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1430
1607
  { key: 'name', label: 'Name' },
1431
1608
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
1432
1609
  { key: 'holder', label: 'Holder', width: 'w-48' },
1433
- { key: 'renewTime', label: 'Last Renewed', width: 'w-32' },
1610
+ { key: 'renewTime', label: 'Last Renewed', width: 'w-32', tooltip: 'Time since the recorded renewal; red means it exceeds leaseDurationSeconds. Check holder health and API connectivity' },
1434
1611
  { key: 'age', label: 'Age', width: 'w-24' },
1435
1612
  ],
1436
1613
  priorityclasses: [
1437
1614
  { key: 'name', label: 'Name' },
1438
- { key: 'value', label: 'Value', width: 'w-24' },
1615
+ { key: 'value', label: 'Value', width: 'w-24', tooltip: 'Higher values receive scheduling preference; Pods can still be preempted by Pods with higher priority' },
1439
1616
  { key: 'globalDefault', label: 'Global Default', width: 'w-36' },
1440
1617
  { key: 'preemptionPolicy', label: 'Preemption', width: 'w-32' },
1441
- { key: 'description', label: 'Description', width: 'w-64', hideOnMobile: true },
1618
+ { key: 'description', label: 'Description', width: 'w-64' },
1442
1619
  { key: 'age', label: 'Age', width: 'w-24' },
1443
1620
  ],
1444
1621
  runtimeclasses: [
@@ -1449,15 +1626,15 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1449
1626
  mutatingwebhookconfigurations: [
1450
1627
  { key: 'name', label: 'Name' },
1451
1628
  { key: 'webhooks', label: 'Webhooks', width: 'w-28' },
1452
- { key: 'failurePolicy', label: 'Failure Policy', width: 'w-36' },
1453
- { key: 'target', label: 'Target', width: 'w-48', hideOnMobile: true },
1629
+ { key: 'failurePolicy', label: 'Failure Policy', width: 'w-36', tooltip: 'If a matching webhook call errors or times out, Fail rejects the API request; Ignore continues without that webhook. This describes failure handling, not current webhook health.' },
1630
+ { key: 'target', label: 'Target', width: 'w-48' },
1454
1631
  { key: 'age', label: 'Age', width: 'w-24' },
1455
1632
  ],
1456
1633
  validatingwebhookconfigurations: [
1457
1634
  { key: 'name', label: 'Name' },
1458
1635
  { key: 'webhooks', label: 'Webhooks', width: 'w-28' },
1459
1636
  { key: 'failurePolicy', label: 'Failure Policy', width: 'w-36' },
1460
- { key: 'target', label: 'Target', width: 'w-48', hideOnMobile: true },
1637
+ { key: 'target', label: 'Target', width: 'w-48' },
1461
1638
  { key: 'age', label: 'Age', width: 'w-24' },
1462
1639
  ],
1463
1640
  events: [
@@ -1466,7 +1643,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1466
1643
  { key: 'type', label: 'Type', width: 'w-24' },
1467
1644
  { key: 'reason', label: 'Reason', width: 'w-32' },
1468
1645
  { key: 'message', label: 'Message', width: 'w-64' },
1469
- { key: 'object', label: 'Object', width: 'w-48', hideOnMobile: true },
1646
+ { key: 'object', label: 'Object', width: 'w-48' },
1470
1647
  { key: 'count', label: 'Count', width: 'w-20' },
1471
1648
  { key: 'lastSeen', label: 'Last Seen', width: 'w-28' },
1472
1649
  ],
@@ -1479,7 +1656,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1479
1656
  { key: 'url', label: 'URL', width: 'w-64' },
1480
1657
  { key: 'ref', label: 'Ref', width: 'w-32', tooltip: 'Branch, tag, or semver' },
1481
1658
  { key: 'status', label: 'Status', width: 'w-24' },
1482
- { key: 'revision', label: 'Revision', width: 'w-24', hideOnMobile: true, tooltip: 'Last fetched commit SHA' },
1659
+ { key: 'revision', label: 'Revision', width: 'w-24', tooltip: 'Last fetched commit SHA' },
1483
1660
  { key: 'age', label: 'Age', width: 'w-24' },
1484
1661
  ],
1485
1662
  ocirepositories: [
@@ -1488,7 +1665,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1488
1665
  { key: 'url', label: 'URL', width: 'w-64' },
1489
1666
  { key: 'ref', label: 'Tag', width: 'w-24', tooltip: 'OCI tag or semver' },
1490
1667
  { key: 'status', label: 'Status', width: 'w-24' },
1491
- { key: 'revision', label: 'Digest', width: 'w-24', hideOnMobile: true },
1668
+ { key: 'revision', label: 'Digest', width: 'w-24' },
1492
1669
  { key: 'age', label: 'Age', width: 'w-24' },
1493
1670
  ],
1494
1671
  helmrepositories: [
@@ -1503,9 +1680,9 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1503
1680
  { key: 'name', label: 'Name' },
1504
1681
  { key: 'namespace', label: 'Namespace', width: 'w-36' },
1505
1682
  { key: 'source', label: 'Source', width: 'w-48', tooltip: 'Source GitRepository or OCIRepository' },
1506
- { key: 'path', label: 'Path', width: 'w-36', hideOnMobile: true },
1683
+ { key: 'path', label: 'Path', width: 'w-36' },
1507
1684
  { key: 'status', label: 'Status', width: 'w-24' },
1508
- { key: 'revision', label: 'Revision', width: 'w-48', hideOnMobile: true, tooltip: 'Applied git revision' },
1685
+ { key: 'revision', label: 'Revision', width: 'w-48', tooltip: 'Applied git revision' },
1509
1686
  { key: 'inventory', label: 'Resources', width: 'w-24', tooltip: 'Number of managed resources' },
1510
1687
  { key: 'lastUpdated', label: 'Last Updated', width: 'w-28', tooltip: 'Time since last successful reconciliation' },
1511
1688
  { key: 'age', label: 'Age', width: 'w-24' },
@@ -1516,8 +1693,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1516
1693
  { key: 'chart', label: 'Chart', width: 'w-40' },
1517
1694
  { key: 'version', label: 'Version', width: 'w-24' },
1518
1695
  { key: 'status', label: 'Status', width: 'w-24' },
1519
- { key: 'message', label: 'Message', width: 'w-64', hideOnMobile: true, tooltip: 'Last diagnostic message — distinguishes dependency-wait, install/upgrade failure, and test failure' },
1520
- { key: 'revision', label: 'Rev', width: 'w-16', hideOnMobile: true, tooltip: 'Helm release revision number' },
1696
+ { key: 'message', label: 'Message', width: 'w-64', tooltip: 'Last diagnostic message — distinguishes dependency-wait, install/upgrade failure, and test failure' },
1697
+ { key: 'revision', label: 'Rev', width: 'w-16', tooltip: 'Helm release revision number' },
1521
1698
  { key: 'lastUpdated', label: 'Last Updated', width: 'w-28', tooltip: 'Time since last successful reconciliation' },
1522
1699
  { key: 'age', label: 'Age', width: 'w-24' },
1523
1700
  ],
@@ -1538,14 +1715,14 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1538
1715
  { key: 'project', label: 'Project', width: 'w-28' },
1539
1716
  { key: 'sync', label: 'Sync', width: 'w-24' },
1540
1717
  { key: 'health', label: 'Health', width: 'w-24' },
1541
- { key: 'repo', label: 'Repository', width: 'w-48', hideOnMobile: true },
1718
+ { key: 'repo', label: 'Repository', width: 'w-48' },
1542
1719
  { key: 'age', label: 'Age', width: 'w-24' },
1543
1720
  ],
1544
1721
  applicationsets: [
1545
1722
  { key: 'name', label: 'Name' },
1546
1723
  { key: 'namespace', label: 'Namespace', width: 'w-36' },
1547
1724
  { key: 'generators', label: 'Generators', width: 'w-32' },
1548
- { key: 'template', label: 'Template', width: 'w-40', hideOnMobile: true },
1725
+ { key: 'template', label: 'Template', width: 'w-40' },
1549
1726
  { key: 'applications', label: 'Apps', width: 'w-20', tooltip: 'Number of generated applications' },
1550
1727
  { key: 'status', label: 'Status', width: 'w-24' },
1551
1728
  { key: 'age', label: 'Age', width: 'w-24' },
@@ -1656,8 +1833,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1656
1833
  { key: 'namespace', label: 'Namespace', width: 'w-36' },
1657
1834
  { key: 'status', label: 'Status', width: 'w-24' },
1658
1835
  { key: 'store', label: 'Store', width: 'w-36' },
1659
- { key: 'provider', label: 'Provider', width: 'w-28' },
1660
- { key: 'refreshInterval', label: 'Refresh', width: 'w-24' },
1836
+ { key: 'refreshInterval', label: 'Refresh', width: 'w-24', tooltip: 'How often the value is re-pulled from the provider. A zero interval means it is fetched once and not updated afterward.' },
1661
1837
  { key: 'lastSync', label: 'Last Sync', width: 'w-28' },
1662
1838
  { key: 'age', label: 'Age', width: 'w-24' },
1663
1839
  ],
@@ -1913,16 +2089,16 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1913
2089
  destinationrules: [
1914
2090
  { key: 'name', label: 'Name' },
1915
2091
  { key: 'namespace', label: 'Namespace', width: 'w-36' },
1916
- { key: 'status', label: 'Status', width: 'w-24' },
1917
- { key: 'host', label: 'Host', width: 'w-48' },
1918
- { key: 'subsets', label: 'Subsets', width: 'w-20' },
1919
- { key: 'loadBalancer', label: 'LB Policy', width: 'w-28' },
2092
+ { key: 'status', label: 'Status', width: 'w-24', defaultVisible: false },
2093
+ { key: 'host', label: 'Host', width: 'w-48', tooltip: "The service this rule's traffic policy applies to" },
2094
+ { key: 'subsets', label: 'Subsets', width: 'w-20', tooltip: 'Named backend groups that routes can select, often for canary releases. Defining subsets does not send traffic to them.' },
2095
+ { key: 'tlsMode', label: 'Client TLS', width: 'w-28', tooltip: 'Declared client-side TLS mode for this host at the rule level. Subset and port policies can override it; this does not establish the effective mTLS posture.' },
2096
+ { key: 'loadBalancer', label: 'LB Policy', width: 'w-28', tooltip: "Load balancing algorithm declared by this rule. '-' means none is set, so Istio applies its mesh default (LEAST_REQUEST since 1.14, overridable per mesh). Subset and port policies can differ." },
1920
2097
  { key: 'age', label: 'Age', width: 'w-24' },
1921
2098
  ],
1922
2099
  serviceentries: [
1923
2100
  { key: 'name', label: 'Name' },
1924
2101
  { key: 'namespace', label: 'Namespace', width: 'w-36' },
1925
- { key: 'status', label: 'Status', width: 'w-24' },
1926
2102
  { key: 'hosts', label: 'Hosts', width: 'w-48' },
1927
2103
  { key: 'location', label: 'Location', width: 'w-28' },
1928
2104
  { key: 'ports', label: 'Ports', width: 'w-32' },
@@ -1931,7 +2107,6 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1931
2107
  peerauthentications: [
1932
2108
  { key: 'name', label: 'Name' },
1933
2109
  { key: 'namespace', label: 'Namespace', width: 'w-36' },
1934
- { key: 'status', label: 'Status', width: 'w-24' },
1935
2110
  { key: 'mode', label: 'mTLS Mode', width: 'w-28' },
1936
2111
  { key: 'selector', label: 'Selector', width: 'w-48' },
1937
2112
  { key: 'age', label: 'Age', width: 'w-24' },
@@ -1939,10 +2114,10 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1939
2114
  authorizationpolicies: [
1940
2115
  { key: 'name', label: 'Name' },
1941
2116
  { key: 'namespace', label: 'Namespace', width: 'w-36' },
1942
- { key: 'status', label: 'Status', width: 'w-24' },
1943
- { key: 'action', label: 'Action', width: 'w-24' },
1944
- { key: 'rules', label: 'Rules', width: 'w-20' },
1945
- { key: 'selector', label: 'Selector', width: 'w-48' },
2117
+ { key: 'status', label: 'Status', width: 'w-24', defaultVisible: false },
2118
+ { key: 'action', label: 'Action', width: 'w-24', tooltip: 'ALLOW contributes permitted matches; DENY blocks matches; AUDIT marks matches for a configured audit plugin; CUSTOM adds an external check. Other policies also affect the decision.' },
2119
+ { key: 'rules', label: 'Rules', width: 'w-20', tooltip: 'Rules are alternatives — zero rules match nothing, an empty rule matches everything. An ALLOW policy with zero rules permits nothing on its own.' },
2120
+ { key: 'selector', label: 'Applies to', width: 'w-48', tooltip: 'Declared workload labels or resource attachments. With neither, scope is inherited — the namespace, or the whole mesh when the policy sits in the mesh root namespace.' },
1946
2121
  { key: 'age', label: 'Age', width: 'w-24' },
1947
2122
  ],
1948
2123
  // Knative Serving
@@ -2116,7 +2291,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
2116
2291
  { key: 'namespace', label: 'Namespace', width: 'w-36 shrink-0' },
2117
2292
  { key: 'entrypoints', label: 'Entry Points', width: 'w-32 shrink-0' },
2118
2293
  { key: 'hosts', label: 'Hosts', width: 'min-w-44' },
2119
- { key: 'routes', label: 'Routes', width: 'min-w-48', hideOnMobile: true },
2294
+ { key: 'routes', label: 'Routes', width: 'min-w-48' },
2120
2295
  { key: 'tls', label: 'TLS', width: 'w-14 shrink-0' },
2121
2296
  { key: 'middlewares', label: 'MW', width: 'w-14 shrink-0', tooltip: 'Unique middleware count' },
2122
2297
  { key: 'age', label: 'Age', width: 'w-24 shrink-0' },
@@ -2126,7 +2301,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
2126
2301
  { key: 'namespace', label: 'Namespace', width: 'w-36 shrink-0' },
2127
2302
  { key: 'entrypoints', label: 'Entry Points', width: 'w-32 shrink-0' },
2128
2303
  { key: 'hosts', label: 'Hosts', width: 'min-w-44' },
2129
- { key: 'routes', label: 'Routes', width: 'min-w-48', hideOnMobile: true },
2304
+ { key: 'routes', label: 'Routes', width: 'min-w-48' },
2130
2305
  { key: 'tls', label: 'TLS', width: 'w-14 shrink-0' },
2131
2306
  { key: 'middlewares', label: 'MW', width: 'w-14 shrink-0', tooltip: 'Unique middleware count' },
2132
2307
  { key: 'age', label: 'Age', width: 'w-24 shrink-0' },
@@ -2390,15 +2565,15 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
2390
2565
  { key: 'name', label: 'Name', width: 'min-w-40' },
2391
2566
  { key: 'namespace', label: 'Namespace', width: 'w-32 shrink-0' },
2392
2567
  { key: 'kind', label: 'Kind', width: 'w-32 shrink-0' },
2393
- { key: 'external', label: 'External Name', width: 'min-w-48', hideOnMobile: true },
2394
- { key: 'provider', label: 'Provider Config', width: 'w-40 shrink-0', hideOnMobile: true },
2568
+ { key: 'external', label: 'External Name', width: 'min-w-48' },
2569
+ { key: 'provider', label: 'Provider Config', width: 'w-40 shrink-0' },
2395
2570
  { key: 'status', label: 'Status', width: 'w-32 shrink-0' },
2396
2571
  { key: 'age', label: 'Age', width: 'w-24 shrink-0' },
2397
2572
  ],
2398
2573
  providers: [
2399
2574
  { key: 'name', label: 'Name', width: 'min-w-40' },
2400
2575
  { key: 'package', label: 'Package', width: 'min-w-48' },
2401
- { key: 'revision', label: 'Revision', width: 'w-36 shrink-0', hideOnMobile: true },
2576
+ { key: 'revision', label: 'Revision', width: 'w-36 shrink-0' },
2402
2577
  { key: 'status', label: 'Status', width: 'w-32 shrink-0' },
2403
2578
  { key: 'age', label: 'Age', width: 'w-24 shrink-0' },
2404
2579
  ],
@@ -2412,13 +2587,13 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
2412
2587
  { key: 'name', label: 'Name', width: 'min-w-40' },
2413
2588
  { key: 'composite', label: 'Composite Kind', width: 'w-44 shrink-0' },
2414
2589
  { key: 'mode', label: 'Mode', width: 'w-24 shrink-0' },
2415
- { key: 'functions', label: 'Functions', width: 'w-28 shrink-0', hideOnMobile: true },
2590
+ { key: 'functions', label: 'Functions', width: 'w-28 shrink-0' },
2416
2591
  { key: 'age', label: 'Age', width: 'w-24 shrink-0' },
2417
2592
  ],
2418
2593
  compositeresourcedefinitions: [
2419
2594
  { key: 'name', label: 'Name', width: 'min-w-40' },
2420
2595
  { key: 'kind', label: 'Kind', width: 'w-40 shrink-0' },
2421
- { key: 'claim', label: 'Claim Kind', width: 'w-40 shrink-0', hideOnMobile: true },
2596
+ { key: 'claim', label: 'Claim Kind', width: 'w-40 shrink-0' },
2422
2597
  { key: 'age', label: 'Age', width: 'w-24 shrink-0' },
2423
2598
  ],
2424
2599
  }
@@ -2961,6 +3136,8 @@ export function mergeSavedVisibleColumns(
2961
3136
  savedVisible: string[],
2962
3137
  extraKeys: string[],
2963
3138
  printerColumns: ExtraColumn[],
3139
+ effective?: Column[],
3140
+ known?: string[],
2964
3141
  ): Set<string> {
2965
3142
  const merged = new Set(savedVisible)
2966
3143
  for (const k of extraKeys) merged.add(k)
@@ -2969,6 +3146,24 @@ export function mergeSavedVisibleColumns(
2969
3146
  if (c.defaultVisible !== false) merged.add(c.key)
2970
3147
  }
2971
3148
  }
3149
+ // Curated columns the blob predates. Same principle as the two above, but it
3150
+ // needs a record of what the user was actually offered: the blob stores only
3151
+ // what is visible, so a curated key's absence is ambiguous between "hidden on
3152
+ // purpose" and "not offered yet". `known` is that record. Without one the two
3153
+ // cases are indistinguishable, so nothing is inferred and visibility is left
3154
+ // to the saved set alone.
3155
+ //
3156
+ // Printer columns are excluded even when `known` lists none of them. They are
3157
+ // fetched, so `known` can be written during the window before the printer
3158
+ // table arrives; treating their absence as "new" would re-show ones the user
3159
+ // hid. Their own rule above already covers them.
3160
+ if (effective && Array.isArray(known)) {
3161
+ const seen = new Set(known)
3162
+ for (const c of effective) {
3163
+ if (c.key.startsWith(PRINTER_COLUMN_PREFIX)) continue
3164
+ if (!seen.has(c.key) && c.defaultVisible !== false) merged.add(c.key)
3165
+ }
3166
+ }
2972
3167
  return merged
2973
3168
  }
2974
3169
 
@@ -3024,6 +3219,10 @@ interface ColumnSettings {
3024
3219
  visible: string[]
3025
3220
  widths: Record<string, number>
3026
3221
  custom?: CustomColumnDef[]
3222
+ /** Every column key the user was offered when this was written. Lets a later
3223
+ * load tell a column they hid from one that did not exist yet. Absent on
3224
+ * blobs written before this field. */
3225
+ known?: string[]
3027
3226
  }
3028
3227
 
3029
3228
  function loadColumnSettings(kind: string, group?: string): ColumnSettings | null {
@@ -3601,8 +3800,17 @@ export function ResourcesView({
3601
3800
  return [...filteredExtras, ...kindColumns, ...builtCustomColumns]
3602
3801
  }, [selectedKind.name, selectedKind.group, extraLeadingColumns, builtCustomColumns, builtPrinterColumns])
3603
3802
 
3803
+ // The offered key set, and a stable signature for it. allColumns is a new
3804
+ // identity on every printer-column refetch; depending on the array itself
3805
+ // would rewrite the settings blob on each poll.
3806
+ const allColumnKeys = useMemo(() => allColumns.map(c => c.key), [allColumns])
3807
+ const allColumnKeysSig = allColumnKeys.join('\u0000')
3808
+
3604
3809
  // Map of extra column keys for fast O(1) lookup on each render path
3605
3810
  // (cell render, sort, column-filter unique-values).
3811
+ // Recompute header-derived widths once the web font swaps in (see watchHeaderFont).
3812
+ const headerFontEpoch = useHeaderFontEpoch()
3813
+
3606
3814
  const extraColumnsByKey = useMemo(() => {
3607
3815
  const m = new Map<string, ExtraColumn>()
3608
3816
  extraLeadingColumns?.forEach(c => m.set(c.key, c))
@@ -3614,6 +3822,13 @@ export function ResourcesView({
3614
3822
  // Guards the save effect from persisting on the initial load of each kind
3615
3823
  // (set false by the load effect, flipped true on its first skipped save).
3616
3824
  const isColumnSettingsLoaded = useRef(false)
3825
+ // Every column key this kind has ever offered, accumulated. A single save
3826
+ // cannot stand in for it: the offered set shrinks as well as grows — an
3827
+ // uncurated kind's generic Status disappears once its printer columns arrive
3828
+ // — and a shrunk snapshot would forget that a column absent from `visible`
3829
+ // was one the user hid rather than one never shown.
3830
+ const knownColumnKeys = useRef<Set<string>>(new Set())
3831
+ const knownColumnKeysKind = useRef('')
3617
3832
  // Whether the user has a persisted column blob for this kind — data-driven
3618
3833
  // column defaults (GPU auto-show) must never override explicit user choices.
3619
3834
  const hadSavedColumnSettings = useRef(false)
@@ -3639,6 +3854,15 @@ export function ResourcesView({
3639
3854
  // (non-array, blank path, bad source) must not crash the later .map or add dead columns.
3640
3855
  const savedCustom = sanitizeCustomColumnDefs(saved?.custom)
3641
3856
  setCustomColumns(savedCustom)
3857
+ // Reset on a kind change only. This effect also re-runs when the printer
3858
+ // columns arrive, and that is the moment the offered set shrinks — clearing
3859
+ // here would drop the evidence for the column they replaced just before the
3860
+ // next save persists the record without it.
3861
+ const knownKeysIdentity = selectedKindIdentityOf(selectedKind)
3862
+ if (knownColumnKeysKind.current !== knownKeysIdentity) {
3863
+ knownColumnKeys.current = new Set()
3864
+ knownColumnKeysKind.current = knownKeysIdentity
3865
+ }
3642
3866
  const kindColumns = columnsForKindWithPrinter(selectedKind.name, selectedKind.group, builtPrinterColumns)
3643
3867
  const builtinKeys = new Set(kindColumns.map(c => c.key))
3644
3868
  const extras = filterHostExtras(extraLeadingColumns, builtinKeys, selectedKind.name)
@@ -3655,18 +3879,32 @@ export function ResourcesView({
3655
3879
  // discard the stale save and use the specialized columns instead. User-added
3656
3880
  // custom columns mean the blob isn't a stale pure-defaults save — keep it, or
3657
3881
  // the migration would clear them from storage (and un-hide hidden ones).
3882
+ // Persisted JSON: only an array of keys is usable, anything else reads as
3883
+ // no record at all.
3884
+ const savedKnown = Array.isArray(saved.known)
3885
+ ? saved.known.filter((k): k is string => typeof k === 'string')
3886
+ : undefined
3887
+ for (const k of savedKnown ?? []) knownColumnKeys.current.add(k)
3888
+
3658
3889
  const defaultKeys = DEFAULT_COLUMNS.map(c => c.key)
3890
+ // A blob carrying an offered-key record is not a pre-curation leftover:
3891
+ // its visible set matching the generic defaults means the user hid their
3892
+ // way down to them, and clearing it would undo exactly that.
3659
3893
  const isStaleDefaults = kindColumns !== DEFAULT_COLUMNS &&
3894
+ !savedKnown &&
3660
3895
  !savedCustom.length &&
3661
3896
  saved.visible.length === defaultKeys.length &&
3662
3897
  saved.visible.every(v => defaultKeys.includes(v))
3663
3898
  if (isStaleDefaults) {
3664
3899
  clearColumnSettings(selectedKind.name, selectedKind.group)
3900
+ knownColumnKeys.current = new Set()
3665
3901
  hadSavedColumnSettings.current = false
3666
3902
  setVisibleColumns(getDefaultVisibleColumns(effective))
3667
3903
  setColumnWidths({})
3668
3904
  } else {
3669
- setVisibleColumns(mergeSavedVisibleColumns(saved.visible, extraKeys, builtPrinterColumns))
3905
+ setVisibleColumns(mergeSavedVisibleColumns(
3906
+ saved.visible, extraKeys, builtPrinterColumns, effective, savedKnown,
3907
+ ))
3670
3908
  setColumnWidths(saved.widths || {})
3671
3909
  }
3672
3910
  } else {
@@ -3682,6 +3920,7 @@ export function ResourcesView({
3682
3920
  // Save column settings when they change (skip the initial load of each kind)
3683
3921
  useEffect(() => {
3684
3922
  if (visibleColumns.size === 0) return // not loaded yet
3923
+ for (const k of allColumnKeys) knownColumnKeys.current.add(k)
3685
3924
  if (!isColumnSettingsLoaded.current) {
3686
3925
  isColumnSettingsLoaded.current = true
3687
3926
  return
@@ -3690,11 +3929,13 @@ export function ResourcesView({
3690
3929
  visible: Array.from(visibleColumns),
3691
3930
  widths: columnWidths,
3692
3931
  custom: customColumns,
3932
+ known: Array.from(knownColumnKeys.current),
3693
3933
  })
3694
3934
  // A persisted blob blocks GPU auto-show from here on — same rule in-session
3695
3935
  // as across sessions, so a later data refresh can't override user choices.
3696
3936
  hadSavedColumnSettings.current = true
3697
- }, [visibleColumns, columnWidths, customColumns, selectedKind.name, selectedKind.group])
3937
+ // allColumnKeysSig, not allColumnKeys: see the memo above.
3938
+ }, [visibleColumns, columnWidths, customColumns, selectedKind.name, selectedKind.group, allColumnKeysSig])
3698
3939
 
3699
3940
  // Close column picker on outside click or Escape
3700
3941
  useEffect(() => {
@@ -3797,6 +4038,7 @@ export function ResourcesView({
3797
4038
  // Reset column settings to defaults (also drops user-added custom columns)
3798
4039
  const resetColumnSettings = useCallback(() => {
3799
4040
  clearColumnSettings(selectedKind.name, selectedKind.group)
4041
+ knownColumnKeys.current = new Set()
3800
4042
  setCustomColumns([])
3801
4043
  const customKeys = new Set(builtCustomColumns.map(c => c.key))
3802
4044
  setVisibleColumns(getDefaultVisibleColumns(allColumns.filter(c => !customKeys.has(c.key))))
@@ -5248,12 +5490,12 @@ export function ResourcesView({
5248
5490
  // scroll horizontally when the viewport is too narrow.
5249
5491
  const tableMinWidth = useMemo(() => {
5250
5492
  const compareColumnWidth = compareMode ? COMPARE_COLUMN_WIDTH : 0
5251
- const baseMinWidth = columns.reduce((sum, col) => sum + (columnWidths[col.key] || getColumnMinWidth(col)), compareColumnWidth)
5493
+ const baseMinWidth = columns.reduce((sum, col) => sum + (columnWidths[col.key] || getColumnMinWidth(col, extraColumnsByKey)), compareColumnWidth)
5252
5494
  const flexibleNameColumn = columns.find(col => col.key === 'name' && !columnWidths[col.key])
5253
5495
 
5254
5496
  if (!hasResizedColumns || !flexibleNameColumn) return baseMinWidth
5255
- return baseMinWidth + getColumnMinWidth(flexibleNameColumn)
5256
- }, [columns, columnWidths, compareMode, hasResizedColumns])
5497
+ return baseMinWidth + getColumnMinWidth(flexibleNameColumn, extraColumnsByKey)
5498
+ }, [columns, columnWidths, compareMode, hasResizedColumns, extraColumnsByKey, headerFontEpoch])
5257
5499
 
5258
5500
  // Stable virtuoso components — memoized to avoid remounting the table on every render
5259
5501
  const virtuosoComponents = useMemo(() => ({
@@ -5281,7 +5523,7 @@ export function ResourcesView({
5281
5523
  style={{
5282
5524
  width: columnWidths[col.key]
5283
5525
  ? `${columnWidths[col.key]}px`
5284
- : col.key === 'name' ? undefined : `${getColumnMinWidth(col)}px`,
5526
+ : col.key === 'name' ? undefined : `${getColumnMinWidth(col, extraColumnsByKey)}px`,
5285
5527
  }}
5286
5528
  />
5287
5529
  ))}
@@ -5292,7 +5534,7 @@ export function ResourcesView({
5292
5534
  )
5293
5535
  }),
5294
5536
  TableRow: VirtuosoTableRow,
5295
- }), [columns, columnWidths, hasResizedColumns, compareMode, tableMinWidth, isCheckboxMode])
5537
+ }), [columns, columnWidths, hasResizedColumns, compareMode, tableMinWidth, isCheckboxMode, extraColumnsByKey, headerFontEpoch])
5296
5538
 
5297
5539
  // Calculate filter options with counts based on current resources (before filtering)
5298
5540
  const filterOptions = useMemo(() => {
@@ -6143,8 +6385,7 @@ export function ResourcesView({
6143
6385
  )}
6144
6386
  {columns.map((col, colIdx) => {
6145
6387
  // Built-in sortable keys, plus any extra/custom column that carries its own getSortValue.
6146
- const isSortable = ['name', 'namespace', 'age', 'status', 'ready', 'restarts', 'type', 'version', 'desired', 'available', 'upToDate', 'lastSeen', 'count', 'reason', 'object', 'cpu', 'memory', 'containers'].includes(col.key)
6147
- || !!extraColumnsByKey.get(col.key)?.getSortValue
6388
+ const isSortable = isColumnSortable(col, extraColumnsByKey)
6148
6389
  const isSorted = sortColumn === col.key
6149
6390
  const isLastCol = colIdx === columns.length - 1
6150
6391
  const filterCol = filterableColumnMap.get(col.key)
@@ -6164,11 +6405,11 @@ export function ResourcesView({
6164
6405
  >
6165
6406
  <div className="flex items-center gap-1 overflow-hidden">
6166
6407
  {col.tooltip ? (
6167
- <Tooltip content={col.tooltip}>
6408
+ <Tooltip content={col.tooltip} wrapperClassName="min-w-0">
6168
6409
  <span className="border-b border-dotted border-theme-text-tertiary truncate">{col.label}</span>
6169
6410
  </Tooltip>
6170
6411
  ) : (
6171
- <span className="truncate">{col.label}</span>
6412
+ <HeaderLabel label={col.label} />
6172
6413
  )}
6173
6414
  {isSortable && (
6174
6415
  <span className="text-theme-text-tertiary shrink-0">
@@ -8720,11 +8961,11 @@ function RouteCell({ resource, column }: { resource: any; column: string }) {
8720
8961
  switch (column) {
8721
8962
  case 'status': {
8722
8963
  const status = getRouteStatus(resource)
8723
- return (
8724
- <span className={clsx('badge', status.color)}>
8725
- {status.text}
8726
- </span>
8727
- )
8964
+ const reason = getRouteStatusReason(resource)
8965
+ const badge = <span className={clsx('badge', status.color)}>{status.text}</span>
8966
+ // "Degraded" on its own says something is wrong and not what. The
8967
+ // controller's own reason is usually "the backend you named is missing".
8968
+ return reason ? <Tooltip content={reason}>{badge}</Tooltip> : badge
8728
8969
  }
8729
8970
  case 'hostnames': {
8730
8971
  const hostnames = getRouteHostnames(resource)