@skyhook-io/k8s-ui 1.13.3 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/package.json +4 -4
  2. package/src/components/applications/AppTooltips.tsx +13 -1
  3. package/src/components/applications/ApplicationDetail.test.tsx +25 -0
  4. package/src/components/applications/ApplicationDetail.tsx +39 -7
  5. package/src/components/applications/ApplicationsView.tsx +18 -3
  6. package/src/components/applications/ReadyBar.tsx +3 -2
  7. package/src/components/applications/applications-view.test.tsx +91 -0
  8. package/src/components/cluster-switcher/ClusterSwitcher.tsx +4 -1
  9. package/src/components/issues/IssuesView.tsx +40 -31
  10. package/src/components/issues/ResourceIssuesSection.tsx +3 -2
  11. package/src/components/issues/index.ts +3 -1
  12. package/src/components/issues/issue-timing.ts +123 -4
  13. package/src/components/issues/issues.test.ts +221 -10
  14. package/src/components/issues/types.ts +31 -5
  15. package/src/components/namespace-switcher/NamespacePicker.tsx +21 -13
  16. package/src/components/resources/ResourcesView.tsx +966 -98
  17. package/src/components/resources/column-settings-key.test.ts +98 -0
  18. package/src/components/resources/column-visibility-merge.test.ts +64 -0
  19. package/src/components/resources/curated-column-ownership.test.ts +236 -0
  20. package/src/components/resources/gateway-policy-status.test.ts +291 -0
  21. package/src/components/resources/gateway-policy-status.ts +237 -0
  22. package/src/components/resources/generic-status.test.ts +199 -0
  23. package/src/components/resources/generic-status.ts +171 -0
  24. package/src/components/resources/gpu-ecosystem-registry.test.ts +63 -0
  25. package/src/components/resources/index.ts +9 -1
  26. package/src/components/resources/istio-gateway-filter.test.ts +97 -0
  27. package/src/components/resources/kyverno-legacy-action.test.ts +100 -1
  28. package/src/components/resources/kyverno-modern-posture.test.ts +36 -0
  29. package/src/components/resources/printer-columns.test.ts +298 -0
  30. package/src/components/resources/printer-columns.ts +249 -0
  31. package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +29 -0
  32. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +3 -2
  33. package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +69 -3
  34. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +6 -2
  35. package/src/components/resources/renderers/EventRenderer.test.tsx +50 -1
  36. package/src/components/resources/renderers/EventRenderer.tsx +3 -5
  37. package/src/components/resources/renderers/GenericRenderer.tsx +18 -83
  38. package/src/components/resources/renderers/HPARenderer.test.tsx +28 -0
  39. package/src/components/resources/renderers/HPARenderer.tsx +6 -1
  40. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +12 -7
  41. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +2 -1
  42. package/src/components/resources/renderers/PVCRenderer.test.tsx +44 -0
  43. package/src/components/resources/renderers/PVCRenderer.tsx +19 -2
  44. package/src/components/resources/renderers/PolicyCoverageSection.test.tsx +92 -1
  45. package/src/components/resources/renderers/PolicyCoverageSection.tsx +64 -8
  46. package/src/components/resources/renderers/RolloutRenderer.test.tsx +35 -0
  47. package/src/components/resources/renderers/RolloutRenderer.tsx +22 -3
  48. package/src/components/resources/renderers/VeleroBSLRenderer.test.tsx +47 -0
  49. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +16 -1
  50. package/src/components/resources/renderers/VeleroRestoreRenderer.test.tsx +88 -0
  51. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +44 -2
  52. package/src/components/resources/renderers/WorkloadRenderer.test.tsx +83 -0
  53. package/src/components/resources/renderers/WorkloadRenderer.tsx +25 -65
  54. package/src/components/resources/renderers/amd-gpu-cells.tsx +36 -0
  55. package/src/components/resources/renderers/cnpg-cells.tsx +12 -1
  56. package/src/components/resources/renderers/inference-gateway-cells.tsx +62 -0
  57. package/src/components/resources/renderers/jobset-lws-cells.tsx +82 -0
  58. package/src/components/resources/renderers/kai-cells.tsx +67 -0
  59. package/src/components/resources/renderers/kaito-cells.tsx +62 -0
  60. package/src/components/resources/renderers/kserve-cells.tsx +145 -0
  61. package/src/components/resources/renderers/kubeflow-training-cells.tsx +72 -0
  62. package/src/components/resources/renderers/kueue-cells.tsx +176 -0
  63. package/src/components/resources/renderers/kyverno-cells.tsx +8 -4
  64. package/src/components/resources/renderers/nim-cells.tsx +81 -0
  65. package/src/components/resources/renderers/ray-cells.tsx +124 -0
  66. package/src/components/resources/renderers/volcano-cells.tsx +140 -0
  67. package/src/components/resources/resource-utils-amd-gpu.ts +61 -0
  68. package/src/components/resources/resource-utils-cnpg.golden.test.ts +13 -5
  69. package/src/components/resources/resource-utils-cnpg.test.ts +142 -2
  70. package/src/components/resources/resource-utils-cnpg.ts +132 -7
  71. package/src/components/resources/resource-utils-gpu-ecosystem.test.ts +236 -0
  72. package/src/components/resources/resource-utils-hpa.test.ts +4 -0
  73. package/src/components/resources/resource-utils-hpa.ts +6 -2
  74. package/src/components/resources/resource-utils-inference-gateway.ts +119 -0
  75. package/src/components/resources/resource-utils-jobset-lws.ts +117 -0
  76. package/src/components/resources/resource-utils-kai.ts +105 -0
  77. package/src/components/resources/resource-utils-kaito.ts +114 -0
  78. package/src/components/resources/resource-utils-kserve.ts +186 -0
  79. package/src/components/resources/resource-utils-kubeflow-training.ts +121 -0
  80. package/src/components/resources/resource-utils-kueue.ts +200 -0
  81. package/src/components/resources/resource-utils-kyverno-modern.ts +3 -1
  82. package/src/components/resources/resource-utils-kyverno.ts +51 -2
  83. package/src/components/resources/resource-utils-nim.ts +109 -0
  84. package/src/components/resources/resource-utils-ray.ts +214 -0
  85. package/src/components/resources/resource-utils-rollout.test.ts +183 -0
  86. package/src/components/resources/resource-utils-velero.test.ts +30 -0
  87. package/src/components/resources/resource-utils-velero.ts +32 -0
  88. package/src/components/resources/resource-utils-volcano.ts +176 -0
  89. package/src/components/resources/resource-utils.ts +156 -47
  90. package/src/components/resources/resources-column-filter.test.ts +27 -1
  91. package/src/components/resources/workload-rollout-golden.test.ts +55 -0
  92. package/src/components/shared/CreateResourceDialog.tsx +1 -0
  93. package/src/components/shared/EditableYamlView.tsx +4 -2
  94. package/src/components/shared/ResourceActionsBar.tsx +170 -25
  95. package/src/components/shared/ResourceRendererDispatch.test.tsx +206 -1
  96. package/src/components/shared/ResourceRendererDispatch.tsx +84 -40
  97. package/src/components/shared/SetImageDialog.test.ts +129 -0
  98. package/src/components/shared/SetImageDialog.tsx +577 -0
  99. package/src/components/shared/index.ts +1 -0
  100. package/src/components/shared/revision-role-badges.test.ts +56 -4
  101. package/src/components/timeline/TimelineList.tsx +19 -3
  102. package/src/components/timeline/TimelineSwimlanes.tsx +11 -3
  103. package/src/components/trace/ReachabilityView.tsx +1 -1
  104. package/src/components/ui/SelectMenu.tsx +10 -1
  105. package/src/components/ui/drawer-components.tsx +12 -0
  106. package/src/components/workload/WorkloadRolloutNotice.tsx +60 -0
  107. package/src/components/workload/WorkloadView.tsx +144 -81
  108. package/src/hooks/useProgressiveRefresh.ts +37 -0
  109. package/src/source-hygiene.test.ts +28 -0
  110. package/src/theme/tailwind-theme.css +4 -0
  111. package/src/types/core.ts +55 -0
  112. package/src/utils/api-resources.test.ts +40 -1
  113. package/src/utils/api-resources.ts +11 -1
  114. package/src/utils/applications.ts +50 -0
  115. package/src/utils/bulk-workload-actions.test.ts +20 -0
  116. package/src/utils/bulk-workload-actions.ts +15 -0
  117. package/src/utils/navigation.test.ts +31 -1
  118. package/src/utils/navigation.ts +21 -2
  119. package/src/utils/workload-rollout.ts +292 -0
@@ -1,7 +1,10 @@
1
1
  import { clsx } from 'clsx'
2
- import { SEVERITY_BADGE } from '../../utils/badge-colors'
2
+ import { SEVERITY_BADGE, HEALTH_BADGE_COLORS } from '../../utils/badge-colors'
3
+ import { isArgoRolloutResource } from '../../utils/workload-rollout'
4
+ import { getGenericResourceStatus } from '../resources/generic-status'
3
5
  import {
4
6
  getPodStatus,
7
+ getWorkloadDisplayStatus,
5
8
  getWorkloadStatus,
6
9
  getJobStatus,
7
10
  getCronJobStatus,
@@ -9,7 +12,6 @@ import {
9
12
  getServiceStatus,
10
13
  getNodeStatus,
11
14
  getPVCStatus,
12
- getRolloutStatus,
13
15
  getAnalysisRunStatus,
14
16
  getWorkflowStatus,
15
17
  getCronWorkflowStatus,
@@ -65,6 +67,17 @@ import {
65
67
  } from '../resources/resource-utils-kyverno-exceptions'
66
68
  import { getResourceClaimStatus, getResourceClaimTemplateStatus, getDeviceClassStatus, getResourceSliceStatus } from '../resources/resource-utils-dra'
67
69
  import { getNvidiaClusterPolicyStatus, getNvidiaDriverStatus } from '../resources/resource-utils-nvidia'
70
+ import { getClusterQueueStatus, getLocalQueueStatus, getKueueWorkloadStatus, getResourceFlavorStatus, getAdmissionCheckStatus, getProvisioningRequestStatus } from '../resources/resource-utils-kueue'
71
+ import { getRayClusterStatus, getRayJobStatus, getRayServiceStatus, getRayCronJobStatus } from '../resources/resource-utils-ray'
72
+ import { getLeaderWorkerSetStatus, getJobSetStatus } from '../resources/resource-utils-jobset-lws'
73
+ import { getInferenceServiceStatus, getServingRuntimeStatus, getInferenceGraphStatus, getTrainedModelStatus, getLLMInferenceServiceStatus } from '../resources/resource-utils-kserve'
74
+ import { getInferencePoolStatus, getInferenceObjectiveStatus } from '../resources/resource-utils-inference-gateway'
75
+ import { getVolcanoJobStatus, getVolcanoQueueStatus, getVolcanoPodGroupStatus, getJobFlowStatus, getJobTemplateStatus } from '../resources/resource-utils-volcano'
76
+ import { getKaiQueueStatus, getKaiPodGroupStatus } from '../resources/resource-utils-kai'
77
+ import { getKaitoWorkspaceStatus, getRAGEngineStatus } from '../resources/resource-utils-kaito'
78
+ import { getNIMServiceStatus, getNIMCacheStatus, getNIMPipelineStatus } from '../resources/resource-utils-nim'
79
+ import { getAMDDeviceConfigStatus } from '../resources/resource-utils-amd-gpu'
80
+ import { getPyTorchJobStatus, getTFJobStatus, getMPIJobStatus, getTrainJobStatus } from '../resources/resource-utils-kubeflow-training'
68
81
  import { getBackupStatus, getRestoreStatus, getScheduleStatus, getBSLStatus, getBackupRepositoryStatus, isVeleroResource } from '../resources/resource-utils-velero'
69
82
  import {
70
83
  getVirtualServiceStatus,
@@ -272,7 +285,7 @@ import {
272
285
  isComposite,
273
286
  isClaim,
274
287
  } from '../resources/resource-utils-crossplane'
275
- import type { SelectedResource, Relationships, ResourceRef, SecretCertificateInfo, ResolvedEnvFrom, TimelineEvent, HPADiagnosis } from '../../types'
288
+ import type { SelectedResource, Relationships, ResourceRef, SecretCertificateInfo, ResolvedEnvFrom, TimelineEvent, HPADiagnosis, WorkloadPodInfo } from '../../types'
276
289
  import type { CopyHandler } from '../ui/drawer-components'
277
290
  import { AlertBanner } from '../ui/drawer-components'
278
291
  import { replicaScalers } from '../../utils/replica-scalers'
@@ -310,6 +323,7 @@ export interface RendererOverrides {
310
323
  relationships?: Relationships
311
324
  scaleBlockedBy?: ResourceRef[]
312
325
  scalerDiagnostics?: ScalerDiagnosis[]
326
+ workloadPods?: WorkloadPodInfo[]
313
327
  }>
314
328
  // Optional override for Crossplane Composite / Claim — host wraps the
315
329
  // package renderer to fan out per-composed-ref status fetches via React Query.
@@ -500,6 +514,7 @@ interface ResourceRendererDispatchProps {
500
514
  certificateInfo?: SecretCertificateInfo
501
515
  hpaDiagnosis?: HPADiagnosis
502
516
  scalerDiagnostics?: ScalerDiagnosis[]
517
+ workloadPods?: WorkloadPodInfo[]
503
518
  onCopy: (text: string, key: string) => void
504
519
  copied: string | null
505
520
  onNavigate?: (ref: ResourceRef) => void
@@ -545,6 +560,7 @@ export function ResourceRendererDispatch({
545
560
  certificateInfo,
546
561
  hpaDiagnosis,
547
562
  scalerDiagnostics,
563
+ workloadPods,
548
564
  onCopy,
549
565
  copied,
550
566
  onNavigate,
@@ -646,7 +662,7 @@ export function ResourceRendererDispatch({
646
662
  kind === 'clusters' || kind === 'backups' || kind === 'scheduledbackups' || kind === 'poolers'
647
663
  || kind === 'objectstores' || kind === 'databases' || kind === 'publications'
648
664
  || kind === 'subscriptions' || kind === 'imagecatalogs' || kind === 'clusterimagecatalogs'
649
- || kind === 'policies'
665
+ || kind === 'policies' || kind === 'rollouts'
650
666
  const isCNPGApiVersion = isApiGroup(data?.apiVersion, CNPG_GROUP)
651
667
  const groupGatedMatched =
652
668
  (kind === 'clusters' && (isCNPGApiVersion || isApiGroup(data?.apiVersion, 'cluster.x-k8s.io')))
@@ -658,6 +674,7 @@ export function ResourceRendererDispatch({
658
674
  || (kind === 'subscriptions'
659
675
  && (isCNPGApiVersion || isApiGroup(data?.apiVersion, 'messaging.knative.dev')))
660
676
  || (kind === 'policies' && isApiGroup(data?.apiVersion, 'kyverno.io'))
677
+ || (kind === 'rollouts' && isArgoRolloutResource(data))
661
678
  const groupGatedFallthrough = isGroupGatedKind && !groupGatedMatched
662
679
 
663
680
  const calicoApiVersionMatched = isCalicoApiVersion(data?.apiVersion)
@@ -672,6 +689,10 @@ export function ResourceRendererDispatch({
672
689
  isCalicoPolicyKind(kind) || kind === 'hostendpoints' || kind === 'ippools' || kind === 'tiers'
673
690
  ) && !isCoreNetworkPolicy && !((isCalicoPolicy || isCalicoHostEndpoint || isCalicoIPPool || isCalicoTier))
674
691
 
692
+ const nonCoreJobFallthrough = kind === 'jobs'
693
+ && !!data?.apiVersion
694
+ && !data.apiVersion.startsWith('batch/')
695
+
675
696
  const isKnownKind = KNOWN_KINDS.has(kind) || isCrossplaneMR || isCrossplaneClaim || isCrossplaneXR
676
697
 
677
698
  const PodComp = rendererOverrides?.PodRenderer ?? PodRenderer
@@ -727,6 +748,7 @@ export function ResourceRendererDispatch({
727
748
  relationships={relationships}
728
749
  scaleBlockedBy={scaleBlockedBy}
729
750
  scalerDiagnostics={scalerDiagnostics}
751
+ workloadPods={workloadPods}
730
752
  />
731
753
  )}
732
754
  {kind === 'replicasets' && <ReplicaSetRenderer data={data} />}
@@ -735,13 +757,13 @@ export function ResourceRendererDispatch({
735
757
  {kind === 'ingresses' && !data?.apiVersion?.includes('networking.internal.knative.dev') && <IngressRenderer data={data} onNavigate={onNavigate} />}
736
758
  {kind === 'configmaps' && <ConfigMapRenderer data={data} />}
737
759
  {kind === 'secrets' && <SecretRenderer data={data} certificateInfo={certificateInfo} resourceData={data} onSaveSecretValue={onSaveSecretValue} isSaving={isSavingSecret} />}
738
- {kind === 'jobs' && <JobRenderer data={data} />}
760
+ {kind === 'jobs' && !nonCoreJobFallthrough && <JobRenderer data={data} />}
739
761
  {kind === 'cronjobs' && <CronJobRenderer data={data} onNavigate={onNavigate} />}
740
762
  {kind === 'cronworkflows' && <CronWorkflowRenderer data={data} onNavigate={onNavigate} />}
741
763
  {(kind === 'hpas' || kind === 'horizontalpodautoscalers') && <HPAComp data={data} onNavigate={onNavigate} hpaDiagnosis={hpaDiagnosis} />}
742
764
  {kind === 'nodes' && <NodeComp data={data} relationships={relationships} />}
743
765
  {kind === 'persistentvolumeclaims' && <PVCComp data={data} onNavigate={onNavigate} />}
744
- {kind === 'rollouts' && <RolloutComp data={data} onNavigate={onNavigate} />}
766
+ {kind === 'rollouts' && isArgoRolloutResource(data) && <RolloutComp data={data} onNavigate={onNavigate} />}
745
767
  {kind === 'analysisruns' && <AnalysisRunRenderer data={data} onNavigate={onNavigate} />}
746
768
  {kind === 'certificates' && !data?.apiVersion?.includes('networking.internal.knative.dev') && <CertificateRenderer data={data} />}
747
769
  {kind === 'workflows' && <WorkflowRenderer data={data} onNavigate={onNavigate} />}
@@ -934,7 +956,7 @@ export function ResourceRendererDispatch({
934
956
  for known-plural collisions where no apiVersion-gated renderer
935
957
  matched (e.g. a Knative Configuration sharing the `configurations`
936
958
  plural with Crossplane Configuration). */}
937
- {(!isKnownKind || crossplaneCollisionFallthrough || kyvernoCollisionFallthrough || veleroCollisionFallthrough || groupGatedFallthrough || calicoCollisionFallthrough) && <GenericRenderer data={data} />}
959
+ {(!isKnownKind || crossplaneCollisionFallthrough || kyvernoCollisionFallthrough || veleroCollisionFallthrough || groupGatedFallthrough || calicoCollisionFallthrough || nonCoreJobFallthrough) && <GenericRenderer data={data} />}
938
960
 
939
961
  {/* Common sections - can be disabled when parent handles them separately */}
940
962
  {showCommonSections && (
@@ -988,7 +1010,10 @@ export function getResourceStatus(kind: string, data: any): { text: string; colo
988
1010
  const k = kind.toLowerCase()
989
1011
 
990
1012
  if (k === 'pods') return getPodStatus(data)
991
- if (['deployments', 'statefulsets', 'replicasets', 'daemonsets'].includes(k)) return getWorkloadStatus(data, k)
1013
+ if (['deployments', 'statefulsets', 'daemonsets', 'rollouts'].includes(k)) {
1014
+ return getWorkloadDisplayStatus(data, k).status
1015
+ }
1016
+ if (k === 'replicasets') return getWorkloadStatus(data, k)
992
1017
  if (k === 'services') {
993
1018
  if (data.apiVersion?.includes('serving.knative.dev')) {
994
1019
  const status = getKnativeConditionStatus(data)
@@ -1006,12 +1031,59 @@ export function getResourceStatus(kind: string, data: any): { text: string; colo
1006
1031
  SEVERITY_BADGE.error
1007
1032
  return { text, color }
1008
1033
  }
1009
- if (k === 'jobs') return getJobStatus(data)
1034
+ if (k === 'jobs') {
1035
+ if (data?.apiVersion?.startsWith('batch.volcano.sh/')) return getVolcanoJobStatus(data)
1036
+ if (!data?.apiVersion || data.apiVersion.startsWith('batch/')) return getJobStatus(data)
1037
+ }
1038
+ if (k === 'queues') {
1039
+ if (data?.apiVersion?.startsWith('scheduling.run.ai/')) return getKaiQueueStatus(data)
1040
+ if (data?.apiVersion?.startsWith('scheduling.volcano.sh/')) return getVolcanoQueueStatus(data)
1041
+ }
1042
+ if (k === 'podgroups') {
1043
+ if (data?.apiVersion?.startsWith('scheduling.run.ai/')) return getKaiPodGroupStatus(data)
1044
+ if (data?.apiVersion?.startsWith('scheduling.volcano.sh/')) return getVolcanoPodGroupStatus(data)
1045
+ }
1046
+ if (k === 'workspaces' && data?.apiVersion?.startsWith('kaito.sh/')) return getKaitoWorkspaceStatus(data)
1047
+ if (k === 'workloads' && data?.apiVersion?.startsWith('kueue.x-k8s.io/')) return getKueueWorkloadStatus(data)
1048
+ if (k === 'clusterqueues' && data?.apiVersion?.startsWith('kueue.x-k8s.io/')) return getClusterQueueStatus(data)
1049
+ if (k === 'localqueues' && data?.apiVersion?.startsWith('kueue.x-k8s.io/')) return getLocalQueueStatus(data)
1050
+ if (k === 'resourceflavors' && data?.apiVersion?.startsWith('kueue.x-k8s.io/')) return getResourceFlavorStatus(data)
1051
+ if (k === 'admissionchecks' && data?.apiVersion?.startsWith('kueue.x-k8s.io/')) return getAdmissionCheckStatus(data)
1052
+ if (k === 'provisioningrequests' && data?.apiVersion?.startsWith('autoscaling.x-k8s.io/')) return getProvisioningRequestStatus(data)
1053
+ if (k === 'rayclusters' && data?.apiVersion?.startsWith('ray.io/')) return getRayClusterStatus(data)
1054
+ if (k === 'rayjobs' && data?.apiVersion?.startsWith('ray.io/')) return getRayJobStatus(data)
1055
+ if (k === 'rayservices' && data?.apiVersion?.startsWith('ray.io/')) return getRayServiceStatus(data)
1056
+ if (k === 'raycronjobs' && data?.apiVersion?.startsWith('ray.io/')) return getRayCronJobStatus(data)
1057
+ if (k === 'leaderworkersets' && data?.apiVersion?.startsWith('leaderworkerset.x-k8s.io/')) return getLeaderWorkerSetStatus(data)
1058
+ if (k === 'jobsets' && data?.apiVersion?.startsWith('jobset.x-k8s.io/')) return getJobSetStatus(data)
1059
+ if (k === 'inferenceservices' && data?.apiVersion?.startsWith('serving.kserve.io/')) return getInferenceServiceStatus(data)
1060
+ if ((k === 'servingruntimes' || k === 'clusterservingruntimes') && data?.apiVersion?.startsWith('serving.kserve.io/')) return getServingRuntimeStatus(data)
1061
+ if (k === 'inferencegraphs' && data?.apiVersion?.startsWith('serving.kserve.io/')) return getInferenceGraphStatus(data)
1062
+ if (k === 'trainedmodels' && data?.apiVersion?.startsWith('serving.kserve.io/')) return getTrainedModelStatus(data)
1063
+ if (k === 'llminferenceservices' && data?.apiVersion?.startsWith('serving.kserve.io/')) return getLLMInferenceServiceStatus(data)
1064
+ if (k === 'inferencepools' && (
1065
+ data?.apiVersion?.startsWith('inference.networking.k8s.io/')
1066
+ || data?.apiVersion?.startsWith('inference.networking.x-k8s.io/')
1067
+ )) return getInferencePoolStatus(data)
1068
+ if (k === 'inferenceobjectives' && (
1069
+ data?.apiVersion?.startsWith('inference.networking.x-k8s.io/')
1070
+ || data?.apiVersion?.startsWith('llm-d.ai/')
1071
+ )) return getInferenceObjectiveStatus(data)
1072
+ if (k === 'jobflows' && data?.apiVersion?.startsWith('flow.volcano.sh/')) return getJobFlowStatus(data)
1073
+ if (k === 'jobtemplates' && data?.apiVersion?.startsWith('flow.volcano.sh/')) return getJobTemplateStatus(data)
1074
+ if (k === 'ragengines' && data?.apiVersion?.startsWith('kaito.sh/')) return getRAGEngineStatus(data)
1075
+ if (k === 'nimservices' && data?.apiVersion?.startsWith('apps.nvidia.com/')) return getNIMServiceStatus(data)
1076
+ if (k === 'nimcaches' && data?.apiVersion?.startsWith('apps.nvidia.com/')) return getNIMCacheStatus(data)
1077
+ if (k === 'nimpipelines' && data?.apiVersion?.startsWith('apps.nvidia.com/')) return getNIMPipelineStatus(data)
1078
+ if (k === 'deviceconfigs' && data?.apiVersion?.startsWith('amd.com/')) return getAMDDeviceConfigStatus(data)
1079
+ if (k === 'pytorchjobs' && data?.apiVersion?.startsWith('kubeflow.org/')) return getPyTorchJobStatus(data)
1080
+ if (k === 'tfjobs' && data?.apiVersion?.startsWith('kubeflow.org/')) return getTFJobStatus(data)
1081
+ if (k === 'mpijobs' && data?.apiVersion?.startsWith('kubeflow.org/')) return getMPIJobStatus(data)
1082
+ if (k === 'trainjobs' && data?.apiVersion?.startsWith('trainer.kubeflow.org/')) return getTrainJobStatus(data)
1010
1083
  if (k === 'cronjobs') return getCronJobStatus(data)
1011
1084
  if (k === 'hpas' || k === 'horizontalpodautoscalers') return getHPAStatus(data)
1012
1085
  if (k === 'nodes') return getNodeStatus(data)
1013
1086
  if (k === 'persistentvolumeclaims') return getPVCStatus(data)
1014
- if (k === 'rollouts') return getRolloutStatus(data)
1015
1087
  if (k === 'analysisruns') return getAnalysisRunStatus(data)
1016
1088
  if (k === 'workflows') return getWorkflowStatus(data)
1017
1089
  if (k === 'cronworkflows') return getCronWorkflowStatus(data)
@@ -1193,36 +1265,8 @@ export function getResourceStatus(kind: string, data: any): { text: string; colo
1193
1265
  return { text: status.text, color: status.color }
1194
1266
  }
1195
1267
 
1196
- // Generic status extraction
1197
- const status = data.status
1198
- if (status) {
1199
- if (status.phase) {
1200
- const phase = String(status.phase)
1201
- const healthyPhases = ['Running', 'Active', 'Succeeded', 'Ready', 'Healthy', 'Available', 'Bound']
1202
- const warningPhases = ['Pending', 'Progressing', 'Unknown', 'Terminating']
1203
- const isHealthy = healthyPhases.includes(phase)
1204
- const isWarning = warningPhases.includes(phase)
1205
- return {
1206
- text: phase,
1207
- color: isHealthy ? SEVERITY_BADGE.success :
1208
- isWarning ? SEVERITY_BADGE.warning :
1209
- SEVERITY_BADGE.error
1210
- }
1211
- }
1212
-
1213
- if (status.conditions && Array.isArray(status.conditions)) {
1214
- const readyCondition = status.conditions.find((c: any) =>
1215
- c.type === 'Ready' || c.type === 'Available' || c.type === 'Progressing'
1216
- )
1217
- if (readyCondition) {
1218
- const isReady = readyCondition.status === 'True'
1219
- return {
1220
- text: isReady ? 'Ready' : 'Not Ready',
1221
- color: isReady ? SEVERITY_BADGE.success : SEVERITY_BADGE.warning
1222
- }
1223
- }
1224
- }
1225
- }
1268
+ const derived = getGenericResourceStatus(data)
1269
+ if (derived) return { text: derived.text, color: HEALTH_BADGE_COLORS[derived.tone] }
1226
1270
 
1227
1271
  return null
1228
1272
  }
@@ -0,0 +1,129 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { WorkloadContainerImage } from '../../types/core'
4
+ import {
5
+ canSubmitImageUpdates,
6
+ changedImageUpdates,
7
+ describeImageUpdateBehavior,
8
+ reconcileRefreshedImageDrafts,
9
+ } from './SetImageDialog'
10
+
11
+ const containers: WorkloadContainerImage[] = [
12
+ { type: 'container', name: 'app', image: 'repo/app:v1' },
13
+ { type: 'container', name: 'sidecar', image: 'repo/sidecar:v1' },
14
+ { type: 'initContainer', name: 'migrate', image: 'repo/migrate:v1' },
15
+ ]
16
+
17
+ describe('changedImageUpdates', () => {
18
+ it('submits only changed rows with their reviewed previous images', () => {
19
+ expect(
20
+ changedImageUpdates(containers, {
21
+ ['container\u0000app']: 'repo/app:v2',
22
+ ['container\u0000sidecar']: 'repo/sidecar:v1',
23
+ ['initContainer\u0000migrate']: ' repo/migrate:v2 ',
24
+ }),
25
+ ).toEqual([
26
+ {
27
+ type: 'container',
28
+ name: 'app',
29
+ previousImage: 'repo/app:v1',
30
+ image: 'repo/app:v2',
31
+ },
32
+ {
33
+ type: 'initContainer',
34
+ name: 'migrate',
35
+ previousImage: 'repo/migrate:v1',
36
+ image: 'repo/migrate:v2',
37
+ },
38
+ ])
39
+ })
40
+
41
+ it('does not turn an empty draft into a mutation', () => {
42
+ expect(
43
+ changedImageUpdates(containers.slice(0, 1), {
44
+ ['container\u0000app']: ' ',
45
+ }),
46
+ ).toEqual([])
47
+ })
48
+ })
49
+
50
+ describe('describeImageUpdateBehavior', () => {
51
+ it('does not promise an immediate rollout for paused, OnDelete, or partitioned controllers', () => {
52
+ expect(describeImageUpdateBehavior({ type: 'paused' })).toContain(
53
+ 'no rollout starts',
54
+ )
55
+ expect(describeImageUpdateBehavior({ type: 'onDelete' })).toContain(
56
+ 'until they are deleted',
57
+ )
58
+ expect(
59
+ describeImageUpdateBehavior({ type: 'partitioned', partition: 3 }),
60
+ ).toContain('ordinal 3 or higher')
61
+ })
62
+
63
+ it('describes Argo promotion behavior from the Rollout strategy', () => {
64
+ expect(
65
+ describeImageUpdateBehavior({ type: 'canary', gated: true }),
66
+ ).toContain('configured steps')
67
+ expect(
68
+ describeImageUpdateBehavior({ type: 'canary', gated: false }),
69
+ ).toContain('100% without a pause')
70
+ expect(
71
+ describeImageUpdateBehavior({ type: 'blueGreen', autoPromote: true }),
72
+ ).toContain('automatically')
73
+ expect(
74
+ describeImageUpdateBehavior({ type: 'blueGreen', autoPromote: false }),
75
+ ).toContain('Promotion remains a separate action')
76
+ })
77
+ })
78
+
79
+ describe('reconcileRefreshedImageDrafts', () => {
80
+ it('preserves proposed images while adopting untouched concurrent changes', () => {
81
+ expect(
82
+ reconcileRefreshedImageDrafts(
83
+ containers,
84
+ [
85
+ { type: 'container', name: 'app', image: 'repo/app:other' },
86
+ { type: 'container', name: 'sidecar', image: 'repo/sidecar:other' },
87
+ { type: 'initContainer', name: 'migrate', image: 'repo/migrate:v1' },
88
+ ],
89
+ {
90
+ ['container\u0000app']: 'repo/app:proposed',
91
+ ['container\u0000sidecar']: 'repo/sidecar:v1',
92
+ ['initContainer\u0000migrate']: 'repo/migrate:v1',
93
+ },
94
+ ),
95
+ ).toEqual({
96
+ drafts: {
97
+ ['container\u0000app']: 'repo/app:proposed',
98
+ ['container\u0000sidecar']: 'repo/sidecar:other',
99
+ ['initContainer\u0000migrate']: 'repo/migrate:v1',
100
+ },
101
+ changedCurrentKeys: ['container\u0000app', 'container\u0000sidecar'],
102
+ })
103
+ })
104
+ })
105
+
106
+ describe('canSubmitImageUpdates', () => {
107
+ const ready = {
108
+ updateCount: 1,
109
+ hasEmptyImage: false,
110
+ managed: false,
111
+ ownershipResolved: true,
112
+ acknowledged: false,
113
+ busy: false,
114
+ }
115
+
116
+ it('requires break-glass acknowledgement for managed resources', () => {
117
+ expect(canSubmitImageUpdates({ ...ready, managed: true })).toBe(false)
118
+ expect(
119
+ canSubmitImageUpdates({ ...ready, managed: true, acknowledged: true }),
120
+ ).toBe(true)
121
+ })
122
+
123
+ it('blocks empty images and concurrent submissions', () => {
124
+ expect(canSubmitImageUpdates({ ...ready, hasEmptyImage: true })).toBe(false)
125
+ expect(canSubmitImageUpdates({ ...ready, busy: true })).toBe(false)
126
+ expect(canSubmitImageUpdates({ ...ready, loadFailed: true })).toBe(false)
127
+ expect(canSubmitImageUpdates({ ...ready, ownershipResolved: false })).toBe(false)
128
+ })
129
+ })