@skyhook-io/k8s-ui 1.14.0 → 1.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -227,6 +227,12 @@ export const SKIP_FILTER_COLUMNS = new Set([
227
227
  'lastUpdated', 'chart', 'events', 'repo',
228
228
  'generators', 'applications', 'destinations', 'sources', 'budget', 'healthy', 'allowed',
229
229
  'secrets', 'subjects', 'role', 'entrypoint', 'templates',
230
+ // Trivy and Kyverno report counts. These look filterable but cannot be:
231
+ // getCellFilterValue has no case for them and its fallback probes
232
+ // status[key]/spec[key], while the counts live under report.summary /
233
+ // status.summary. Listing them here stops the header reserving room for a
234
+ // button that can never render.
235
+ 'critical', 'high', 'medium', 'low', 'pass', 'fail', 'warn', 'error', 'skip',
230
236
  ])
231
237
 
232
238
  // Namespace/node cardinality scales with cluster size, not kind enum size;
@@ -240,11 +246,10 @@ export function isColumnFilterableByDistinctCount(colKey: string, distinctCount:
240
246
  }
241
247
 
242
248
  // Column definitions per resource kind
243
- interface Column {
249
+ export interface Column {
244
250
  key: string
245
251
  label: string
246
252
  width?: string
247
- hideOnMobile?: boolean
248
253
  tooltip?: string // Explanation of what this column means
249
254
  defaultVisible?: boolean // false = hidden by default, shown via column picker
250
255
  defaultWidth?: number // default width in px (used for resizable columns)
@@ -321,7 +326,179 @@ const COMPARE_COLUMN_STYLE: React.CSSProperties = {
321
326
  maxWidth: COMPARE_COLUMN_WIDTH,
322
327
  }
323
328
 
324
- function getColumnMinWidth(col: Column): number {
329
+ // Built-in sortable keys. Hoisted so getColumnMinWidth can reserve room for the
330
+ // sort icon without the header render and the width math drifting apart.
331
+ const BUILTIN_SORTABLE_COLUMN_KEYS = new Set([
332
+ 'name', 'namespace', 'age', 'status', 'ready', 'restarts', 'type', 'version',
333
+ 'desired', 'available', 'upToDate', 'lastSeen', 'count', 'reason', 'object',
334
+ 'cpu', 'memory', 'containers',
335
+ ])
336
+
337
+ // The header row is px-4 with a `truncate` label, under table-layout:fixed with
338
+ // explicit <col> widths — so a column narrower than its own heading clips to
339
+ // "ADDRESS T…" and stays clipped at every viewport size, because only the Name
340
+ // column flexes. Deriving a floor from the label keeps a declared w-* from
341
+ // being narrower than the word it has to show.
342
+ const HEADER_PADDING_PX = 32 // px-4 on both sides
343
+ const HEADER_SORT_AFFORDANCE_PX = 20 // gap-1 + the w-3.5 chevron / ArrowUpDown
344
+ const HEADER_FILTER_AFFORDANCE_PX = 20 // gap-1 + the p-0.5 filter button
345
+ // Deliberately an upper bound on the rendered header font (DM Sans 500, 12px,
346
+ // uppercase, tracking-wide): measured per-character widths across the curated
347
+ // labels ranged 7.2–8.1px, so 8.2 never under-reserves. Over-reserving costs
348
+ // space the flexible Name column has to spare; under-reserving clips again.
349
+ // Printer and host-extra columns take their label from vendor CRD text, which
350
+ // can be long or non-Latin. Cap the floor so one such label can't size a column
351
+ // off the screen; past this a truncated heading (with its tooltip) is better.
352
+ const HEADER_FLOOR_MAX_PX = 320
353
+
354
+ // Must match the rendered header: text-xs (12px) / font-medium (500) / uppercase
355
+ // / tracking-wide, in the app font. measureText does not apply letter-spacing,
356
+ // so it is added per gap.
357
+ const HEADER_FONT = "500 12px 'DM Sans Variable', 'DM Sans', system-ui, sans-serif"
358
+ const HEADER_LETTER_SPACING_PX = 0.3 // tracking-wide = 0.025em at 12px
359
+ // Fallback only — used where there is no canvas (SSR, jsdom). Deliberately an
360
+ // upper bound: measured per-character widths across the curated labels ranged
361
+ // 7.2–8.1px, so this never under-reserves for Latin text.
362
+ const HEADER_CHAR_PX_FALLBACK = 8.2
363
+ const HEADER_SPACE_PX_FALLBACK = 4
364
+
365
+ let headerMeasureCtx: CanvasRenderingContext2D | null | undefined
366
+ const headerLabelWidthCache = new Map<string, number>()
367
+
368
+ // DM Sans is loaded with font-display:swap, so the first tables can render — and
369
+ // be measured — while the browser is still painting the fallback face. Those
370
+ // measurements would otherwise be cached against the wrong glyph metrics for the
371
+ // rest of the session: a wider real face clips a label whose tooltip never
372
+ // activates, a narrower one leaves the table permanently too wide. Drop the
373
+ // cache when the real font arrives and let subscribers recompute.
374
+ let headerFontEpoch = 0
375
+ const headerFontListeners = new Set<() => void>()
376
+ let headerFontWatchStarted = false
377
+
378
+ function watchHeaderFont(): void {
379
+ if (headerFontWatchStarted) return
380
+ headerFontWatchStarted = true
381
+ const fonts = typeof document === 'undefined' ? undefined : (document as Document & { fonts?: FontFaceSet }).fonts
382
+ if (!fonts?.ready) return
383
+ void fonts.ready.then(() => {
384
+ headerLabelWidthCache.clear()
385
+ headerMeasureCtx = undefined // re-created so ctx.font resolves against the loaded face
386
+ headerFontEpoch++
387
+ for (const notify of headerFontListeners) notify()
388
+ })
389
+ }
390
+
391
+ /**
392
+ * Re-renders once the web font has loaded, so column widths measured against the
393
+ * fallback face are recomputed against the real one.
394
+ */
395
+ export function useHeaderFontEpoch(): number {
396
+ const [epoch, setEpoch] = useState(headerFontEpoch)
397
+ useEffect(() => {
398
+ const notify = () => setEpoch(headerFontEpoch)
399
+ headerFontListeners.add(notify)
400
+ notify() // the font may have loaded between render and effect
401
+ return () => { headerFontListeners.delete(notify) }
402
+ }, [])
403
+ return epoch
404
+ }
405
+
406
+ /**
407
+ * Width of a header label as the browser will actually draw it.
408
+ *
409
+ * Measured rather than estimated because the labels are not a closed set: an
410
+ * uncurated CRD's printer columns take their names from the vendor's own
411
+ * additionalPrinterColumns, so they can be any length, any script, and a
412
+ * per-character constant calibrated on curated ASCII silently stops holding.
413
+ */
414
+ function headerLabelWidth(label: string): number {
415
+ watchHeaderFont()
416
+ const cached = headerLabelWidthCache.get(label)
417
+ if (cached !== undefined) return cached
418
+ const upper = label.toUpperCase() // the header renders uppercase
419
+ let width: number
420
+ if (headerMeasureCtx === undefined) {
421
+ headerMeasureCtx = typeof document === 'undefined'
422
+ ? null
423
+ : document.createElement('canvas').getContext('2d')
424
+ if (headerMeasureCtx) headerMeasureCtx.font = HEADER_FONT
425
+ }
426
+ if (headerMeasureCtx) {
427
+ width = headerMeasureCtx.measureText(upper).width
428
+ + HEADER_LETTER_SPACING_PX * Math.max(0, upper.length - 1)
429
+ } else {
430
+ width = 0
431
+ for (const ch of upper) width += ch === ' ' ? HEADER_SPACE_PX_FALLBACK : HEADER_CHAR_PX_FALLBACK
432
+ }
433
+ headerLabelWidthCache.set(label, width)
434
+ return width
435
+ }
436
+
437
+ /**
438
+ * Header label that carries its own full text in a tooltip when it truncates.
439
+ *
440
+ * The width floor in getColumnMinWidth keeps a heading from being clipped by its
441
+ * own column, but three cases still truncate: a label past the floor cap, a
442
+ * column the user dragged narrower, and the sort/filter controls sharing the
443
+ * row — whose width depends on their state (an active filter renders padding,
444
+ * an icon and a count). Their rendered width isn't knowable from the column
445
+ * definition, so this reads the element's actual overflow rather than computing
446
+ * it.
447
+ *
448
+ * The Tooltip wrapper stays mounted whether or not the label truncates: swapping
449
+ * it in on overflow would remount the measured span, stranding the
450
+ * ResizeObserver on the detached node so the state could never flip back. It
451
+ * also needs min-w-0 — the wrapper is the flex item in the header row, and an
452
+ * inline-flex box won't shrink below its content unless told to, which would
453
+ * push the sort and filter controls out of the cell.
454
+ */
455
+ function HeaderLabel({ label, className }: { label: string; className?: string }) {
456
+ const ref = useRef<HTMLSpanElement>(null)
457
+ const [truncated, setTruncated] = useState(false)
458
+ useEffect(() => {
459
+ const el = ref.current
460
+ if (!el) return
461
+ const check = () => setTruncated(el.scrollWidth > el.clientWidth + 0.5)
462
+ check()
463
+ if (typeof ResizeObserver === 'undefined') return
464
+ const ro = new ResizeObserver(check)
465
+ ro.observe(el)
466
+ return () => { ro.disconnect() }
467
+ }, [label])
468
+ return (
469
+ <Tooltip content={label} disabled={!truncated} preserveWrapperWhenDisabled wrapperClassName="min-w-0">
470
+ <span ref={ref} className={clsx('truncate', className)}>{label}</span>
471
+ </Tooltip>
472
+ )
473
+ }
474
+
475
+ /**
476
+ * Whether a column's header renders a sort control, which takes width beside the
477
+ * label. Shared by the header render and the width math so the two can't
478
+ * disagree: printer and custom columns are sortable through their own
479
+ * getSortValue even though their keys aren't in the built-in list.
480
+ */
481
+ export function isColumnSortable(col: Column, extras?: Map<string, ExtraColumn>): boolean {
482
+ return BUILTIN_SORTABLE_COLUMN_KEYS.has(col.key) || !!extras?.get(col.key)?.getSortValue
483
+ }
484
+
485
+ export function getColumnMinWidth(col: Column, extras?: Map<string, ExtraColumn>): number {
486
+ const declared = declaredColumnWidth(col)
487
+ // A column-filter button also sits in this row, but whether one renders
488
+ // depends on the distinct values in the data (isColumnFilterableByDistinctCount),
489
+ // which isn't knowable here — those columns stay ~20px tighter than ideal.
490
+ // Both controls are reserved statically. The filter button only renders once
491
+ // the data has a filterable spread of values (isColumnFilterableByDistinctCount),
492
+ // but sizing the column from that would make it change width as rows load or
493
+ // are filtered — the jumping-column problem fixed layout exists to avoid. So
494
+ // reserve for any column that could ever carry one, and keep the width stable.
495
+ const affordance = (isColumnSortable(col, extras) ? HEADER_SORT_AFFORDANCE_PX : 0)
496
+ + (SKIP_FILTER_COLUMNS.has(col.key) ? 0 : HEADER_FILTER_AFFORDANCE_PX)
497
+ const headerFloor = HEADER_PADDING_PX + headerLabelWidth(col.label) + affordance
498
+ return Math.max(declared, Math.min(Math.ceil(headerFloor), HEADER_FLOOR_MAX_PX))
499
+ }
500
+
501
+ function declaredColumnWidth(col: Column): number {
325
502
  if (col.minWidth) return col.minWidth
326
503
  if (!col.width) return 200 // Name column (no width class) gets wider minimum
327
504
  const match = col.width.match(/(?:min-)?w-(\d+)/)
@@ -372,7 +549,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
372
549
  { key: 'restarts', label: 'Restarts', width: 'w-28' },
373
550
  { key: 'gpu', label: 'GPU', width: 'w-20', tooltip: 'Effective GPU request (requests, falling back to limits) summed across containers', defaultVisible: false },
374
551
  { key: 'podIP', label: 'Pod IP', width: 'w-32', defaultVisible: false },
375
- { key: 'node', label: 'Node', width: 'w-44', hideOnMobile: true },
552
+ { key: 'node', label: 'Node', width: 'w-44' },
376
553
  { key: 'age', label: 'Age', width: 'w-24' },
377
554
  ],
378
555
  deployments: [
@@ -380,9 +557,9 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
380
557
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
381
558
  { key: 'status', label: 'Status', width: 'w-40' },
382
559
  { 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 },
560
+ { key: 'upToDate', label: 'Up-to-date', width: 'w-32', tooltip: 'Number of pods running the current pod template' },
561
+ { key: 'available', label: 'Available', width: 'w-28', tooltip: 'Number of pods available (ready for minReadySeconds)' },
562
+ { key: 'images', label: 'Images', width: 'w-48' },
386
563
  { key: 'age', label: 'Age', width: 'w-24' },
387
564
  ],
388
565
  daemonsets: [
@@ -391,9 +568,9 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
391
568
  { key: 'status', label: 'Status', width: 'w-40' },
392
569
  { key: 'desired', label: 'Desired', width: 'w-20', tooltip: 'Number of nodes that should run the daemon pod (based on node selector)' },
393
570
  { 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 },
571
+ { key: 'upToDate', label: 'Up-to-date', width: 'w-32', tooltip: 'Number of pods running the current pod template spec' },
572
+ { key: 'available', label: 'Available', width: 'w-28', tooltip: 'Number of pods available (ready for minReadySeconds duration)' },
573
+ { key: 'images', label: 'Images', width: 'w-48' },
397
574
  { key: 'age', label: 'Age', width: 'w-24' },
398
575
  ],
399
576
  statefulsets: [
@@ -401,8 +578,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
401
578
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
402
579
  { key: 'status', label: 'Status', width: 'w-40' },
403
580
  { 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 },
581
+ { key: 'upToDate', label: 'Up-to-date', width: 'w-32', tooltip: 'Number of pods running the current pod template' },
582
+ { key: 'images', label: 'Images', width: 'w-48' },
406
583
  { key: 'age', label: 'Age', width: 'w-24' },
407
584
  ],
408
585
  replicasets: [
@@ -410,17 +587,17 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
410
587
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
411
588
  { key: 'ready', label: 'Ready', width: 'w-24' },
412
589
  { key: 'owner', label: 'Owner', width: 'w-48' },
413
- { key: 'status', label: 'Status', width: 'w-24', hideOnMobile: true },
590
+ { key: 'status', label: 'Status', width: 'w-24' },
414
591
  { key: 'age', label: 'Age', width: 'w-24' },
415
592
  ],
416
593
  services: [
417
594
  { key: 'name', label: 'Name' },
418
595
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
419
596
  { key: 'type', label: 'Type', width: 'w-28' },
420
- { key: 'selector', label: 'Selector', width: 'w-48', hideOnMobile: true },
597
+ { key: 'selector', label: 'Selector', width: 'w-48' },
421
598
  { key: 'endpoints', label: 'Endpoints', width: 'w-24' },
422
599
  { key: 'ports', label: 'Ports', width: 'w-40' },
423
- { key: 'externalIP', label: 'External', width: 'w-40', hideOnMobile: true },
600
+ { key: 'externalIP', label: 'External', width: 'w-40' },
424
601
  { key: 'age', label: 'Age', width: 'w-24' },
425
602
  ],
426
603
  endpointslices: [
@@ -430,7 +607,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
430
607
  { key: 'addressType', label: 'Address Type', width: 'w-28' },
431
608
  { key: 'endpoints', label: 'Endpoints', width: 'w-32' },
432
609
  { key: 'addresses', label: 'Addresses', width: 'w-24' },
433
- { key: 'ports', label: 'Ports', width: 'w-40', hideOnMobile: true },
610
+ { key: 'ports', label: 'Ports', width: 'w-40' },
434
611
  { key: 'age', label: 'Age', width: 'w-24' },
435
612
  ],
436
613
  ingresses: [
@@ -438,9 +615,9 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
438
615
  { key: 'namespace', label: 'Namespace', width: 'w-36 shrink-0' },
439
616
  { key: 'class', label: 'Class', width: 'w-24 shrink-0' },
440
617
  { key: 'hosts', label: 'Hosts', width: 'min-w-48' },
441
- { key: 'rules', label: 'Rules', width: 'min-w-56', hideOnMobile: true },
618
+ { key: 'rules', label: 'Rules', width: 'min-w-56' },
442
619
  { key: 'tls', label: 'TLS', width: 'w-14 shrink-0' },
443
- { key: 'address', label: 'Address', width: 'min-w-32', hideOnMobile: true },
620
+ { key: 'address', label: 'Address', width: 'min-w-32' },
444
621
  { key: 'age', label: 'Age', width: 'w-24 shrink-0' },
445
622
  ],
446
623
  nodes: [
@@ -451,8 +628,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
451
628
  { key: 'memory', label: 'Memory', width: 'w-40', tooltip: 'Current memory usage / allocatable' },
452
629
  { key: 'pods', label: 'Pods', width: 'w-28', tooltip: 'Pods running / allocatable' },
453
630
  { 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 },
631
+ { key: 'conditions', label: 'Conditions', width: 'w-40' },
632
+ { key: 'taints', label: 'Taints', width: 'w-24' },
456
633
  { key: 'version', label: 'Version', width: 'w-28' },
457
634
  { key: 'age', label: 'Age', width: 'w-24' },
458
635
  ],
@@ -476,7 +653,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
476
653
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
477
654
  { key: 'status', label: 'Status', width: 'w-28' },
478
655
  { key: 'completions', label: 'Completions', width: 'w-32' },
479
- { key: 'duration', label: 'Duration', width: 'w-24', hideOnMobile: true },
656
+ { key: 'duration', label: 'Duration', width: 'w-24' },
480
657
  { key: 'age', label: 'Age', width: 'w-24' },
481
658
  ],
482
659
  cronjobs: [
@@ -484,7 +661,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
484
661
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
485
662
  { key: 'schedule', label: 'Schedule', width: 'w-40' },
486
663
  { key: 'status', label: 'Status', width: 'w-28' },
487
- { key: 'lastRun', label: 'Last Run', width: 'w-28', hideOnMobile: true },
664
+ { key: 'lastRun', label: 'Last Run', width: 'w-28' },
488
665
  { key: 'age', label: 'Age', width: 'w-24' },
489
666
  ],
490
667
  hpas: [
@@ -492,7 +669,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
492
669
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
493
670
  { key: 'target', label: 'Target', width: 'w-48' },
494
671
  { key: 'replicas', label: 'Replicas', width: 'w-32' },
495
- { key: 'metrics', label: 'Metrics', width: 'w-36', hideOnMobile: true },
672
+ { key: 'metrics', label: 'Metrics', width: 'w-36' },
496
673
  { key: 'status', label: 'Status', width: 'w-28' },
497
674
  { key: 'age', label: 'Age', width: 'w-24' },
498
675
  ],
@@ -501,7 +678,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
501
678
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
502
679
  { key: 'target', label: 'Target', width: 'w-48' },
503
680
  { key: 'replicas', label: 'Replicas', width: 'w-32' },
504
- { key: 'metrics', label: 'Metrics', width: 'w-36', hideOnMobile: true },
681
+ { key: 'metrics', label: 'Metrics', width: 'w-36' },
505
682
  { key: 'status', label: 'Status', width: 'w-28' },
506
683
  { key: 'age', label: 'Age', width: 'w-24' },
507
684
  ],
@@ -510,9 +687,9 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
510
687
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
511
688
  { key: 'status', label: 'Status', width: 'w-24' },
512
689
  { key: 'capacity', label: 'Capacity', width: 'w-24' },
513
- { key: 'storageClass', label: 'Storage Class', width: 'w-40', hideOnMobile: true },
690
+ { key: 'storageClass', label: 'Storage Class', width: 'w-40' },
514
691
  { 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 },
692
+ { key: 'volume', label: 'Volume', width: 'w-48' },
516
693
  { key: 'age', label: 'Age', width: 'w-24' },
517
694
  ],
518
695
  rollouts: [
@@ -521,8 +698,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
521
698
  { key: 'status', label: 'Status', width: 'w-32' },
522
699
  { key: 'ready', label: 'Ready', width: 'w-24', tooltip: 'Available / Desired replicas' },
523
700
  { 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 },
701
+ { key: 'step', label: 'Step', width: 'w-20', tooltip: 'Current canary step / Total steps' },
702
+ { key: 'images', label: 'Images', width: 'w-48' },
526
703
  { key: 'age', label: 'Age', width: 'w-24' },
527
704
  ],
528
705
  analysisruns: [
@@ -530,7 +707,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
530
707
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
531
708
  { key: 'status', label: 'Phase', width: 'w-28' },
532
709
  { 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' },
710
+ { key: 'metrics', label: 'Metrics', width: 'w-40', tooltip: 'Per-metric verdicts' },
534
711
  { key: 'age', label: 'Age', width: 'w-24' },
535
712
  ],
536
713
  workflows: [
@@ -538,8 +715,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
538
715
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
539
716
  { key: 'status', label: 'Phase', width: 'w-28' },
540
717
  { 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 },
718
+ { key: 'progress', label: 'Progress', width: 'w-24', tooltip: 'Succeeded steps / Total steps' },
719
+ { key: 'template', label: 'Template', width: 'w-40' },
543
720
  { key: 'age', label: 'Age', width: 'w-24' },
544
721
  ],
545
722
  cronworkflows: [
@@ -547,8 +724,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
547
724
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
548
725
  { key: 'schedule', label: 'Schedule', width: 'w-40' },
549
726
  { 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 },
727
+ { key: 'lastRun', label: 'Last Run', width: 'w-28' },
728
+ { key: 'template', label: 'Template', width: 'w-40' },
552
729
  { key: 'age', label: 'Age', width: 'w-24' },
553
730
  ],
554
731
  certificates: [
@@ -556,7 +733,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
556
733
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
557
734
  { key: 'status', label: 'Ready', width: 'w-24' },
558
735
  { key: 'domains', label: 'Domains', width: 'w-56' },
559
- { key: 'issuer', label: 'Issuer', width: 'w-36', hideOnMobile: true },
736
+ { key: 'issuer', label: 'Issuer', width: 'w-36' },
560
737
  { key: 'expires', label: 'Expires', width: 'w-24', tooltip: 'Days until certificate expires' },
561
738
  { key: 'age', label: 'Age', width: 'w-24' },
562
739
  ],
@@ -566,8 +743,8 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
566
743
  { key: 'capacity', label: 'Capacity', width: 'w-24' },
567
744
  { key: 'accessModes', label: 'Access', width: 'w-20', tooltip: 'RWO=ReadWriteOnce, ROX=ReadOnlyMany, RWX=ReadWriteMany' },
568
745
  { 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 },
746
+ { key: 'storageClass', label: 'Storage Class', width: 'w-40' },
747
+ { key: 'claim', label: 'Claim', width: 'w-48' },
571
748
  { key: 'age', label: 'Age', width: 'w-24' },
572
749
  ],
573
750
  storageclasses: [
@@ -604,7 +781,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
604
781
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
605
782
  { key: 'state', label: 'State', width: 'w-24' },
606
783
  { key: 'domains', label: 'Domains', width: 'w-48' },
607
- { key: 'issuer', label: 'Issuer', width: 'w-36', hideOnMobile: true },
784
+ { key: 'issuer', label: 'Issuer', width: 'w-36' },
608
785
  { key: 'age', label: 'Age', width: 'w-24' },
609
786
  ],
610
787
  challenges: [
@@ -613,7 +790,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
613
790
  { key: 'challengeType', label: 'Type', width: 'w-20' },
614
791
  { key: 'state', label: 'State', width: 'w-24' },
615
792
  { key: 'domain', label: 'Domain', width: 'w-48' },
616
- { key: 'presented', label: 'Presented', width: 'w-24', hideOnMobile: true },
793
+ { key: 'presented', label: 'Presented', width: 'w-24' },
617
794
  { key: 'age', label: 'Age', width: 'w-24' },
618
795
  ],
619
796
  gateways: [
@@ -623,7 +800,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
623
800
  { key: 'class', label: 'Class', width: 'w-36' },
624
801
  { key: 'listeners', label: 'Listeners', width: 'w-40', tooltip: 'Protocol:Port for each listener' },
625
802
  { 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
+ { key: 'addresses', label: 'Addresses', width: 'w-48' },
627
804
  { key: 'age', label: 'Age', width: 'w-24' },
628
805
  ],
629
806
  httproutes: [
@@ -633,7 +810,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
633
810
  { key: 'hostnames', label: 'Hostnames', width: 'w-48' },
634
811
  { key: 'parents', label: 'Gateways', width: 'w-36' },
635
812
  { key: 'backends', label: 'Backends', width: 'w-48', tooltip: 'Backend services receiving traffic' },
636
- { key: 'rules', label: 'Rules', width: 'w-16', hideOnMobile: true },
813
+ { key: 'rules', label: 'Rules', width: 'w-20' },
637
814
  { key: 'age', label: 'Age', width: 'w-24' },
638
815
  ],
639
816
  istiogateways: [
@@ -647,7 +824,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
647
824
  gatewayclasses: [
648
825
  { key: 'name', label: 'Name' },
649
826
  { key: 'controller', label: 'Controller', width: 'w-64', tooltip: 'Gateway controller implementation (spec.controllerName)' },
650
- { key: 'description', label: 'Description', width: 'w-64', hideOnMobile: true },
827
+ { key: 'description', label: 'Description', width: 'w-64' },
651
828
  { key: 'status', label: 'Status', width: 'w-28' },
652
829
  { key: 'age', label: 'Age', width: 'w-24' },
653
830
  ],
@@ -656,7 +833,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
656
833
  { key: 'status', label: 'Status', width: 'w-24' },
657
834
  { key: 'nodeClass', label: 'Node Class', width: 'w-36' },
658
835
  { key: 'limits', label: 'Limits', width: 'w-36', tooltip: 'CPU and memory limits' },
659
- { key: 'disruption', label: 'Disruption', width: 'w-40', hideOnMobile: true },
836
+ { key: 'disruption', label: 'Disruption', width: 'w-40' },
660
837
  { key: 'age', label: 'Age', width: 'w-24' },
661
838
  ],
662
839
  nodeclaims: [
@@ -665,9 +842,9 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
665
842
  { key: 'status', label: 'Status', width: 'w-24' },
666
843
  { key: 'instanceType', label: 'Instance Type', width: 'w-32' },
667
844
  { key: 'capacityType', label: 'Capacity', width: 'w-24', tooltip: 'Spot or On-Demand' },
668
- { key: 'zone', label: 'Zone', width: 'w-28', hideOnMobile: true },
845
+ { key: 'zone', label: 'Zone', width: 'w-28' },
669
846
  { key: 'nodePool', label: 'Node Pool', width: 'w-32' },
670
- { key: 'nodeName', label: 'Node', width: 'w-40', hideOnMobile: true },
847
+ { key: 'nodeName', label: 'Node', width: 'w-40' },
671
848
  { key: 'age', label: 'Age', width: 'w-24' },
672
849
  ],
673
850
  ec2nodeclasses: [
@@ -1117,14 +1294,14 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1117
1294
  { key: 'namespace', label: 'Namespace', width: 'w-36' },
1118
1295
  { key: 'status', label: 'Status', width: 'w-24' },
1119
1296
  { 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' },
1297
+ { key: 'rules', label: 'Rules', width: 'w-20' },
1121
1298
  { key: 'age', label: 'Age', width: 'w-24' },
1122
1299
  ],
1123
1300
  clusterpolicies: [
1124
1301
  { key: 'name', label: 'Name' },
1125
1302
  { key: 'status', label: 'Status', width: 'w-24' },
1126
1303
  { 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' },
1304
+ { key: 'rules', label: 'Rules', width: 'w-20' },
1128
1305
  { key: 'age', label: 'Age', width: 'w-24' },
1129
1306
  ],
1130
1307
  // Kyverno modern CEL family (policies.kyverno.io). "Enforcement" is the
@@ -1247,7 +1424,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1247
1424
  { key: 'hostnames', label: 'Hostnames', width: 'w-48' },
1248
1425
  { key: 'parents', label: 'Gateways', width: 'w-36' },
1249
1426
  { key: 'backends', label: 'Backends', width: 'w-48', tooltip: 'Backend services receiving traffic' },
1250
- { key: 'rules', label: 'Rules', width: 'w-16', hideOnMobile: true },
1427
+ { key: 'rules', label: 'Rules', width: 'w-20' },
1251
1428
  { key: 'age', label: 'Age', width: 'w-24' },
1252
1429
  ],
1253
1430
  tcproutes: [
@@ -1256,7 +1433,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1256
1433
  { key: 'status', label: 'Status', width: 'w-28' },
1257
1434
  { key: 'parents', label: 'Gateways', width: 'w-36' },
1258
1435
  { key: 'backends', label: 'Backends', width: 'w-48', tooltip: 'Backend services receiving traffic' },
1259
- { key: 'rules', label: 'Rules', width: 'w-16', hideOnMobile: true },
1436
+ { key: 'rules', label: 'Rules', width: 'w-20' },
1260
1437
  { key: 'age', label: 'Age', width: 'w-24' },
1261
1438
  ],
1262
1439
  tlsroutes: [
@@ -1266,7 +1443,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1266
1443
  { key: 'hostnames', label: 'Hostnames', width: 'w-48', tooltip: 'SNI hostnames for TLS routing' },
1267
1444
  { key: 'parents', label: 'Gateways', width: 'w-36' },
1268
1445
  { key: 'backends', label: 'Backends', width: 'w-48', tooltip: 'Backend services receiving traffic' },
1269
- { key: 'rules', label: 'Rules', width: 'w-16', hideOnMobile: true },
1446
+ { key: 'rules', label: 'Rules', width: 'w-20' },
1270
1447
  { key: 'age', label: 'Age', width: 'w-24' },
1271
1448
  ],
1272
1449
  sealedsecrets: [
@@ -1274,7 +1451,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1274
1451
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
1275
1452
  { key: 'status', label: 'Synced', width: 'w-24' },
1276
1453
  { key: 'keys', label: 'Keys', width: 'w-20' },
1277
- { key: 'type', label: 'Type', width: 'w-36', hideOnMobile: true },
1454
+ { key: 'type', label: 'Type', width: 'w-36' },
1278
1455
  { key: 'age', label: 'Age', width: 'w-24' },
1279
1456
  ],
1280
1457
  workflowtemplates: [
@@ -1438,7 +1615,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1438
1615
  { key: 'value', label: 'Value', width: 'w-24' },
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: [
@@ -1450,14 +1627,14 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
1450
1627
  { key: 'name', label: 'Name' },
1451
1628
  { key: 'webhooks', label: 'Webhooks', width: 'w-28' },
1452
1629
  { key: 'failurePolicy', label: 'Failure Policy', width: 'w-36' },
1453
- { key: 'target', label: 'Target', width: 'w-48', hideOnMobile: true },
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' },
@@ -2116,7 +2293,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
2116
2293
  { key: 'namespace', label: 'Namespace', width: 'w-36 shrink-0' },
2117
2294
  { key: 'entrypoints', label: 'Entry Points', width: 'w-32 shrink-0' },
2118
2295
  { key: 'hosts', label: 'Hosts', width: 'min-w-44' },
2119
- { key: 'routes', label: 'Routes', width: 'min-w-48', hideOnMobile: true },
2296
+ { key: 'routes', label: 'Routes', width: 'min-w-48' },
2120
2297
  { key: 'tls', label: 'TLS', width: 'w-14 shrink-0' },
2121
2298
  { key: 'middlewares', label: 'MW', width: 'w-14 shrink-0', tooltip: 'Unique middleware count' },
2122
2299
  { key: 'age', label: 'Age', width: 'w-24 shrink-0' },
@@ -2126,7 +2303,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
2126
2303
  { key: 'namespace', label: 'Namespace', width: 'w-36 shrink-0' },
2127
2304
  { key: 'entrypoints', label: 'Entry Points', width: 'w-32 shrink-0' },
2128
2305
  { key: 'hosts', label: 'Hosts', width: 'min-w-44' },
2129
- { key: 'routes', label: 'Routes', width: 'min-w-48', hideOnMobile: true },
2306
+ { key: 'routes', label: 'Routes', width: 'min-w-48' },
2130
2307
  { key: 'tls', label: 'TLS', width: 'w-14 shrink-0' },
2131
2308
  { key: 'middlewares', label: 'MW', width: 'w-14 shrink-0', tooltip: 'Unique middleware count' },
2132
2309
  { key: 'age', label: 'Age', width: 'w-24 shrink-0' },
@@ -2390,15 +2567,15 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
2390
2567
  { key: 'name', label: 'Name', width: 'min-w-40' },
2391
2568
  { key: 'namespace', label: 'Namespace', width: 'w-32 shrink-0' },
2392
2569
  { 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 },
2570
+ { key: 'external', label: 'External Name', width: 'min-w-48' },
2571
+ { key: 'provider', label: 'Provider Config', width: 'w-40 shrink-0' },
2395
2572
  { key: 'status', label: 'Status', width: 'w-32 shrink-0' },
2396
2573
  { key: 'age', label: 'Age', width: 'w-24 shrink-0' },
2397
2574
  ],
2398
2575
  providers: [
2399
2576
  { key: 'name', label: 'Name', width: 'min-w-40' },
2400
2577
  { key: 'package', label: 'Package', width: 'min-w-48' },
2401
- { key: 'revision', label: 'Revision', width: 'w-36 shrink-0', hideOnMobile: true },
2578
+ { key: 'revision', label: 'Revision', width: 'w-36 shrink-0' },
2402
2579
  { key: 'status', label: 'Status', width: 'w-32 shrink-0' },
2403
2580
  { key: 'age', label: 'Age', width: 'w-24 shrink-0' },
2404
2581
  ],
@@ -2412,13 +2589,13 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
2412
2589
  { key: 'name', label: 'Name', width: 'min-w-40' },
2413
2590
  { key: 'composite', label: 'Composite Kind', width: 'w-44 shrink-0' },
2414
2591
  { key: 'mode', label: 'Mode', width: 'w-24 shrink-0' },
2415
- { key: 'functions', label: 'Functions', width: 'w-28 shrink-0', hideOnMobile: true },
2592
+ { key: 'functions', label: 'Functions', width: 'w-28 shrink-0' },
2416
2593
  { key: 'age', label: 'Age', width: 'w-24 shrink-0' },
2417
2594
  ],
2418
2595
  compositeresourcedefinitions: [
2419
2596
  { key: 'name', label: 'Name', width: 'min-w-40' },
2420
2597
  { key: 'kind', label: 'Kind', width: 'w-40 shrink-0' },
2421
- { key: 'claim', label: 'Claim Kind', width: 'w-40 shrink-0', hideOnMobile: true },
2598
+ { key: 'claim', label: 'Claim Kind', width: 'w-40 shrink-0' },
2422
2599
  { key: 'age', label: 'Age', width: 'w-24 shrink-0' },
2423
2600
  ],
2424
2601
  }
@@ -3603,6 +3780,9 @@ export function ResourcesView({
3603
3780
 
3604
3781
  // Map of extra column keys for fast O(1) lookup on each render path
3605
3782
  // (cell render, sort, column-filter unique-values).
3783
+ // Recompute header-derived widths once the web font swaps in (see watchHeaderFont).
3784
+ const headerFontEpoch = useHeaderFontEpoch()
3785
+
3606
3786
  const extraColumnsByKey = useMemo(() => {
3607
3787
  const m = new Map<string, ExtraColumn>()
3608
3788
  extraLeadingColumns?.forEach(c => m.set(c.key, c))
@@ -5248,12 +5428,12 @@ export function ResourcesView({
5248
5428
  // scroll horizontally when the viewport is too narrow.
5249
5429
  const tableMinWidth = useMemo(() => {
5250
5430
  const compareColumnWidth = compareMode ? COMPARE_COLUMN_WIDTH : 0
5251
- const baseMinWidth = columns.reduce((sum, col) => sum + (columnWidths[col.key] || getColumnMinWidth(col)), compareColumnWidth)
5431
+ const baseMinWidth = columns.reduce((sum, col) => sum + (columnWidths[col.key] || getColumnMinWidth(col, extraColumnsByKey)), compareColumnWidth)
5252
5432
  const flexibleNameColumn = columns.find(col => col.key === 'name' && !columnWidths[col.key])
5253
5433
 
5254
5434
  if (!hasResizedColumns || !flexibleNameColumn) return baseMinWidth
5255
- return baseMinWidth + getColumnMinWidth(flexibleNameColumn)
5256
- }, [columns, columnWidths, compareMode, hasResizedColumns])
5435
+ return baseMinWidth + getColumnMinWidth(flexibleNameColumn, extraColumnsByKey)
5436
+ }, [columns, columnWidths, compareMode, hasResizedColumns, extraColumnsByKey, headerFontEpoch])
5257
5437
 
5258
5438
  // Stable virtuoso components — memoized to avoid remounting the table on every render
5259
5439
  const virtuosoComponents = useMemo(() => ({
@@ -5281,7 +5461,7 @@ export function ResourcesView({
5281
5461
  style={{
5282
5462
  width: columnWidths[col.key]
5283
5463
  ? `${columnWidths[col.key]}px`
5284
- : col.key === 'name' ? undefined : `${getColumnMinWidth(col)}px`,
5464
+ : col.key === 'name' ? undefined : `${getColumnMinWidth(col, extraColumnsByKey)}px`,
5285
5465
  }}
5286
5466
  />
5287
5467
  ))}
@@ -5292,7 +5472,7 @@ export function ResourcesView({
5292
5472
  )
5293
5473
  }),
5294
5474
  TableRow: VirtuosoTableRow,
5295
- }), [columns, columnWidths, hasResizedColumns, compareMode, tableMinWidth, isCheckboxMode])
5475
+ }), [columns, columnWidths, hasResizedColumns, compareMode, tableMinWidth, isCheckboxMode, extraColumnsByKey, headerFontEpoch])
5296
5476
 
5297
5477
  // Calculate filter options with counts based on current resources (before filtering)
5298
5478
  const filterOptions = useMemo(() => {
@@ -6143,8 +6323,7 @@ export function ResourcesView({
6143
6323
  )}
6144
6324
  {columns.map((col, colIdx) => {
6145
6325
  // 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
6326
+ const isSortable = isColumnSortable(col, extraColumnsByKey)
6148
6327
  const isSorted = sortColumn === col.key
6149
6328
  const isLastCol = colIdx === columns.length - 1
6150
6329
  const filterCol = filterableColumnMap.get(col.key)
@@ -6164,11 +6343,11 @@ export function ResourcesView({
6164
6343
  >
6165
6344
  <div className="flex items-center gap-1 overflow-hidden">
6166
6345
  {col.tooltip ? (
6167
- <Tooltip content={col.tooltip}>
6346
+ <Tooltip content={col.tooltip} wrapperClassName="min-w-0">
6168
6347
  <span className="border-b border-dotted border-theme-text-tertiary truncate">{col.label}</span>
6169
6348
  </Tooltip>
6170
6349
  ) : (
6171
- <span className="truncate">{col.label}</span>
6350
+ <HeaderLabel label={col.label} />
6172
6351
  )}
6173
6352
  {isSortable && (
6174
6353
  <span className="text-theme-text-tertiary shrink-0">