@skyhook-io/k8s-ui 1.13.3 → 1.13.4

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 (97) hide show
  1. package/package.json +1 -1
  2. package/src/components/applications/AppTooltips.tsx +13 -1
  3. package/src/components/applications/ApplicationDetail.test.tsx +25 -0
  4. package/src/components/applications/ApplicationDetail.tsx +39 -7
  5. package/src/components/applications/ApplicationsView.tsx +18 -3
  6. package/src/components/applications/ReadyBar.tsx +3 -2
  7. package/src/components/applications/applications-view.test.tsx +91 -0
  8. package/src/components/cluster-switcher/ClusterSwitcher.tsx +4 -1
  9. package/src/components/issues/IssuesView.tsx +40 -31
  10. package/src/components/issues/ResourceIssuesSection.tsx +3 -2
  11. package/src/components/issues/index.ts +3 -1
  12. package/src/components/issues/issue-timing.ts +123 -4
  13. package/src/components/issues/issues.test.ts +221 -10
  14. package/src/components/issues/types.ts +31 -5
  15. package/src/components/namespace-switcher/NamespacePicker.tsx +21 -13
  16. package/src/components/resources/ResourcesView.tsx +964 -96
  17. package/src/components/resources/column-settings-key.test.ts +98 -0
  18. package/src/components/resources/column-visibility-merge.test.ts +64 -0
  19. package/src/components/resources/curated-column-ownership.test.ts +236 -0
  20. package/src/components/resources/gateway-policy-status.test.ts +291 -0
  21. package/src/components/resources/gateway-policy-status.ts +237 -0
  22. package/src/components/resources/generic-status.test.ts +199 -0
  23. package/src/components/resources/generic-status.ts +171 -0
  24. package/src/components/resources/gpu-ecosystem-registry.test.ts +63 -0
  25. package/src/components/resources/index.ts +9 -1
  26. package/src/components/resources/istio-gateway-filter.test.ts +97 -0
  27. package/src/components/resources/printer-columns.test.ts +298 -0
  28. package/src/components/resources/printer-columns.ts +249 -0
  29. package/src/components/resources/renderers/EventRenderer.test.tsx +50 -1
  30. package/src/components/resources/renderers/EventRenderer.tsx +3 -5
  31. package/src/components/resources/renderers/GenericRenderer.tsx +18 -83
  32. package/src/components/resources/renderers/HPARenderer.test.tsx +28 -0
  33. package/src/components/resources/renderers/HPARenderer.tsx +6 -1
  34. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +2 -1
  35. package/src/components/resources/renderers/PVCRenderer.test.tsx +44 -0
  36. package/src/components/resources/renderers/PVCRenderer.tsx +19 -2
  37. package/src/components/resources/renderers/RolloutRenderer.test.tsx +35 -0
  38. package/src/components/resources/renderers/RolloutRenderer.tsx +22 -3
  39. package/src/components/resources/renderers/WorkloadRenderer.test.tsx +83 -0
  40. package/src/components/resources/renderers/WorkloadRenderer.tsx +25 -65
  41. package/src/components/resources/renderers/amd-gpu-cells.tsx +36 -0
  42. package/src/components/resources/renderers/inference-gateway-cells.tsx +62 -0
  43. package/src/components/resources/renderers/jobset-lws-cells.tsx +82 -0
  44. package/src/components/resources/renderers/kai-cells.tsx +67 -0
  45. package/src/components/resources/renderers/kaito-cells.tsx +62 -0
  46. package/src/components/resources/renderers/kserve-cells.tsx +145 -0
  47. package/src/components/resources/renderers/kubeflow-training-cells.tsx +72 -0
  48. package/src/components/resources/renderers/kueue-cells.tsx +176 -0
  49. package/src/components/resources/renderers/nim-cells.tsx +81 -0
  50. package/src/components/resources/renderers/ray-cells.tsx +124 -0
  51. package/src/components/resources/renderers/volcano-cells.tsx +140 -0
  52. package/src/components/resources/resource-utils-amd-gpu.ts +61 -0
  53. package/src/components/resources/resource-utils-gpu-ecosystem.test.ts +222 -0
  54. package/src/components/resources/resource-utils-hpa.test.ts +4 -0
  55. package/src/components/resources/resource-utils-hpa.ts +6 -2
  56. package/src/components/resources/resource-utils-inference-gateway.ts +119 -0
  57. package/src/components/resources/resource-utils-jobset-lws.ts +117 -0
  58. package/src/components/resources/resource-utils-kai.ts +105 -0
  59. package/src/components/resources/resource-utils-kaito.ts +114 -0
  60. package/src/components/resources/resource-utils-kserve.ts +186 -0
  61. package/src/components/resources/resource-utils-kubeflow-training.ts +121 -0
  62. package/src/components/resources/resource-utils-kueue.ts +198 -0
  63. package/src/components/resources/resource-utils-nim.ts +109 -0
  64. package/src/components/resources/resource-utils-ray.ts +214 -0
  65. package/src/components/resources/resource-utils-rollout.test.ts +183 -0
  66. package/src/components/resources/resource-utils-velero.test.ts +30 -0
  67. package/src/components/resources/resource-utils-volcano.ts +176 -0
  68. package/src/components/resources/resource-utils.ts +156 -47
  69. package/src/components/resources/resources-column-filter.test.ts +27 -1
  70. package/src/components/resources/workload-rollout-golden.test.ts +55 -0
  71. package/src/components/shared/CreateResourceDialog.tsx +1 -0
  72. package/src/components/shared/EditableYamlView.tsx +4 -2
  73. package/src/components/shared/ResourceActionsBar.tsx +170 -25
  74. package/src/components/shared/ResourceRendererDispatch.test.tsx +206 -1
  75. package/src/components/shared/ResourceRendererDispatch.tsx +84 -40
  76. package/src/components/shared/SetImageDialog.test.ts +129 -0
  77. package/src/components/shared/SetImageDialog.tsx +577 -0
  78. package/src/components/shared/index.ts +1 -0
  79. package/src/components/shared/revision-role-badges.test.ts +56 -4
  80. package/src/components/timeline/TimelineList.tsx +19 -3
  81. package/src/components/timeline/TimelineSwimlanes.tsx +11 -3
  82. package/src/components/trace/ReachabilityView.tsx +1 -1
  83. package/src/components/ui/drawer-components.tsx +12 -0
  84. package/src/components/workload/WorkloadRolloutNotice.tsx +60 -0
  85. package/src/components/workload/WorkloadView.tsx +144 -81
  86. package/src/hooks/useProgressiveRefresh.ts +37 -0
  87. package/src/source-hygiene.test.ts +28 -0
  88. package/src/theme/tailwind-theme.css +4 -0
  89. package/src/types/core.ts +55 -0
  90. package/src/utils/api-resources.test.ts +40 -1
  91. package/src/utils/api-resources.ts +11 -1
  92. package/src/utils/applications.ts +50 -0
  93. package/src/utils/bulk-workload-actions.test.ts +20 -0
  94. package/src/utils/bulk-workload-actions.ts +15 -0
  95. package/src/utils/navigation.test.ts +31 -1
  96. package/src/utils/navigation.ts +21 -2
  97. package/src/utils/workload-rollout.ts +292 -0
@@ -34,12 +34,14 @@ import { ResourceBar } from '../ui/ResourceBar'
34
34
  import type { SelectedResource, APIResource } from '../../types'
35
35
  import { isForbiddenError } from '../../types/fetch-error'
36
36
  import type { NavigateToResource } from '../../utils/navigation'
37
- import { categorizeResources, CORE_RESOURCES, findAPIResourceForRoute } from '../../utils/api-resources'
37
+ import { categorizeResources, CORE_RESOURCES, findAPIResourceForRoute, isCoreBatchJob } from '../../utils/api-resources'
38
38
  import { copyText } from '../../utils/clipboard'
39
39
  import {
40
40
  getPodStatus,
41
41
  getPodRestarts,
42
42
  getPodProblems,
43
+ getWorkloadDisplayStatus,
44
+ workloadStatusLabel,
43
45
  getWorkloadProblems,
44
46
  workloadMatchesProblemCategory,
45
47
  getContainerSquareStates,
@@ -79,7 +81,6 @@ import {
79
81
  getPVCStatus,
80
82
  getPVCCapacity,
81
83
  getPVCAccessModes,
82
- getRolloutStatus,
83
84
  getAnalysisRunStatus,
84
85
  summarizeAnalysisMetrics,
85
86
  getRolloutStrategy,
@@ -136,12 +137,20 @@ import {
136
137
  parseColumnFilterExcludes,
137
138
  podMatchesProblemCategory,
138
139
  SEVERITY_DOT_COLOR,
140
+ healthColors,
139
141
  } from './resource-utils'
142
+ import { getGenericResourceStatus } from './generic-status'
143
+ import { getIstioGatewayServerCount, getIstioGatewaySelectorString } from './resource-utils-istio'
144
+ import {
145
+ type PrinterColumnDef, type PrinterTable, PRINTER_COLUMN_PREFIX, formatPrinterCell, printerCellSortValue,
146
+ printerCellTone, printerColumnKey, printerColumnWidth, printerTableKey, printerTableMatchesKind, readPrinterCell,
147
+ } from './printer-columns'
140
148
  import { SEVERITY_BADGE, EVENT_TYPE_COLORS } from '../../utils/badge-colors'
141
149
  import { pluralize } from '../../utils/pluralize'
142
150
  import { getPodGpuCount, getNodeGpuCount } from '../../utils/extended-resources'
143
151
  import { parseQuantityToNumber } from '../../utils/format'
144
152
  import { type CustomColumnDef, type CustomColumnSource, customColumnKey, readCustomColumnValue, sanitizeCustomColumnDefs } from '../../utils/custom-columns'
153
+ import { isRolloutActivityVisible } from '../../utils/workload-rollout'
145
154
  import { FreshnessControl, type FreshnessConnection } from '../ui/FreshnessControl'
146
155
  import { Tooltip } from '../ui/Tooltip'
147
156
  import { AuditBadgeTooltip, type AuditBadgeMessage } from '../audit/AuditBadgeTooltip'
@@ -155,6 +164,18 @@ import { NodePoolCell, NodeClaimCell, EC2NodeClassCell } from './renderers/karpe
155
164
  import { ScaledObjectCell, ScaledJobCell, TriggerAuthenticationCell, ClusterTriggerAuthenticationCell } from './renderers/keda-cells'
156
165
  import { ResourceClaimCell, ResourceClaimTemplateCell, DeviceClassCell, ResourceSliceCell } from './renderers/dra-cells'
157
166
  import { NvidiaClusterPolicyCell, NvidiaDriverCell } from './renderers/nvidia-cells'
167
+ import { ClusterQueueCell, LocalQueueCell, KueueWorkloadCell, ResourceFlavorCell, AdmissionCheckCell, ProvisioningRequestCell } from './renderers/kueue-cells'
168
+ import { RayClusterCell, RayJobCell, RayServiceCell, RayCronJobCell } from './renderers/ray-cells'
169
+ import { LeaderWorkerSetCell, JobSetCell } from './renderers/jobset-lws-cells'
170
+ import { getJobSetReadyJobs, getJobSetRestarts } from './resource-utils-jobset-lws'
171
+ import { InferenceServiceCell, ServingRuntimeCell, InferenceGraphCell, TrainedModelCell, LLMInferenceServiceCell } from './renderers/kserve-cells'
172
+ import { InferencePoolCell, InferenceObjectiveCell } from './renderers/inference-gateway-cells'
173
+ import { VolcanoJobCell, VolcanoQueueCell, VolcanoPodGroupCell, JobFlowCell, JobTemplateCell } from './renderers/volcano-cells'
174
+ import { KaiQueueCell, KaiPodGroupCell } from './renderers/kai-cells'
175
+ import { KaitoWorkspaceCell, RAGEngineCell } from './renderers/kaito-cells'
176
+ import { NIMServiceCell, NIMCacheCell, NIMPipelineCell } from './renderers/nim-cells'
177
+ import { AMDDeviceConfigCell } from './renderers/amd-gpu-cells'
178
+ import { PyTorchJobCell, TFJobCell, MPIJobCell, TrainJobCell } from './renderers/kubeflow-training-cells'
158
179
  import { ServiceMonitorCell, PrometheusRuleCell, PodMonitorCell } from './renderers/prometheus-cells'
159
180
  import { PolicyReportCell, ClusterPolicyReportCell, KyvernoPolicyCell, ClusterPolicyCell,
160
181
  KyvernoUpdateRequestCell,
@@ -357,6 +378,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
357
378
  deployments: [
358
379
  { key: 'name', label: 'Name' },
359
380
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
381
+ { key: 'status', label: 'Status', width: 'w-40' },
360
382
  { key: 'ready', label: 'Ready', width: 'w-24', tooltip: 'Ready pods / Desired replicas' },
361
383
  { key: 'upToDate', label: 'Up-to-date', width: 'w-32', hideOnMobile: true, tooltip: 'Number of pods running the current pod template' },
362
384
  { key: 'available', label: 'Available', width: 'w-28', hideOnMobile: true, tooltip: 'Number of pods available (ready for minReadySeconds)' },
@@ -366,6 +388,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
366
388
  daemonsets: [
367
389
  { key: 'name', label: 'Name' },
368
390
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
391
+ { key: 'status', label: 'Status', width: 'w-40' },
369
392
  { key: 'desired', label: 'Desired', width: 'w-20', tooltip: 'Number of nodes that should run the daemon pod (based on node selector)' },
370
393
  { key: 'ready', label: 'Ready', width: 'w-20', tooltip: 'Number of pods that are ready (passing readiness probes)' },
371
394
  { key: 'upToDate', label: 'Up-to-date', width: 'w-32', hideOnMobile: true, tooltip: 'Number of pods running the current pod template spec' },
@@ -376,6 +399,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
376
399
  statefulsets: [
377
400
  { key: 'name', label: 'Name' },
378
401
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
402
+ { key: 'status', label: 'Status', width: 'w-40' },
379
403
  { key: 'ready', label: 'Ready', width: 'w-24', tooltip: 'Ready pods / Desired replicas' },
380
404
  { key: 'upToDate', label: 'Up-to-date', width: 'w-32', hideOnMobile: true, tooltip: 'Number of pods running the current pod template' },
381
405
  { key: 'images', label: 'Images', width: 'w-48', hideOnMobile: true },
@@ -494,7 +518,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
494
518
  rollouts: [
495
519
  { key: 'name', label: 'Name' },
496
520
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
497
- { key: 'status', label: 'Phase', width: 'w-28' },
521
+ { key: 'status', label: 'Status', width: 'w-32' },
498
522
  { key: 'ready', label: 'Ready', width: 'w-24', tooltip: 'Available / Desired replicas' },
499
523
  { key: 'strategy', label: 'Strategy', width: 'w-24' },
500
524
  { key: 'step', label: 'Step', width: 'w-20', hideOnMobile: true, tooltip: 'Current canary step / Total steps' },
@@ -612,6 +636,14 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
612
636
  { key: 'rules', label: 'Rules', width: 'w-16', hideOnMobile: true },
613
637
  { key: 'age', label: 'Age', width: 'w-24' },
614
638
  ],
639
+ istiogateways: [
640
+ { key: 'name', label: 'Name' },
641
+ { key: 'namespace', label: 'Namespace', width: 'w-48' },
642
+ { key: 'status', label: 'Status', width: 'w-28' },
643
+ { key: 'servers', label: 'Servers', width: 'w-24' },
644
+ { key: 'selector', label: 'Selector', width: 'w-56' },
645
+ { key: 'age', label: 'Age', width: 'w-24' },
646
+ ],
615
647
  gatewayclasses: [
616
648
  { key: 'name', label: 'Name' },
617
649
  { key: 'controller', label: 'Controller', width: 'w-64', tooltip: 'Gateway controller implementation (spec.controllerName)' },
@@ -697,6 +729,314 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
697
729
  { key: 'version', label: 'Version', width: 'w-32' },
698
730
  { key: 'age', label: 'Age', width: 'w-24' },
699
731
  ],
732
+ clusterqueues: [
733
+ { key: 'name', label: 'Name' },
734
+ { key: 'status', label: 'Status', width: 'w-24' },
735
+ { key: 'cohort', label: 'Cohort', width: 'w-32' },
736
+ { key: 'pendingWorkloads', label: 'Pending', width: 'w-24', tooltip: 'Pending workloads' },
737
+ { key: 'admittedWorkloads', label: 'Admitted', width: 'w-24', tooltip: 'Admitted workloads' },
738
+ { key: 'flavors', label: 'Flavors', width: 'w-40' },
739
+ { key: 'age', label: 'Age', width: 'w-24' },
740
+ ],
741
+ localqueues: [
742
+ { key: 'name', label: 'Name' },
743
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
744
+ { key: 'status', label: 'Status', width: 'w-24' },
745
+ { key: 'clusterQueue', label: 'Cluster Queue', width: 'w-40' },
746
+ { key: 'pendingWorkloads', label: 'Pending', width: 'w-24' },
747
+ { key: 'admittedWorkloads', label: 'Admitted', width: 'w-24' },
748
+ { key: 'age', label: 'Age', width: 'w-24' },
749
+ ],
750
+ workloads: [
751
+ { key: 'name', label: 'Name' },
752
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
753
+ { key: 'status', label: 'Status', width: 'w-28' },
754
+ { key: 'queueName', label: 'Queue', width: 'w-36' },
755
+ { key: 'admittedBy', label: 'Admitted By', width: 'w-36', tooltip: 'Admitting ClusterQueue' },
756
+ { key: 'priority', label: 'Priority', width: 'w-24' },
757
+ { key: 'age', label: 'Age', width: 'w-24' },
758
+ ],
759
+ resourceflavors: [
760
+ { key: 'name', label: 'Name' },
761
+ { key: 'status', label: 'Status', width: 'w-24' },
762
+ { key: 'nodeLabels', label: 'Node Labels', width: 'w-28', tooltip: 'Node label selector count' },
763
+ { key: 'taints', label: 'Taints', width: 'w-24' },
764
+ { key: 'age', label: 'Age', width: 'w-24' },
765
+ ],
766
+ admissionchecks: [
767
+ { key: 'name', label: 'Name' },
768
+ { key: 'status', label: 'Status', width: 'w-24' },
769
+ { key: 'controllerName', label: 'Controller', width: 'w-64' },
770
+ { key: 'age', label: 'Age', width: 'w-24' },
771
+ ],
772
+ provisioningrequests: [
773
+ { key: 'name', label: 'Name' },
774
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
775
+ { key: 'status', label: 'Status', width: 'w-28' },
776
+ { key: 'provisioningClassName', label: 'Class', width: 'w-56' },
777
+ { key: 'podSets', label: 'Pod Sets', width: 'w-24' },
778
+ { key: 'age', label: 'Age', width: 'w-24' },
779
+ ],
780
+ rayclusters: [
781
+ { key: 'name', label: 'Name' },
782
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
783
+ { key: 'status', label: 'Status', width: 'w-28' },
784
+ { key: 'rayVersion', label: 'Ray Version', width: 'w-28' },
785
+ { key: 'workers', label: 'Workers', width: 'w-24', tooltip: 'Available/desired worker replicas' },
786
+ { key: 'headService', label: 'Head Service', width: 'w-48' },
787
+ { key: 'age', label: 'Age', width: 'w-24' },
788
+ ],
789
+ rayjobs: [
790
+ { key: 'name', label: 'Name' },
791
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
792
+ { key: 'status', label: 'Status', width: 'w-28' },
793
+ { key: 'jobStatus', label: 'Job Status', width: 'w-28' },
794
+ { key: 'deploymentStatus', label: 'Deployment', width: 'w-32' },
795
+ { key: 'cluster', label: 'Cluster', width: 'w-48' },
796
+ { key: 'age', label: 'Age', width: 'w-24' },
797
+ ],
798
+ rayservices: [
799
+ { key: 'name', label: 'Name' },
800
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
801
+ { key: 'status', label: 'Status', width: 'w-28' },
802
+ { key: 'serviceStatus', label: 'Service Status', width: 'w-28' },
803
+ { key: 'clusters', label: 'Clusters', width: 'w-56', tooltip: 'Active and pending RayCluster names' },
804
+ { key: 'age', label: 'Age', width: 'w-24' },
805
+ ],
806
+ raycronjobs: [
807
+ { key: 'name', label: 'Name' },
808
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
809
+ { key: 'status', label: 'Status', width: 'w-24' },
810
+ { key: 'schedule', label: 'Schedule', width: 'w-32' },
811
+ { key: 'suspend', label: 'Suspend', width: 'w-20' },
812
+ { key: 'lastSchedule', label: 'Last Schedule', width: 'w-28' },
813
+ { key: 'age', label: 'Age', width: 'w-24' },
814
+ ],
815
+ leaderworkersets: [
816
+ { key: 'name', label: 'Name' },
817
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
818
+ { key: 'status', label: 'Status', width: 'w-28' },
819
+ { key: 'replicas', label: 'Groups', width: 'w-20', tooltip: 'Desired leader-worker groups' },
820
+ { key: 'size', label: 'Size', width: 'w-20', tooltip: 'Pods per group' },
821
+ { key: 'ready', label: 'Ready', width: 'w-20', tooltip: 'Ready groups / desired groups' },
822
+ { key: 'updated', label: 'Updated', width: 'w-20' },
823
+ { key: 'age', label: 'Age', width: 'w-24' },
824
+ ],
825
+ jobsets: [
826
+ { key: 'name', label: 'Name' },
827
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
828
+ { key: 'status', label: 'Status', width: 'w-28' },
829
+ { key: 'replicatedJobs', label: 'Repl. Jobs', width: 'w-24', tooltip: 'Number of replicated job templates' },
830
+ { key: 'ready', label: 'Ready', width: 'w-20' },
831
+ { key: 'succeeded', label: 'Succeeded', width: 'w-24' },
832
+ { key: 'failed', label: 'Failed', width: 'w-20' },
833
+ { key: 'restarts', label: 'Restarts', width: 'w-24' },
834
+ { key: 'age', label: 'Age', width: 'w-24' },
835
+ ],
836
+ inferenceservices: [
837
+ { key: 'name', label: 'Name' },
838
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
839
+ { key: 'status', label: 'Status', width: 'w-36', tooltip: 'Ready condition + model load state' },
840
+ { key: 'url', label: 'URL', width: 'w-64' },
841
+ { key: 'modelFormat', label: 'Format', width: 'w-28' },
842
+ { key: 'runtime', label: 'Runtime', width: 'w-40' },
843
+ { key: 'deploymentMode', label: 'Mode', width: 'w-28', tooltip: 'Serverless, RawDeployment, or ModelMesh' },
844
+ { key: 'age', label: 'Age', width: 'w-24' },
845
+ ],
846
+ servingruntimes: [
847
+ { key: 'name', label: 'Name' },
848
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
849
+ { key: 'status', label: 'Status', width: 'w-24' },
850
+ { key: 'modelFormats', label: 'Model Formats', width: 'w-44', tooltip: 'Supported model formats' },
851
+ { key: 'image', label: 'Image', width: 'w-64' },
852
+ { key: 'age', label: 'Age', width: 'w-24' },
853
+ ],
854
+ clusterservingruntimes: [
855
+ { key: 'name', label: 'Name' },
856
+ { key: 'status', label: 'Status', width: 'w-24' },
857
+ { key: 'modelFormats', label: 'Model Formats', width: 'w-44', tooltip: 'Supported model formats' },
858
+ { key: 'image', label: 'Image', width: 'w-64' },
859
+ { key: 'age', label: 'Age', width: 'w-24' },
860
+ ],
861
+ inferencegraphs: [
862
+ { key: 'name', label: 'Name' },
863
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
864
+ { key: 'status', label: 'Status', width: 'w-24' },
865
+ { key: 'nodes', label: 'Nodes', width: 'w-20', tooltip: 'Router nodes in the graph' },
866
+ { key: 'age', label: 'Age', width: 'w-24' },
867
+ ],
868
+ trainedmodels: [
869
+ { key: 'name', label: 'Name' },
870
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
871
+ { key: 'status', label: 'Status', width: 'w-24' },
872
+ { key: 'framework', label: 'Framework', width: 'w-28' },
873
+ { key: 'storageUri', label: 'Storage URI', width: 'w-56' },
874
+ { key: 'inferenceService', label: 'Inference Service', width: 'w-40', tooltip: 'Parent InferenceService' },
875
+ { key: 'age', label: 'Age', width: 'w-24' },
876
+ ],
877
+ llminferenceservices: [
878
+ { key: 'name', label: 'Name' },
879
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
880
+ { key: 'status', label: 'Status', width: 'w-28' },
881
+ { key: 'model', label: 'Model', width: 'w-56', tooltip: 'Model name or URI' },
882
+ { key: 'replicas', label: 'Replicas', width: 'w-24' },
883
+ { key: 'age', label: 'Age', width: 'w-24' },
884
+ ],
885
+ inferencepools: [
886
+ { key: 'name', label: 'Name' },
887
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
888
+ { key: 'status', label: 'Status', width: 'w-36', tooltip: 'Gateway acceptance and EPP resolution' },
889
+ { key: 'selector', label: 'Selector', width: 'w-48' },
890
+ { key: 'targetPorts', label: 'Target Ports', width: 'w-28' },
891
+ { key: 'extensionRef', label: 'Endpoint Picker', width: 'w-40', tooltip: 'EPP extension service' },
892
+ { key: 'age', label: 'Age', width: 'w-24' },
893
+ ],
894
+ inferenceobjectives: [
895
+ { key: 'name', label: 'Name' },
896
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
897
+ { key: 'status', label: 'Status', width: 'w-24' },
898
+ { key: 'poolRef', label: 'Pool', width: 'w-40', tooltip: 'Target InferencePool' },
899
+ { key: 'priority', label: 'Priority', width: 'w-24' },
900
+ { key: 'age', label: 'Age', width: 'w-24' },
901
+ ],
902
+ volcanojobs: [
903
+ { key: 'name', label: 'Name' },
904
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
905
+ { key: 'status', label: 'Status', width: 'w-28' },
906
+ { key: 'queue', label: 'Queue', width: 'w-32' },
907
+ { key: 'minAvailable', label: 'Min Available', width: 'w-28' },
908
+ { key: 'pods', label: 'Pods', width: 'w-44', tooltip: 'Running / succeeded / failed pod counts' },
909
+ { key: 'age', label: 'Age', width: 'w-24' },
910
+ ],
911
+ volcanoqueues: [
912
+ { key: 'name', label: 'Name' },
913
+ { key: 'status', label: 'Status', width: 'w-24' },
914
+ { key: 'weight', label: 'Weight', width: 'w-20' },
915
+ { key: 'capability', label: 'Capability', width: 'w-48' },
916
+ { key: 'allocated', label: 'Allocated', width: 'w-48' },
917
+ { key: 'podGroups', label: 'PodGroups', width: 'w-44', tooltip: 'PodGroup counts by state' },
918
+ { key: 'age', label: 'Age', width: 'w-24' },
919
+ ],
920
+ volcanopodgroups: [
921
+ { key: 'name', label: 'Name' },
922
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
923
+ { key: 'status', label: 'Status', width: 'w-32' },
924
+ { key: 'queue', label: 'Queue', width: 'w-32' },
925
+ { key: 'minMember', label: 'Min Member', width: 'w-28' },
926
+ { key: 'age', label: 'Age', width: 'w-24' },
927
+ ],
928
+ jobflows: [
929
+ { key: 'name', label: 'Name' },
930
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
931
+ { key: 'status', label: 'Status', width: 'w-28' },
932
+ { key: 'flows', label: 'Flows', width: 'w-20' },
933
+ { key: 'age', label: 'Age', width: 'w-24' },
934
+ ],
935
+ jobtemplates: [
936
+ { key: 'name', label: 'Name' },
937
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
938
+ { key: 'status', label: 'Status', width: 'w-24' },
939
+ { key: 'tasks', label: 'Tasks', width: 'w-20' },
940
+ { key: 'age', label: 'Age', width: 'w-24' },
941
+ ],
942
+ kaiqueues: [
943
+ { key: 'name', label: 'Name' },
944
+ { key: 'status', label: 'Status', width: 'w-24' },
945
+ { key: 'parentQueue', label: 'Parent', width: 'w-32' },
946
+ { key: 'priority', label: 'Priority', width: 'w-20' },
947
+ { key: 'quota', label: 'Quota', width: 'w-52', tooltip: 'GPU fractions, CPU millicpus, memory MB; -1 = unlimited' },
948
+ { key: 'allocated', label: 'Allocated', width: 'w-48' },
949
+ { key: 'age', label: 'Age', width: 'w-24' },
950
+ ],
951
+ kaipodgroups: [
952
+ { key: 'name', label: 'Name' },
953
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
954
+ { key: 'status', label: 'Status', width: 'w-32' },
955
+ { key: 'queue', label: 'Queue', width: 'w-32' },
956
+ { key: 'minMember', label: 'Min Member', width: 'w-28' },
957
+ { key: 'age', label: 'Age', width: 'w-24' },
958
+ ],
959
+ kaitoworkspaces: [
960
+ { key: 'name', label: 'Name' },
961
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
962
+ { key: 'status', label: 'Status', width: 'w-28' },
963
+ { key: 'instanceType', label: 'Instance Type', width: 'w-40', tooltip: 'GPU node SKU provisioned for the workspace' },
964
+ { key: 'preset', label: 'Model', width: 'w-44', tooltip: 'Preset model (inference or tuning)' },
965
+ { key: 'nodes', label: 'Nodes', width: 'w-20', tooltip: 'Worker nodes ready / target' },
966
+ { key: 'age', label: 'Age', width: 'w-24' },
967
+ ],
968
+ ragengines: [
969
+ { key: 'name', label: 'Name' },
970
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
971
+ { key: 'status', label: 'Status', width: 'w-28' },
972
+ { key: 'embedding', label: 'Embedding Model', width: 'w-56', tooltip: 'Local model ID/image or remote endpoint' },
973
+ { key: 'instanceType', label: 'Instance Type', width: 'w-40' },
974
+ { key: 'age', label: 'Age', width: 'w-24' },
975
+ ],
976
+ nimservices: [
977
+ { key: 'name', label: 'Name' },
978
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
979
+ { key: 'status', label: 'Status', width: 'w-24' },
980
+ { key: 'model', label: 'Model / Image', width: 'w-64', tooltip: 'Served model name (falls back to NIM image)' },
981
+ { key: 'replicas', label: 'Replicas', width: 'w-28', tooltip: 'Available / desired (HPA range when autoscaling)' },
982
+ { key: 'age', label: 'Age', width: 'w-24' },
983
+ ],
984
+ nimcaches: [
985
+ { key: 'name', label: 'Name' },
986
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
987
+ { key: 'status', label: 'Status', width: 'w-28' },
988
+ { key: 'source', label: 'Model Source', width: 'w-64', tooltip: 'NGC model puller image, or DataStore/HF model name' },
989
+ { key: 'storage', label: 'Storage', width: 'w-24', tooltip: 'Requested PVC size' },
990
+ { key: 'age', label: 'Age', width: 'w-24' },
991
+ ],
992
+ nimpipelines: [
993
+ { key: 'name', label: 'Name' },
994
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
995
+ { key: 'status', label: 'Status', width: 'w-24' },
996
+ { key: 'services', label: 'Services', width: 'w-28', tooltip: 'NIM services in pipeline' },
997
+ { key: 'age', label: 'Age', width: 'w-24' },
998
+ ],
999
+ deviceconfigs: [
1000
+ { key: 'name', label: 'Name' },
1001
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
1002
+ { key: 'status', label: 'Status', width: 'w-24' },
1003
+ { key: 'driver', label: 'Driver', width: 'w-40', tooltip: 'Out-of-tree driver install enabled (version)' },
1004
+ { key: 'devicePluginImage', label: 'Device Plugin', width: 'w-64' },
1005
+ { key: 'nodes', label: 'Nodes', width: 'w-20', tooltip: 'Device plugin DaemonSet available / desired' },
1006
+ { key: 'age', label: 'Age', width: 'w-24' },
1007
+ ],
1008
+ pytorchjobs: [
1009
+ { key: 'name', label: 'Name' },
1010
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
1011
+ { key: 'status', label: 'Status', width: 'w-28' },
1012
+ { key: 'replicas', label: 'Progress', width: 'w-48', tooltip: 'Active + succeeded / desired per replica type; failures shown separately' },
1013
+ { key: 'elapsed', label: 'Elapsed', width: 'w-24', tooltip: 'Start to completion (or now)' },
1014
+ { key: 'age', label: 'Age', width: 'w-24' },
1015
+ ],
1016
+ tfjobs: [
1017
+ { key: 'name', label: 'Name' },
1018
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
1019
+ { key: 'status', label: 'Status', width: 'w-28' },
1020
+ { key: 'replicas', label: 'Progress', width: 'w-56', tooltip: 'Active + succeeded / desired per replica type; failures shown separately' },
1021
+ { key: 'elapsed', label: 'Elapsed', width: 'w-24', tooltip: 'Start to completion (or now)' },
1022
+ { key: 'age', label: 'Age', width: 'w-24' },
1023
+ ],
1024
+ mpijobs: [
1025
+ { key: 'name', label: 'Name' },
1026
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
1027
+ { key: 'status', label: 'Status', width: 'w-28' },
1028
+ { key: 'replicas', label: 'Progress', width: 'w-48', tooltip: 'Active + succeeded / desired per replica type; failures shown separately' },
1029
+ { key: 'elapsed', label: 'Elapsed', width: 'w-24', tooltip: 'Start to completion (or now)' },
1030
+ { key: 'age', label: 'Age', width: 'w-24' },
1031
+ ],
1032
+ trainjobs: [
1033
+ { key: 'name', label: 'Name' },
1034
+ { key: 'namespace', label: 'Namespace', width: 'w-36' },
1035
+ { key: 'status', label: 'Status', width: 'w-28' },
1036
+ { key: 'runtime', label: 'Runtime', width: 'w-44', tooltip: 'Training runtime reference' },
1037
+ { key: 'suspended', label: 'Suspended', width: 'w-24' },
1038
+ { key: 'age', label: 'Age', width: 'w-24' },
1039
+ ],
700
1040
  scaledjobs: [
701
1041
  { key: 'name', label: 'Name' },
702
1042
  { key: 'namespace', label: 'Namespace', width: 'w-36' },
@@ -2100,6 +2440,21 @@ const GROUP_QUALIFIED_COLUMN_KEYS: Record<string, Record<string, string>> = {
2100
2440
  // completely different shape (cluster + method, no storage location/expiry).
2101
2441
  backups: { 'postgresql.cnpg.io': 'cnpgbackups' },
2102
2442
  clusterpolicies: { 'nvidia.com': 'nvidiaclusterpolicies' },
2443
+ // Kyverno's namespaced Policy. `policies` is a plural several projects use,
2444
+ // so it is only Kyverno's when the group says so; the curated set and its
2445
+ // cells already existed but nothing routed to them.
2446
+ policies: { 'kyverno.io': 'kyvernopolicies' },
2447
+ // Istio Gateway is not a Gateway API Gateway: it has servers and a workload
2448
+ // selector, and none of Class/Listeners/Routes/Addresses.
2449
+ gateways: { 'networking.istio.io': 'istiogateways' },
2450
+ job: { batch: 'jobs', 'batch.volcano.sh': 'volcanojobs' },
2451
+ jobs: { batch: 'jobs', 'batch.volcano.sh': 'volcanojobs' },
2452
+ queue: { 'scheduling.volcano.sh': 'volcanoqueues', 'scheduling.run.ai': 'kaiqueues' },
2453
+ queues: { 'scheduling.volcano.sh': 'volcanoqueues', 'scheduling.run.ai': 'kaiqueues' },
2454
+ podgroup: { 'scheduling.volcano.sh': 'volcanopodgroups', 'scheduling.run.ai': 'kaipodgroups' },
2455
+ podgroups: { 'scheduling.volcano.sh': 'volcanopodgroups', 'scheduling.run.ai': 'kaipodgroups' },
2456
+ workspace: { 'kaito.sh': 'kaitoworkspaces' },
2457
+ workspaces: { 'kaito.sh': 'kaitoworkspaces' },
2103
2458
  services: { 'serving.knative.dev': 'knativeservices' },
2104
2459
  configurations: { 'serving.knative.dev': 'knativeconfigurations' },
2105
2460
  revisions: { 'serving.knative.dev': 'knativerevisions' },
@@ -2168,6 +2523,7 @@ const GROUP_QUALIFIED_COLUMN_KEYS: Record<string, Record<string, string>> = {
2168
2523
 
2169
2524
  const GROUP_QUALIFIED_ONLY_COLUMN_KEYS = new Set([
2170
2525
  'networkpolicy', 'networkpolicies',
2526
+ 'job', 'jobs',
2171
2527
  // Other projects serve their own IPPool CRD, so only a Calico group earns the
2172
2528
  // Calico columns.
2173
2529
  'ippool', 'ippools',
@@ -2176,6 +2532,7 @@ const GROUP_QUALIFIED_ONLY_COLUMN_KEYS = new Set([
2176
2532
  'stagedglobalnetworkpolicy', 'stagedglobalnetworkpolicies',
2177
2533
  'stagedkubernetesnetworkpolicy', 'stagedkubernetesnetworkpolicies',
2178
2534
  ])
2535
+
2179
2536
  const CALICO_ONLY_COLUMN_KEYS = new Set([
2180
2537
  'globalnetworkpolicy', 'globalnetworkpolicies',
2181
2538
  'stagednetworkpolicy', 'stagednetworkpolicies',
@@ -2186,7 +2543,7 @@ const CALICO_ONLY_COLUMN_KEYS = new Set([
2186
2543
  // Normalize a kind name to its plural API form used in KNOWN_COLUMNS keys.
2187
2544
  // Handles CRD singular names from URLs: 'ScaledObject' → 'scaledobjects', 'NodePool' → 'nodepools'
2188
2545
  // When group is provided, resolves collisions (e.g., 'services' + 'serving.knative.dev' → 'knativeservices')
2189
- function normalizeKindToPlural(kind: string, group?: string): string {
2546
+ export function normalizeKindToPlural(kind: string, group?: string): string {
2190
2547
  const lower = kind.toLowerCase()
2191
2548
  // Check group-qualified mapping first for collision resolution
2192
2549
  if (group && GROUP_QUALIFIED_COLUMN_KEYS[lower]?.[group]) {
@@ -2231,20 +2588,396 @@ function isLikelyCrossplaneMRGroup(kind: string, group: string): boolean {
2231
2588
  return false
2232
2589
  }
2233
2590
 
2591
+ /**
2592
+ * API groups the Kubernetes API server reserves. A CRD cannot be created in
2593
+ * one, so a curated set keyed on a core kind can never be claimed by a foreign
2594
+ * resource and needs no ownership entry. Note the deliberate omissions:
2595
+ * gateway.networking.k8s.io, snapshot.storage.k8s.io and cluster.x-k8s.io end
2596
+ * in k8s.io but ARE CRD groups, so this is an explicit list, not a suffix test.
2597
+ */
2598
+ const CORE_API_GROUPS = new Set([
2599
+ '', 'apps', 'batch', 'autoscaling', 'policy', 'networking.k8s.io',
2600
+ 'rbac.authorization.k8s.io', 'storage.k8s.io', 'apiextensions.k8s.io',
2601
+ 'coordination.k8s.io', 'discovery.k8s.io', 'scheduling.k8s.io',
2602
+ 'admissionregistration.k8s.io', 'certificates.k8s.io', 'node.k8s.io',
2603
+ 'flowcontrol.apiserver.k8s.io', 'authentication.k8s.io', 'authorization.k8s.io',
2604
+ 'apiregistration.k8s.io', 'events.k8s.io', 'resource.k8s.io',
2605
+ 'internal.apiserver.k8s.io', 'storagemigration.k8s.io',
2606
+ ])
2607
+
2608
+ // ProviderConfig groups are unbounded — one per provider — so they are matched
2609
+ // by suffix. Managed resources take the isLikelyCrossplaneMRGroup branch above.
2610
+ function isCuratedUnboundedGroup(key: string, group: string): boolean {
2611
+ if (key !== 'providerconfigs') return false
2612
+ return group.endsWith('.crossplane.io') || group.endsWith('.upbound.io')
2613
+ }
2614
+
2615
+ /**
2616
+ * Which API group(s) each curated column set was written for.
2617
+ *
2618
+ * A CRD is not the kind Radar curated just because it reuses its plural:
2619
+ * chaos-mesh ships workflows.chaos-mesh.org, Istio ships gateways, and Flux
2620
+ * ships providers. Rendering Argo's or Gateway API's columns for those reads
2621
+ * every field off a schema the object does not have. Anything not claimed here
2622
+ * falls through to the generic set (and, for a CRD, to its own printer
2623
+ * columns), which is the honest answer for a kind Radar has never seen.
2624
+ *
2625
+ * Core kinds need no entry: the API server reserves their groups, so a CRD
2626
+ * cannot occupy one. See CORE_API_GROUPS.
2627
+ */
2628
+ const CURATED_COLUMN_GROUPS: Record<string, readonly string[]> = {
2629
+ challenges: ['acme.cert-manager.io'],
2630
+ orders: ['acme.cert-manager.io'],
2631
+ compositeresourcedefinitions: ['apiextensions.crossplane.io'],
2632
+ compositions: ['apiextensions.crossplane.io'],
2633
+ clustercompliancereports: ['aquasecurity.github.io'],
2634
+ clusterinfraassessmentreports: ['aquasecurity.github.io'],
2635
+ clusterrbacassessmentreports: ['aquasecurity.github.io'],
2636
+ clustersbomreports: ['aquasecurity.github.io'],
2637
+ configauditreports: ['aquasecurity.github.io'],
2638
+ exposedsecretreports: ['aquasecurity.github.io'],
2639
+ infraassessmentreports: ['aquasecurity.github.io'],
2640
+ rbacassessmentreports: ['aquasecurity.github.io'],
2641
+ sbomreports: ['aquasecurity.github.io'],
2642
+ vulnerabilityreports: ['aquasecurity.github.io'],
2643
+ analysisruns: ['argoproj.io'],
2644
+ applications: ['argoproj.io'],
2645
+ applicationsets: ['argoproj.io'],
2646
+ appprojects: ['argoproj.io'],
2647
+ clusterworkflowtemplates: ['argoproj.io'],
2648
+ cronworkflows: ['argoproj.io'],
2649
+ rollouts: ['argoproj.io'],
2650
+ workflows: ['argoproj.io'],
2651
+ workflowtemplates: ['argoproj.io'],
2652
+ barmanobjectstores: ['barmancloud.cnpg.io'],
2653
+ sealedsecrets: ['bitnami.com'],
2654
+ certificaterequests: ['cert-manager.io'],
2655
+ certificates: ['cert-manager.io'],
2656
+ clusterissuers: ['cert-manager.io'],
2657
+ issuers: ['cert-manager.io'],
2658
+ capiclusters: ['cluster.x-k8s.io'],
2659
+ clusterclasses: ['cluster.x-k8s.io'],
2660
+ machinedeployments: ['cluster.x-k8s.io'],
2661
+ machinehealthchecks: ['cluster.x-k8s.io'],
2662
+ machinepools: ['cluster.x-k8s.io'],
2663
+ machines: ['cluster.x-k8s.io'],
2664
+ machinesets: ['cluster.x-k8s.io'],
2665
+ kubeadmcontrolplanes: ['controlplane.cluster.x-k8s.io'],
2666
+ brokers: ['eventing.knative.dev'],
2667
+ eventtypes: ['eventing.knative.dev'],
2668
+ triggers: ['eventing.knative.dev'],
2669
+ clusterexternalsecrets: ['external-secrets.io'],
2670
+ clustersecretstores: ['external-secrets.io'],
2671
+ externalsecrets: ['external-secrets.io'],
2672
+ secretstores: ['external-secrets.io'],
2673
+ parallels: ['flows.knative.dev'],
2674
+ sequences: ['flows.knative.dev'],
2675
+ gatewayclasses: ['gateway.networking.k8s.io'],
2676
+ gateways: ['gateway.networking.k8s.io'],
2677
+ istiogateways: ['networking.istio.io'],
2678
+ grpcroutes: ['gateway.networking.k8s.io'],
2679
+ httproutes: ['gateway.networking.k8s.io'],
2680
+ tcproutes: ['gateway.networking.k8s.io'],
2681
+ tlsroutes: ['gateway.networking.k8s.io'],
2682
+ helmreleases: ['helm.toolkit.fluxcd.io'],
2683
+ awsmachines: ['infrastructure.cluster.x-k8s.io'],
2684
+ awsmachinetemplates: ['infrastructure.cluster.x-k8s.io'],
2685
+ awsmanagedclusters: ['infrastructure.cluster.x-k8s.io'],
2686
+ awsmanagedcontrolplanes: ['controlplane.cluster.x-k8s.io'],
2687
+ awsmanagedmachinepools: ['infrastructure.cluster.x-k8s.io'],
2688
+ azuremachines: ['infrastructure.cluster.x-k8s.io'],
2689
+ azuremachinetemplates: ['infrastructure.cluster.x-k8s.io'],
2690
+ azuremanagedclusters: ['infrastructure.cluster.x-k8s.io'],
2691
+ azuremanagedcontrolplanes: ['infrastructure.cluster.x-k8s.io'],
2692
+ azuremanagedmachinepools: ['infrastructure.cluster.x-k8s.io'],
2693
+ gcpmachines: ['infrastructure.cluster.x-k8s.io'],
2694
+ gcpmachinetemplates: ['infrastructure.cluster.x-k8s.io'],
2695
+ gcpmanagedclusters: ['infrastructure.cluster.x-k8s.io'],
2696
+ gcpmanagedcontrolplanes: ['infrastructure.cluster.x-k8s.io'],
2697
+ gcpmanagedmachinepools: ['infrastructure.cluster.x-k8s.io'],
2698
+ ec2nodeclasses: ['karpenter.k8s.aws'],
2699
+ nodeclaims: ['karpenter.sh'],
2700
+ nodepools: ['karpenter.sh'],
2701
+ clustertriggerauthentications: ['keda.sh'],
2702
+ scaledjobs: ['keda.sh'],
2703
+ scaledobjects: ['keda.sh'],
2704
+ triggerauthentications: ['keda.sh'],
2705
+ kustomizations: ['kustomize.toolkit.fluxcd.io'],
2706
+ cleanuppolicies: ['kyverno.io'],
2707
+ clustercleanuppolicies: ['kyverno.io'],
2708
+ clusterpolicies: ['kyverno.io'],
2709
+ kyvernopolicies: ['kyverno.io'],
2710
+ updaterequests: ['kyverno.io'],
2711
+ policyexceptions: ['kyverno.io', 'policies.kyverno.io'],
2712
+ channels: ['messaging.knative.dev'],
2713
+ inmemorychannels: ['messaging.knative.dev'],
2714
+ knativesubscriptions: ['messaging.knative.dev'],
2715
+ podmonitors: ['monitoring.coreos.com'],
2716
+ prometheusrules: ['monitoring.coreos.com'],
2717
+ servicemonitors: ['monitoring.coreos.com'],
2718
+ knativecertificates: ['networking.internal.knative.dev'],
2719
+ knativeingresses: ['networking.internal.knative.dev'],
2720
+ serverlessservices: ['networking.internal.knative.dev'],
2721
+ destinationrules: ['networking.istio.io'],
2722
+ serviceentries: ['networking.istio.io'],
2723
+ virtualservices: ['networking.istio.io'],
2724
+ alerts: ['notification.toolkit.fluxcd.io'],
2725
+ nvidiaclusterpolicies: ['nvidia.com'],
2726
+ nvidiadrivers: ['nvidia.com'],
2727
+ providers: ['pkg.crossplane.io'],
2728
+ deletingpolicies: ['policies.kyverno.io'],
2729
+ generatingpolicies: ['policies.kyverno.io'],
2730
+ imagevalidatingpolicies: ['policies.kyverno.io'],
2731
+ mutatingpolicies: ['policies.kyverno.io'],
2732
+ namespaceddeletingpolicies: ['policies.kyverno.io'],
2733
+ namespacedgeneratingpolicies: ['policies.kyverno.io'],
2734
+ namespacedimagevalidatingpolicies: ['policies.kyverno.io'],
2735
+ namespacedmutatingpolicies: ['policies.kyverno.io'],
2736
+ namespacedvalidatingpolicies: ['policies.kyverno.io'],
2737
+ validatingpolicies: ['policies.kyverno.io'],
2738
+ cnpgbackups: ['postgresql.cnpg.io'],
2739
+ cnpgclusterimagecatalogs: ['postgresql.cnpg.io'],
2740
+ cnpgclusters: ['postgresql.cnpg.io'],
2741
+ cnpgdatabases: ['postgresql.cnpg.io'],
2742
+ cnpgimagecatalogs: ['postgresql.cnpg.io'],
2743
+ cnpgpublications: ['postgresql.cnpg.io'],
2744
+ cnpgsubscriptions: ['postgresql.cnpg.io'],
2745
+ poolers: ['postgresql.cnpg.io'],
2746
+ scheduledbackups: ['postgresql.cnpg.io'],
2747
+ calicoglobalnetworkpolicies: ['projectcalico.org', 'crd.projectcalico.org'],
2748
+ calicohostendpoints: ['projectcalico.org', 'crd.projectcalico.org'],
2749
+ calicoippools: ['projectcalico.org', 'crd.projectcalico.org'],
2750
+ caliconetworkpolicies: ['projectcalico.org', 'crd.projectcalico.org'],
2751
+ calicostagedglobalnetworkpolicies: ['projectcalico.org', 'crd.projectcalico.org'],
2752
+ calicostagedkubernetesnetworkpolicies: ['projectcalico.org', 'crd.projectcalico.org'],
2753
+ calicostagednetworkpolicies: ['projectcalico.org', 'crd.projectcalico.org'],
2754
+ calicotiers: ['projectcalico.org', 'crd.projectcalico.org'],
2755
+ httpproxies: ['projectcontour.io'],
2756
+ clusterephemeralreports: ['reports.kyverno.io'],
2757
+ ephemeralreports: ['reports.kyverno.io'],
2758
+ authorizationpolicies: ['security.istio.io'],
2759
+ peerauthentications: ['security.istio.io'],
2760
+ domainmappings: ['serving.knative.dev'],
2761
+ knativeconfigurations: ['serving.knative.dev'],
2762
+ knativerevisions: ['serving.knative.dev'],
2763
+ knativeroutes: ['serving.knative.dev'],
2764
+ knativeservices: ['serving.knative.dev'],
2765
+ gitrepositories: ['source.toolkit.fluxcd.io'],
2766
+ helmrepositories: ['source.toolkit.fluxcd.io'],
2767
+ ocirepositories: ['source.toolkit.fluxcd.io'],
2768
+ apiserversources: ['sources.knative.dev'],
2769
+ containersources: ['sources.knative.dev'],
2770
+ pingsources: ['sources.knative.dev'],
2771
+ sinkbindings: ['sources.knative.dev'],
2772
+ ingressroutes: ['traefik.io', 'traefik.containo.us'],
2773
+ ingressroutetcps: ['traefik.io', 'traefik.containo.us'],
2774
+ ingressrouteudps: ['traefik.io', 'traefik.containo.us'],
2775
+ middlewares: ['traefik.io', 'traefik.containo.us'],
2776
+ middlewaretcps: ['traefik.io', 'traefik.containo.us'],
2777
+ serverstransports: ['traefik.io', 'traefik.containo.us'],
2778
+ serverstransporttcps: ['traefik.io', 'traefik.containo.us'],
2779
+ tlsoptions: ['traefik.io', 'traefik.containo.us'],
2780
+ tlsstores: ['traefik.io', 'traefik.containo.us'],
2781
+ traefikservices: ['traefik.io', 'traefik.containo.us'],
2782
+ backuprepositories: ['velero.io'],
2783
+ backups: ['velero.io'],
2784
+ backupstoragelocations: ['velero.io'],
2785
+ velerorestores: ['velero.io'],
2786
+ veleroschedules: ['velero.io'],
2787
+ volumesnapshotlocations: ['velero.io'],
2788
+ clusterpolicyreports: ['wgpolicyk8s.io'],
2789
+ policyreports: ['wgpolicyk8s.io'],
2790
+ deviceconfigs: ['amd.com'],
2791
+ nimcaches: ['apps.nvidia.com'],
2792
+ nimpipelines: ['apps.nvidia.com'],
2793
+ nimservices: ['apps.nvidia.com'],
2794
+ provisioningrequests: ['autoscaling.x-k8s.io'],
2795
+ volcanojobs: ['batch.volcano.sh'],
2796
+ jobflows: ['flow.volcano.sh'],
2797
+ jobtemplates: ['flow.volcano.sh'],
2798
+ inferencepools: ['inference.networking.k8s.io', 'inference.networking.x-k8s.io'],
2799
+ inferenceobjectives: ['inference.networking.x-k8s.io', 'llm-d.ai'],
2800
+ jobsets: ['jobset.x-k8s.io'],
2801
+ kaitoworkspaces: ['kaito.sh'],
2802
+ ragengines: ['kaito.sh'],
2803
+ admissionchecks: ['kueue.x-k8s.io'],
2804
+ clusterqueues: ['kueue.x-k8s.io'],
2805
+ localqueues: ['kueue.x-k8s.io'],
2806
+ resourceflavors: ['kueue.x-k8s.io'],
2807
+ workloads: ['kueue.x-k8s.io'],
2808
+ mpijobs: ['kubeflow.org'],
2809
+ pytorchjobs: ['kubeflow.org'],
2810
+ tfjobs: ['kubeflow.org'],
2811
+ leaderworkersets: ['leaderworkerset.x-k8s.io'],
2812
+ rayclusters: ['ray.io'],
2813
+ raycronjobs: ['ray.io'],
2814
+ rayjobs: ['ray.io'],
2815
+ rayservices: ['ray.io'],
2816
+ kaipodgroups: ['scheduling.run.ai'],
2817
+ kaiqueues: ['scheduling.run.ai'],
2818
+ volcanopodgroups: ['scheduling.volcano.sh'],
2819
+ volcanoqueues: ['scheduling.volcano.sh'],
2820
+ clusterservingruntimes: ['serving.kserve.io'],
2821
+ inferencegraphs: ['serving.kserve.io'],
2822
+ inferenceservices: ['serving.kserve.io'],
2823
+ llminferenceservices: ['serving.kserve.io'],
2824
+ servingruntimes: ['serving.kserve.io'],
2825
+ trainedmodels: ['serving.kserve.io'],
2826
+ trainjobs: ['trainer.kubeflow.org'],
2827
+ }
2828
+
2234
2829
  function getColumnsForKind(kind: string, group?: string): Column[] {
2235
- const key = normalizeKindToPlural(kind, group)
2236
- if (KNOWN_COLUMNS[key]) return KNOWN_COLUMNS[key]
2830
+ // Ahead of the curated lookup: a provider ships one CRD per service, so a
2831
+ // managed resource routinely collides with a curated plural (acm's
2832
+ // Certificate, s3's Service). Resolving the collision the other way would
2833
+ // return DEFAULT_COLUMNS and never reach this branch at all.
2237
2834
  if (group && isLikelyCrossplaneMRGroup(kind, group)) {
2238
2835
  return KNOWN_COLUMNS.crossplanemanagedresources
2239
2836
  }
2837
+ const key = normalizeKindToPlural(kind, group)
2838
+ const curated = KNOWN_COLUMNS[key]
2839
+ if (curated) {
2840
+ const owners = CURATED_COLUMN_GROUPS[key]
2841
+ if (group && owners) return owners.includes(group) ? curated : DEFAULT_COLUMNS
2842
+ // A core kind cannot be shadowed by a CRD, so it keeps its columns without
2843
+ // an ownership entry. Anything else has to be claimed for this exact group.
2844
+ if (!group || CORE_API_GROUPS.has(group)) return curated
2845
+ if (isCuratedUnboundedGroup(key, group)) return curated
2846
+ return DEFAULT_COLUMNS
2847
+ }
2240
2848
  return DEFAULT_COLUMNS
2241
2849
  }
2242
2850
 
2851
+ /**
2852
+ * Whether Radar hand-curates this kind's columns. The single definition of
2853
+ * "curated": the printer-column XOR, the storage-key qualifier, and the host's
2854
+ * decision whether to request table mode at all must all agree, or the server
2855
+ * does work the client throws away — or worse, they disagree about which set a
2856
+ * kind is on.
2857
+ */
2858
+ export function hasCuratedColumns(kind: string, group?: string): boolean {
2859
+ return getColumnsForKind(kind, group) !== DEFAULT_COLUMNS
2860
+ }
2861
+
2862
+ export function getCellFilterKind(kind: string, group?: string): string {
2863
+ const normalized = normalizeKindToPlural(kind, group)
2864
+ if (!group || hasCuratedColumns(kind, group) || normalized.startsWith('__generic_')) return normalized
2865
+ return `__generic_${normalized}`
2866
+ }
2867
+
2243
2868
  // Get the default visible columns for a kind
2244
2869
  function getDefaultVisibleColumns(columns: Column[]): Set<string> {
2245
2870
  return new Set(columns.filter(c => c.defaultVisible !== false).map(c => c.key))
2246
2871
  }
2247
2872
 
2873
+ // Materializes one vendor-declared printer column into a self-contained
2874
+ // ExtraColumn, so it rides the same render/sort/filter rails as a user's custom
2875
+ // column. Values were evaluated server-side and arrive keyed by uid.
2876
+ function buildPrinterColumn(def: PrinterColumnDef, index: number, table: PrinterTable): ExtraColumn {
2877
+ const read = (resource: any) => readPrinterCell(table, resource?.metadata?.uid, index)
2878
+ return {
2879
+ key: printerColumnKey(def),
2880
+ label: def.name,
2881
+ width: printerColumnWidth(def),
2882
+ // Columns in kubectl's `-o wide` tier are declared but not shown by
2883
+ // default; the column picker still offers them.
2884
+ defaultVisible: (def.priority ?? 0) === 0,
2885
+ tooltip: def.description || undefined,
2886
+ render: (resource: any) => {
2887
+ const value = read(resource)
2888
+ // `date` cells arrive already humanized ("5m") — the API server's table
2889
+ // converter does that, so re-formatting here would parse "5m" as a date.
2890
+ const text = formatPrinterCell(value)
2891
+ if (!text) return <span className="text-sm text-theme-text-tertiary">-</span>
2892
+ // A column the vendor named Ready/Healthy/Established carries the only
2893
+ // scannable signal these kinds have — they have no curated Status column.
2894
+ // Rendering it as grey text would leave a screen of uniform "True".
2895
+ const tone = printerCellTone(def, value)
2896
+ if (tone) {
2897
+ return (
2898
+ // The value, not the column's description — the header tooltip
2899
+ // carries that. A cell tooltip exists to reveal a truncated value.
2900
+ <Tooltip content={text}>
2901
+ <span className={clsx('badge', healthColors[tone])}>{text}</span>
2902
+ </Tooltip>
2903
+ )
2904
+ }
2905
+ return (
2906
+ <Tooltip content={text}>
2907
+ <span className="text-sm text-theme-text-secondary truncate block">{text}</span>
2908
+ </Tooltip>
2909
+ )
2910
+ },
2911
+ getSortValue: (resource: any) => printerCellSortValue(read(resource), def),
2912
+ getFilterValue: (resource: any) => formatPrinterCell(read(resource)),
2913
+ }
2914
+ }
2915
+
2916
+ // Printer columns fill in only where Radar has no curated set for the kind —
2917
+ // the two are exclusive, never merged. A curated set is hand-tuned and often
2918
+ // encodes why a vendor-obvious field is the wrong one to show, so it wins.
2919
+ function buildPrinterColumns(
2920
+ kind: string,
2921
+ group: string | undefined,
2922
+ table: PrinterTable | null | undefined,
2923
+ ): ExtraColumn[] {
2924
+ if (!table?.columns?.length) return []
2925
+ if (!printerTableMatchesKind(table, kind, group)) return []
2926
+ if (hasCuratedColumns(kind, group)) return []
2927
+ return table.columns.map((def, i) => buildPrinterColumn(def, i, table))
2928
+ }
2929
+
2930
+ // The kind's effective column set. Printer columns replace the generic
2931
+ // Name/Namespace/Status/Age set rather than joining it: the vendor's columns
2932
+ // are the answer to the same question the generic Status column was guessing
2933
+ // at, and showing both would ask the reader which one to believe. Name,
2934
+ // Namespace and Age stay because Radar renders those itself (the server drops
2935
+ // the CRD's own duplicates of them before sending).
2936
+ function columnsForKindWithPrinter(
2937
+ kind: string,
2938
+ group: string | undefined,
2939
+ printerColumns: ExtraColumn[],
2940
+ ): Column[] {
2941
+ const curated = getColumnsForKind(kind, group)
2942
+ if (!printerColumns.length || hasCuratedColumns(kind, group)) return curated
2943
+ return [
2944
+ { key: 'name', label: 'Name' },
2945
+ { key: 'namespace', label: 'Namespace', width: 'w-48' },
2946
+ ...printerColumns,
2947
+ { key: 'age', label: 'Age', width: 'w-24' },
2948
+ ]
2949
+ }
2950
+
2951
+ /**
2952
+ * The visible-column set for a kind the user has saved settings for.
2953
+ *
2954
+ * Saved choices win, with two seeded exceptions for columns the blob predates —
2955
+ * a user cannot have decided to hide something they were never shown:
2956
+ * - host extras, which are injected by the embedding app;
2957
+ * - printer columns, but only when the blob names none at all. Once it names
2958
+ * one, the user has seen the set and an absent key is a deliberate hide.
2959
+ */
2960
+ export function mergeSavedVisibleColumns(
2961
+ savedVisible: string[],
2962
+ extraKeys: string[],
2963
+ printerColumns: ExtraColumn[],
2964
+ ): Set<string> {
2965
+ const merged = new Set(savedVisible)
2966
+ for (const k of extraKeys) merged.add(k)
2967
+ if (!savedVisible.some(k => k.startsWith(PRINTER_COLUMN_PREFIX))) {
2968
+ for (const c of printerColumns) {
2969
+ if (c.defaultVisible !== false) merged.add(c.key)
2970
+ }
2971
+ }
2972
+ return merged
2973
+ }
2974
+
2975
+ /** Storage/reset identity for a selection. Two CRDs can share a plural across
2976
+ * API groups, and their columns are unrelated. */
2977
+ export function selectedKindIdentityOf(kind: { name: string; group?: string }): string {
2978
+ return `${kind.name} ${kind.group ?? ''}`
2979
+ }
2980
+
2248
2981
  // Host-injected leading columns minus any whose key collides with a built-in.
2249
2982
  // Rendering two columns under one key duplicates React keys and corrupts the
2250
2983
  // visibleColumns / columnWidths maps, so a colliding extra is dropped (dev-warn).
@@ -2271,6 +3004,22 @@ function filterHostExtras(
2271
3004
  // localStorage helpers for column settings
2272
3005
  const COLUMN_SETTINGS_PREFIX = 'radar-columns-'
2273
3006
 
3007
+ // Storage identity for a kind's column settings. normalizeKindToPlural falls
3008
+ // through to the bare plural for anything outside the curated collision
3009
+ // catalog, so two uncurated CRDs sharing a plural in different API groups —
3010
+ // Crossplane ships two `Usage` kinds — would share one blob and leak column
3011
+ // visibility, widths and custom columns between them. Harmless while both got
3012
+ // identical generic columns; not harmless once each has its own printer
3013
+ // columns. Only the fallthrough kinds are qualified, so curated and core kinds
3014
+ // keep the key their saved preferences are already under.
3015
+ export function columnSettingsKey(kind: string, group?: string): string {
3016
+ const normalized = normalizeKindToPlural(kind, group)
3017
+ if (group && !hasCuratedColumns(kind, group)) {
3018
+ return `${normalized}.${group}`
3019
+ }
3020
+ return normalized
3021
+ }
3022
+
2274
3023
  interface ColumnSettings {
2275
3024
  visible: string[]
2276
3025
  widths: Record<string, number>
@@ -2279,7 +3028,7 @@ interface ColumnSettings {
2279
3028
 
2280
3029
  function loadColumnSettings(kind: string, group?: string): ColumnSettings | null {
2281
3030
  try {
2282
- const key = COLUMN_SETTINGS_PREFIX + normalizeKindToPlural(kind, group)
3031
+ const key = COLUMN_SETTINGS_PREFIX + columnSettingsKey(kind, group)
2283
3032
  const raw = localStorage.getItem(key)
2284
3033
  if (raw) return JSON.parse(raw)
2285
3034
  } catch { /* ignore */ }
@@ -2288,14 +3037,14 @@ function loadColumnSettings(kind: string, group?: string): ColumnSettings | null
2288
3037
 
2289
3038
  function saveColumnSettings(kind: string, group: string | undefined, settings: ColumnSettings) {
2290
3039
  try {
2291
- const key = COLUMN_SETTINGS_PREFIX + normalizeKindToPlural(kind, group)
3040
+ const key = COLUMN_SETTINGS_PREFIX + columnSettingsKey(kind, group)
2292
3041
  localStorage.setItem(key, JSON.stringify(settings))
2293
3042
  } catch { /* ignore */ }
2294
3043
  }
2295
3044
 
2296
3045
  function clearColumnSettings(kind: string, group?: string) {
2297
3046
  try {
2298
- const key = COLUMN_SETTINGS_PREFIX + normalizeKindToPlural(kind, group)
3047
+ const key = COLUMN_SETTINGS_PREFIX + columnSettingsKey(kind, group)
2299
3048
  localStorage.removeItem(key)
2300
3049
  } catch { /* ignore */ }
2301
3050
  }
@@ -2403,6 +3152,11 @@ interface ResourcesViewProps {
2403
3152
  * doesn't need to extend KNOWN_COLUMNS or per-kind cell renderers.
2404
3153
  * When undefined, behavior is byte-identical to single-cluster mode. */
2405
3154
  extraLeadingColumns?: ExtraColumn[]
3155
+ /** Vendor-declared columns from the kind's CRD (additionalPrinterColumns),
3156
+ * evaluated server-side. Used ONLY for kinds Radar has no curated column
3157
+ * set for — the two are exclusive. Hosts that don't fetch them get the
3158
+ * generic columns and nothing changes. */
3159
+ printerTable?: PrinterTable | null
2406
3160
  /** Escape hatch for full-page-nav row selection: receive the FULL
2407
3161
  * resource object on row click / Enter / `d` / search-Enter, instead
2408
3162
  * of the stripped {kind, namespace, name, group} shape onResourceClick
@@ -2632,6 +3386,7 @@ export function ResourcesView({
2632
3386
  onCreateResource,
2633
3387
  defaultKind = DEFAULT_KIND_INFO,
2634
3388
  extraLeadingColumns,
3389
+ printerTable,
2635
3390
  onRowSelect,
2636
3391
  rowHrefFor,
2637
3392
  onCompareSubmit,
@@ -2829,22 +3584,32 @@ export function ResourcesView({
2829
3584
  // collision: a colliding extra is filtered out (with a dev-mode warn)
2830
3585
  // so we don't render two columns sharing one key — that would yield
2831
3586
  // duplicate React keys and corrupt visibleColumns / columnWidths state.
3587
+ const builtPrinterColumns = useMemo(
3588
+ () => buildPrinterColumns(selectedKind.name, selectedKind.group, printerTable),
3589
+ [selectedKind.name, selectedKind.group, printerTable],
3590
+ )
3591
+ const printerColumnsKey = useMemo(
3592
+ () => printerTableKey(builtPrinterColumns.length ? printerTable : null),
3593
+ [printerTable, builtPrinterColumns],
3594
+ )
3595
+
2832
3596
  const allColumns = useMemo(() => {
2833
- const kindColumns = getColumnsForKind(selectedKind.name, selectedKind.group)
3597
+ const kindColumns = columnsForKindWithPrinter(selectedKind.name, selectedKind.group, builtPrinterColumns)
2834
3598
  if (!extraLeadingColumns?.length && !builtCustomColumns.length) return kindColumns
2835
3599
  const builtinKeys = new Set(kindColumns.map(c => c.key))
2836
3600
  const filteredExtras = filterHostExtras(extraLeadingColumns, builtinKeys, selectedKind.name)
2837
3601
  return [...filteredExtras, ...kindColumns, ...builtCustomColumns]
2838
- }, [selectedKind.name, selectedKind.group, extraLeadingColumns, builtCustomColumns])
3602
+ }, [selectedKind.name, selectedKind.group, extraLeadingColumns, builtCustomColumns, builtPrinterColumns])
2839
3603
 
2840
3604
  // Map of extra column keys for fast O(1) lookup on each render path
2841
3605
  // (cell render, sort, column-filter unique-values).
2842
3606
  const extraColumnsByKey = useMemo(() => {
2843
3607
  const m = new Map<string, ExtraColumn>()
2844
3608
  extraLeadingColumns?.forEach(c => m.set(c.key, c))
3609
+ builtPrinterColumns.forEach(c => m.set(c.key, c))
2845
3610
  builtCustomColumns.forEach(c => m.set(c.key, c))
2846
3611
  return m
2847
- }, [extraLeadingColumns, builtCustomColumns])
3612
+ }, [extraLeadingColumns, builtCustomColumns, builtPrinterColumns])
2848
3613
 
2849
3614
  // Guards the save effect from persisting on the initial load of each kind
2850
3615
  // (set false by the load effect, flipped true on its first skipped save).
@@ -2874,7 +3639,7 @@ export function ResourcesView({
2874
3639
  // (non-array, blank path, bad source) must not crash the later .map or add dead columns.
2875
3640
  const savedCustom = sanitizeCustomColumnDefs(saved?.custom)
2876
3641
  setCustomColumns(savedCustom)
2877
- const kindColumns = getColumnsForKind(selectedKind.name, selectedKind.group)
3642
+ const kindColumns = columnsForKindWithPrinter(selectedKind.name, selectedKind.group, builtPrinterColumns)
2878
3643
  const builtinKeys = new Set(kindColumns.map(c => c.key))
2879
3644
  const extras = filterHostExtras(extraLeadingColumns, builtinKeys, selectedKind.name)
2880
3645
  const effective = [...extras, ...kindColumns, ...savedCustom.map(buildCustomColumn)]
@@ -2901,16 +3666,18 @@ export function ResourcesView({
2901
3666
  setVisibleColumns(getDefaultVisibleColumns(effective))
2902
3667
  setColumnWidths({})
2903
3668
  } else {
2904
- const merged = new Set(saved.visible)
2905
- for (const k of extraKeys) merged.add(k)
2906
- setVisibleColumns(merged)
3669
+ setVisibleColumns(mergeSavedVisibleColumns(saved.visible, extraKeys, builtPrinterColumns))
2907
3670
  setColumnWidths(saved.widths || {})
2908
3671
  }
2909
3672
  } else {
2910
3673
  setVisibleColumns(getDefaultVisibleColumns(effective))
2911
3674
  setColumnWidths({})
2912
3675
  }
2913
- }, [selectedKind.name, selectedKind.group, extraLeadingColumns])
3676
+ // printerColumnsKey, not builtPrinterColumns: the built array is a new
3677
+ // identity on every refetch, and re-running this effect would reset the
3678
+ // user's in-session column choices each time the list polls. The key only
3679
+ // changes when the kind changes or an operator upgrade changes the CRD.
3680
+ }, [selectedKind.name, selectedKind.group, extraLeadingColumns, printerColumnsKey])
2914
3681
 
2915
3682
  // Save column settings when they change (skip the initial load of each kind)
2916
3683
  useEffect(() => {
@@ -3305,11 +4072,14 @@ export function ResourcesView({
3305
4072
  if (containers.length > 0) {
3306
4073
  openLogs?.({ namespace: ns, podName: name, containers })
3307
4074
  }
3308
- } else if (['deployments', 'statefulsets', 'daemonsets', 'replicasets', 'jobs', 'workflows', 'cronjobs', 'cronworkflows', 'workflowtemplates', 'clusterworkflowtemplates', 'scaledjobs'].includes(kindLower)) {
4075
+ } else if (['deployments', 'statefulsets', 'daemonsets', 'replicasets', 'workflows', 'cronjobs', 'cronworkflows', 'workflowtemplates', 'clusterworkflowtemplates', 'scaledjobs'].includes(kindLower) || isCoreBatchJob(kindLower, selectedKind.group)) {
3309
4076
  openWorkloadLogs?.({ namespace: ns, workloadKind: selectedKind.kind, workloadName: name })
3310
4077
  }
3311
4078
  },
3312
- enabled: highlightedIndex >= 0 && !compareMode && ['pods', 'deployments', 'statefulsets', 'daemonsets', 'replicasets', 'jobs', 'workflows', 'cronjobs', 'cronworkflows', 'workflowtemplates', 'clusterworkflowtemplates', 'scaledjobs'].includes(selectedKind.name.toLowerCase()),
4079
+ enabled: highlightedIndex >= 0 && !compareMode && (
4080
+ ['pods', 'deployments', 'statefulsets', 'daemonsets', 'replicasets', 'workflows', 'cronjobs', 'cronworkflows', 'workflowtemplates', 'clusterworkflowtemplates', 'scaledjobs'].includes(selectedKind.name.toLowerCase()) ||
4081
+ isCoreBatchJob(selectedKind.name, selectedKind.group)
4082
+ ),
3313
4083
  },
3314
4084
  {
3315
4085
  id: 'resources-sort-name',
@@ -3964,12 +4734,17 @@ export function ResourcesView({
3964
4734
 
3965
4735
  // Reset sort and filters when kind changes (but not when syncing from URL navigation)
3966
4736
  // Track previous kind to skip on mount (where the effect fires but kind hasn't actually changed)
3967
- const prevKindRef = useRef(selectedKind.name)
4737
+ // Keyed on group as well as plural: two CRDs can share a plural across API
4738
+ // groups, and their printer columns are unrelated. Resetting on the plural
4739
+ // alone would carry a `printer:*` sort or filter onto a table with no such
4740
+ // column, where every row fails the filter and the list looks empty.
4741
+ const selectedKindIdentity = selectedKindIdentityOf(selectedKind)
4742
+ const prevKindRef = useRef(selectedKindIdentity)
3968
4743
  useEffect(() => {
3969
- if (prevKindRef.current === selectedKind.name) {
4744
+ if (prevKindRef.current === selectedKindIdentity) {
3970
4745
  return
3971
4746
  }
3972
- prevKindRef.current = selectedKind.name
4747
+ prevKindRef.current = selectedKindIdentity
3973
4748
  setSortColumn(null)
3974
4749
  setSortDirection(null)
3975
4750
  setOpenColumnFilter(null)
@@ -3978,7 +4753,7 @@ export function ResourcesView({
3978
4753
  setColumnFilterExcludes({})
3979
4754
  }
3980
4755
  setProblemFilters([])
3981
- }, [selectedKind.name])
4756
+ }, [selectedKindIdentity])
3982
4757
 
3983
4758
  // Toggle sort for a column
3984
4759
  const handleSort = useCallback((column: string) => {
@@ -4012,7 +4787,28 @@ export function ResourcesView({
4012
4787
  case 'age':
4013
4788
  return meta.creationTimestamp ? new Date(meta.creationTimestamp).getTime() : 0
4014
4789
  case 'status':
4015
- return status.phase || ''
4790
+ // Phase first for a curated kind: it keeps the most-used lists — Pods,
4791
+ // Deployments, Nodes — ordering as they always have.
4792
+ //
4793
+ // Everything else routes through getCellFilterValue, the same reader
4794
+ // the cell and the filter dropdown use, so all three agree on one
4795
+ // string. Deriving it here instead ordered rows on a status the row
4796
+ // never displayed: a Gateway shows `Programmed` while the generic
4797
+ // ladder, which knows neither Programmed nor Accepted, called it
4798
+ // `Accepted` — so a healthy Gateway and a pending one sorted equal.
4799
+ // getCellFilterValue ends in that same generic derivation, so an
4800
+ // uncurated kind still sorts on the text its badge shows.
4801
+ if (hasCuratedColumns(selectedKind.name, selectedKind.group) && status.phase) {
4802
+ return status.phase
4803
+ }
4804
+ return getCellFilterValue(resource, 'status', kindLower)
4805
+ case 'servers':
4806
+ // Numeric so 10 does not sort before 9.
4807
+ if (kindLower === 'istiogateways') return getIstioGatewayServerCount(resource)
4808
+ return ''
4809
+ case 'selector':
4810
+ if (kindLower === 'istiogateways') return getIstioGatewaySelectorString(resource)
4811
+ return ''
4016
4812
  case 'containers':
4017
4813
  // Pod containers column — sort by readiness ratio
4018
4814
  if (status.containerStatuses) {
@@ -4022,6 +4818,7 @@ export function ResourcesView({
4022
4818
  }
4023
4819
  return 0
4024
4820
  case 'ready':
4821
+ if (kindLower === 'jobsets') return getJobSetReadyJobs(resource)
4025
4822
  // For DaemonSets, use numberReady/desiredNumberScheduled
4026
4823
  if (kindLower === 'daemonsets') {
4027
4824
  const desired = status.desiredNumberScheduled ?? 0
@@ -4042,6 +4839,7 @@ export function ResourcesView({
4042
4839
  // DaemonSet: updatedNumberScheduled, others: updatedReplicas
4043
4840
  return status.updatedNumberScheduled ?? status.updatedReplicas ?? 0
4044
4841
  case 'restarts':
4842
+ if (kindLower === 'jobsets') return getJobSetRestarts(resource)
4045
4843
  return getPodRestarts(resource)
4046
4844
  case 'lastSeen': {
4047
4845
  const lastTs = resource.lastTimestamp || meta.creationTimestamp
@@ -4091,7 +4889,7 @@ export function ResourcesView({
4091
4889
  default:
4092
4890
  return ''
4093
4891
  }
4094
- }, [metricsLookup])
4892
+ }, [metricsLookup, selectedKind.name, selectedKind.group])
4095
4893
 
4096
4894
  // Helper to check if a pod matches problem filters
4097
4895
  const podMatchesProblemFilter = useCallback((pod: any, filters: string[]): boolean => {
@@ -4144,11 +4942,11 @@ export function ResourcesView({
4144
4942
  // when the parent supplied a custom getFilterValue.
4145
4943
  const activeColFilters = Object.entries(columnFilters).filter(([, vals]) => vals.length > 0)
4146
4944
  if (activeColFilters.length > 0) {
4147
- const kindLower = normalizeKindToPlural(selectedKind.name, selectedKind.group)
4945
+ const cellFilterKind = getCellFilterKind(selectedKind.name, selectedKind.group)
4148
4946
  result = result.filter((r: any) =>
4149
4947
  activeColFilters.every(([col, vals]) => {
4150
4948
  const extra = extraColumnsByKey.get(col)
4151
- const cellVal = extra?.getFilterValue ? extra.getFilterValue(r) : getCellFilterValue(r, col, kindLower)
4949
+ const cellVal = extra?.getFilterValue ? extra.getFilterValue(r) : getCellFilterValue(r, col, cellFilterKind)
4152
4950
  const match = vals.includes(cellVal)
4153
4951
  return columnFilterExcludes[col] ? !match : match
4154
4952
  })
@@ -4210,17 +5008,26 @@ export function ResourcesView({
4210
5008
  // the built-in getSortValue for their key.
4211
5009
  if (sortColumn && sortDirection) {
4212
5010
  const extra = extraColumnsByKey.get(sortColumn)
4213
- result = [...result].sort((a: any, b: any) => {
4214
- const aVal = extra?.getSortValue ? extra.getSortValue(a) : getSortValue(a, sortColumn, selectedKind.name)
4215
- const bVal = extra?.getSortValue ? extra.getSortValue(b) : getSortValue(b, sortColumn, selectedKind.name)
5011
+ // Normalized, matching the filter call sites: a group-qualified kind
5012
+ // (istiogateways, cnpgclusters) otherwise arrives here as its bare plural
5013
+ // and misses its own sort readers. Hoisted out of the comparator.
5014
+ // The ownership-aware wrapper also keeps a foreign CRD that reuses a
5015
+ // curated plural on the generic reader used by its cells and filters.
5016
+ const sortKind = getCellFilterKind(selectedKind.name, selectedKind.group)
5017
+ const keyOf = extra?.getSortValue ?? ((r: any) => getSortValue(r, sortColumn, sortKind))
5018
+ // Derived once per row, not once per comparison: a sort visits O(n log n)
5019
+ // pairs, and the status key now runs a per-kind status derivation.
5020
+ const decorated = result.map((r: any) => ({ r, k: keyOf(r) }))
5021
+ decorated.sort((a, b) => {
4216
5022
  let comparison = 0
4217
- if (typeof aVal === 'number' && typeof bVal === 'number') {
4218
- comparison = aVal - bVal
5023
+ if (typeof a.k === 'number' && typeof b.k === 'number') {
5024
+ comparison = a.k - b.k
4219
5025
  } else {
4220
- comparison = String(aVal).localeCompare(String(bVal))
5026
+ comparison = String(a.k).localeCompare(String(b.k))
4221
5027
  }
4222
5028
  return sortDirection === 'desc' ? -comparison : comparison
4223
5029
  })
5030
+ result = decorated.map(d => d.r)
4224
5031
  } else {
4225
5032
  // Default sort by kind
4226
5033
  const kindLower = normalizeKindToPlural(selectedKind.name, selectedKind.group)
@@ -4492,6 +5299,7 @@ export function ResourcesView({
4492
5299
  if (!resources || resources.length === 0) return null
4493
5300
 
4494
5301
  const kindLower = normalizeKindToPlural(selectedKind.name, selectedKind.group)
5302
+ const cellFilterKind = getCellFilterKind(selectedKind.name, selectedKind.group)
4495
5303
  // Iterate over allColumns (built-ins + injected extras) so an
4496
5304
  // ExtraColumn with getFilterValue gets a column-filter dropdown like
4497
5305
  // any built-in does. The previous formulation iterated KNOWN_COLUMNS
@@ -4518,7 +5326,7 @@ export function ResourcesView({
4518
5326
  // Count distinct values for this column
4519
5327
  const valueCounts: Record<string, number> = {}
4520
5328
  for (const r of resources) {
4521
- const val = extra?.getFilterValue ? extra.getFilterValue(r) : getCellFilterValue(r, col.key, kindLower)
5329
+ const val = extra?.getFilterValue ? extra.getFilterValue(r) : getCellFilterValue(r, col.key, cellFilterKind)
4522
5330
  if (val) {
4523
5331
  valueCounts[val] = (valueCounts[val] || 0) + 1
4524
5332
  }
@@ -5943,6 +6751,9 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
5943
6751
 
5944
6752
  // Kind-specific columns (normalize CRD singular names like 'ScaledObject' → 'scaledobjects')
5945
6753
  const kindLower = normalizeKindToPlural(kind, group)
6754
+ if (group && !hasCuratedColumns(kind, group)) {
6755
+ return <GenericCell resource={resource} column={column} />
6756
+ }
5946
6757
 
5947
6758
  // Kyverno cells are group-gated ahead of the switch so a non-matching CR
5948
6759
  // falls through to the switch's default (GenericCell) instead of being
@@ -5998,6 +6809,9 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
5998
6809
  case 'persistentvolumeclaims':
5999
6810
  return <PVCCell resource={resource} column={column} />
6000
6811
  case 'rollouts':
6812
+ if (!resource.apiVersion?.startsWith('argoproj.io/')) {
6813
+ return <GenericCell resource={resource} column={column} />
6814
+ }
6001
6815
  return <RolloutCell resource={resource} column={column} />
6002
6816
  case 'analysisruns':
6003
6817
  return <AnalysisRunCell resource={resource} column={column} />
@@ -6021,8 +6835,12 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
6021
6835
  return <OrderCell resource={resource} column={column} />
6022
6836
  case 'challenges':
6023
6837
  return <ChallengeCell resource={resource} column={column} />
6838
+ case 'istiogateways':
6839
+ return <IstioGatewayCell resource={resource} column={column} />
6024
6840
  case 'gateways':
6025
- // Disambiguate Gateway API vs Istio Gateway by apiVersion
6841
+ // Disambiguate Gateway API vs Istio Gateway by apiVersion. Retained
6842
+ // alongside the case above because not every call site normalizes the
6843
+ // kind through GROUP_QUALIFIED_COLUMN_KEYS before dispatching.
6026
6844
  if (resource.apiVersion?.includes('networking.istio.io')) {
6027
6845
  return <IstioGatewayCell resource={resource} column={column} />
6028
6846
  }
@@ -6128,6 +6946,90 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
6128
6946
  return <NvidiaClusterPolicyCell resource={resource} column={column} />
6129
6947
  case 'nvidiadrivers':
6130
6948
  return <NvidiaDriverCell resource={resource} column={column} />
6949
+ // Kueue + Cluster Autoscaler
6950
+ case 'clusterqueues':
6951
+ return <ClusterQueueCell resource={resource} column={column} />
6952
+ case 'localqueues':
6953
+ return <LocalQueueCell resource={resource} column={column} />
6954
+ case 'workloads':
6955
+ return <KueueWorkloadCell resource={resource} column={column} />
6956
+ case 'resourceflavors':
6957
+ return <ResourceFlavorCell resource={resource} column={column} />
6958
+ case 'admissionchecks':
6959
+ return <AdmissionCheckCell resource={resource} column={column} />
6960
+ case 'provisioningrequests':
6961
+ return <ProvisioningRequestCell resource={resource} column={column} />
6962
+ // KubeRay
6963
+ case 'rayclusters':
6964
+ return <RayClusterCell resource={resource} column={column} />
6965
+ case 'rayjobs':
6966
+ return <RayJobCell resource={resource} column={column} />
6967
+ case 'rayservices':
6968
+ return <RayServiceCell resource={resource} column={column} />
6969
+ case 'raycronjobs':
6970
+ return <RayCronJobCell resource={resource} column={column} />
6971
+ // LeaderWorkerSet + JobSet
6972
+ case 'leaderworkersets':
6973
+ return <LeaderWorkerSetCell resource={resource} column={column} />
6974
+ case 'jobsets':
6975
+ return <JobSetCell resource={resource} column={column} />
6976
+ // KServe
6977
+ case 'inferenceservices':
6978
+ return <InferenceServiceCell resource={resource} column={column} />
6979
+ case 'servingruntimes':
6980
+ case 'clusterservingruntimes':
6981
+ return <ServingRuntimeCell resource={resource} column={column} />
6982
+ case 'inferencegraphs':
6983
+ return <InferenceGraphCell resource={resource} column={column} />
6984
+ case 'trainedmodels':
6985
+ return <TrainedModelCell resource={resource} column={column} />
6986
+ case 'llminferenceservices':
6987
+ return <LLMInferenceServiceCell resource={resource} column={column} />
6988
+ // Gateway API Inference Extension
6989
+ case 'inferencepools':
6990
+ return <InferencePoolCell resource={resource} column={column} />
6991
+ case 'inferenceobjectives':
6992
+ return <InferenceObjectiveCell resource={resource} column={column} />
6993
+ // Volcano (group-qualified keys disambiguate from batch Jobs and KAI)
6994
+ case 'volcanojobs':
6995
+ return <VolcanoJobCell resource={resource} column={column} />
6996
+ case 'volcanoqueues':
6997
+ return <VolcanoQueueCell resource={resource} column={column} />
6998
+ case 'volcanopodgroups':
6999
+ return <VolcanoPodGroupCell resource={resource} column={column} />
7000
+ case 'jobflows':
7001
+ return <JobFlowCell resource={resource} column={column} />
7002
+ case 'jobtemplates':
7003
+ return <JobTemplateCell resource={resource} column={column} />
7004
+ // KAI Scheduler
7005
+ case 'kaiqueues':
7006
+ return <KaiQueueCell resource={resource} column={column} />
7007
+ case 'kaipodgroups':
7008
+ return <KaiPodGroupCell resource={resource} column={column} />
7009
+ // KAITO
7010
+ case 'kaitoworkspaces':
7011
+ return <KaitoWorkspaceCell resource={resource} column={column} />
7012
+ case 'ragengines':
7013
+ return <RAGEngineCell resource={resource} column={column} />
7014
+ // NVIDIA NIM Operator
7015
+ case 'nimservices':
7016
+ return <NIMServiceCell resource={resource} column={column} />
7017
+ case 'nimcaches':
7018
+ return <NIMCacheCell resource={resource} column={column} />
7019
+ case 'nimpipelines':
7020
+ return <NIMPipelineCell resource={resource} column={column} />
7021
+ // AMD GPU Operator
7022
+ case 'deviceconfigs':
7023
+ return <AMDDeviceConfigCell resource={resource} column={column} />
7024
+ // Kubeflow training
7025
+ case 'pytorchjobs':
7026
+ return <PyTorchJobCell resource={resource} column={column} />
7027
+ case 'tfjobs':
7028
+ return <TFJobCell resource={resource} column={column} />
7029
+ case 'mpijobs':
7030
+ return <MPIJobCell resource={resource} column={column} />
7031
+ case 'trainjobs':
7032
+ return <TrainJobCell resource={resource} column={column} />
6131
7033
  // ArgoCD GitOps resources
6132
7034
  case 'applications':
6133
7035
  return <ArgoApplicationCell resource={resource} column={column} />
@@ -6428,53 +7330,13 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
6428
7330
  function GenericCell({ resource, column }: { resource: any; column: string }) {
6429
7331
  switch (column) {
6430
7332
  case 'status': {
6431
- // Try to extract status from common patterns
6432
- const status = resource.status
6433
- if (!status) return <span className="text-sm text-theme-text-tertiary">-</span>
6434
-
6435
- // Check for phase (common in many CRDs)
6436
- if (status.phase) {
6437
- const phase = status.phase as string
6438
- const isHealthy = ['Running', 'Active', 'Succeeded', 'Ready', 'Healthy', 'Available'].includes(phase)
6439
- const isWarning = ['Pending', 'Progressing', 'Unknown'].includes(phase)
6440
- return (
6441
- <span className={clsx(
6442
- 'badge',
6443
- isHealthy ? 'status-healthy' :
6444
- isWarning ? 'status-degraded' :
6445
- 'status-unhealthy'
6446
- )}>
6447
- {phase}
6448
- </span>
6449
- )
6450
- }
6451
-
6452
- // Check for conditions (common pattern)
6453
- if (status.conditions && Array.isArray(status.conditions)) {
6454
- const readyCondition = status.conditions.find((c: any) => c.type === 'Ready' || c.type === 'Available')
6455
- if (readyCondition) {
6456
- const isReady = readyCondition.status === 'True'
6457
- return (
6458
- <span className={clsx(
6459
- 'badge',
6460
- isReady ? 'status-healthy' : 'status-degraded'
6461
- )}>
6462
- {isReady ? 'Ready' : 'Not Ready'}
6463
- </span>
6464
- )
6465
- }
6466
- }
6467
-
6468
- // Check for state field
6469
- if (status.state) {
6470
- return (
6471
- <span className="text-sm text-theme-text-secondary truncate">
6472
- {String(status.state)}
6473
- </span>
6474
- )
6475
- }
6476
-
6477
- return <span className="text-sm text-theme-text-tertiary">-</span>
7333
+ const derived = getGenericResourceStatus(resource)
7334
+ if (!derived) return <span className="text-sm text-theme-text-tertiary">-</span>
7335
+ return (
7336
+ <Tooltip content={derived.reason ?? derived.text}>
7337
+ <span className={clsx('badge', healthColors[derived.tone])}>{derived.text}</span>
7338
+ </Tooltip>
7339
+ )
6478
7340
  }
6479
7341
  default:
6480
7342
  return <span className="text-sm text-theme-text-tertiary">-</span>
@@ -6763,15 +7625,25 @@ function PodCell({ resource, column }: { resource: any; column: string }) {
6763
7625
  }
6764
7626
  }
6765
7627
 
7628
+ function WorkloadStatusCell({ resource, kind }: { resource: any; kind: string }) {
7629
+ const { activity, status } = getWorkloadDisplayStatus(resource, kind)
7630
+ const rolloutVisible = isRolloutActivityVisible(activity)
7631
+ const label = rolloutVisible ? status.text : workloadStatusLabel(status)
7632
+ const detail = rolloutVisible && activity.detail ? `${activity.detail} · ${status.text}` : status.text
7633
+ return <Tooltip content={detail}><span className={clsx('badge', status.color)}>{label}</span></Tooltip>
7634
+ }
7635
+
6766
7636
  function WorkloadCell({ resource, column, kind }: { resource: any; kind: string; column: string }) {
6767
7637
  const status = resource.status || {}
6768
7638
  const spec = resource.spec || {}
6769
7639
 
6770
7640
  switch (column) {
7641
+ case 'status':
7642
+ return <WorkloadStatusCell resource={resource} kind={kind} />
6771
7643
  case 'ready': {
6772
- const desired = spec.replicas ?? 0
7644
+ const desired = spec.replicas ?? 1
6773
7645
  const ready = status.readyReplicas || 0
6774
- const allReady = ready === desired && desired > 0
7646
+ const allReady = ready >= desired && desired > 0
6775
7647
  const problems = getWorkloadProblems(resource, kind)
6776
7648
  return (
6777
7649
  <div className="flex items-center gap-1.5">
@@ -6779,7 +7651,7 @@ function WorkloadCell({ resource, column, kind }: { resource: any; kind: string;
6779
7651
  'text-sm font-medium',
6780
7652
  desired === 0 ? 'text-theme-text-secondary' : allReady ? 'text-green-400' : ready > 0 ? 'text-yellow-400' : 'text-red-400'
6781
7653
  )}>
6782
- {ready}/{desired}
7654
+ {Math.min(ready, desired)}/{desired}
6783
7655
  </span>
6784
7656
  {problems.length > 0 && (
6785
7657
  <Tooltip content={
@@ -6842,12 +7714,14 @@ function DaemonSetCell({ resource, column }: { resource: any; column: string })
6842
7714
  const status = resource.status || {}
6843
7715
 
6844
7716
  switch (column) {
7717
+ case 'status':
7718
+ return <WorkloadStatusCell resource={resource} kind="daemonsets" />
6845
7719
  case 'desired':
6846
7720
  return <span className="text-sm text-theme-text-secondary">{status.desiredNumberScheduled || 0}</span>
6847
7721
  case 'ready': {
6848
7722
  const desired = status.desiredNumberScheduled || 0
6849
7723
  const ready = status.numberReady || 0
6850
- const allReady = ready === desired && desired > 0
7724
+ const allReady = ready >= desired && desired > 0
6851
7725
  const problems = getWorkloadProblems(resource, 'daemonsets')
6852
7726
  return (
6853
7727
  <div className="flex items-center gap-1.5">
@@ -6855,7 +7729,7 @@ function DaemonSetCell({ resource, column }: { resource: any; column: string })
6855
7729
  'text-sm font-medium',
6856
7730
  allReady ? 'text-green-400' : ready > 0 ? 'text-yellow-400' : 'text-red-400'
6857
7731
  )}>
6858
- {ready}
7732
+ {Math.min(ready, desired)}
6859
7733
  </span>
6860
7734
  {problems.length > 0 && (
6861
7735
  <Tooltip content={
@@ -6903,7 +7777,7 @@ function ReplicaSetCell({ resource, column }: { resource: any; column: string })
6903
7777
 
6904
7778
  switch (column) {
6905
7779
  case 'ready': {
6906
- const desired = spec.replicas ?? 0
7780
+ const desired = spec.replicas ?? 1
6907
7781
  const ready = status.readyReplicas || 0
6908
7782
  const allReady = ready === desired && desired > 0
6909
7783
  return (
@@ -7571,14 +8445,8 @@ function PVCCell({ resource, column }: { resource: any; column: string }) {
7571
8445
 
7572
8446
  function RolloutCell({ resource, column }: { resource: any; column: string }) {
7573
8447
  switch (column) {
7574
- case 'status': {
7575
- const status = getRolloutStatus(resource)
7576
- return (
7577
- <span className={clsx('badge', status.color)}>
7578
- {status.text}
7579
- </span>
7580
- )
7581
- }
8448
+ case 'status':
8449
+ return <WorkloadStatusCell resource={resource} kind="rollouts" />
7582
8450
  case 'ready': {
7583
8451
  const ready = getRolloutReady(resource)
7584
8452
  const parts = ready.split('/')