@skyhook-io/k8s-ui 1.14.9 → 1.14.11
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 +3 -2
- package/src/components/applications/ApplicationDetail.tsx +24 -11
- package/src/components/audit/AuditAlerts.tsx +1 -3
- package/src/components/audit/AuditCoveragePresentation.test.tsx +48 -2
- package/src/components/checks/ChecksView.test.tsx +55 -0
- package/src/components/checks/ChecksView.tsx +38 -27
- package/src/components/dock/NodeTerminalTab.test.tsx +89 -0
- package/src/components/dock/NodeTerminalTab.tsx +51 -47
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +17 -9
- package/src/components/gitops/detail-helpers.test.ts +32 -0
- package/src/components/gitops/detail-helpers.ts +21 -0
- package/src/components/gitops/health-provenance.test.ts +4 -3
- package/src/components/gitops/index.ts +2 -1
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +131 -54
- package/src/components/issues/IssuesView.tsx +6 -0
- package/src/components/issues/ResourceIssuesSection.test.tsx +36 -0
- package/src/components/issues/diagnostic.ts +4 -0
- package/src/components/logs/LogCore.tsx +3 -3
- package/src/components/logs/WorkloadLogsViewer.tsx +41 -11
- package/src/components/logs/useLogBuffer.ts +1 -0
- package/src/components/logs/useLogStream.ts +1 -2
- package/src/components/resources/KueueAdmissionSection.test.tsx +95 -0
- package/src/components/resources/KueueAdmissionSection.tsx +87 -0
- package/src/components/resources/ResourcesView.default-sort.test.ts +26 -0
- package/src/components/resources/ResourcesView.tsx +91 -16
- package/src/components/resources/curated-column-ownership.test.ts +45 -0
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +4 -3
- package/src/components/resources/renderers/ContainerEnvironmentSection.tsx +10 -3
- package/src/components/resources/renderers/JobSetRenderer.test.tsx +280 -0
- package/src/components/resources/renderers/JobSetRenderer.tsx +322 -0
- package/src/components/resources/renderers/KueueProvisioningRenderers.test.tsx +67 -0
- package/src/components/resources/renderers/KueueProvisioningRenderers.tsx +91 -0
- package/src/components/resources/renderers/KueueQueueNavigation.test.tsx +69 -0
- package/src/components/resources/renderers/KueueQueueRenderers.test.tsx +123 -0
- package/src/components/resources/renderers/KueueQueueRenderers.tsx +299 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.test.tsx +121 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +230 -0
- package/src/components/resources/renderers/RayClusterRenderer.test.tsx +25 -0
- package/src/components/resources/renderers/RayClusterRenderer.tsx +93 -0
- package/src/components/resources/renderers/RayServiceRenderer.test.tsx +62 -0
- package/src/components/resources/renderers/RayServiceRenderer.tsx +114 -0
- package/src/components/resources/renderers/index.ts +11 -0
- package/src/components/resources/resource-utils-gpu-ecosystem.test.ts +103 -2
- package/src/components/resources/resource-utils-jobset-lws.ts +43 -9
- package/src/components/resources/resource-utils-kueue.ts +145 -50
- package/src/components/resources/resource-utils-ray.test.ts +58 -1
- package/src/components/resources/resource-utils-ray.ts +56 -90
- package/src/components/shared/ResourceRendererDispatch.test.tsx +117 -2
- package/src/components/shared/ResourceRendererDispatch.tsx +47 -10
- package/src/components/timeline/TimelineList.tsx +5 -0
- package/src/components/timeline/TimelineSwimlanes.test.tsx +15 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +43 -23
- package/src/components/topology/K8sResourceNode.tsx +3 -3
- package/src/components/ui/CodeViewer.tsx +4 -0
- package/src/components/ui/Toast.tsx +9 -3
- package/src/components/ui/drawer-components.tsx +3 -1
- package/src/components/workload/PodList.tsx +154 -0
- package/src/components/workload/WorkloadView.tsx +29 -159
- package/src/components/workload/index.ts +11 -1
- package/src/components/workload/workload-logs-availability.test.ts +20 -0
- package/src/components/workload/workload-pod-presentation.test.ts +27 -0
- package/src/types/core.ts +24 -0
- package/src/types/gitops-tree.ts +5 -2
- package/src/types/scheduling.ts +71 -0
- package/src/utils/api-resources.test.ts +20 -1
- package/src/utils/api-resources.ts +26 -0
- package/src/utils/application-topology.test.ts +25 -0
- package/src/utils/application-topology.ts +2 -1
- package/src/utils/applications.test.ts +82 -0
- package/src/utils/applications.ts +62 -24
- package/src/utils/gitops-route.test.ts +20 -1
- package/src/utils/gitops-route.ts +15 -7
- package/src/utils/log-format.ts +5 -2
- package/src/utils/log-stream-error.test.ts +13 -0
- package/src/utils/navigation.test.ts +2 -0
- package/src/utils/navigation.ts +1 -1
- package/src/utils/resource-hierarchy.test.ts +402 -15
- package/src/utils/resource-hierarchy.ts +263 -178
- package/src/utils/topology-neighborhood.test.ts +72 -1
- package/src/utils/topology-neighborhood.ts +40 -18
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { getRayCronJobTimeZone, getRayCronJobStatus } from './resource-utils-ray'
|
|
2
|
+
import { getRayCronJobTimeZone, getRayCronJobStatus, getRayServiceStatus } from './resource-utils-ray'
|
|
3
3
|
import { getCellFilterValue } from './resource-utils'
|
|
4
4
|
|
|
5
5
|
describe('getRayCronJobTimeZone', () => {
|
|
@@ -34,3 +34,60 @@ describe('getCellFilterValue for raycronjobs timeZone', () => {
|
|
|
34
34
|
.toBe('Europe/Berlin')
|
|
35
35
|
})
|
|
36
36
|
})
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
describe('RayService observed lifecycle', () => {
|
|
40
|
+
const data = (conditions: any[], spec = {}) => ({ metadata: { generation: 3 }, spec, status: { observedGeneration: 3, conditions } })
|
|
41
|
+
it('prioritizes observed rollback over upgrade without hiding readiness in the detail', () => {
|
|
42
|
+
expect(getRayServiceStatus(data(['Ready', 'UpgradeInProgress', 'RollbackInProgress'].map(type => ({ type, status: 'True' })))).text).toBe('RollingBack')
|
|
43
|
+
})
|
|
44
|
+
it('does not treat requested suspension as completed teardown', () => {
|
|
45
|
+
expect(getRayServiceStatus(data([], { suspend: true })).text).toBe('Suspension requested')
|
|
46
|
+
expect(getRayServiceStatus(data([{ type: 'Ready', status: 'True' }], { suspend: true })).text).toBe('Ready')
|
|
47
|
+
expect(getRayServiceStatus(data([{ type: 'Suspending', status: 'True' }], { suspend: false })).text).toBe('Suspending')
|
|
48
|
+
expect(getRayServiceStatus(data([{ type: 'Suspended', status: 'True' }])).text).toBe('Suspended')
|
|
49
|
+
})
|
|
50
|
+
it('retains stale and unknown observations without using deprecated Running as proof', () => {
|
|
51
|
+
expect(getRayServiceStatus(data([{ type: 'Ready', status: 'False', observedGeneration: 2 }])).text).toBe('NotReady')
|
|
52
|
+
for (const [status, label] of [['True', 'Ready'], ['False', 'NotReady'], ['Unknown', 'Unknown']]) {
|
|
53
|
+
const resource = data([{ type: 'Ready', status }])
|
|
54
|
+
resource.status.observedGeneration = 2
|
|
55
|
+
expect(getRayServiceStatus(resource).text).toBe(status === 'True' ? `${label} (stale)` : label)
|
|
56
|
+
}
|
|
57
|
+
for (const condition of [{ type: 'Suspending', status: 'True' }, { type: 'Ready', status: 'False', reason: 'ValidationFailed' }, { type: 'Ready', status: 'False', reason: 'InitializingTimeout' }]) {
|
|
58
|
+
const resource = data([condition], { suspend: false })
|
|
59
|
+
resource.status.observedGeneration = 2
|
|
60
|
+
expect(getRayServiceStatus(resource).text).toBe(condition.reason || condition.type)
|
|
61
|
+
}
|
|
62
|
+
const suspended = data([{ type: 'Suspended', status: 'True' }], { suspend: true })
|
|
63
|
+
suspended.status.observedGeneration = 2
|
|
64
|
+
expect(getRayServiceStatus(suspended).text).toBe('Suspended')
|
|
65
|
+
expect(getRayServiceStatus({ status: { serviceStatus: 'Running', conditions: [{ type: 'Ready', status: 'Unknown' }] } }).text).toBe('Unknown')
|
|
66
|
+
expect(getRayServiceStatus({ status: { serviceStatus: 'Running' } }).text).toBe('Unknown')
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
import { getRayClusterStatus, getRayClusterWorkers, rayClusterWorkerCounts } from './resource-utils-ray'
|
|
71
|
+
|
|
72
|
+
describe('RayCluster readiness', () => {
|
|
73
|
+
const root = (status: any = {}, spec = {}) => ({ metadata: { generation: 2 }, spec, status: { observedGeneration: 2, conditions: [{ type: 'HeadPodReady', status: 'True' }], ...status } })
|
|
74
|
+
it('distinguishes running from ready and does not bless unready excess workers', () => {
|
|
75
|
+
expect(getRayClusterStatus(root({ readyWorkerReplicas: 1, availableWorkerReplicas: 2, desiredWorkerReplicas: 2 })).level).toBe('degraded')
|
|
76
|
+
expect(getRayClusterStatus(root({ readyWorkerReplicas: 1, availableWorkerReplicas: 2, desiredWorkerReplicas: 1 })).level).toBe('degraded')
|
|
77
|
+
expect(getRayClusterStatus(root({ readyWorkerReplicas: 2, availableWorkerReplicas: 2, desiredWorkerReplicas: 2 })).text).toBe('Ready')
|
|
78
|
+
expect(getRayClusterWorkers(root({ readyWorkerReplicas: 1, availableWorkerReplicas: 2, desiredWorkerReplicas: 2 }))).toBe('1/2')
|
|
79
|
+
})
|
|
80
|
+
it('decodes native omitted zeros only after a witnessed reconciliation', () => {
|
|
81
|
+
expect(rayClusterWorkerCounts(root())).toEqual({ ready: 0, running: 0, desired: 0 })
|
|
82
|
+
expect(getRayClusterStatus(root()).text).toBe('Ready')
|
|
83
|
+
expect(getRayClusterWorkers({})).toBe('?/?')
|
|
84
|
+
expect(getRayClusterStatus(root({ observedGeneration: undefined })).text).toBe('Head ready')
|
|
85
|
+
})
|
|
86
|
+
it('separates suspension intent and historical observations from health', () => {
|
|
87
|
+
expect(getRayClusterStatus(root({}, { suspend: true })).text).toBe('Suspension requested')
|
|
88
|
+
expect(getRayClusterStatus(root({ conditions: [{ type: 'RayClusterSuspended', status: 'True' }] })).text).toBe('Suspended')
|
|
89
|
+
expect(getRayClusterStatus(root({ observedGeneration: 1 })).level).toBe('unknown')
|
|
90
|
+
expect(getRayClusterStatus(root({ state: 'ready', conditions: [] })).level).toBe('unknown')
|
|
91
|
+
expect(getRayClusterStatus(root({ conditions: [{ type: 'RayClusterProvisioned', status: 'True' }] })).level).toBe('neutral')
|
|
92
|
+
})
|
|
93
|
+
})
|
|
@@ -7,60 +7,35 @@ import { healthColors, formatAge } from './resource-utils'
|
|
|
7
7
|
// RAYCLUSTER UTILITIES
|
|
8
8
|
// ============================================================================
|
|
9
9
|
|
|
10
|
-
export function
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
}
|
|
10
|
+
export function rayClusterWorkerCounts(resource: any): { ready?: number; running?: number; desired?: number } {
|
|
11
|
+
const status = resource.status ?? {}
|
|
12
|
+
// Native integer counters omit zero; observedGeneration witnesses their shared status write.
|
|
13
|
+
const count = (key: string) => status[key] ?? (status.observedGeneration > 0 ? 0 : undefined)
|
|
14
|
+
return { ready: count('readyWorkerReplicas'), running: count('availableWorkerReplicas'), desired: count('desiredWorkerReplicas') }
|
|
15
|
+
}
|
|
62
16
|
|
|
63
|
-
|
|
17
|
+
export function getRayClusterStatus(resource: any): StatusBadge {
|
|
18
|
+
const conditions = resource.status?.conditions ?? []
|
|
19
|
+
const condition = (type: string) => conditions.find((c: any) => c.type === type)
|
|
20
|
+
const badge = (text: string, level: StatusBadge['level']): StatusBadge => ({ text, level, color: healthColors[level!] })
|
|
21
|
+
if (isRayObservationStale(resource.metadata?.generation, resource.status?.observedGeneration)) return badge('Status stale', 'unknown')
|
|
22
|
+
if (condition('RayClusterSuspending')?.status === 'True') return badge('Suspending', 'degraded')
|
|
23
|
+
if (condition('RayClusterSuspended')?.status === 'True') return badge('Suspended', 'neutral')
|
|
24
|
+
if (resource.spec?.suspend === true) return badge('Suspension requested', 'neutral')
|
|
25
|
+
const failure = condition('RayClusterReplicaFailure')
|
|
26
|
+
if (failure?.status === 'True') return badge(failure.reason || 'Replica failure', 'unhealthy')
|
|
27
|
+
if (resource.status?.state === 'failed') return badge('Failed', 'unhealthy')
|
|
28
|
+
const head = condition('HeadPodReady')
|
|
29
|
+
if (head?.status === 'False') return badge(head.reason || 'Head not ready', 'degraded')
|
|
30
|
+
if (head?.status === 'True') {
|
|
31
|
+
const { ready, running, desired } = rayClusterWorkerCounts(resource)
|
|
32
|
+
if (ready == null || desired == null) return badge('Head ready', 'neutral')
|
|
33
|
+
if (ready >= desired && (running == null || ready >= running)) return badge('Ready', 'healthy')
|
|
34
|
+
if (ready >= desired && running != null && running > ready) return badge(`${running - ready} workers unready`, 'degraded')
|
|
35
|
+
return badge(`Workers ${ready}/${desired} ready`, 'degraded')
|
|
36
|
+
}
|
|
37
|
+
if (condition('RayClusterProvisioned')?.status === 'True') return badge('Provisioned', 'neutral')
|
|
38
|
+
return badge('Unknown', 'unknown')
|
|
64
39
|
}
|
|
65
40
|
|
|
66
41
|
export function getRayClusterVersion(resource: any): string {
|
|
@@ -68,11 +43,8 @@ export function getRayClusterVersion(resource: any): string {
|
|
|
68
43
|
}
|
|
69
44
|
|
|
70
45
|
export function getRayClusterWorkers(resource: any): string {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
const available = status.availableWorkerReplicas ?? 0
|
|
74
|
-
const desired = status.desiredWorkerReplicas ?? 0
|
|
75
|
-
return `${available}/${desired}`
|
|
46
|
+
const { ready, desired } = rayClusterWorkerCounts(resource)
|
|
47
|
+
return `${ready ?? '?'}/${desired ?? '?'}`
|
|
76
48
|
}
|
|
77
49
|
|
|
78
50
|
export function getRayClusterHeadService(resource: any): string {
|
|
@@ -137,41 +109,35 @@ export function getRayJobClusterName(resource: any): string {
|
|
|
137
109
|
// RAYSERVICE UTILITIES
|
|
138
110
|
// ============================================================================
|
|
139
111
|
|
|
140
|
-
export function
|
|
141
|
-
|
|
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
|
-
}
|
|
112
|
+
export function isRayObservationStale(generation?: number, observedGeneration?: number): boolean {
|
|
113
|
+
return generation != null && observedGeneration != null && observedGeneration < generation
|
|
114
|
+
}
|
|
162
115
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
return { text: readyCond.reason || 'NotReady', color: healthColors.degraded, level: 'degraded' }
|
|
169
|
-
}
|
|
116
|
+
// KubeRay can freeze observedGeneration during validation failures and suspension.
|
|
117
|
+
export function isRayServiceConditionStale(resource: any, condition: any): boolean {
|
|
118
|
+
return condition?.status === 'True' && ['Ready', 'UpgradeInProgress', 'RollbackInProgress'].includes(condition.type)
|
|
119
|
+
&& isRayObservationStale(resource.metadata?.generation, resource.status?.observedGeneration)
|
|
120
|
+
}
|
|
170
121
|
|
|
171
|
-
|
|
172
|
-
|
|
122
|
+
export function getRayServiceStatus(resource: any): StatusBadge {
|
|
123
|
+
const conditions = resource.status?.conditions ?? []
|
|
124
|
+
const labels: Record<string, string> = { Suspending: 'Suspending', Suspended: 'Suspended', RollbackInProgress: 'RollingBack', UpgradeInProgress: 'Upgrading' }
|
|
125
|
+
for (const type of ['Suspending', 'Suspended', 'RollbackInProgress', 'UpgradeInProgress', 'Ready']) {
|
|
126
|
+
const condition = conditions.find((c: any) => c.type === type)
|
|
127
|
+
if (!condition || (type !== 'Ready' && condition.status !== 'True')) continue
|
|
128
|
+
const label = type === 'Ready' ? (condition.status === 'True' ? 'Ready' : condition.status === 'False' ? condition.reason || 'NotReady' : 'Unknown') : labels[type]
|
|
129
|
+
if (isRayServiceConditionStale(resource, condition)) {
|
|
130
|
+
return { text: `${label} (stale)`, color: healthColors.unknown, level: 'unknown' }
|
|
131
|
+
}
|
|
132
|
+
if (type === 'Ready') {
|
|
133
|
+
if (condition.status === 'True') return { text: 'Ready', color: healthColors.healthy, level: 'healthy' }
|
|
134
|
+
if (condition.status === 'False') return { text: condition.reason || 'NotReady', color: healthColors.degraded, level: 'degraded' }
|
|
135
|
+
return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
|
|
136
|
+
}
|
|
137
|
+
const level = type === 'Suspended' ? 'neutral' : 'degraded'
|
|
138
|
+
return { text: labels[type], color: healthColors[level], level }
|
|
173
139
|
}
|
|
174
|
-
|
|
140
|
+
if (resource.spec?.suspend === true) return { text: 'Suspension requested', color: healthColors.neutral, level: 'neutral' }
|
|
175
141
|
return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
|
|
176
142
|
}
|
|
177
143
|
|
|
@@ -944,17 +944,64 @@ describe('GPU ecosystem kind collisions', () => {
|
|
|
944
944
|
})
|
|
945
945
|
|
|
946
946
|
describe('GPU ecosystem status edge cases', () => {
|
|
947
|
-
it('
|
|
947
|
+
it('dispatches the current JobSet API to typed detail', () => {
|
|
948
|
+
const html = renderKind('jobsets', {
|
|
949
|
+
apiVersion: 'jobset.x-k8s.io/v1alpha2',
|
|
950
|
+
kind: 'JobSet',
|
|
951
|
+
metadata: { name: 'training', namespace: 'ml' },
|
|
952
|
+
spec: { replicatedJobs: [{ name: 'workers', replicas: 2, template: { spec: {} } }] },
|
|
953
|
+
status: { replicatedJobsStatus: [{ name: 'workers', active: 1, ready: 1, succeeded: 0, failed: 0, suspended: 0 }] },
|
|
954
|
+
}, 'ml')
|
|
955
|
+
|
|
956
|
+
expect(html).toContain('JobSet status')
|
|
957
|
+
expect(html).toContain('Replicated jobs (1)')
|
|
958
|
+
expect(html).not.toContain('Specification')
|
|
959
|
+
})
|
|
960
|
+
|
|
961
|
+
it.each(['jobset.x-k8s.io/v1alpha1', 'batch.example.io/v1'])(
|
|
962
|
+
'keeps unsupported or foreign JobSet collisions generic for %s',
|
|
963
|
+
(apiVersion) => {
|
|
964
|
+
const data = {
|
|
965
|
+
apiVersion,
|
|
966
|
+
kind: 'JobSet',
|
|
967
|
+
metadata: { name: 'foreign', namespace: 'ml' },
|
|
968
|
+
spec: { collisionProbe: COLLISION_PROBE },
|
|
969
|
+
}
|
|
970
|
+
const html = renderKind('jobsets', data, 'ml')
|
|
971
|
+
|
|
972
|
+
expect(html).toContain(COLLISION_PROBE)
|
|
973
|
+
expect(html).not.toContain('JobSet status')
|
|
974
|
+
expect(getResourceStatus('jobsets', data)).toBeNull()
|
|
975
|
+
},
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
it('JobSet distinguishes observed inactivity from active child Jobs', () => {
|
|
948
979
|
const fresh = getResourceStatus('jobsets', {
|
|
949
980
|
apiVersion: 'jobset.x-k8s.io/v1alpha2',
|
|
981
|
+
kind: 'JobSet',
|
|
950
982
|
status: { replicatedJobsStatus: [{ name: 'w', active: 0, ready: 0 }] },
|
|
951
983
|
})
|
|
952
984
|
const live = getResourceStatus('jobsets', {
|
|
953
985
|
apiVersion: 'jobset.x-k8s.io/v1alpha2',
|
|
986
|
+
kind: 'JobSet',
|
|
954
987
|
status: { replicatedJobsStatus: [{ name: 'w', active: 1, ready: 1 }] },
|
|
955
988
|
})
|
|
956
989
|
expect(fresh?.text).toBe('Pending')
|
|
957
|
-
expect(live?.text).toBe('
|
|
990
|
+
expect(live?.text).toBe('Active')
|
|
991
|
+
})
|
|
992
|
+
|
|
993
|
+
it('keeps a wrong-kind object generic even at the supported JobSet API version', () => {
|
|
994
|
+
const data = {
|
|
995
|
+
apiVersion: 'jobset.x-k8s.io/v1alpha2',
|
|
996
|
+
kind: 'OtherSet',
|
|
997
|
+
metadata: { name: 'collision', namespace: 'ml' },
|
|
998
|
+
spec: { collisionProbe: COLLISION_PROBE },
|
|
999
|
+
}
|
|
1000
|
+
const html = renderKind('jobsets', data, 'ml')
|
|
1001
|
+
|
|
1002
|
+
expect(html).toContain(COLLISION_PROBE)
|
|
1003
|
+
expect(html).not.toContain('JobSet status')
|
|
1004
|
+
expect(getResourceStatus('jobsets', data)).toBeNull()
|
|
958
1005
|
})
|
|
959
1006
|
|
|
960
1007
|
it('InferencePool with only an empty-parentRef default entry reads Not referenced', () => {
|
|
@@ -965,3 +1012,71 @@ describe('GPU ecosystem status edge cases', () => {
|
|
|
965
1012
|
expect(pool?.text).toBe('Not referenced')
|
|
966
1013
|
})
|
|
967
1014
|
})
|
|
1015
|
+
|
|
1016
|
+
describe('Kueue Workload detail dispatch', () => {
|
|
1017
|
+
it.each(['kueue.x-k8s.io/v1beta1', 'kueue.x-k8s.io/v1beta2'])('renders %s with curated admission detail', (apiVersion) => {
|
|
1018
|
+
expect(renderKind('workloads', { apiVersion, kind: 'Workload', spec: { podSets: [] } })).toContain('No PodSets declared')
|
|
1019
|
+
})
|
|
1020
|
+
it.each(['example.com/v1', 'fake.kueue.x-k8s.io/v1', undefined])('leaves %s generic', (apiVersion) => {
|
|
1021
|
+
const data = { apiVersion, kind: 'Workload', spec: { uniqueForeignField: 'visible' } }
|
|
1022
|
+
const html = renderKind('workloads', data)
|
|
1023
|
+
expect(html).not.toContain('No PodSets declared')
|
|
1024
|
+
expect(html).toContain('visible')
|
|
1025
|
+
expect(getResourceStatus('workloads', data)).toBeNull()
|
|
1026
|
+
})
|
|
1027
|
+
})
|
|
1028
|
+
|
|
1029
|
+
describe('Kueue queue detail dispatch', () => {
|
|
1030
|
+
it.each(['localqueues', 'clusterqueues'])('curates supported %s and preserves foreign and future versions', (kind) => {
|
|
1031
|
+
for (const version of ['v1beta1', 'v1beta2']) {
|
|
1032
|
+
expect(renderKind(kind, { apiVersion: `kueue.x-k8s.io/${version}`, spec: {} })).toContain('Queue Status')
|
|
1033
|
+
}
|
|
1034
|
+
for (const apiVersion of ['example.com/v1', 'fake.kueue.x-k8s.io/v1', 'kueue.x-k8s.io/v99']) {
|
|
1035
|
+
const html = renderKind(kind, { apiVersion, spec: { foreignField: 'preserved-value' } })
|
|
1036
|
+
expect(html).not.toContain('Queue Status')
|
|
1037
|
+
expect(html).toContain('preserved-value')
|
|
1038
|
+
}
|
|
1039
|
+
})
|
|
1040
|
+
})
|
|
1041
|
+
|
|
1042
|
+
describe('Kueue provisioning detail identity', () => {
|
|
1043
|
+
it('dispatches supported versions only, preserving foreign same-kind generic rendering', () => {
|
|
1044
|
+
for (const [kind, group, versions, title] of [
|
|
1045
|
+
['admissionchecks', 'kueue.x-k8s.io', ['v1beta1', 'v1beta2'], 'Check Controller'],
|
|
1046
|
+
['provisioningrequests', 'autoscaling.x-k8s.io', ['v1beta1', 'v1'], 'Requested Pod Sets'],
|
|
1047
|
+
] as const) {
|
|
1048
|
+
for (const version of versions) {
|
|
1049
|
+
const html = renderKind(kind, { apiVersion: `${group}/${version}`, spec: { controllerName: 'test-controller', provisioningClassName: 'test-class' } })
|
|
1050
|
+
expect(html).toContain(title)
|
|
1051
|
+
expect(html).not.toContain('This resource type doesn')
|
|
1052
|
+
expect(html).not.toContain('Configuration')
|
|
1053
|
+
}
|
|
1054
|
+
expect(renderKind(kind, { apiVersion: 'foreign.io/v1', spec: {} })).not.toContain(title)
|
|
1055
|
+
expect(renderKind(kind, { apiVersion: `${group}/v99`, spec: {} })).not.toContain(title)
|
|
1056
|
+
}
|
|
1057
|
+
})
|
|
1058
|
+
})
|
|
1059
|
+
|
|
1060
|
+
describe('RayService native detail identity', () => {
|
|
1061
|
+
it('renders the exact supported version once and leaves foreign/future resources generic', () => {
|
|
1062
|
+
const data = { apiVersion: 'ray.io/v1', kind: 'RayService', spec: { customField: 'preserved-value' } }
|
|
1063
|
+
const html = renderKind('rayservices', data)
|
|
1064
|
+
expect(html).toContain('Serving and Rollout')
|
|
1065
|
+
expect(html).not.toContain('preserved-value')
|
|
1066
|
+
expect(html).not.toContain('No specialized view')
|
|
1067
|
+
for (const apiVersion of ['foreign.io/v1', 'fake.ray.io/v1', 'ray.io/v99', 'ray.io/v1alpha1']) {
|
|
1068
|
+
const foreign = { ...data, apiVersion }
|
|
1069
|
+
expect(renderKind('rayservices', foreign)).not.toContain('Serving and Rollout')
|
|
1070
|
+
expect(renderKind('rayservices', foreign)).toContain('preserved-value')
|
|
1071
|
+
expect(getResourceStatus('rayservices', foreign)).toBeNull()
|
|
1072
|
+
}
|
|
1073
|
+
})
|
|
1074
|
+
})
|
|
1075
|
+
|
|
1076
|
+
it('uses native RayCluster detail only for ray.io/v1', () => {
|
|
1077
|
+
for (const apiVersion of ['ray.io/v1', 'foreign.io/v1', 'ray.io/v9']) {
|
|
1078
|
+
const html = renderKind('rayclusters', { apiVersion, kind: 'RayCluster', metadata: { name: 'cluster', namespace: 'ml' }, spec: { rayVersion: '2.55.0' } }, 'ml')
|
|
1079
|
+
expect(html.includes('Runtime Health')).toBe(apiVersion === 'ray.io/v1')
|
|
1080
|
+
if (apiVersion !== 'ray.io/v1') expect(html).toContain('2.55.0')
|
|
1081
|
+
}
|
|
1082
|
+
})
|
|
@@ -68,9 +68,9 @@ import {
|
|
|
68
68
|
} from '../resources/resource-utils-kyverno-exceptions'
|
|
69
69
|
import { getResourceClaimStatus, getResourceClaimTemplateStatus, getDeviceClassStatus, getResourceSliceStatus } from '../resources/resource-utils-dra'
|
|
70
70
|
import { getNvidiaClusterPolicyStatus, getNvidiaDriverStatus } from '../resources/resource-utils-nvidia'
|
|
71
|
-
import { getClusterQueueStatus, getLocalQueueStatus, getKueueWorkloadStatus, getResourceFlavorStatus, getAdmissionCheckStatus, getProvisioningRequestStatus } from '../resources/resource-utils-kueue'
|
|
71
|
+
import { isKueueQueueResource, getClusterQueueStatus, getLocalQueueStatus, getKueueWorkloadStatus, getResourceFlavorStatus, getAdmissionCheckStatus, getProvisioningRequestStatus } from '../resources/resource-utils-kueue'
|
|
72
72
|
import { getRayClusterStatus, getRayJobStatus, getRayServiceStatus, getRayCronJobStatus } from '../resources/resource-utils-ray'
|
|
73
|
-
import { getLeaderWorkerSetStatus, getJobSetStatus } from '../resources/resource-utils-jobset-lws'
|
|
73
|
+
import { getLeaderWorkerSetStatus, getJobSetStatus, isJobSetV1Alpha2 } from '../resources/resource-utils-jobset-lws'
|
|
74
74
|
import { getInferenceServiceStatus, getServingRuntimeStatus, getInferenceGraphStatus, getTrainedModelStatus, getLLMInferenceServiceStatus } from '../resources/resource-utils-kserve'
|
|
75
75
|
import { getInferencePoolStatus, getInferenceObjectiveStatus } from '../resources/resource-utils-inference-gateway'
|
|
76
76
|
import { getVolcanoJobStatus, getVolcanoQueueStatus, getVolcanoPodGroupStatus, getJobFlowStatus, getJobTemplateStatus } from '../resources/resource-utils-volcano'
|
|
@@ -114,6 +114,14 @@ import {
|
|
|
114
114
|
ConfigMapRenderer,
|
|
115
115
|
SecretRenderer,
|
|
116
116
|
JobRenderer,
|
|
117
|
+
JobSetRenderer,
|
|
118
|
+
KueueWorkloadRenderer,
|
|
119
|
+
RayServiceRenderer,
|
|
120
|
+
RayClusterRenderer,
|
|
121
|
+
AdmissionCheckRenderer,
|
|
122
|
+
ProvisioningRequestRenderer,
|
|
123
|
+
LocalQueueRenderer,
|
|
124
|
+
ClusterQueueRenderer,
|
|
117
125
|
CronJobRenderer,
|
|
118
126
|
CronWorkflowRenderer,
|
|
119
127
|
HPARenderer,
|
|
@@ -304,6 +312,9 @@ import type { ScalerDiagnosis } from '../resources/renderers/WorkloadRenderer'
|
|
|
304
312
|
* When an override is not provided, the base (shared) renderer is used.
|
|
305
313
|
*/
|
|
306
314
|
export interface RendererOverrides {
|
|
315
|
+
RayClusterRenderer?: React.ComponentType<{ data: any; onNavigate?: (ref: ResourceRef) => void }>
|
|
316
|
+
RayServiceRenderer?: React.ComponentType<{ data: any; onNavigate?: (ref: ResourceRef) => void }>
|
|
317
|
+
KueueWorkloadRenderer?: React.ComponentType<{ data: any; onNavigate?: (ref: ResourceRef) => void }>
|
|
307
318
|
PodRenderer?: React.ComponentType<{
|
|
308
319
|
data: any; onCopy: CopyHandler; copied: string | null
|
|
309
320
|
onNavigate?: (ref: ResourceRef) => void
|
|
@@ -400,6 +411,10 @@ export interface RendererOverrides {
|
|
|
400
411
|
// Namespace RBAC summary: host fetches /api/rbac/namespace/{ns} so the
|
|
401
412
|
// namespace page can show bindings configured here without falling
|
|
402
413
|
// through to GenericRenderer.
|
|
414
|
+
CAPIClusterRenderer?: React.ComponentType<{
|
|
415
|
+
data: any
|
|
416
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
417
|
+
}>
|
|
403
418
|
NamespaceRenderer?: React.ComponentType<{
|
|
404
419
|
data: any
|
|
405
420
|
onNavigate?: (ref: ResourceRef) => void
|
|
@@ -429,6 +444,8 @@ export interface RendererOverrides {
|
|
|
429
444
|
const KNOWN_KINDS = new Set([
|
|
430
445
|
'pods', 'deployments', 'statefulsets', 'daemonsets', 'replicasets',
|
|
431
446
|
'services', 'endpointslices', 'ingresses', 'configmaps', 'secrets', 'jobs', 'cronjobs', 'cronworkflows',
|
|
447
|
+
'jobsets',
|
|
448
|
+
'rayclusters', 'rayservices', 'workloads', 'localqueues', 'clusterqueues', 'admissionchecks', 'provisioningrequests',
|
|
432
449
|
'hpas', 'horizontalpodautoscalers', 'nodes', 'persistentvolumeclaims',
|
|
433
450
|
'rollouts', 'analysisruns', 'analysistemplates', 'clusteranalysistemplates', 'experiments', 'certificates', 'workflows', 'persistentvolumes',
|
|
434
451
|
'storageclasses', 'certificaterequests', 'clusterissuers', 'issuers',
|
|
@@ -681,9 +698,10 @@ export function ResourceRendererDispatch({
|
|
|
681
698
|
const isGroupGatedKind =
|
|
682
699
|
kind === 'clusters' || kind === 'backups' || kind === 'scheduledbackups' || kind === 'poolers'
|
|
683
700
|
|| kind === 'objectstores' || kind === 'databases' || kind === 'publications'
|
|
684
|
-
|| kind === 'subscriptions' || kind === 'imagecatalogs' || kind === 'clusterimagecatalogs'
|
|
701
|
+
|| kind === 'subscriptions' || kind === 'imagecatalogs' || kind === 'clusterimagecatalogs' || kind === 'jobsets'
|
|
685
702
|
|| kind === 'policies' || kind === 'rollouts' || kind === 'experiments'
|
|
686
|
-
|| kind === '
|
|
703
|
+
|| kind === 'rayclusters' || kind === 'rayservices' || kind === 'admissionchecks' || kind === 'provisioningrequests'
|
|
704
|
+
|| kind === 'machines' || kind === 'machinesets' || kind === 'workloads' || kind === 'localqueues' || kind === 'clusterqueues'
|
|
687
705
|
const isCNPGApiVersion = isApiGroup(data?.apiVersion, CNPG_GROUP)
|
|
688
706
|
const groupGatedMatched =
|
|
689
707
|
(kind === 'clusters' && (isCNPGApiVersion || isApiGroup(data?.apiVersion, 'cluster.x-k8s.io')))
|
|
@@ -703,6 +721,13 @@ export function ResourceRendererDispatch({
|
|
|
703
721
|
|| (kind === 'experiments' && isApiGroup(data?.apiVersion, 'argoproj.io'))
|
|
704
722
|
|| ((kind === 'machines' || kind === 'machinesets')
|
|
705
723
|
&& isApiGroup(data?.apiVersion, 'cluster.x-k8s.io'))
|
|
724
|
+
|| (kind === 'rayclusters' && data?.apiVersion === 'ray.io/v1')
|
|
725
|
+
|| (kind === 'rayservices' && data?.apiVersion === 'ray.io/v1')
|
|
726
|
+
|| (kind === 'jobsets' && isJobSetV1Alpha2(data))
|
|
727
|
+
|| (kind === 'workloads' && isApiGroup(data?.apiVersion, 'kueue.x-k8s.io'))
|
|
728
|
+
|| (kind === 'admissionchecks' && isKueueQueueResource(data))
|
|
729
|
+
|| (kind === 'provisioningrequests' && ['autoscaling.x-k8s.io/v1', 'autoscaling.x-k8s.io/v1beta1'].includes(data?.apiVersion))
|
|
730
|
+
|| ((kind === 'localqueues' || kind === 'clusterqueues') && isKueueQueueResource(data))
|
|
706
731
|
const groupGatedFallthrough = isGroupGatedKind && !groupGatedMatched
|
|
707
732
|
|
|
708
733
|
const calicoApiVersionMatched = isCalicoApiVersion(data?.apiVersion)
|
|
@@ -723,6 +748,9 @@ export function ResourceRendererDispatch({
|
|
|
723
748
|
|
|
724
749
|
const isKnownKind = KNOWN_KINDS.has(kind) || isCrossplaneMR || isCrossplaneClaim || isCrossplaneXR
|
|
725
750
|
|
|
751
|
+
const RayClusterComp = rendererOverrides?.RayClusterRenderer ?? RayClusterRenderer
|
|
752
|
+
const RayServiceComp = rendererOverrides?.RayServiceRenderer ?? RayServiceRenderer
|
|
753
|
+
const KueueWorkloadComp = rendererOverrides?.KueueWorkloadRenderer ?? KueueWorkloadRenderer
|
|
726
754
|
const PodComp = rendererOverrides?.PodRenderer ?? PodRenderer
|
|
727
755
|
const KarpenterNodePoolComp = rendererOverrides?.KarpenterNodePoolRenderer ?? KarpenterNodePoolRenderer
|
|
728
756
|
const WorkloadComp = rendererOverrides?.WorkloadRenderer ?? WorkloadRenderer
|
|
@@ -748,6 +776,7 @@ export function ResourceRendererDispatch({
|
|
|
748
776
|
const RoleComp = rendererOverrides?.RoleRenderer ?? RoleRenderer
|
|
749
777
|
const RoleBindingComp = rendererOverrides?.RoleBindingRenderer ?? RoleBindingRenderer
|
|
750
778
|
const NamespaceComp = rendererOverrides?.NamespaceRenderer ?? NamespaceRenderer
|
|
779
|
+
const CAPIClusterComp = rendererOverrides?.CAPIClusterRenderer ?? CAPIClusterRenderer
|
|
751
780
|
const HPAComp = rendererOverrides?.HPARenderer ?? HPARenderer
|
|
752
781
|
const PVCComp = rendererOverrides?.PVCRenderer ?? PVCRenderer
|
|
753
782
|
const RolloutComp = rendererOverrides?.RolloutRenderer ?? RolloutRenderer
|
|
@@ -790,6 +819,14 @@ export function ResourceRendererDispatch({
|
|
|
790
819
|
{kind === 'configmaps' && <ConfigMapRenderer data={data} relationships={relationships} onNavigate={onNavigate} />}
|
|
791
820
|
{kind === 'secrets' && <SecretRenderer data={data} relationships={relationships} onNavigate={onNavigate} certificateInfo={certificateInfo} resourceData={data} onSaveSecretValue={onSaveSecretValue} isSaving={isSavingSecret} />}
|
|
792
821
|
{kind === 'jobs' && !nonCoreJobFallthrough && <JobRenderer data={data} />}
|
|
822
|
+
{kind === 'rayclusters' && data?.apiVersion === 'ray.io/v1' && <RayClusterComp data={data} onNavigate={onNavigate} />}
|
|
823
|
+
{kind === 'rayservices' && data?.apiVersion === 'ray.io/v1' && <RayServiceComp data={data} onNavigate={onNavigate} />}
|
|
824
|
+
{kind === 'workloads' && isApiGroup(data?.apiVersion, 'kueue.x-k8s.io') && <KueueWorkloadComp data={data} onNavigate={onNavigate} />}
|
|
825
|
+
{kind === 'admissionchecks' && isKueueQueueResource(data) && <AdmissionCheckRenderer data={data} onNavigate={onNavigate} />}
|
|
826
|
+
{kind === 'provisioningrequests' && ['autoscaling.x-k8s.io/v1', 'autoscaling.x-k8s.io/v1beta1'].includes(data?.apiVersion) && <ProvisioningRequestRenderer data={data} onNavigate={onNavigate} />}
|
|
827
|
+
{kind === 'localqueues' && isKueueQueueResource(data) && <LocalQueueRenderer data={data} onNavigate={onNavigate} />}
|
|
828
|
+
{kind === 'clusterqueues' && isKueueQueueResource(data) && <ClusterQueueRenderer data={data} onNavigate={onNavigate} />}
|
|
829
|
+
{kind === 'jobsets' && isJobSetV1Alpha2(data) && <JobSetRenderer data={data} />}
|
|
793
830
|
{kind === 'cronjobs' && <CronJobRenderer data={data} onNavigate={onNavigate} />}
|
|
794
831
|
{kind === 'cronworkflows' && <CronWorkflowRenderer data={data} onNavigate={onNavigate} />}
|
|
795
832
|
{(kind === 'hpas' || kind === 'horizontalpodautoscalers') && <HPAComp data={data} onNavigate={onNavigate} hpaDiagnosis={hpaDiagnosis} />}
|
|
@@ -892,7 +929,7 @@ export function ResourceRendererDispatch({
|
|
|
892
929
|
{kind === 'clusterexternalsecrets' && <ClusterExternalSecretRenderer data={data} onNavigate={onNavigate} />}
|
|
893
930
|
{(kind === 'secretstores' || kind === 'clustersecretstores') && <SecretStoreRenderer data={data} />}
|
|
894
931
|
{kind === 'clusters' && isApiGroup(data?.apiVersion, CNPG_GROUP) && <CNPGClusterComp data={data} onNavigate={onNavigate} />}
|
|
895
|
-
{kind === 'clusters' && isApiGroup(data?.apiVersion, 'cluster.x-k8s.io') && <
|
|
932
|
+
{kind === 'clusters' && isApiGroup(data?.apiVersion, 'cluster.x-k8s.io') && <CAPIClusterComp data={data} onNavigate={onNavigate} />}
|
|
896
933
|
{kind === 'scheduledbackups' && isApiGroup(data?.apiVersion, CNPG_GROUP) && <CNPGScheduledBackupRenderer data={data} onNavigate={onNavigate} />}
|
|
897
934
|
{kind === 'poolers' && isApiGroup(data?.apiVersion, CNPG_GROUP) && <CNPGPoolerRenderer data={data} onNavigate={onNavigate} />}
|
|
898
935
|
{/* Cluster API (CAPI) */}
|
|
@@ -1083,14 +1120,14 @@ export function getResourceStatus(kind: string, data: any): { text: string; colo
|
|
|
1083
1120
|
if (k === 'clusterqueues' && data?.apiVersion?.startsWith('kueue.x-k8s.io/')) return getClusterQueueStatus(data)
|
|
1084
1121
|
if (k === 'localqueues' && data?.apiVersion?.startsWith('kueue.x-k8s.io/')) return getLocalQueueStatus(data)
|
|
1085
1122
|
if (k === 'resourceflavors' && data?.apiVersion?.startsWith('kueue.x-k8s.io/')) return getResourceFlavorStatus(data)
|
|
1086
|
-
if (k === 'admissionchecks' && data
|
|
1087
|
-
if (k === 'provisioningrequests' &&
|
|
1088
|
-
if (k === 'rayclusters' && data?.apiVersion
|
|
1123
|
+
if (k === 'admissionchecks' && isKueueQueueResource(data)) return getAdmissionCheckStatus(data)
|
|
1124
|
+
if (k === 'provisioningrequests' && ['autoscaling.x-k8s.io/v1', 'autoscaling.x-k8s.io/v1beta1'].includes(data?.apiVersion)) return getProvisioningRequestStatus(data)
|
|
1125
|
+
if (k === 'rayclusters' && data?.apiVersion === 'ray.io/v1') return getRayClusterStatus(data)
|
|
1089
1126
|
if (k === 'rayjobs' && data?.apiVersion?.startsWith('ray.io/')) return getRayJobStatus(data)
|
|
1090
|
-
if (k === 'rayservices' && data?.apiVersion
|
|
1127
|
+
if (k === 'rayservices' && data?.apiVersion === 'ray.io/v1') return getRayServiceStatus(data)
|
|
1091
1128
|
if (k === 'raycronjobs' && data?.apiVersion?.startsWith('ray.io/')) return getRayCronJobStatus(data)
|
|
1092
1129
|
if (k === 'leaderworkersets' && data?.apiVersion?.startsWith('leaderworkerset.x-k8s.io/')) return getLeaderWorkerSetStatus(data)
|
|
1093
|
-
if (k === 'jobsets' && data
|
|
1130
|
+
if (k === 'jobsets' && isJobSetV1Alpha2(data)) return getJobSetStatus(data)
|
|
1094
1131
|
if (k === 'inferenceservices' && data?.apiVersion?.startsWith('serving.kserve.io/')) return getInferenceServiceStatus(data)
|
|
1095
1132
|
if ((k === 'servingruntimes' || k === 'clusterservingruntimes') && data?.apiVersion?.startsWith('serving.kserve.io/')) return getServingRuntimeStatus(data)
|
|
1096
1133
|
if (k === 'inferencegraphs' && data?.apiVersion?.startsWith('serving.kserve.io/')) return getInferenceGraphStatus(data)
|
|
@@ -30,6 +30,7 @@ import { isChangeEvent, isHistoricalEvent } from '../../types'
|
|
|
30
30
|
import { getHealthBadgeColor, SEVERITY_BADGE } from '../../utils/badge-colors'
|
|
31
31
|
import { ResourceRefBadge } from '../ui/drawer-components'
|
|
32
32
|
import type { NavigateToResource } from '../../utils/navigation'
|
|
33
|
+
import { builtinGroupForKind } from '../../utils/api-resources'
|
|
33
34
|
import { kindToPluralWithGroup, refToSelectedResource, apiVersionToGroup } from '../../utils/navigation'
|
|
34
35
|
import { pluralize } from '../../utils/pluralize'
|
|
35
36
|
|
|
@@ -602,8 +603,10 @@ function ActivityCard({ item, expanded, onToggle, onResourceClick, compact, sele
|
|
|
602
603
|
{/* Resource info */}
|
|
603
604
|
<div className="flex items-center gap-2 flex-wrap">
|
|
604
605
|
<button
|
|
606
|
+
disabled={!item.apiVersion && builtinGroupForKind(item.kind) === undefined}
|
|
605
607
|
onClick={(e) => {
|
|
606
608
|
e.stopPropagation()
|
|
609
|
+
if (!item.apiVersion && builtinGroupForKind(item.kind) === undefined) return
|
|
607
610
|
onResourceClick?.({
|
|
608
611
|
kind: kindToPluralWithGroup(
|
|
609
612
|
item.kind,
|
|
@@ -786,8 +789,10 @@ function AggregatedActivityCard({ first, last, count, reason, expanded, onToggle
|
|
|
786
789
|
{/* Resource info */}
|
|
787
790
|
<div className="flex items-center gap-2 flex-wrap">
|
|
788
791
|
<button
|
|
792
|
+
disabled={!first.apiVersion && builtinGroupForKind(first.kind) === undefined}
|
|
789
793
|
onClick={(e) => {
|
|
790
794
|
e.stopPropagation()
|
|
795
|
+
if (!first.apiVersion && builtinGroupForKind(first.kind) === undefined) return
|
|
791
796
|
onResourceClick?.({
|
|
792
797
|
kind: kindToPluralWithGroup(
|
|
793
798
|
first.kind,
|
|
@@ -324,6 +324,21 @@ describe('EventDetailPanel cluster mode (a ×N pill exposes every member)', () =
|
|
|
324
324
|
// Ordered most-severe-first, as the drawer opens it (Warning → delete → add → update).
|
|
325
325
|
const ordered = [...clusterEvents].sort((a, b) => eventSeverityRank(b) - eventSeverityRank(a))
|
|
326
326
|
|
|
327
|
+
it('does not link an ambiguous event through the detail badge', () => {
|
|
328
|
+
const event = mk('ambiguous', 'update', T0, {kind: 'Cluster', name: 'shared'});
|
|
329
|
+
const lanes = [{id: 'unresolved', kind: 'Cluster', name: 'shared', namespace: 'default', events: [event], isWorkload: false, identityAmbiguous: true}];
|
|
330
|
+
const html = renderToString(<EventDetailPanel events={[event]} selectedId={event.id} onSelectId={() => {}} onClose={() => {}} onResourceClick={() => {}} resourceLanes={lanes} />);
|
|
331
|
+
expect(html).not.toMatch(/<button[^>]*class="badge[^>]*>/);
|
|
332
|
+
expect(html).toContain('shared');
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('keeps an unambiguous resolved historical event link', () => {
|
|
336
|
+
const event = mk('resolved', 'update', T0, {kind: 'Cluster', name: 'shared'});
|
|
337
|
+
const lanes = [{id: 'exact', kind: 'Cluster', name: 'shared', namespace: 'default', group: 'postgresql.cnpg.io', events: [event], isWorkload: false, identityResolved: true}];
|
|
338
|
+
const html = renderToString(<EventDetailPanel events={[event]} selectedId={event.id} onSelectId={() => {}} onClose={() => {}} onResourceClick={() => {}} resourceLanes={lanes} />);
|
|
339
|
+
expect(html).toMatch(/<button[^>]*class="badge[^>]*>/);
|
|
340
|
+
})
|
|
341
|
+
|
|
327
342
|
it('renders a row for every clustered event plus the summary header', () => {
|
|
328
343
|
const html = renderToString(
|
|
329
344
|
<EventDetailPanel events={ordered} selectedId={ordered[0].id} onSelectId={() => {}} onClose={() => {}} />,
|