@skyhook-io/k8s-ui 1.13.0 → 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.
- package/package.json +2 -2
- package/src/components/applications/AppTooltips.tsx +13 -1
- package/src/components/applications/ApplicationDetail.test.tsx +25 -0
- package/src/components/applications/ApplicationDetail.tsx +41 -9
- package/src/components/applications/ApplicationsView.tsx +18 -3
- package/src/components/applications/ReadyBar.tsx +3 -2
- package/src/components/applications/applications-view.test.tsx +91 -0
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +35 -7
- package/src/components/issues/IssuesView.tsx +40 -31
- package/src/components/issues/ResourceIssuesSection.tsx +3 -2
- package/src/components/issues/index.ts +3 -1
- package/src/components/issues/issue-timing.ts +123 -4
- package/src/components/issues/issues.test.ts +221 -10
- package/src/components/issues/types.ts +31 -5
- package/src/components/namespace-switcher/NamespacePicker.tsx +21 -13
- package/src/components/resources/ResourcesView.tsx +973 -100
- package/src/components/resources/column-settings-key.test.ts +98 -0
- package/src/components/resources/column-visibility-merge.test.ts +64 -0
- package/src/components/resources/curated-column-ownership.test.ts +236 -0
- package/src/components/resources/gateway-policy-status.test.ts +291 -0
- package/src/components/resources/gateway-policy-status.ts +237 -0
- package/src/components/resources/generic-status.test.ts +199 -0
- package/src/components/resources/generic-status.ts +171 -0
- package/src/components/resources/gpu-ecosystem-registry.test.ts +63 -0
- package/src/components/resources/index.ts +9 -1
- package/src/components/resources/istio-gateway-filter.test.ts +97 -0
- package/src/components/resources/printer-columns.test.ts +298 -0
- package/src/components/resources/printer-columns.ts +249 -0
- package/src/components/resources/renderers/EventRenderer.test.tsx +50 -1
- package/src/components/resources/renderers/EventRenderer.tsx +3 -5
- package/src/components/resources/renderers/GenericRenderer.tsx +18 -83
- package/src/components/resources/renderers/HPARenderer.test.tsx +28 -0
- package/src/components/resources/renderers/HPARenderer.tsx +6 -1
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +2 -1
- package/src/components/resources/renderers/NamespaceRenderer.test.ts +30 -0
- package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -3
- package/src/components/resources/renderers/PVCRenderer.test.tsx +44 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +19 -2
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +35 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +22 -3
- package/src/components/resources/renderers/WorkloadRenderer.test.tsx +83 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +25 -65
- package/src/components/resources/renderers/amd-gpu-cells.tsx +36 -0
- package/src/components/resources/renderers/inference-gateway-cells.tsx +62 -0
- package/src/components/resources/renderers/jobset-lws-cells.tsx +82 -0
- package/src/components/resources/renderers/kai-cells.tsx +67 -0
- package/src/components/resources/renderers/kaito-cells.tsx +62 -0
- package/src/components/resources/renderers/kserve-cells.tsx +145 -0
- package/src/components/resources/renderers/kubeflow-training-cells.tsx +72 -0
- package/src/components/resources/renderers/kueue-cells.tsx +176 -0
- package/src/components/resources/renderers/nim-cells.tsx +81 -0
- package/src/components/resources/renderers/ray-cells.tsx +124 -0
- package/src/components/resources/renderers/volcano-cells.tsx +140 -0
- package/src/components/resources/resource-utils-amd-gpu.ts +61 -0
- package/src/components/resources/resource-utils-gpu-ecosystem.test.ts +222 -0
- package/src/components/resources/resource-utils-hpa.test.ts +4 -0
- package/src/components/resources/resource-utils-hpa.ts +6 -2
- package/src/components/resources/resource-utils-inference-gateway.ts +119 -0
- package/src/components/resources/resource-utils-jobset-lws.ts +117 -0
- package/src/components/resources/resource-utils-kai.ts +105 -0
- package/src/components/resources/resource-utils-kaito.ts +114 -0
- package/src/components/resources/resource-utils-kserve.ts +186 -0
- package/src/components/resources/resource-utils-kubeflow-training.ts +121 -0
- package/src/components/resources/resource-utils-kueue.ts +198 -0
- package/src/components/resources/resource-utils-nim.ts +109 -0
- package/src/components/resources/resource-utils-ray.ts +214 -0
- package/src/components/resources/resource-utils-rollout.test.ts +183 -0
- package/src/components/resources/resource-utils-velero.test.ts +30 -0
- package/src/components/resources/resource-utils-volcano.ts +176 -0
- package/src/components/resources/resource-utils.ts +156 -47
- package/src/components/resources/resources-column-filter.test.ts +27 -1
- package/src/components/resources/workload-rollout-golden.test.ts +55 -0
- package/src/components/shared/CreateResourceDialog.tsx +1 -0
- package/src/components/shared/EditableYamlView.tsx +4 -2
- package/src/components/shared/ResourceActionsBar.tsx +170 -25
- package/src/components/shared/ResourceRendererDispatch.test.tsx +206 -1
- package/src/components/shared/ResourceRendererDispatch.tsx +84 -40
- package/src/components/shared/SetImageDialog.test.ts +129 -0
- package/src/components/shared/SetImageDialog.tsx +577 -0
- package/src/components/shared/index.ts +1 -0
- package/src/components/shared/revision-role-badges.test.ts +56 -4
- package/src/components/timeline/TimelineList.tsx +19 -3
- package/src/components/timeline/TimelineSwimlanes.tsx +11 -3
- package/src/components/trace/ReachabilityView.tsx +1 -1
- package/src/components/trace/TracePanel.tsx +1 -0
- package/src/components/ui/InClusterConsentDialog.tsx +5 -4
- package/src/components/ui/SelectMenu.tsx +202 -28
- package/src/components/ui/drawer-components.tsx +12 -0
- package/src/components/workload/WorkloadRolloutNotice.tsx +60 -0
- package/src/components/workload/WorkloadView.tsx +146 -83
- package/src/hooks/useProgressiveRefresh.ts +37 -0
- package/src/source-hygiene.test.ts +28 -0
- package/src/theme/tailwind-theme.css +4 -0
- package/src/types/core.ts +62 -4
- package/src/utils/api-resources.test.ts +40 -1
- package/src/utils/api-resources.ts +11 -1
- package/src/utils/applications.ts +50 -0
- package/src/utils/bulk-workload-actions.test.ts +20 -0
- package/src/utils/bulk-workload-actions.ts +15 -0
- package/src/utils/clipboard.test.ts +86 -0
- package/src/utils/clipboard.ts +50 -0
- package/src/utils/extended-resources.ts +4 -7
- package/src/utils/format.test.ts +83 -1
- package/src/utils/format.ts +80 -1
- package/src/utils/inClusterConsent.test.ts +9 -0
- package/src/utils/inClusterConsent.ts +9 -9
- package/src/utils/index.ts +1 -0
- package/src/utils/navigation.test.ts +31 -1
- package/src/utils/navigation.ts +21 -2
- package/src/utils/workload-rollout.ts +292 -0
|
@@ -34,11 +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
|
+
import { copyText } from '../../utils/clipboard'
|
|
38
39
|
import {
|
|
39
40
|
getPodStatus,
|
|
40
41
|
getPodRestarts,
|
|
41
42
|
getPodProblems,
|
|
43
|
+
getWorkloadDisplayStatus,
|
|
44
|
+
workloadStatusLabel,
|
|
42
45
|
getWorkloadProblems,
|
|
43
46
|
workloadMatchesProblemCategory,
|
|
44
47
|
getContainerSquareStates,
|
|
@@ -78,7 +81,6 @@ import {
|
|
|
78
81
|
getPVCStatus,
|
|
79
82
|
getPVCCapacity,
|
|
80
83
|
getPVCAccessModes,
|
|
81
|
-
getRolloutStatus,
|
|
82
84
|
getAnalysisRunStatus,
|
|
83
85
|
summarizeAnalysisMetrics,
|
|
84
86
|
getRolloutStrategy,
|
|
@@ -135,11 +137,20 @@ import {
|
|
|
135
137
|
parseColumnFilterExcludes,
|
|
136
138
|
podMatchesProblemCategory,
|
|
137
139
|
SEVERITY_DOT_COLOR,
|
|
140
|
+
healthColors,
|
|
138
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'
|
|
139
148
|
import { SEVERITY_BADGE, EVENT_TYPE_COLORS } from '../../utils/badge-colors'
|
|
140
149
|
import { pluralize } from '../../utils/pluralize'
|
|
141
150
|
import { getPodGpuCount, getNodeGpuCount } from '../../utils/extended-resources'
|
|
151
|
+
import { parseQuantityToNumber } from '../../utils/format'
|
|
142
152
|
import { type CustomColumnDef, type CustomColumnSource, customColumnKey, readCustomColumnValue, sanitizeCustomColumnDefs } from '../../utils/custom-columns'
|
|
153
|
+
import { isRolloutActivityVisible } from '../../utils/workload-rollout'
|
|
143
154
|
import { FreshnessControl, type FreshnessConnection } from '../ui/FreshnessControl'
|
|
144
155
|
import { Tooltip } from '../ui/Tooltip'
|
|
145
156
|
import { AuditBadgeTooltip, type AuditBadgeMessage } from '../audit/AuditBadgeTooltip'
|
|
@@ -153,6 +164,18 @@ import { NodePoolCell, NodeClaimCell, EC2NodeClassCell } from './renderers/karpe
|
|
|
153
164
|
import { ScaledObjectCell, ScaledJobCell, TriggerAuthenticationCell, ClusterTriggerAuthenticationCell } from './renderers/keda-cells'
|
|
154
165
|
import { ResourceClaimCell, ResourceClaimTemplateCell, DeviceClassCell, ResourceSliceCell } from './renderers/dra-cells'
|
|
155
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'
|
|
156
179
|
import { ServiceMonitorCell, PrometheusRuleCell, PodMonitorCell } from './renderers/prometheus-cells'
|
|
157
180
|
import { PolicyReportCell, ClusterPolicyReportCell, KyvernoPolicyCell, ClusterPolicyCell,
|
|
158
181
|
KyvernoUpdateRequestCell,
|
|
@@ -355,6 +378,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
355
378
|
deployments: [
|
|
356
379
|
{ key: 'name', label: 'Name' },
|
|
357
380
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
381
|
+
{ key: 'status', label: 'Status', width: 'w-40' },
|
|
358
382
|
{ key: 'ready', label: 'Ready', width: 'w-24', tooltip: 'Ready pods / Desired replicas' },
|
|
359
383
|
{ key: 'upToDate', label: 'Up-to-date', width: 'w-32', hideOnMobile: true, tooltip: 'Number of pods running the current pod template' },
|
|
360
384
|
{ key: 'available', label: 'Available', width: 'w-28', hideOnMobile: true, tooltip: 'Number of pods available (ready for minReadySeconds)' },
|
|
@@ -364,6 +388,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
364
388
|
daemonsets: [
|
|
365
389
|
{ key: 'name', label: 'Name' },
|
|
366
390
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
391
|
+
{ key: 'status', label: 'Status', width: 'w-40' },
|
|
367
392
|
{ key: 'desired', label: 'Desired', width: 'w-20', tooltip: 'Number of nodes that should run the daemon pod (based on node selector)' },
|
|
368
393
|
{ key: 'ready', label: 'Ready', width: 'w-20', tooltip: 'Number of pods that are ready (passing readiness probes)' },
|
|
369
394
|
{ key: 'upToDate', label: 'Up-to-date', width: 'w-32', hideOnMobile: true, tooltip: 'Number of pods running the current pod template spec' },
|
|
@@ -374,6 +399,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
374
399
|
statefulsets: [
|
|
375
400
|
{ key: 'name', label: 'Name' },
|
|
376
401
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
402
|
+
{ key: 'status', label: 'Status', width: 'w-40' },
|
|
377
403
|
{ key: 'ready', label: 'Ready', width: 'w-24', tooltip: 'Ready pods / Desired replicas' },
|
|
378
404
|
{ key: 'upToDate', label: 'Up-to-date', width: 'w-32', hideOnMobile: true, tooltip: 'Number of pods running the current pod template' },
|
|
379
405
|
{ key: 'images', label: 'Images', width: 'w-48', hideOnMobile: true },
|
|
@@ -492,7 +518,7 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
492
518
|
rollouts: [
|
|
493
519
|
{ key: 'name', label: 'Name' },
|
|
494
520
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
495
|
-
{ key: 'status', label: '
|
|
521
|
+
{ key: 'status', label: 'Status', width: 'w-32' },
|
|
496
522
|
{ key: 'ready', label: 'Ready', width: 'w-24', tooltip: 'Available / Desired replicas' },
|
|
497
523
|
{ key: 'strategy', label: 'Strategy', width: 'w-24' },
|
|
498
524
|
{ key: 'step', label: 'Step', width: 'w-20', hideOnMobile: true, tooltip: 'Current canary step / Total steps' },
|
|
@@ -610,6 +636,14 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
610
636
|
{ key: 'rules', label: 'Rules', width: 'w-16', hideOnMobile: true },
|
|
611
637
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
612
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
|
+
],
|
|
613
647
|
gatewayclasses: [
|
|
614
648
|
{ key: 'name', label: 'Name' },
|
|
615
649
|
{ key: 'controller', label: 'Controller', width: 'w-64', tooltip: 'Gateway controller implementation (spec.controllerName)' },
|
|
@@ -695,6 +729,314 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
695
729
|
{ key: 'version', label: 'Version', width: 'w-32' },
|
|
696
730
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
697
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
|
+
],
|
|
698
1040
|
scaledjobs: [
|
|
699
1041
|
{ key: 'name', label: 'Name' },
|
|
700
1042
|
{ key: 'namespace', label: 'Namespace', width: 'w-36' },
|
|
@@ -2098,6 +2440,21 @@ const GROUP_QUALIFIED_COLUMN_KEYS: Record<string, Record<string, string>> = {
|
|
|
2098
2440
|
// completely different shape (cluster + method, no storage location/expiry).
|
|
2099
2441
|
backups: { 'postgresql.cnpg.io': 'cnpgbackups' },
|
|
2100
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' },
|
|
2101
2458
|
services: { 'serving.knative.dev': 'knativeservices' },
|
|
2102
2459
|
configurations: { 'serving.knative.dev': 'knativeconfigurations' },
|
|
2103
2460
|
revisions: { 'serving.knative.dev': 'knativerevisions' },
|
|
@@ -2166,6 +2523,7 @@ const GROUP_QUALIFIED_COLUMN_KEYS: Record<string, Record<string, string>> = {
|
|
|
2166
2523
|
|
|
2167
2524
|
const GROUP_QUALIFIED_ONLY_COLUMN_KEYS = new Set([
|
|
2168
2525
|
'networkpolicy', 'networkpolicies',
|
|
2526
|
+
'job', 'jobs',
|
|
2169
2527
|
// Other projects serve their own IPPool CRD, so only a Calico group earns the
|
|
2170
2528
|
// Calico columns.
|
|
2171
2529
|
'ippool', 'ippools',
|
|
@@ -2174,6 +2532,7 @@ const GROUP_QUALIFIED_ONLY_COLUMN_KEYS = new Set([
|
|
|
2174
2532
|
'stagedglobalnetworkpolicy', 'stagedglobalnetworkpolicies',
|
|
2175
2533
|
'stagedkubernetesnetworkpolicy', 'stagedkubernetesnetworkpolicies',
|
|
2176
2534
|
])
|
|
2535
|
+
|
|
2177
2536
|
const CALICO_ONLY_COLUMN_KEYS = new Set([
|
|
2178
2537
|
'globalnetworkpolicy', 'globalnetworkpolicies',
|
|
2179
2538
|
'stagednetworkpolicy', 'stagednetworkpolicies',
|
|
@@ -2184,7 +2543,7 @@ const CALICO_ONLY_COLUMN_KEYS = new Set([
|
|
|
2184
2543
|
// Normalize a kind name to its plural API form used in KNOWN_COLUMNS keys.
|
|
2185
2544
|
// Handles CRD singular names from URLs: 'ScaledObject' → 'scaledobjects', 'NodePool' → 'nodepools'
|
|
2186
2545
|
// When group is provided, resolves collisions (e.g., 'services' + 'serving.knative.dev' → 'knativeservices')
|
|
2187
|
-
function normalizeKindToPlural(kind: string, group?: string): string {
|
|
2546
|
+
export function normalizeKindToPlural(kind: string, group?: string): string {
|
|
2188
2547
|
const lower = kind.toLowerCase()
|
|
2189
2548
|
// Check group-qualified mapping first for collision resolution
|
|
2190
2549
|
if (group && GROUP_QUALIFIED_COLUMN_KEYS[lower]?.[group]) {
|
|
@@ -2229,20 +2588,396 @@ function isLikelyCrossplaneMRGroup(kind: string, group: string): boolean {
|
|
|
2229
2588
|
return false
|
|
2230
2589
|
}
|
|
2231
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
|
+
|
|
2232
2829
|
function getColumnsForKind(kind: string, group?: string): Column[] {
|
|
2233
|
-
|
|
2234
|
-
|
|
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.
|
|
2235
2834
|
if (group && isLikelyCrossplaneMRGroup(kind, group)) {
|
|
2236
2835
|
return KNOWN_COLUMNS.crossplanemanagedresources
|
|
2237
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
|
+
}
|
|
2238
2848
|
return DEFAULT_COLUMNS
|
|
2239
2849
|
}
|
|
2240
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
|
+
|
|
2241
2868
|
// Get the default visible columns for a kind
|
|
2242
2869
|
function getDefaultVisibleColumns(columns: Column[]): Set<string> {
|
|
2243
2870
|
return new Set(columns.filter(c => c.defaultVisible !== false).map(c => c.key))
|
|
2244
2871
|
}
|
|
2245
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
|
+
|
|
2246
2981
|
// Host-injected leading columns minus any whose key collides with a built-in.
|
|
2247
2982
|
// Rendering two columns under one key duplicates React keys and corrupts the
|
|
2248
2983
|
// visibleColumns / columnWidths maps, so a colliding extra is dropped (dev-warn).
|
|
@@ -2269,6 +3004,22 @@ function filterHostExtras(
|
|
|
2269
3004
|
// localStorage helpers for column settings
|
|
2270
3005
|
const COLUMN_SETTINGS_PREFIX = 'radar-columns-'
|
|
2271
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
|
+
|
|
2272
3023
|
interface ColumnSettings {
|
|
2273
3024
|
visible: string[]
|
|
2274
3025
|
widths: Record<string, number>
|
|
@@ -2277,7 +3028,7 @@ interface ColumnSettings {
|
|
|
2277
3028
|
|
|
2278
3029
|
function loadColumnSettings(kind: string, group?: string): ColumnSettings | null {
|
|
2279
3030
|
try {
|
|
2280
|
-
const key = COLUMN_SETTINGS_PREFIX +
|
|
3031
|
+
const key = COLUMN_SETTINGS_PREFIX + columnSettingsKey(kind, group)
|
|
2281
3032
|
const raw = localStorage.getItem(key)
|
|
2282
3033
|
if (raw) return JSON.parse(raw)
|
|
2283
3034
|
} catch { /* ignore */ }
|
|
@@ -2286,14 +3037,14 @@ function loadColumnSettings(kind: string, group?: string): ColumnSettings | null
|
|
|
2286
3037
|
|
|
2287
3038
|
function saveColumnSettings(kind: string, group: string | undefined, settings: ColumnSettings) {
|
|
2288
3039
|
try {
|
|
2289
|
-
const key = COLUMN_SETTINGS_PREFIX +
|
|
3040
|
+
const key = COLUMN_SETTINGS_PREFIX + columnSettingsKey(kind, group)
|
|
2290
3041
|
localStorage.setItem(key, JSON.stringify(settings))
|
|
2291
3042
|
} catch { /* ignore */ }
|
|
2292
3043
|
}
|
|
2293
3044
|
|
|
2294
3045
|
function clearColumnSettings(kind: string, group?: string) {
|
|
2295
3046
|
try {
|
|
2296
|
-
const key = COLUMN_SETTINGS_PREFIX +
|
|
3047
|
+
const key = COLUMN_SETTINGS_PREFIX + columnSettingsKey(kind, group)
|
|
2297
3048
|
localStorage.removeItem(key)
|
|
2298
3049
|
} catch { /* ignore */ }
|
|
2299
3050
|
}
|
|
@@ -2401,6 +3152,11 @@ interface ResourcesViewProps {
|
|
|
2401
3152
|
* doesn't need to extend KNOWN_COLUMNS or per-kind cell renderers.
|
|
2402
3153
|
* When undefined, behavior is byte-identical to single-cluster mode. */
|
|
2403
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
|
|
2404
3160
|
/** Escape hatch for full-page-nav row selection: receive the FULL
|
|
2405
3161
|
* resource object on row click / Enter / `d` / search-Enter, instead
|
|
2406
3162
|
* of the stripped {kind, namespace, name, group} shape onResourceClick
|
|
@@ -2630,6 +3386,7 @@ export function ResourcesView({
|
|
|
2630
3386
|
onCreateResource,
|
|
2631
3387
|
defaultKind = DEFAULT_KIND_INFO,
|
|
2632
3388
|
extraLeadingColumns,
|
|
3389
|
+
printerTable,
|
|
2633
3390
|
onRowSelect,
|
|
2634
3391
|
rowHrefFor,
|
|
2635
3392
|
onCompareSubmit,
|
|
@@ -2827,22 +3584,32 @@ export function ResourcesView({
|
|
|
2827
3584
|
// collision: a colliding extra is filtered out (with a dev-mode warn)
|
|
2828
3585
|
// so we don't render two columns sharing one key — that would yield
|
|
2829
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
|
+
|
|
2830
3596
|
const allColumns = useMemo(() => {
|
|
2831
|
-
const kindColumns =
|
|
3597
|
+
const kindColumns = columnsForKindWithPrinter(selectedKind.name, selectedKind.group, builtPrinterColumns)
|
|
2832
3598
|
if (!extraLeadingColumns?.length && !builtCustomColumns.length) return kindColumns
|
|
2833
3599
|
const builtinKeys = new Set(kindColumns.map(c => c.key))
|
|
2834
3600
|
const filteredExtras = filterHostExtras(extraLeadingColumns, builtinKeys, selectedKind.name)
|
|
2835
3601
|
return [...filteredExtras, ...kindColumns, ...builtCustomColumns]
|
|
2836
|
-
}, [selectedKind.name, selectedKind.group, extraLeadingColumns, builtCustomColumns])
|
|
3602
|
+
}, [selectedKind.name, selectedKind.group, extraLeadingColumns, builtCustomColumns, builtPrinterColumns])
|
|
2837
3603
|
|
|
2838
3604
|
// Map of extra column keys for fast O(1) lookup on each render path
|
|
2839
3605
|
// (cell render, sort, column-filter unique-values).
|
|
2840
3606
|
const extraColumnsByKey = useMemo(() => {
|
|
2841
3607
|
const m = new Map<string, ExtraColumn>()
|
|
2842
3608
|
extraLeadingColumns?.forEach(c => m.set(c.key, c))
|
|
3609
|
+
builtPrinterColumns.forEach(c => m.set(c.key, c))
|
|
2843
3610
|
builtCustomColumns.forEach(c => m.set(c.key, c))
|
|
2844
3611
|
return m
|
|
2845
|
-
}, [extraLeadingColumns, builtCustomColumns])
|
|
3612
|
+
}, [extraLeadingColumns, builtCustomColumns, builtPrinterColumns])
|
|
2846
3613
|
|
|
2847
3614
|
// Guards the save effect from persisting on the initial load of each kind
|
|
2848
3615
|
// (set false by the load effect, flipped true on its first skipped save).
|
|
@@ -2872,7 +3639,7 @@ export function ResourcesView({
|
|
|
2872
3639
|
// (non-array, blank path, bad source) must not crash the later .map or add dead columns.
|
|
2873
3640
|
const savedCustom = sanitizeCustomColumnDefs(saved?.custom)
|
|
2874
3641
|
setCustomColumns(savedCustom)
|
|
2875
|
-
const kindColumns =
|
|
3642
|
+
const kindColumns = columnsForKindWithPrinter(selectedKind.name, selectedKind.group, builtPrinterColumns)
|
|
2876
3643
|
const builtinKeys = new Set(kindColumns.map(c => c.key))
|
|
2877
3644
|
const extras = filterHostExtras(extraLeadingColumns, builtinKeys, selectedKind.name)
|
|
2878
3645
|
const effective = [...extras, ...kindColumns, ...savedCustom.map(buildCustomColumn)]
|
|
@@ -2899,16 +3666,18 @@ export function ResourcesView({
|
|
|
2899
3666
|
setVisibleColumns(getDefaultVisibleColumns(effective))
|
|
2900
3667
|
setColumnWidths({})
|
|
2901
3668
|
} else {
|
|
2902
|
-
|
|
2903
|
-
for (const k of extraKeys) merged.add(k)
|
|
2904
|
-
setVisibleColumns(merged)
|
|
3669
|
+
setVisibleColumns(mergeSavedVisibleColumns(saved.visible, extraKeys, builtPrinterColumns))
|
|
2905
3670
|
setColumnWidths(saved.widths || {})
|
|
2906
3671
|
}
|
|
2907
3672
|
} else {
|
|
2908
3673
|
setVisibleColumns(getDefaultVisibleColumns(effective))
|
|
2909
3674
|
setColumnWidths({})
|
|
2910
3675
|
}
|
|
2911
|
-
|
|
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])
|
|
2912
3681
|
|
|
2913
3682
|
// Save column settings when they change (skip the initial load of each kind)
|
|
2914
3683
|
useEffect(() => {
|
|
@@ -3303,11 +4072,14 @@ export function ResourcesView({
|
|
|
3303
4072
|
if (containers.length > 0) {
|
|
3304
4073
|
openLogs?.({ namespace: ns, podName: name, containers })
|
|
3305
4074
|
}
|
|
3306
|
-
} else if (['deployments', 'statefulsets', 'daemonsets', 'replicasets', '
|
|
4075
|
+
} else if (['deployments', 'statefulsets', 'daemonsets', 'replicasets', 'workflows', 'cronjobs', 'cronworkflows', 'workflowtemplates', 'clusterworkflowtemplates', 'scaledjobs'].includes(kindLower) || isCoreBatchJob(kindLower, selectedKind.group)) {
|
|
3307
4076
|
openWorkloadLogs?.({ namespace: ns, workloadKind: selectedKind.kind, workloadName: name })
|
|
3308
4077
|
}
|
|
3309
4078
|
},
|
|
3310
|
-
enabled: highlightedIndex >= 0 && !compareMode &&
|
|
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
|
+
),
|
|
3311
4083
|
},
|
|
3312
4084
|
{
|
|
3313
4085
|
id: 'resources-sort-name',
|
|
@@ -3962,12 +4734,17 @@ export function ResourcesView({
|
|
|
3962
4734
|
|
|
3963
4735
|
// Reset sort and filters when kind changes (but not when syncing from URL navigation)
|
|
3964
4736
|
// Track previous kind to skip on mount (where the effect fires but kind hasn't actually changed)
|
|
3965
|
-
|
|
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)
|
|
3966
4743
|
useEffect(() => {
|
|
3967
|
-
if (prevKindRef.current ===
|
|
4744
|
+
if (prevKindRef.current === selectedKindIdentity) {
|
|
3968
4745
|
return
|
|
3969
4746
|
}
|
|
3970
|
-
prevKindRef.current =
|
|
4747
|
+
prevKindRef.current = selectedKindIdentity
|
|
3971
4748
|
setSortColumn(null)
|
|
3972
4749
|
setSortDirection(null)
|
|
3973
4750
|
setOpenColumnFilter(null)
|
|
@@ -3976,7 +4753,7 @@ export function ResourcesView({
|
|
|
3976
4753
|
setColumnFilterExcludes({})
|
|
3977
4754
|
}
|
|
3978
4755
|
setProblemFilters([])
|
|
3979
|
-
}, [
|
|
4756
|
+
}, [selectedKindIdentity])
|
|
3980
4757
|
|
|
3981
4758
|
// Toggle sort for a column
|
|
3982
4759
|
const handleSort = useCallback((column: string) => {
|
|
@@ -4010,7 +4787,28 @@ export function ResourcesView({
|
|
|
4010
4787
|
case 'age':
|
|
4011
4788
|
return meta.creationTimestamp ? new Date(meta.creationTimestamp).getTime() : 0
|
|
4012
4789
|
case 'status':
|
|
4013
|
-
|
|
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 ''
|
|
4014
4812
|
case 'containers':
|
|
4015
4813
|
// Pod containers column — sort by readiness ratio
|
|
4016
4814
|
if (status.containerStatuses) {
|
|
@@ -4020,6 +4818,7 @@ export function ResourcesView({
|
|
|
4020
4818
|
}
|
|
4021
4819
|
return 0
|
|
4022
4820
|
case 'ready':
|
|
4821
|
+
if (kindLower === 'jobsets') return getJobSetReadyJobs(resource)
|
|
4023
4822
|
// For DaemonSets, use numberReady/desiredNumberScheduled
|
|
4024
4823
|
if (kindLower === 'daemonsets') {
|
|
4025
4824
|
const desired = status.desiredNumberScheduled ?? 0
|
|
@@ -4040,6 +4839,7 @@ export function ResourcesView({
|
|
|
4040
4839
|
// DaemonSet: updatedNumberScheduled, others: updatedReplicas
|
|
4041
4840
|
return status.updatedNumberScheduled ?? status.updatedReplicas ?? 0
|
|
4042
4841
|
case 'restarts':
|
|
4842
|
+
if (kindLower === 'jobsets') return getJobSetRestarts(resource)
|
|
4043
4843
|
return getPodRestarts(resource)
|
|
4044
4844
|
case 'lastSeen': {
|
|
4045
4845
|
const lastTs = resource.lastTimestamp || meta.creationTimestamp
|
|
@@ -4089,7 +4889,7 @@ export function ResourcesView({
|
|
|
4089
4889
|
default:
|
|
4090
4890
|
return ''
|
|
4091
4891
|
}
|
|
4092
|
-
}, [metricsLookup])
|
|
4892
|
+
}, [metricsLookup, selectedKind.name, selectedKind.group])
|
|
4093
4893
|
|
|
4094
4894
|
// Helper to check if a pod matches problem filters
|
|
4095
4895
|
const podMatchesProblemFilter = useCallback((pod: any, filters: string[]): boolean => {
|
|
@@ -4142,11 +4942,11 @@ export function ResourcesView({
|
|
|
4142
4942
|
// when the parent supplied a custom getFilterValue.
|
|
4143
4943
|
const activeColFilters = Object.entries(columnFilters).filter(([, vals]) => vals.length > 0)
|
|
4144
4944
|
if (activeColFilters.length > 0) {
|
|
4145
|
-
const
|
|
4945
|
+
const cellFilterKind = getCellFilterKind(selectedKind.name, selectedKind.group)
|
|
4146
4946
|
result = result.filter((r: any) =>
|
|
4147
4947
|
activeColFilters.every(([col, vals]) => {
|
|
4148
4948
|
const extra = extraColumnsByKey.get(col)
|
|
4149
|
-
const cellVal = extra?.getFilterValue ? extra.getFilterValue(r) : getCellFilterValue(r, col,
|
|
4949
|
+
const cellVal = extra?.getFilterValue ? extra.getFilterValue(r) : getCellFilterValue(r, col, cellFilterKind)
|
|
4150
4950
|
const match = vals.includes(cellVal)
|
|
4151
4951
|
return columnFilterExcludes[col] ? !match : match
|
|
4152
4952
|
})
|
|
@@ -4208,17 +5008,26 @@ export function ResourcesView({
|
|
|
4208
5008
|
// the built-in getSortValue for their key.
|
|
4209
5009
|
if (sortColumn && sortDirection) {
|
|
4210
5010
|
const extra = extraColumnsByKey.get(sortColumn)
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
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) => {
|
|
4214
5022
|
let comparison = 0
|
|
4215
|
-
if (typeof
|
|
4216
|
-
comparison =
|
|
5023
|
+
if (typeof a.k === 'number' && typeof b.k === 'number') {
|
|
5024
|
+
comparison = a.k - b.k
|
|
4217
5025
|
} else {
|
|
4218
|
-
comparison = String(
|
|
5026
|
+
comparison = String(a.k).localeCompare(String(b.k))
|
|
4219
5027
|
}
|
|
4220
5028
|
return sortDirection === 'desc' ? -comparison : comparison
|
|
4221
5029
|
})
|
|
5030
|
+
result = decorated.map(d => d.r)
|
|
4222
5031
|
} else {
|
|
4223
5032
|
// Default sort by kind
|
|
4224
5033
|
const kindLower = normalizeKindToPlural(selectedKind.name, selectedKind.group)
|
|
@@ -4490,6 +5299,7 @@ export function ResourcesView({
|
|
|
4490
5299
|
if (!resources || resources.length === 0) return null
|
|
4491
5300
|
|
|
4492
5301
|
const kindLower = normalizeKindToPlural(selectedKind.name, selectedKind.group)
|
|
5302
|
+
const cellFilterKind = getCellFilterKind(selectedKind.name, selectedKind.group)
|
|
4493
5303
|
// Iterate over allColumns (built-ins + injected extras) so an
|
|
4494
5304
|
// ExtraColumn with getFilterValue gets a column-filter dropdown like
|
|
4495
5305
|
// any built-in does. The previous formulation iterated KNOWN_COLUMNS
|
|
@@ -4516,7 +5326,7 @@ export function ResourcesView({
|
|
|
4516
5326
|
// Count distinct values for this column
|
|
4517
5327
|
const valueCounts: Record<string, number> = {}
|
|
4518
5328
|
for (const r of resources) {
|
|
4519
|
-
const val = extra?.getFilterValue ? extra.getFilterValue(r) : getCellFilterValue(r, col.key,
|
|
5329
|
+
const val = extra?.getFilterValue ? extra.getFilterValue(r) : getCellFilterValue(r, col.key, cellFilterKind)
|
|
4520
5330
|
if (val) {
|
|
4521
5331
|
valueCounts[val] = (valueCounts[val] || 0) + 1
|
|
4522
5332
|
}
|
|
@@ -5838,10 +6648,11 @@ function CopyNameButton({ name }: { name: string }) {
|
|
|
5838
6648
|
<button
|
|
5839
6649
|
onClick={(e) => {
|
|
5840
6650
|
e.stopPropagation()
|
|
5841
|
-
|
|
6651
|
+
void copyText(name).then((didCopy) => {
|
|
6652
|
+
if (!didCopy) return
|
|
5842
6653
|
setCopied(true)
|
|
5843
6654
|
setTimeout(() => setCopied(false), 1500)
|
|
5844
|
-
})
|
|
6655
|
+
})
|
|
5845
6656
|
}}
|
|
5846
6657
|
className="shrink-0 p-0.5 text-theme-text-tertiary hover:text-theme-text-primary opacity-0 group-hover/row:opacity-100 transition-opacity"
|
|
5847
6658
|
title="Copy name"
|
|
@@ -5940,6 +6751,9 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
5940
6751
|
|
|
5941
6752
|
// Kind-specific columns (normalize CRD singular names like 'ScaledObject' → 'scaledobjects')
|
|
5942
6753
|
const kindLower = normalizeKindToPlural(kind, group)
|
|
6754
|
+
if (group && !hasCuratedColumns(kind, group)) {
|
|
6755
|
+
return <GenericCell resource={resource} column={column} />
|
|
6756
|
+
}
|
|
5943
6757
|
|
|
5944
6758
|
// Kyverno cells are group-gated ahead of the switch so a non-matching CR
|
|
5945
6759
|
// falls through to the switch's default (GenericCell) instead of being
|
|
@@ -5995,6 +6809,9 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
5995
6809
|
case 'persistentvolumeclaims':
|
|
5996
6810
|
return <PVCCell resource={resource} column={column} />
|
|
5997
6811
|
case 'rollouts':
|
|
6812
|
+
if (!resource.apiVersion?.startsWith('argoproj.io/')) {
|
|
6813
|
+
return <GenericCell resource={resource} column={column} />
|
|
6814
|
+
}
|
|
5998
6815
|
return <RolloutCell resource={resource} column={column} />
|
|
5999
6816
|
case 'analysisruns':
|
|
6000
6817
|
return <AnalysisRunCell resource={resource} column={column} />
|
|
@@ -6018,8 +6835,12 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
6018
6835
|
return <OrderCell resource={resource} column={column} />
|
|
6019
6836
|
case 'challenges':
|
|
6020
6837
|
return <ChallengeCell resource={resource} column={column} />
|
|
6838
|
+
case 'istiogateways':
|
|
6839
|
+
return <IstioGatewayCell resource={resource} column={column} />
|
|
6021
6840
|
case 'gateways':
|
|
6022
|
-
// 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.
|
|
6023
6844
|
if (resource.apiVersion?.includes('networking.istio.io')) {
|
|
6024
6845
|
return <IstioGatewayCell resource={resource} column={column} />
|
|
6025
6846
|
}
|
|
@@ -6125,6 +6946,90 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
6125
6946
|
return <NvidiaClusterPolicyCell resource={resource} column={column} />
|
|
6126
6947
|
case 'nvidiadrivers':
|
|
6127
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} />
|
|
6128
7033
|
// ArgoCD GitOps resources
|
|
6129
7034
|
case 'applications':
|
|
6130
7035
|
return <ArgoApplicationCell resource={resource} column={column} />
|
|
@@ -6425,53 +7330,13 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
6425
7330
|
function GenericCell({ resource, column }: { resource: any; column: string }) {
|
|
6426
7331
|
switch (column) {
|
|
6427
7332
|
case 'status': {
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
const isHealthy = ['Running', 'Active', 'Succeeded', 'Ready', 'Healthy', 'Available'].includes(phase)
|
|
6436
|
-
const isWarning = ['Pending', 'Progressing', 'Unknown'].includes(phase)
|
|
6437
|
-
return (
|
|
6438
|
-
<span className={clsx(
|
|
6439
|
-
'badge',
|
|
6440
|
-
isHealthy ? 'status-healthy' :
|
|
6441
|
-
isWarning ? 'status-degraded' :
|
|
6442
|
-
'status-unhealthy'
|
|
6443
|
-
)}>
|
|
6444
|
-
{phase}
|
|
6445
|
-
</span>
|
|
6446
|
-
)
|
|
6447
|
-
}
|
|
6448
|
-
|
|
6449
|
-
// Check for conditions (common pattern)
|
|
6450
|
-
if (status.conditions && Array.isArray(status.conditions)) {
|
|
6451
|
-
const readyCondition = status.conditions.find((c: any) => c.type === 'Ready' || c.type === 'Available')
|
|
6452
|
-
if (readyCondition) {
|
|
6453
|
-
const isReady = readyCondition.status === 'True'
|
|
6454
|
-
return (
|
|
6455
|
-
<span className={clsx(
|
|
6456
|
-
'badge',
|
|
6457
|
-
isReady ? 'status-healthy' : 'status-degraded'
|
|
6458
|
-
)}>
|
|
6459
|
-
{isReady ? 'Ready' : 'Not Ready'}
|
|
6460
|
-
</span>
|
|
6461
|
-
)
|
|
6462
|
-
}
|
|
6463
|
-
}
|
|
6464
|
-
|
|
6465
|
-
// Check for state field
|
|
6466
|
-
if (status.state) {
|
|
6467
|
-
return (
|
|
6468
|
-
<span className="text-sm text-theme-text-secondary truncate">
|
|
6469
|
-
{String(status.state)}
|
|
6470
|
-
</span>
|
|
6471
|
-
)
|
|
6472
|
-
}
|
|
6473
|
-
|
|
6474
|
-
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
|
+
)
|
|
6475
7340
|
}
|
|
6476
7341
|
default:
|
|
6477
7342
|
return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
@@ -6760,15 +7625,25 @@ function PodCell({ resource, column }: { resource: any; column: string }) {
|
|
|
6760
7625
|
}
|
|
6761
7626
|
}
|
|
6762
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
|
+
|
|
6763
7636
|
function WorkloadCell({ resource, column, kind }: { resource: any; kind: string; column: string }) {
|
|
6764
7637
|
const status = resource.status || {}
|
|
6765
7638
|
const spec = resource.spec || {}
|
|
6766
7639
|
|
|
6767
7640
|
switch (column) {
|
|
7641
|
+
case 'status':
|
|
7642
|
+
return <WorkloadStatusCell resource={resource} kind={kind} />
|
|
6768
7643
|
case 'ready': {
|
|
6769
|
-
const desired = spec.replicas ??
|
|
7644
|
+
const desired = spec.replicas ?? 1
|
|
6770
7645
|
const ready = status.readyReplicas || 0
|
|
6771
|
-
const allReady = ready
|
|
7646
|
+
const allReady = ready >= desired && desired > 0
|
|
6772
7647
|
const problems = getWorkloadProblems(resource, kind)
|
|
6773
7648
|
return (
|
|
6774
7649
|
<div className="flex items-center gap-1.5">
|
|
@@ -6776,7 +7651,7 @@ function WorkloadCell({ resource, column, kind }: { resource: any; kind: string;
|
|
|
6776
7651
|
'text-sm font-medium',
|
|
6777
7652
|
desired === 0 ? 'text-theme-text-secondary' : allReady ? 'text-green-400' : ready > 0 ? 'text-yellow-400' : 'text-red-400'
|
|
6778
7653
|
)}>
|
|
6779
|
-
{ready}/{desired}
|
|
7654
|
+
{Math.min(ready, desired)}/{desired}
|
|
6780
7655
|
</span>
|
|
6781
7656
|
{problems.length > 0 && (
|
|
6782
7657
|
<Tooltip content={
|
|
@@ -6839,12 +7714,14 @@ function DaemonSetCell({ resource, column }: { resource: any; column: string })
|
|
|
6839
7714
|
const status = resource.status || {}
|
|
6840
7715
|
|
|
6841
7716
|
switch (column) {
|
|
7717
|
+
case 'status':
|
|
7718
|
+
return <WorkloadStatusCell resource={resource} kind="daemonsets" />
|
|
6842
7719
|
case 'desired':
|
|
6843
7720
|
return <span className="text-sm text-theme-text-secondary">{status.desiredNumberScheduled || 0}</span>
|
|
6844
7721
|
case 'ready': {
|
|
6845
7722
|
const desired = status.desiredNumberScheduled || 0
|
|
6846
7723
|
const ready = status.numberReady || 0
|
|
6847
|
-
const allReady = ready
|
|
7724
|
+
const allReady = ready >= desired && desired > 0
|
|
6848
7725
|
const problems = getWorkloadProblems(resource, 'daemonsets')
|
|
6849
7726
|
return (
|
|
6850
7727
|
<div className="flex items-center gap-1.5">
|
|
@@ -6852,7 +7729,7 @@ function DaemonSetCell({ resource, column }: { resource: any; column: string })
|
|
|
6852
7729
|
'text-sm font-medium',
|
|
6853
7730
|
allReady ? 'text-green-400' : ready > 0 ? 'text-yellow-400' : 'text-red-400'
|
|
6854
7731
|
)}>
|
|
6855
|
-
{ready}
|
|
7732
|
+
{Math.min(ready, desired)}
|
|
6856
7733
|
</span>
|
|
6857
7734
|
{problems.length > 0 && (
|
|
6858
7735
|
<Tooltip content={
|
|
@@ -6900,7 +7777,7 @@ function ReplicaSetCell({ resource, column }: { resource: any; column: string })
|
|
|
6900
7777
|
|
|
6901
7778
|
switch (column) {
|
|
6902
7779
|
case 'ready': {
|
|
6903
|
-
const desired = spec.replicas ??
|
|
7780
|
+
const desired = spec.replicas ?? 1
|
|
6904
7781
|
const ready = status.readyReplicas || 0
|
|
6905
7782
|
const allReady = ready === desired && desired > 0
|
|
6906
7783
|
return (
|
|
@@ -7518,8 +8395,10 @@ function NodeCell({ resource, column, majorityNodeMinorVersion }: { resource: an
|
|
|
7518
8395
|
const allocatable = resource.status?.allocatable?.pods
|
|
7519
8396
|
const podCount = m?.podCount ?? 0
|
|
7520
8397
|
if (!allocatable) return <span className="text-sm text-theme-text-tertiary font-mono">{podCount || '-'}</span>
|
|
7521
|
-
|
|
7522
|
-
|
|
8398
|
+
// Allocatable pods is a Quantity, so a 1000-pod node reports "1k" —
|
|
8399
|
+
// a raw-integer read would see 1 and peg the bar at 21600%.
|
|
8400
|
+
const max = parseQuantityToNumber(allocatable)
|
|
8401
|
+
if (!Number.isFinite(max) || max <= 0) return <span className="text-sm text-theme-text-tertiary font-mono">{podCount || '-'}</span>
|
|
7523
8402
|
const pct = (podCount / max) * 100
|
|
7524
8403
|
return <ResourceBar used={String(podCount)} total={String(max)} percent={pct} colorScheme="count" />
|
|
7525
8404
|
}
|
|
@@ -7566,14 +8445,8 @@ function PVCCell({ resource, column }: { resource: any; column: string }) {
|
|
|
7566
8445
|
|
|
7567
8446
|
function RolloutCell({ resource, column }: { resource: any; column: string }) {
|
|
7568
8447
|
switch (column) {
|
|
7569
|
-
case 'status':
|
|
7570
|
-
|
|
7571
|
-
return (
|
|
7572
|
-
<span className={clsx('badge', status.color)}>
|
|
7573
|
-
{status.text}
|
|
7574
|
-
</span>
|
|
7575
|
-
)
|
|
7576
|
-
}
|
|
8448
|
+
case 'status':
|
|
8449
|
+
return <WorkloadStatusCell resource={resource} kind="rollouts" />
|
|
7577
8450
|
case 'ready': {
|
|
7578
8451
|
const ready = getRolloutReady(resource)
|
|
7579
8452
|
const parts = ready.split('/')
|