@skyhook-io/k8s-ui 1.13.0 → 1.13.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/components/applications/AppTooltips.tsx +13 -1
- package/src/components/applications/ApplicationDetail.test.tsx +25 -0
- package/src/components/applications/ApplicationDetail.tsx +41 -9
- package/src/components/applications/ApplicationsView.tsx +18 -3
- package/src/components/applications/ReadyBar.tsx +3 -2
- package/src/components/applications/applications-view.test.tsx +91 -0
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +35 -7
- package/src/components/issues/IssuesView.tsx +40 -31
- package/src/components/issues/ResourceIssuesSection.tsx +3 -2
- package/src/components/issues/index.ts +3 -1
- package/src/components/issues/issue-timing.ts +123 -4
- package/src/components/issues/issues.test.ts +221 -10
- package/src/components/issues/types.ts +31 -5
- package/src/components/namespace-switcher/NamespacePicker.tsx +21 -13
- package/src/components/resources/ResourcesView.tsx +973 -100
- package/src/components/resources/column-settings-key.test.ts +98 -0
- package/src/components/resources/column-visibility-merge.test.ts +64 -0
- package/src/components/resources/curated-column-ownership.test.ts +236 -0
- package/src/components/resources/gateway-policy-status.test.ts +291 -0
- package/src/components/resources/gateway-policy-status.ts +237 -0
- package/src/components/resources/generic-status.test.ts +199 -0
- package/src/components/resources/generic-status.ts +171 -0
- package/src/components/resources/gpu-ecosystem-registry.test.ts +63 -0
- package/src/components/resources/index.ts +9 -1
- package/src/components/resources/istio-gateway-filter.test.ts +97 -0
- package/src/components/resources/printer-columns.test.ts +298 -0
- package/src/components/resources/printer-columns.ts +249 -0
- package/src/components/resources/renderers/EventRenderer.test.tsx +50 -1
- package/src/components/resources/renderers/EventRenderer.tsx +3 -5
- package/src/components/resources/renderers/GenericRenderer.tsx +18 -83
- package/src/components/resources/renderers/HPARenderer.test.tsx +28 -0
- package/src/components/resources/renderers/HPARenderer.tsx +6 -1
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +2 -1
- package/src/components/resources/renderers/NamespaceRenderer.test.ts +30 -0
- package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -3
- package/src/components/resources/renderers/PVCRenderer.test.tsx +44 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +19 -2
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +35 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +22 -3
- package/src/components/resources/renderers/WorkloadRenderer.test.tsx +83 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +25 -65
- package/src/components/resources/renderers/amd-gpu-cells.tsx +36 -0
- package/src/components/resources/renderers/inference-gateway-cells.tsx +62 -0
- package/src/components/resources/renderers/jobset-lws-cells.tsx +82 -0
- package/src/components/resources/renderers/kai-cells.tsx +67 -0
- package/src/components/resources/renderers/kaito-cells.tsx +62 -0
- package/src/components/resources/renderers/kserve-cells.tsx +145 -0
- package/src/components/resources/renderers/kubeflow-training-cells.tsx +72 -0
- package/src/components/resources/renderers/kueue-cells.tsx +176 -0
- package/src/components/resources/renderers/nim-cells.tsx +81 -0
- package/src/components/resources/renderers/ray-cells.tsx +124 -0
- package/src/components/resources/renderers/volcano-cells.tsx +140 -0
- package/src/components/resources/resource-utils-amd-gpu.ts +61 -0
- package/src/components/resources/resource-utils-gpu-ecosystem.test.ts +222 -0
- package/src/components/resources/resource-utils-hpa.test.ts +4 -0
- package/src/components/resources/resource-utils-hpa.ts +6 -2
- package/src/components/resources/resource-utils-inference-gateway.ts +119 -0
- package/src/components/resources/resource-utils-jobset-lws.ts +117 -0
- package/src/components/resources/resource-utils-kai.ts +105 -0
- package/src/components/resources/resource-utils-kaito.ts +114 -0
- package/src/components/resources/resource-utils-kserve.ts +186 -0
- package/src/components/resources/resource-utils-kubeflow-training.ts +121 -0
- package/src/components/resources/resource-utils-kueue.ts +198 -0
- package/src/components/resources/resource-utils-nim.ts +109 -0
- package/src/components/resources/resource-utils-ray.ts +214 -0
- package/src/components/resources/resource-utils-rollout.test.ts +183 -0
- package/src/components/resources/resource-utils-velero.test.ts +30 -0
- package/src/components/resources/resource-utils-volcano.ts +176 -0
- package/src/components/resources/resource-utils.ts +156 -47
- package/src/components/resources/resources-column-filter.test.ts +27 -1
- package/src/components/resources/workload-rollout-golden.test.ts +55 -0
- package/src/components/shared/CreateResourceDialog.tsx +1 -0
- package/src/components/shared/EditableYamlView.tsx +4 -2
- package/src/components/shared/ResourceActionsBar.tsx +170 -25
- package/src/components/shared/ResourceRendererDispatch.test.tsx +206 -1
- package/src/components/shared/ResourceRendererDispatch.tsx +84 -40
- package/src/components/shared/SetImageDialog.test.ts +129 -0
- package/src/components/shared/SetImageDialog.tsx +577 -0
- package/src/components/shared/index.ts +1 -0
- package/src/components/shared/revision-role-badges.test.ts +56 -4
- package/src/components/timeline/TimelineList.tsx +19 -3
- package/src/components/timeline/TimelineSwimlanes.tsx +11 -3
- package/src/components/trace/ReachabilityView.tsx +1 -1
- package/src/components/trace/TracePanel.tsx +1 -0
- package/src/components/ui/InClusterConsentDialog.tsx +5 -4
- package/src/components/ui/SelectMenu.tsx +202 -28
- package/src/components/ui/drawer-components.tsx +12 -0
- package/src/components/workload/WorkloadRolloutNotice.tsx +60 -0
- package/src/components/workload/WorkloadView.tsx +146 -83
- package/src/hooks/useProgressiveRefresh.ts +37 -0
- package/src/source-hygiene.test.ts +28 -0
- package/src/theme/tailwind-theme.css +4 -0
- package/src/types/core.ts +62 -4
- package/src/utils/api-resources.test.ts +40 -1
- package/src/utils/api-resources.ts +11 -1
- package/src/utils/applications.ts +50 -0
- package/src/utils/bulk-workload-actions.test.ts +20 -0
- package/src/utils/bulk-workload-actions.ts +15 -0
- package/src/utils/clipboard.test.ts +86 -0
- package/src/utils/clipboard.ts +50 -0
- package/src/utils/extended-resources.ts +4 -7
- package/src/utils/format.test.ts +83 -1
- package/src/utils/format.ts +80 -1
- package/src/utils/inClusterConsent.test.ts +9 -0
- package/src/utils/inClusterConsent.ts +9 -9
- package/src/utils/index.ts +1 -0
- package/src/utils/navigation.test.ts +31 -1
- package/src/utils/navigation.ts +21 -2
- package/src/utils/workload-rollout.ts +292 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// NVIDIA NIM Operator CRD utility functions
|
|
2
|
+
|
|
3
|
+
import type { StatusBadge } from './resource-utils'
|
|
4
|
+
import { healthColors } from './resource-utils'
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// SHARED HELPERS
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
function stateToBadge(state: string | undefined): StatusBadge {
|
|
11
|
+
switch (state) {
|
|
12
|
+
case 'Ready':
|
|
13
|
+
return { text: 'Ready', color: healthColors.healthy, level: 'healthy' }
|
|
14
|
+
case 'Failed':
|
|
15
|
+
return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
16
|
+
case 'NotReady':
|
|
17
|
+
case 'Pending':
|
|
18
|
+
case 'InProgress':
|
|
19
|
+
case 'Started':
|
|
20
|
+
case 'PVC-Created':
|
|
21
|
+
return { text: state, color: healthColors.degraded, level: 'degraded' }
|
|
22
|
+
default:
|
|
23
|
+
return { text: state || 'Unknown', color: healthColors.unknown, level: 'unknown' }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// NIMSERVICE UTILITIES
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
export function getNIMServiceStatus(resource: any): StatusBadge {
|
|
32
|
+
return stateToBadge(resource.status?.state)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getNIMServiceImage(resource: any): string {
|
|
36
|
+
const image = resource.spec?.image
|
|
37
|
+
if (!image?.repository) return '-'
|
|
38
|
+
return image.tag ? `${image.repository}:${image.tag}` : image.repository
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getNIMServiceModel(resource: any): string {
|
|
42
|
+
const modelName = resource.status?.model?.name
|
|
43
|
+
if (modelName) return modelName
|
|
44
|
+
return getNIMServiceImage(resource)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getNIMServiceReplicas(resource: any): string {
|
|
48
|
+
const available = resource.status?.availableReplicas ?? 0
|
|
49
|
+
const scale = resource.spec?.scale
|
|
50
|
+
if (scale?.enabled) {
|
|
51
|
+
const min = scale.hpa?.minReplicas
|
|
52
|
+
const max = scale.hpa?.maxReplicas
|
|
53
|
+
if (min !== undefined && max !== undefined) return `${available} (${min}-${max})`
|
|
54
|
+
return `${available} (hpa)`
|
|
55
|
+
}
|
|
56
|
+
const desired = resource.spec?.replicas
|
|
57
|
+
if (desired !== undefined && desired !== null) return `${available}/${desired}`
|
|
58
|
+
return `${available}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// NIMCACHE UTILITIES
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
export function getNIMCacheStatus(resource: any): StatusBadge {
|
|
66
|
+
return stateToBadge(resource.status?.state)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getNIMCacheSourceType(resource: any): string {
|
|
70
|
+
const source = resource.spec?.source
|
|
71
|
+
if (!source) return '-'
|
|
72
|
+
if (source.ngc) return 'NGC'
|
|
73
|
+
if (source.dataStore) return 'DataStore'
|
|
74
|
+
if (source.hf) return 'HF'
|
|
75
|
+
return '-'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getNIMCacheModelSource(resource: any): string {
|
|
79
|
+
const source = resource.spec?.source
|
|
80
|
+
if (!source) return '-'
|
|
81
|
+
if (source.ngc) return source.ngc.modelPuller || source.ngc.modelEndpoint || '-'
|
|
82
|
+
const dshf = source.dataStore || source.hf
|
|
83
|
+
if (dshf) return dshf.modelName || dshf.datasetName || '-'
|
|
84
|
+
return '-'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getNIMCacheStorageSize(resource: any): string {
|
|
88
|
+
return resource.spec?.storage?.pvc?.size || '-'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getNIMCachePVCName(resource: any): string {
|
|
92
|
+
return resource.status?.pvc || resource.spec?.storage?.pvc?.name || '-'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// NIMPIPELINE UTILITIES
|
|
97
|
+
// ============================================================================
|
|
98
|
+
|
|
99
|
+
export function getNIMPipelineStatus(resource: any): StatusBadge {
|
|
100
|
+
return stateToBadge(resource.status?.state)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getNIMPipelineServiceCount(resource: any): string {
|
|
104
|
+
const services = resource.spec?.services || []
|
|
105
|
+
if (services.length === 0) return '0'
|
|
106
|
+
const enabled = services.filter((s: any) => s?.enabled !== false).length
|
|
107
|
+
if (enabled < services.length) return `${enabled}/${services.length} enabled`
|
|
108
|
+
return `${services.length}`
|
|
109
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// KubeRay CRD utility functions (ray.io/v1)
|
|
2
|
+
|
|
3
|
+
import type { StatusBadge } from './resource-utils'
|
|
4
|
+
import { healthColors, formatAge } from './resource-utils'
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// RAYCLUSTER UTILITIES
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
export function getRayClusterStatus(resource: any): StatusBadge {
|
|
11
|
+
const state = resource.status?.state
|
|
12
|
+
const conditions = resource.status?.conditions || []
|
|
13
|
+
|
|
14
|
+
const suspendingCond = conditions.find((c: any) => c.type === 'RayClusterSuspending')
|
|
15
|
+
if (suspendingCond?.status === 'True') {
|
|
16
|
+
return { text: 'Suspending', color: healthColors.degraded, level: 'degraded' }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const suspendedCond = conditions.find((c: any) => c.type === 'RayClusterSuspended')
|
|
20
|
+
if (suspendedCond?.status === 'True') {
|
|
21
|
+
return { text: 'Suspended', color: healthColors.neutral, level: 'neutral' }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (resource.spec?.suspend === true || state === 'suspended') {
|
|
25
|
+
return { text: 'Suspended', color: healthColors.neutral, level: 'neutral' }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const replicaFailureCond = conditions.find((c: any) => c.type === 'RayClusterReplicaFailure')
|
|
29
|
+
if (replicaFailureCond?.status === 'True') {
|
|
30
|
+
return { text: replicaFailureCond.reason || 'ReplicaFailure', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (state === 'failed') {
|
|
34
|
+
return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const headReadyCond = conditions.find((c: any) => c.type === 'HeadPodReady')
|
|
38
|
+
if (headReadyCond?.status === 'False') {
|
|
39
|
+
return { text: headReadyCond.reason || 'HeadNotReady', color: healthColors.degraded, level: 'degraded' }
|
|
40
|
+
}
|
|
41
|
+
if (headReadyCond?.status === 'True') {
|
|
42
|
+
const availableWorkers = resource.status?.availableWorkerReplicas ?? 0
|
|
43
|
+
const desiredWorkers = resource.status?.desiredWorkerReplicas ?? 0
|
|
44
|
+
if (availableWorkers >= desiredWorkers) {
|
|
45
|
+
return { text: 'Ready', color: healthColors.healthy, level: 'healthy' }
|
|
46
|
+
}
|
|
47
|
+
return { text: `Workers ${availableWorkers}/${desiredWorkers}`, color: healthColors.degraded, level: 'degraded' }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (conditions.length === 0 && state === 'ready') {
|
|
51
|
+
return { text: 'Ready', color: healthColors.healthy, level: 'healthy' }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const provisionedCond = conditions.find((c: any) => c.type === 'RayClusterProvisioned')
|
|
55
|
+
if (provisionedCond?.status === 'True') {
|
|
56
|
+
return { text: 'Provisioned', color: healthColors.neutral, level: 'neutral' }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (resource.status) {
|
|
60
|
+
return { text: 'Provisioning', color: healthColors.degraded, level: 'degraded' }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getRayClusterVersion(resource: any): string {
|
|
67
|
+
return resource.spec?.rayVersion || '-'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getRayClusterWorkers(resource: any): string {
|
|
71
|
+
const status = resource.status
|
|
72
|
+
if (!status) return '-'
|
|
73
|
+
const available = status.availableWorkerReplicas ?? 0
|
|
74
|
+
const desired = status.desiredWorkerReplicas ?? 0
|
|
75
|
+
return `${available}/${desired}`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getRayClusterHeadService(resource: any): string {
|
|
79
|
+
return resource.status?.head?.serviceName || '-'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// RAYJOB UTILITIES
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
export function getRayJobStatus(resource: any): StatusBadge {
|
|
87
|
+
const jobStatus = resource.status?.jobStatus
|
|
88
|
+
const deploymentStatus = resource.status?.jobDeploymentStatus
|
|
89
|
+
|
|
90
|
+
if (deploymentStatus === 'Failed' || deploymentStatus === 'ValidationFailed') {
|
|
91
|
+
return { text: deploymentStatus, color: healthColors.unhealthy, level: 'unhealthy' }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
switch (jobStatus) {
|
|
95
|
+
case 'SUCCEEDED':
|
|
96
|
+
return { text: 'Succeeded', color: healthColors.neutral, level: 'neutral' }
|
|
97
|
+
case 'RUNNING':
|
|
98
|
+
return { text: 'Running', color: healthColors.healthy, level: 'healthy' }
|
|
99
|
+
case 'FAILED':
|
|
100
|
+
return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
101
|
+
case 'STOPPED':
|
|
102
|
+
return { text: 'Stopped', color: healthColors.neutral, level: 'neutral' }
|
|
103
|
+
case 'PENDING':
|
|
104
|
+
return { text: 'Pending', color: healthColors.neutral, level: 'neutral' }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
switch (deploymentStatus) {
|
|
108
|
+
case 'Suspended':
|
|
109
|
+
return { text: 'Suspended', color: healthColors.neutral, level: 'neutral' }
|
|
110
|
+
case 'Complete':
|
|
111
|
+
return { text: 'Complete', color: healthColors.neutral, level: 'neutral' }
|
|
112
|
+
case 'Running':
|
|
113
|
+
return { text: 'Running', color: healthColors.healthy, level: 'healthy' }
|
|
114
|
+
case 'Initializing':
|
|
115
|
+
case 'Suspending':
|
|
116
|
+
case 'Retrying':
|
|
117
|
+
case 'Waiting':
|
|
118
|
+
return { text: deploymentStatus, color: healthColors.degraded, level: 'degraded' }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function getRayJobJobStatus(resource: any): string {
|
|
125
|
+
return resource.status?.jobStatus || '-'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function getRayJobDeploymentStatus(resource: any): string {
|
|
129
|
+
return resource.status?.jobDeploymentStatus || '-'
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function getRayJobClusterName(resource: any): string {
|
|
133
|
+
return resource.status?.rayClusterName || '-'
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// RAYSERVICE UTILITIES
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
export function getRayServiceStatus(resource: any): StatusBadge {
|
|
141
|
+
const conditions = resource.status?.conditions || []
|
|
142
|
+
|
|
143
|
+
const suspendingCond = conditions.find((c: any) => c.type === 'Suspending')
|
|
144
|
+
if (suspendingCond?.status === 'True') {
|
|
145
|
+
return { text: 'Suspending', color: healthColors.degraded, level: 'degraded' }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const suspendedCond = conditions.find((c: any) => c.type === 'Suspended')
|
|
149
|
+
if (suspendedCond?.status === 'True' || resource.spec?.suspend === true) {
|
|
150
|
+
return { text: 'Suspended', color: healthColors.neutral, level: 'neutral' }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const upgradeCond = conditions.find((c: any) => c.type === 'UpgradeInProgress')
|
|
154
|
+
if (upgradeCond?.status === 'True') {
|
|
155
|
+
return { text: 'Upgrading', color: healthColors.degraded, level: 'degraded' }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const rollbackCond = conditions.find((c: any) => c.type === 'RollbackInProgress')
|
|
159
|
+
if (rollbackCond?.status === 'True') {
|
|
160
|
+
return { text: 'RollingBack', color: healthColors.degraded, level: 'degraded' }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const readyCond = conditions.find((c: any) => c.type === 'Ready')
|
|
164
|
+
if (readyCond?.status === 'True') {
|
|
165
|
+
return { text: 'Ready', color: healthColors.healthy, level: 'healthy' }
|
|
166
|
+
}
|
|
167
|
+
if (readyCond?.status === 'False') {
|
|
168
|
+
return { text: readyCond.reason || 'NotReady', color: healthColors.degraded, level: 'degraded' }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (resource.status?.serviceStatus === 'Running') {
|
|
172
|
+
return { text: 'Running', color: healthColors.healthy, level: 'healthy' }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function getRayServiceServiceStatus(resource: any): string {
|
|
179
|
+
return resource.status?.serviceStatus || '-'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function getRayServiceClusters(resource: any): string {
|
|
183
|
+
const active = resource.status?.activeServiceStatus?.rayClusterName
|
|
184
|
+
const pending = resource.status?.pendingServiceStatus?.rayClusterName
|
|
185
|
+
const parts: string[] = []
|
|
186
|
+
if (active) parts.push(active)
|
|
187
|
+
if (pending) parts.push(`pending: ${pending}`)
|
|
188
|
+
return parts.join(' ') || '-'
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ============================================================================
|
|
192
|
+
// RAYCRONJOB UTILITIES
|
|
193
|
+
// ============================================================================
|
|
194
|
+
|
|
195
|
+
export function getRayCronJobStatus(resource: any): StatusBadge {
|
|
196
|
+
if (resource.spec?.suspend === true) {
|
|
197
|
+
return { text: 'Suspended', color: healthColors.neutral, level: 'neutral' }
|
|
198
|
+
}
|
|
199
|
+
return { text: 'Active', color: healthColors.neutral, level: 'neutral' }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function getRayCronJobSchedule(resource: any): string {
|
|
203
|
+
return resource.spec?.schedule || '-'
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function getRayCronJobSuspend(resource: any): boolean {
|
|
207
|
+
return resource.spec?.suspend === true
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function getRayCronJobLastSchedule(resource: any): string {
|
|
211
|
+
const lastSchedule = resource.status?.lastScheduleTime
|
|
212
|
+
if (!lastSchedule) return '-'
|
|
213
|
+
return formatAge(lastSchedule)
|
|
214
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { getCellFilterValue, getRolloutStep, getWorkloadDisplayStatus, getWorkloadStatus, workloadStatusLabel } from './resource-utils'
|
|
3
|
+
|
|
4
|
+
describe('getRolloutStep', () => {
|
|
5
|
+
it('presents Argo currentStepIndex as a one-based step number', () => {
|
|
6
|
+
expect(getRolloutStep({
|
|
7
|
+
spec: { strategy: { canary: { steps: [{ setWeight: 25 }, { pause: {} }] } } },
|
|
8
|
+
status: { currentStepIndex: 0 },
|
|
9
|
+
})).toBe('1/2')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('clamps Argo completed steps to the declared step count', () => {
|
|
13
|
+
expect(getRolloutStep({
|
|
14
|
+
spec: { strategy: { canary: { steps: [{ setWeight: 25 }, { pause: {} }] } } },
|
|
15
|
+
status: { currentStepIndex: 2 },
|
|
16
|
+
})).toBe('2/2')
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('getWorkloadStatus', () => {
|
|
21
|
+
it('keeps a capacity-preserving surge healthy', () => {
|
|
22
|
+
expect(getWorkloadStatus({
|
|
23
|
+
spec: { replicas: 1 },
|
|
24
|
+
status: { readyReplicas: 2, availableReplicas: 1, updatedReplicas: 1 },
|
|
25
|
+
}, 'deployments')).toMatchObject({ text: '1/1', level: 'healthy' })
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('shows health instead of Stable for an idle under-replicated workload', () => {
|
|
29
|
+
expect(getWorkloadDisplayStatus({
|
|
30
|
+
metadata: { generation: 2 },
|
|
31
|
+
spec: { replicas: 3 },
|
|
32
|
+
status: {
|
|
33
|
+
observedGeneration: 2,
|
|
34
|
+
replicas: 2,
|
|
35
|
+
updatedReplicas: 3,
|
|
36
|
+
readyReplicas: 2,
|
|
37
|
+
availableReplicas: 2,
|
|
38
|
+
},
|
|
39
|
+
}, 'deployments').status).toMatchObject({ text: '2/3', level: 'degraded' })
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('shows rollout activity while a revision is progressing', () => {
|
|
43
|
+
expect(getWorkloadDisplayStatus({
|
|
44
|
+
metadata: { generation: 2 },
|
|
45
|
+
spec: { replicas: 3 },
|
|
46
|
+
status: {
|
|
47
|
+
observedGeneration: 2,
|
|
48
|
+
replicas: 4,
|
|
49
|
+
updatedReplicas: 2,
|
|
50
|
+
readyReplicas: 3,
|
|
51
|
+
availableReplicas: 3,
|
|
52
|
+
},
|
|
53
|
+
}, 'deployments').status).toMatchObject({ text: 'Rolling out', level: 'neutral' })
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('shows health instead of an inactive pause policy', () => {
|
|
57
|
+
expect(getWorkloadDisplayStatus({
|
|
58
|
+
metadata: { generation: 2 },
|
|
59
|
+
spec: { replicas: 3, paused: true },
|
|
60
|
+
status: {
|
|
61
|
+
observedGeneration: 2,
|
|
62
|
+
replicas: 3,
|
|
63
|
+
updatedReplicas: 3,
|
|
64
|
+
readyReplicas: 0,
|
|
65
|
+
availableReplicas: 0,
|
|
66
|
+
},
|
|
67
|
+
}, 'deployments').status).toMatchObject({ text: '0/3', level: 'unhealthy' })
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('keeps active rollout copy while preserving a worse health tone', () => {
|
|
71
|
+
expect(getWorkloadDisplayStatus({
|
|
72
|
+
metadata: { generation: 2 },
|
|
73
|
+
spec: { replicas: 3, paused: true },
|
|
74
|
+
status: {
|
|
75
|
+
observedGeneration: 2,
|
|
76
|
+
replicas: 3,
|
|
77
|
+
updatedReplicas: 0,
|
|
78
|
+
readyReplicas: 0,
|
|
79
|
+
availableReplicas: 0,
|
|
80
|
+
},
|
|
81
|
+
}, 'deployments').status).toMatchObject({ text: 'Unhealthy · Rollout paused', level: 'unhealthy' })
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('preserves unhealthy serving capacity while an Argo Rollout progresses', () => {
|
|
85
|
+
const resource = {
|
|
86
|
+
apiVersion: 'argoproj.io/v1alpha1',
|
|
87
|
+
metadata: { generation: 2 },
|
|
88
|
+
spec: { replicas: 3 },
|
|
89
|
+
status: {
|
|
90
|
+
observedGeneration: '2',
|
|
91
|
+
phase: 'Progressing',
|
|
92
|
+
updatedReplicas: 1,
|
|
93
|
+
readyReplicas: 0,
|
|
94
|
+
availableReplicas: 0,
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
const display = getWorkloadDisplayStatus(resource, 'rollouts')
|
|
98
|
+
|
|
99
|
+
expect(display.status).toMatchObject({ text: 'Degraded · Rolling out', level: 'degraded' })
|
|
100
|
+
expect(getCellFilterValue(resource, 'status', 'rollouts')).toBe('Degraded · Rolling out')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('keeps Argo generation lag informational when observedGeneration is a string', () => {
|
|
104
|
+
const resource = {
|
|
105
|
+
apiVersion: 'argoproj.io/v1alpha1',
|
|
106
|
+
metadata: { generation: 3 },
|
|
107
|
+
spec: { replicas: 3 },
|
|
108
|
+
status: {
|
|
109
|
+
observedGeneration: '2',
|
|
110
|
+
phase: 'Healthy',
|
|
111
|
+
updatedReplicas: 0,
|
|
112
|
+
readyReplicas: 0,
|
|
113
|
+
availableReplicas: 0,
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
expect(getWorkloadDisplayStatus(resource, 'rollouts').status).toMatchObject({ text: 'Applying change', level: 'neutral' })
|
|
118
|
+
expect(getCellFilterValue(resource, 'status', 'rollouts')).toBe('Applying change')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('uses semantic health for an idle Argo Rollout', () => {
|
|
122
|
+
const resource = {
|
|
123
|
+
apiVersion: 'argoproj.io/v1alpha1',
|
|
124
|
+
metadata: { generation: 2 },
|
|
125
|
+
spec: { replicas: 3 },
|
|
126
|
+
status: {
|
|
127
|
+
observedGeneration: '2',
|
|
128
|
+
phase: 'Healthy',
|
|
129
|
+
updatedReplicas: 3,
|
|
130
|
+
readyReplicas: 3,
|
|
131
|
+
availableReplicas: 3,
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
expect(getWorkloadDisplayStatus(resource, 'rollouts').status).toMatchObject({ text: '3/3', level: 'healthy' })
|
|
136
|
+
expect(getCellFilterValue(resource, 'status', 'rollouts')).toBe('Healthy')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('does not fabricate Argo rollout state for a colliding CRD', () => {
|
|
140
|
+
const display = getWorkloadDisplayStatus({
|
|
141
|
+
apiVersion: 'rollouts.kruise.io/v1alpha1',
|
|
142
|
+
kind: 'Rollout',
|
|
143
|
+
metadata: { generation: 1 },
|
|
144
|
+
spec: { replicas: 3 },
|
|
145
|
+
status: { phase: 'Progressing', readyReplicas: 3, availableReplicas: 3, updatedReplicas: 3 },
|
|
146
|
+
}, 'rollouts')
|
|
147
|
+
|
|
148
|
+
expect(display.status).toMatchObject({ text: 'Progressing', level: 'unknown' })
|
|
149
|
+
expect(workloadStatusLabel(display.status)).toBe('Unknown')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('shows health instead of a reached StatefulSet partition', () => {
|
|
153
|
+
const resource = {
|
|
154
|
+
metadata: { generation: 2 },
|
|
155
|
+
spec: { replicas: 5, updateStrategy: { rollingUpdate: { partition: 2 } } },
|
|
156
|
+
status: {
|
|
157
|
+
observedGeneration: 2,
|
|
158
|
+
updatedReplicas: 3,
|
|
159
|
+
readyReplicas: 4,
|
|
160
|
+
availableReplicas: 4,
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
expect(getWorkloadDisplayStatus(resource, 'statefulsets').status).toMatchObject({ text: '4/5', level: 'degraded' })
|
|
165
|
+
expect(getCellFilterValue(resource, 'status', 'statefulsets')).toBe('Degraded')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('uses serving health as the filter value for an inactive pause policy', () => {
|
|
169
|
+
const resource = {
|
|
170
|
+
metadata: { generation: 2 },
|
|
171
|
+
spec: { replicas: 3, paused: true },
|
|
172
|
+
status: {
|
|
173
|
+
observedGeneration: 2,
|
|
174
|
+
replicas: 3,
|
|
175
|
+
updatedReplicas: 3,
|
|
176
|
+
readyReplicas: 0,
|
|
177
|
+
availableReplicas: 0,
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
expect(getCellFilterValue(resource, 'status', 'deployments')).toBe('Unhealthy')
|
|
182
|
+
})
|
|
183
|
+
})
|
|
@@ -94,6 +94,36 @@ describe('getCellFilterValue — Velero status column', () => {
|
|
|
94
94
|
const bsl = { apiVersion: 'velero.io/v1', kind: 'BackupStorageLocation', status: { phase: 'Unavailable' } }
|
|
95
95
|
expect(getCellFilterValue(bsl, 'status', 'backupstoragelocations')).toBe('Unavailable')
|
|
96
96
|
})
|
|
97
|
+
|
|
98
|
+
// The cell dispatch sends a non-Velero BackupStorageLocation/BackupRepository
|
|
99
|
+
// to GenericCell. The filter dropdown and the sort key have to follow it
|
|
100
|
+
// there, or the column offers and orders values that appear on no row.
|
|
101
|
+
it('leaves a foreign BackupStorageLocation on the generic reader', () => {
|
|
102
|
+
// Conditions, not a phase: Velero's reader keys on phase and has nothing to
|
|
103
|
+
// say here, so this only passes if the generic derivation ran.
|
|
104
|
+
const foreign = {
|
|
105
|
+
apiVersion: 'other.io/v1',
|
|
106
|
+
kind: 'BackupStorageLocation',
|
|
107
|
+
status: { conditions: [{ type: 'Ready', status: 'True' }] },
|
|
108
|
+
}
|
|
109
|
+
expect(getCellFilterValue(foreign, 'status', 'backupstoragelocations')).toBe('Ready')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('leaves a foreign BackupRepository on the generic reader', () => {
|
|
113
|
+
const foreign = {
|
|
114
|
+
apiVersion: 'other.io/v1',
|
|
115
|
+
kind: 'BackupRepository',
|
|
116
|
+
status: { conditions: [{ type: 'Ready', status: 'True' }] },
|
|
117
|
+
}
|
|
118
|
+
expect(getCellFilterValue(foreign, 'status', 'backuprepositories')).toBe('Ready')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// Velero's own readers speak a phase vocabulary a foreign CRD does not share,
|
|
122
|
+
// so the two must not produce the same string for the same object.
|
|
123
|
+
it('does not label a foreign resource with Velero vocabulary', () => {
|
|
124
|
+
const foreign = { apiVersion: 'other.io/v1', kind: 'BackupRepository', status: { phase: 'NotReady' } }
|
|
125
|
+
expect(getCellFilterValue(foreign, 'status', 'backuprepositories')).not.toBe('Not ready')
|
|
126
|
+
})
|
|
97
127
|
})
|
|
98
128
|
|
|
99
129
|
describe('Velero Backup phase vocabulary', () => {
|