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