@skyhook-io/k8s-ui 1.14.12 → 1.16.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.
- package/package.json +3 -3
- package/src/components/audit/AuditFindingsTable.tsx +1 -1
- package/src/components/checks/ChecksView.tsx +8 -8
- package/src/components/compare/CompareResourcePicker.tsx +4 -3
- package/src/components/gitops/GitOpsDetailLayout.tsx +3 -1
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +12 -2
- package/src/components/gitops/RollbackDialog.tsx +4 -3
- package/src/components/gitops/SyncOptionsDialog.tsx +4 -3
- package/src/components/gitops/health-provenance.test.ts +1 -1
- package/src/components/gitops/index.ts +1 -0
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +1 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +4 -1
- package/src/components/issues/IssuesView.tsx +2 -2
- package/src/components/logs/LogCore.tsx +131 -97
- package/src/components/logs/StructuredLogLine.tsx +18 -28
- package/src/components/logs/log-palette.ts +4 -0
- package/src/components/logs/useLogBuffer.ts +8 -39
- package/src/components/logs/useLogSearch.ts +52 -24
- package/src/components/namespace-switcher/NamespacePicker.test.tsx +140 -0
- package/src/components/namespace-switcher/NamespacePicker.tsx +26 -20
- package/src/components/resources/KueueAdmissionSection.test.tsx +183 -6
- package/src/components/resources/KueueAdmissionSection.tsx +140 -51
- package/src/components/resources/ResourcesView.tsx +4 -0
- package/src/components/resources/printer-columns.test.ts +1 -1
- package/src/components/resources/renderers/JobRenderer.tsx +5 -1
- package/src/components/resources/renderers/JobSetRenderer.tsx +4 -1
- package/src/components/resources/renderers/KueueQueueNavigation.test.tsx +2 -2
- package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +2 -15
- package/src/components/resources/renderers/PodRenderer.test.tsx +15 -0
- package/src/components/resources/renderers/PodRenderer.tsx +10 -2
- package/src/components/resources/renderers/PolicySection.test.tsx +42 -0
- package/src/components/resources/renderers/PolicySection.tsx +10 -1
- package/src/components/resources/renderers/RayClusterRenderer.tsx +7 -7
- package/src/components/resources/renderers/RayJobRenderer.test.tsx +71 -0
- package/src/components/resources/renderers/RayJobRenderer.tsx +134 -0
- package/src/components/resources/renderers/ReflectorSection.test.tsx +4 -2
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
- package/src/components/resources/renderers/WorkloadRenderer.tsx +7 -3
- package/src/components/resources/renderers/index.ts +2 -0
- package/src/components/resources/resource-utils-kueue.ts +14 -0
- package/src/components/resources/resource-utils-ray.ts +14 -4
- package/src/components/shared/CreateResourceDialog.tsx +6 -2
- package/src/components/shared/DrainPlanDialog.test.tsx +10 -0
- package/src/components/shared/DrainPlanDialog.tsx +14 -3
- package/src/components/shared/ResourceActionsBar.tsx +18 -11
- package/src/components/shared/ResourceRendererDispatch.test.tsx +12 -5
- package/src/components/shared/ResourceRendererDispatch.tsx +15 -6
- package/src/components/shared/SetImageDialog.tsx +4 -2
- package/src/components/shared/revision-role-badges.test.ts +1 -1
- package/src/components/trace/reachGraphModel.test.ts +1 -0
- package/src/components/trace/reachInspector.test.ts +4 -9
- package/src/components/ui/ConfirmDialog.tsx +5 -3
- package/src/components/ui/DialogPortal.test.tsx +279 -0
- package/src/components/ui/DialogPortal.tsx +147 -9
- package/src/components/ui/FilterPill.tsx +1 -1
- package/src/components/ui/RadarUpgradeNote.click.test.tsx +24 -0
- package/src/components/ui/RadarUpgradeNote.test.tsx +66 -0
- package/src/components/ui/RadarUpgradeNote.tsx +129 -0
- package/src/components/ui/ResourceBar.tsx +12 -6
- package/src/components/ui/SelectMenu.tsx +62 -19
- package/src/components/ui/YamlReview.tsx +4 -1
- package/src/components/ui/drawer-components.test.tsx +14 -1
- package/src/components/ui/drawer-components.tsx +50 -40
- package/src/components/ui/index.ts +8 -0
- package/src/components/workload/WorkloadView.tsx +109 -3
- package/src/types/core.ts +24 -4
- package/src/types/fetch-error.ts +33 -0
- package/src/utils/log-format.ts +1 -1
- package/src/utils/log-level.test.ts +269 -0
- package/src/utils/log-level.ts +301 -0
- package/src/utils/resource-hierarchy.test.ts +69 -0
- package/src/utils/resource-hierarchy.ts +87 -52
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { Badge } from '../../ui/Badge'
|
|
4
|
+
import { RayJobRenderer } from './RayJobRenderer'
|
|
5
|
+
import { getRayJobStatus } from '../resource-utils-ray'
|
|
6
|
+
import { RayJobCell } from './ray-cells'
|
|
7
|
+
import { getResourceStatus } from '../../shared/ResourceRendererDispatch'
|
|
8
|
+
|
|
9
|
+
const root = { apiVersion: 'ray.io/v1', kind: 'RayJob', metadata: { name: 'train', namespace: 'ml' }, spec: {} }
|
|
10
|
+
const render = (data: any) => renderToStaticMarkup(<RayJobRenderer data={data} onNavigate={() => {}} />)
|
|
11
|
+
|
|
12
|
+
describe('RayJob native evidence', () => {
|
|
13
|
+
it.each(['Retrying', 'Suspending', 'Suspended', 'Failed', 'ValidationFailed'])('prioritizes lifecycle %s across summary, table and header', jobDeploymentStatus => {
|
|
14
|
+
const data = { ...root, status: { jobDeploymentStatus, jobStatus: 'RUNNING' } }
|
|
15
|
+
expect(getRayJobStatus(data).text).toBe(jobDeploymentStatus)
|
|
16
|
+
expect(getResourceStatus('rayjobs', data)?.text).toBe(jobDeploymentStatus)
|
|
17
|
+
expect(renderToStaticMarkup(<RayJobCell resource={data} column="status" />)).toContain(jobDeploymentStatus)
|
|
18
|
+
expect(render(data)).toContain('Last reported application state')
|
|
19
|
+
expect(render(data)).toContain('RUNNING')
|
|
20
|
+
})
|
|
21
|
+
it('shows completed suspension with a neutral lifecycle badge', () => {
|
|
22
|
+
expect(render({ ...root, status: { jobDeploymentStatus: 'Suspended' } })).toContain(renderToStaticMarkup(<Badge severity="neutral">Suspended</Badge>))
|
|
23
|
+
})
|
|
24
|
+
it('does not call stopped completion success or missing status healthy', () => {
|
|
25
|
+
expect(getRayJobStatus({ status: { jobStatus: 'STOPPED', jobDeploymentStatus: 'Complete' } }).text).toBe('Stopped')
|
|
26
|
+
expect(render(root)).toContain('Not reported')
|
|
27
|
+
expect(render(root)).not.toContain('Succeeded')
|
|
28
|
+
expect(render({ ...root, status: { jobDeploymentStatus: 'Failed', jobStatus: 'STOPPED' } })).not.toContain('does not establish')
|
|
29
|
+
})
|
|
30
|
+
it('keeps application and controller times separate and clears absent evidence', () => {
|
|
31
|
+
const html = render({ ...root, status: { startTime: '2026-09-20T00:00:00Z', rayJobInfo: { startTime: '2026-09-20T00:01:00Z' }, message: 'capacity timeout', reason: 'DeadlineExceeded' } })
|
|
32
|
+
expect(html).toContain('Application started')
|
|
33
|
+
expect(html).toContain('Controller attempt started')
|
|
34
|
+
expect(html).toContain('capacity timeout')
|
|
35
|
+
expect(render({ ...root, status: { jobDeploymentStatus: 'Suspended' } })).not.toContain('Application started')
|
|
36
|
+
})
|
|
37
|
+
it.each(['HTTPMode', 'InteractiveMode', 'SidecarMode', 'NewMode'])('never invents a submitter for %s', submissionMode => {
|
|
38
|
+
const html = render({ ...root, spec: { submissionMode } })
|
|
39
|
+
expect(html).not.toContain('Observed submitter Job')
|
|
40
|
+
expect(html).toContain(submissionMode)
|
|
41
|
+
if (submissionMode === 'InteractiveMode') expect(html).toContain('spec.jobId')
|
|
42
|
+
})
|
|
43
|
+
it('does not link remote runtime names locally', () => {
|
|
44
|
+
const html = render({ ...root, spec: { managedBy: 'kueue.x-k8s.io/multikueue' }, status: { rayClusterName: 'remote-runtime' } })
|
|
45
|
+
expect(html).toContain('remote-runtime')
|
|
46
|
+
expect(html).not.toMatch(/<button[^>]*>remote-runtime<\/button>/)
|
|
47
|
+
expect(html).not.toContain('Observed submitter Job')
|
|
48
|
+
})
|
|
49
|
+
it('explains selected-cluster cleanup and gate-dependent policies', () => {
|
|
50
|
+
expect(render({ ...root, spec: { clusterSelector: { 'ray.io/cluster': 'shared' }, shutdownAfterJobFinishes: true } })).toContain('cleanup is not applied')
|
|
51
|
+
const html = render({ ...root, spec: { deletionStrategy: { deletionRules: [{ policy: 'DeleteWorkers', condition: { jobStatus: 'SUCCEEDED', ttlSeconds: 0 } }] } } })
|
|
52
|
+
expect(html).toContain('RayJobDeletionPolicy')
|
|
53
|
+
expect(html).toContain('DeleteWorkers')
|
|
54
|
+
expect(html).toContain('TTL 0s')
|
|
55
|
+
expect(html).not.toContain('No completion shutdown requested')
|
|
56
|
+
expect(render({ ...root, spec: { clusterSelector: { bad: 'selector' } } })).toContain('cleanup is not applied')
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('controller message disclosure', () => {
|
|
61
|
+
it('keeps short messages inline', () => {
|
|
62
|
+
expect(render({ ...root, status: { message: 'Submission failed.' } })).not.toContain('Full controller message')
|
|
63
|
+
})
|
|
64
|
+
it.each(['x'.repeat(20000), 'Failure\nline two\nline three\nline four\nTraceback: final cause'])('retains the entire long message behind a collapsed disclosure', message => {
|
|
65
|
+
const html = render({ ...root, status: { message } })
|
|
66
|
+
expect(html).toContain('Full controller message')
|
|
67
|
+
expect(html).toContain('aria-expanded="false"')
|
|
68
|
+
expect(html).toContain(message)
|
|
69
|
+
expect(html.split('Full controller message')[0]).toContain(message.trim().split('\n').at(-1)!.slice(-240))
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import type { ResourceRef } from '../../../types'
|
|
3
|
+
import { formatRelativeAgeTime } from '../../../utils/format'
|
|
4
|
+
import { Tooltip } from '../../ui/Tooltip'
|
|
5
|
+
import { Badge } from '../../ui/Badge'
|
|
6
|
+
import { Disclosure } from '../../ui/Disclosure'
|
|
7
|
+
import { AlertBanner, Property, PropertyList, ResourceLink } from '../../ui/drawer-components'
|
|
8
|
+
import { rayJobIsLocal, rayJobSubmissionMode } from '../resource-utils-ray'
|
|
9
|
+
|
|
10
|
+
export interface RayJobRendererProps {
|
|
11
|
+
data: any
|
|
12
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
13
|
+
submitterEvidence?: ReactNode
|
|
14
|
+
selectedClusterEvidence?: ReactNode
|
|
15
|
+
admissionContent?: ReactNode
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const card = 'min-w-0 overflow-hidden rounded-lg border border-theme-border bg-theme-surface'
|
|
19
|
+
const header = 'border-b border-theme-border bg-theme-elevated px-3 py-2.5 text-xs font-medium uppercase tracking-wider text-theme-text-secondary'
|
|
20
|
+
|
|
21
|
+
export function RayJobRenderer({ data, onNavigate, submitterEvidence, selectedClusterEvidence, admissionContent }: RayJobRendererProps) {
|
|
22
|
+
const spec = data.spec ?? {}
|
|
23
|
+
const status = data.status ?? {}
|
|
24
|
+
const local = rayJobIsLocal(data)
|
|
25
|
+
const mode = rayJobSubmissionMode(data)
|
|
26
|
+
const selectsExisting = Object.keys(spec.clusterSelector ?? {}).length > 0
|
|
27
|
+
const selected = spec.clusterSelector?.['ray.io/cluster']
|
|
28
|
+
const cluster = selected || status.rayClusterName
|
|
29
|
+
const lifecycle = status.jobDeploymentStatus
|
|
30
|
+
const jobStatus = status.jobStatus
|
|
31
|
+
const failed = lifecycle === 'Failed' || lifecycle === 'ValidationFailed'
|
|
32
|
+
const transitioning = ['Retrying', 'Suspending', 'Suspended'].includes(lifecycle)
|
|
33
|
+
const rules: any[] = spec.deletionStrategy?.deletionRules ?? []
|
|
34
|
+
const time = (value: string) => <Tooltip content={value}><time dateTime={value}>{formatRelativeAgeTime(value)}</time></Tooltip>
|
|
35
|
+
return <div className="@container/rayjob space-y-4">
|
|
36
|
+
{!local && <AlertBanner variant="info" title="Externally managed RayJob" message={`Managed by ${spec.managedBy}. Reported runtime names may refer to another cluster; local absence does not establish execution state.`} />}
|
|
37
|
+
{admissionContent}
|
|
38
|
+
<div className="grid items-start gap-4 @min-[1024px]/rayjob:grid-cols-[minmax(0,1.7fr)_minmax(320px,0.9fr)]">
|
|
39
|
+
<section className={card} aria-label="RayJob execution">
|
|
40
|
+
<h3 className={header}>Execution</h3>
|
|
41
|
+
<div className="space-y-3 p-3 text-sm">
|
|
42
|
+
<div className="flex flex-wrap gap-x-6 gap-y-3">
|
|
43
|
+
<div><p className="mb-1 text-xs text-theme-text-secondary">Controller lifecycle</p><Badge severity={failed ? 'error' : transitioning && lifecycle !== 'Suspended' ? 'warning' : 'neutral'}>{lifecycle || 'Not reported'}</Badge></div>
|
|
44
|
+
<div><p className="mb-1 text-xs text-theme-text-secondary">Last reported application state</p><Badge severity={failed || transitioning ? 'neutral' : jobStatus === 'FAILED' ? 'error' : jobStatus === 'RUNNING' || jobStatus === 'SUCCEEDED' ? 'success' : 'neutral'}>{jobStatus || 'Not reported'}</Badge></div>
|
|
45
|
+
</div>
|
|
46
|
+
{status.reason && <p className="break-words font-medium">{status.reason}</p>}
|
|
47
|
+
{status.message && <ControllerMessage message={status.message} />}
|
|
48
|
+
{failed && jobStatus && !['FAILED', 'STOPPED'].includes(jobStatus) && <p className="text-xs text-theme-text-secondary">{jobStatus === 'SUCCEEDED' ? 'The application reported success, but the controller lifecycle failed. Review the controller message and submitter evidence.' : 'Controller failure does not establish that the application has stopped.'}</p>}
|
|
49
|
+
{lifecycle === 'Retrying' && <p className="text-xs text-theme-text-secondary">Preparing to retry. The reported failure and submitter may belong to the attempt being cleaned up.</p>}
|
|
50
|
+
<PropertyList>
|
|
51
|
+
<Property label="Suspension requested" value={spec.suspend === true ? 'Yes' : 'No'} />
|
|
52
|
+
{status.jobId && <Property label="Ray job ID" value={<span className="break-all font-mono">{status.jobId}</span>} />}
|
|
53
|
+
{status.rayJobInfo?.startTime && <Property label="Application started" value={time(status.rayJobInfo.startTime)} />}
|
|
54
|
+
{status.rayJobInfo?.endTime && <Property label="Application ended" value={time(status.rayJobInfo.endTime)} />}
|
|
55
|
+
{status.failed != null && <Property label="Reported failed runs" value={status.failed} />}
|
|
56
|
+
{status.succeeded != null && <Property label="Reported successful runs" value={status.succeeded} />}
|
|
57
|
+
</PropertyList>
|
|
58
|
+
{status.jobStatusCheckFailureStartTime && <AlertBanner variant="warning" title="Application status checks are failing" >{'Checks have been failing since '}{time(status.jobStatusCheckFailureStartTime)}. Application state is the last reported observation.</AlertBanner>}
|
|
59
|
+
</div>
|
|
60
|
+
<div className="border-t border-theme-border p-3 text-xs text-theme-text-secondary">
|
|
61
|
+
<Disclosure summary="Timing and observation details">
|
|
62
|
+
<div className="space-y-2 pt-2">
|
|
63
|
+
{status.startTime && <p>Controller attempt started: {time(status.startTime)}</p>}
|
|
64
|
+
{status.endTime && <p>Controller terminal time: {time(status.endTime)}</p>}
|
|
65
|
+
<p>Application and controller times describe different events. Status is a controller snapshot; cleared fields are not retained as attempt history. See Timeline for recorded events and YAML for the full resource.</p>
|
|
66
|
+
</div>
|
|
67
|
+
</Disclosure>
|
|
68
|
+
</div>
|
|
69
|
+
</section>
|
|
70
|
+
<section className={card} aria-label="RayJob submission and runtime">
|
|
71
|
+
<h3 className={header}>Submission and runtime</h3>
|
|
72
|
+
<div className="space-y-3 p-3 text-sm">
|
|
73
|
+
<p className="font-medium break-words">{mode}</p>
|
|
74
|
+
<p className="text-theme-text-secondary">{mode === 'K8sJobMode' ? 'A Kubernetes Job submits the application to Ray.' : mode === 'HTTPMode' ? 'The controller submits directly through the Ray API; there is no submitter Job.' : mode === 'InteractiveMode' ? 'The controller waits for spec.jobId to identify a manually submitted Ray job.' : mode === 'SidecarMode' ? 'A container in the head Pod submits the application; there is no submitter Job.' : 'Submission behavior is defined by the reported mode.'}</p>
|
|
75
|
+
{selectsExisting && !selected && <p className="text-theme-text-secondary">Cluster selection has no ray.io/cluster name.</p>}
|
|
76
|
+
{cluster ? <div className="space-y-1 border-t border-theme-border pt-3">
|
|
77
|
+
<p className="text-xs text-theme-text-secondary">{selected ? 'Selected existing RayCluster' : 'Reported RayCluster'}</p>
|
|
78
|
+
<p className="break-all font-medium">{local ? <ResourceLink kind="rayclusters" group="ray.io" namespace={data.metadata.namespace} name={cluster} onNavigate={onNavigate} /> : cluster}</p>
|
|
79
|
+
{local && <p className="text-xs text-theme-text-secondary">Open the RayCluster for head/worker Pods and their logs.</p>}
|
|
80
|
+
{selectedClusterEvidence}
|
|
81
|
+
{selected && status.rayClusterName && selected !== status.rayClusterName && <div className="pt-2"><p className="text-xs text-theme-text-secondary">Last reported runtime differs from the selected cluster</p>{local ? <ResourceLink kind="rayclusters" group="ray.io" namespace={data.metadata.namespace} name={status.rayClusterName} onNavigate={onNavigate} /> : <p>{status.rayClusterName}</p>}</div>}
|
|
82
|
+
</div> : <p className="text-theme-text-secondary">No runtime cluster reported.</p>}
|
|
83
|
+
{local && mode === 'K8sJobMode' && <div className="space-y-2 border-t border-theme-border pt-3"><p className="text-xs font-medium text-theme-text-secondary">Observed submitter Job</p>{submitterEvidence ?? <p className="text-theme-text-secondary">Submitter evidence is not available.</p>}</div>}
|
|
84
|
+
</div>
|
|
85
|
+
</section>
|
|
86
|
+
</div>
|
|
87
|
+
<section className={card} aria-label="RayJob configuration">
|
|
88
|
+
<h3 className={header}>Job configuration</h3>
|
|
89
|
+
<div className="space-y-3 p-3 text-sm">
|
|
90
|
+
<p className="text-xs text-theme-text-secondary">Entrypoint</p>
|
|
91
|
+
<pre className="max-h-48 overflow-auto whitespace-pre-wrap break-words font-mono text-xs">{spec.entrypoint || 'Not specified'}</pre>
|
|
92
|
+
<div className="grid gap-4 @min-[1024px]/rayjob:grid-cols-2">
|
|
93
|
+
<PropertyList>
|
|
94
|
+
{spec.entrypointNumCpus != null && <Property label="Entrypoint CPUs" value={spec.entrypointNumCpus} />}
|
|
95
|
+
{spec.entrypointNumGpus != null && <Property label="Entrypoint GPUs" value={spec.entrypointNumGpus} />}
|
|
96
|
+
{spec.entrypointResources && <Property label="Entrypoint custom resources" value={<code className="break-all">{spec.entrypointResources}</code>} />}
|
|
97
|
+
{spec.backoffLimit != null && <Property label="RayJob retry limit" value={spec.backoffLimit} />}
|
|
98
|
+
{spec.submitterConfig?.backoffLimit != null && <Property label="Submitter retry limit" value={spec.submitterConfig.backoffLimit} />}
|
|
99
|
+
{spec.activeDeadlineSeconds != null && <Property label="Active deadline" value={`${spec.activeDeadlineSeconds}s`} />}
|
|
100
|
+
{spec.preRunningDeadlineSeconds != null && <Property label="Pre-running deadline" value={`${spec.preRunningDeadlineSeconds}s`} />}
|
|
101
|
+
</PropertyList>
|
|
102
|
+
<div className="space-y-2 text-theme-text-secondary">
|
|
103
|
+
<p className="font-medium text-theme-text-primary">Runtime cleanup</p>
|
|
104
|
+
{selectsExisting ? <p>Runtime cleanup is not applied to a selected existing cluster.</p> : <>
|
|
105
|
+
{!spec.deletionStrategy && <p>{spec.shutdownAfterJobFinishes === true ? `Cluster shutdown requested after completion${spec.ttlSecondsAfterFinished != null ? ` (TTL ${spec.ttlSecondsAfterFinished}s)` : ''}.` : 'No completion shutdown requested.'}</p>}
|
|
106
|
+
{spec.deletionStrategy && <Disclosure summary={`Deletion policy${rules.length ? ` · ${rules.length} ${rules.length === 1 ? 'rule' : 'rules'}` : ''}`}>
|
|
107
|
+
<div className="space-y-2 pt-2">
|
|
108
|
+
<p>Requires the operator’s RayJobDeletionPolicy feature gate. These are declared rules, not proof that cleanup occurred.</p>
|
|
109
|
+
{rules.map((rule, index) => <p className="break-words" key={index}>{rule.policy} · {rule.condition?.jobStatus ? `Application ${rule.condition.jobStatus}` : `Controller ${rule.condition?.jobDeploymentStatus}`} · TTL {rule.condition?.ttlSeconds ?? 0}s</p>)}
|
|
110
|
+
{spec.deletionStrategy.onSuccess && <p>On success: {spec.deletionStrategy.onSuccess.policy}</p>}
|
|
111
|
+
{spec.deletionStrategy.onFailure && <p>On failure: {spec.deletionStrategy.onFailure.policy}</p>}
|
|
112
|
+
</div>
|
|
113
|
+
</Disclosure>}
|
|
114
|
+
</>}
|
|
115
|
+
<p className="text-xs">Operator configuration can affect cleanup. Missing runtime resources do not establish application failure.</p>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</section>
|
|
120
|
+
</div>
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function ControllerMessage({ message }: { message: string }) {
|
|
124
|
+
const text = message.trim()
|
|
125
|
+
const lines = text.split('\n')
|
|
126
|
+
const isLong = text.length > 480 || lines.length > 4
|
|
127
|
+
if (!isLong) return <p className="max-w-3xl whitespace-pre-wrap break-words">{message}</p>
|
|
128
|
+
return <div className="min-w-0 space-y-2">
|
|
129
|
+
<p className="max-w-3xl whitespace-pre-wrap break-words">{lines.slice(0, 2).join('\n').slice(0, 240)}{'\n…\n'}{text.slice(-240).split('\n').slice(-1)[0]}</p>
|
|
130
|
+
<Disclosure summary="Full controller message" summaryClassName="text-xs text-accent-text">
|
|
131
|
+
<pre tabIndex={0} className="mt-2 max-h-96 overflow-auto whitespace-pre-wrap break-words rounded border border-theme-border bg-theme-base p-3 font-mono text-xs">{message}</pre>
|
|
132
|
+
</Disclosure>
|
|
133
|
+
</div>
|
|
134
|
+
}
|
|
@@ -225,13 +225,14 @@ describe("Reflector resource hierarchy", () => {
|
|
|
225
225
|
it("keeps ConfigMap data ahead of detailed settings and source mirror list", () => {
|
|
226
226
|
const html = renderToStaticMarkup(
|
|
227
227
|
<ResourceRendererDispatch
|
|
228
|
-
kind="configmaps"
|
|
229
228
|
data={{
|
|
230
229
|
...resource({ "reflection-allowed": "true" }),
|
|
231
230
|
data: { setting: "value" },
|
|
232
231
|
}}
|
|
233
232
|
resource={{ kind: "configmaps", namespace: "app", name: "mirror" }}
|
|
234
233
|
relationships={{ reflection: { mirrors: [mirror] } }}
|
|
234
|
+
onCopy={() => {}}
|
|
235
|
+
copied={null}
|
|
235
236
|
/>,
|
|
236
237
|
);
|
|
237
238
|
expect(html.indexOf("Data (1 keys)")).toBeLessThan(
|
|
@@ -242,7 +243,6 @@ describe("Reflector resource hierarchy", () => {
|
|
|
242
243
|
it("puts TLS expiry and mirror provenance ahead of data, with details below", () => {
|
|
243
244
|
const html = renderToStaticMarkup(
|
|
244
245
|
<ResourceRendererDispatch
|
|
245
|
-
kind="secrets"
|
|
246
246
|
data={{
|
|
247
247
|
...resource({ reflects: "source/settings" }),
|
|
248
248
|
type: "kubernetes.io/tls",
|
|
@@ -268,6 +268,8 @@ describe("Reflector resource hierarchy", () => {
|
|
|
268
268
|
],
|
|
269
269
|
} as any
|
|
270
270
|
}
|
|
271
|
+
onCopy={() => {}}
|
|
272
|
+
copied={null}
|
|
271
273
|
/>,
|
|
272
274
|
);
|
|
273
275
|
expect(html.indexOf("Certificate has expired")).toBeLessThan(
|
|
@@ -371,7 +371,7 @@ describe('rolloutActions promote reachability', () => {
|
|
|
371
371
|
|
|
372
372
|
it('blocks promote, promote-full and skip-step alike on an aborted rollout', () => {
|
|
373
373
|
const data = canaryRollout({ phase: 'Degraded', abort: true, currentStepIndex: 1 })
|
|
374
|
-
for (const action of ['promote', 'promote-full', 'skip-step']) {
|
|
374
|
+
for (const action of ['promote', 'promote-full', 'skip-step'] as const) {
|
|
375
375
|
expect(blockedReason(data, action)).toBe('Retry the rollout first')
|
|
376
376
|
}
|
|
377
377
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react'
|
|
1
|
+
import { useState, useEffect, useId } from 'react'
|
|
2
2
|
import { Server, ExternalLink, Scale, Minus, Plus, Shield } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import { PolicySection } from './PolicySection'
|
|
@@ -131,6 +131,7 @@ function compactHPASummary(diagnosis: HPADiagnosis): string {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
export function WorkloadRenderer({ kind, data, onNavigate, onViewPods, onScale, isScalePending, scaleBlockedBy, scalerDiagnostics, workloadPods, onRequestRefresh, rbacData, rbacLoading, rbacError, policyData, policyLoading, policyError, namespaceLimitRangeNames }: WorkloadRendererProps) {
|
|
134
|
+
const titleId = useId()
|
|
134
135
|
const status = data.status || {}
|
|
135
136
|
const spec = data.spec || {}
|
|
136
137
|
const metadata = data.metadata || {}
|
|
@@ -288,13 +289,14 @@ export function WorkloadRenderer({ kind, data, onNavigate, onViewPods, onScale,
|
|
|
288
289
|
</Section>
|
|
289
290
|
|
|
290
291
|
{/* Scale Dialog */}
|
|
291
|
-
<DialogPortal open={showScaleDialog} onClose={() => setShowScaleDialog(false)} className="w-80 p-4">
|
|
292
|
-
<h3 className="text-sm font-medium text-theme-text-primary mb-4">
|
|
292
|
+
<DialogPortal ariaLabelledBy={titleId} open={showScaleDialog} onClose={() => setShowScaleDialog(false)} className="w-80 p-4">
|
|
293
|
+
<h3 id={titleId} className="text-sm font-medium text-theme-text-primary mb-4">
|
|
293
294
|
Scale {metadata.name}
|
|
294
295
|
</h3>
|
|
295
296
|
|
|
296
297
|
<div className="flex items-center justify-center gap-4 mb-4">
|
|
297
298
|
<button
|
|
299
|
+
aria-label="Decrease replicas"
|
|
298
300
|
onClick={() => setTargetReplicas(Math.max(0, targetReplicas - 1))}
|
|
299
301
|
className="p-2 rounded-lg bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
300
302
|
disabled={targetReplicas <= 0}
|
|
@@ -307,12 +309,14 @@ export function WorkloadRenderer({ kind, data, onNavigate, onViewPods, onScale,
|
|
|
307
309
|
min="0"
|
|
308
310
|
max="10000"
|
|
309
311
|
value={targetReplicas}
|
|
312
|
+
aria-label="Replicas"
|
|
310
313
|
onChange={(e) => setTargetReplicas(Math.min(10000, Math.max(0, parseInt(e.target.value) || 0)))}
|
|
311
314
|
className="w-20 text-center text-2xl font-semibold bg-theme-elevated border border-theme-border rounded-lg py-2 text-theme-text-primary focus:outline-none focus:border-blue-500"
|
|
312
315
|
autoFocus
|
|
313
316
|
/>
|
|
314
317
|
|
|
315
318
|
<button
|
|
319
|
+
aria-label="Increase replicas"
|
|
316
320
|
onClick={() => setTargetReplicas(Math.min(10000, targetReplicas + 1))}
|
|
317
321
|
disabled={targetReplicas >= 10000}
|
|
318
322
|
className="p-2 rounded-lg bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
@@ -190,3 +190,5 @@ export { AdmissionCheckRenderer, ProvisioningRequestRenderer } from './KueueProv
|
|
|
190
190
|
export { RayServiceRenderer } from './RayServiceRenderer'
|
|
191
191
|
|
|
192
192
|
export { RayClusterRenderer } from './RayClusterRenderer'
|
|
193
|
+
|
|
194
|
+
export { RayJobRenderer } from './RayJobRenderer'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Kueue + Cluster Autoscaler ProvisioningRequest CRD utility functions
|
|
2
2
|
|
|
3
|
+
import type { BadgeSeverity } from '../ui/Badge'
|
|
3
4
|
import type { StatusBadge } from './resource-utils'
|
|
4
5
|
import { healthColors } from './resource-utils'
|
|
5
6
|
|
|
@@ -293,3 +294,16 @@ export function getProvisioningRequestClassName(resource: any): string {
|
|
|
293
294
|
export function getProvisioningRequestPodSetCount(resource: any): number {
|
|
294
295
|
return (resource?.spec?.podSets || []).length
|
|
295
296
|
}
|
|
297
|
+
|
|
298
|
+
export function admissionCheckSeverity(state?: string): BadgeSeverity {
|
|
299
|
+
switch (state) {
|
|
300
|
+
case 'Ready':
|
|
301
|
+
return 'success'
|
|
302
|
+
case 'Rejected':
|
|
303
|
+
return 'error'
|
|
304
|
+
case 'Retry':
|
|
305
|
+
return 'warning'
|
|
306
|
+
default:
|
|
307
|
+
return 'neutral'
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -63,6 +63,12 @@ export function getRayJobStatus(resource: any): StatusBadge {
|
|
|
63
63
|
return { text: deploymentStatus, color: healthColors.unhealthy, level: 'unhealthy' }
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
if (['Retrying', 'Suspending', 'Suspended'].includes(deploymentStatus)) {
|
|
67
|
+
return deploymentStatus === 'Suspended'
|
|
68
|
+
? { text: deploymentStatus, color: healthColors.neutral, level: 'neutral' }
|
|
69
|
+
: { text: deploymentStatus, color: healthColors.degraded, level: 'degraded' }
|
|
70
|
+
}
|
|
71
|
+
|
|
66
72
|
switch (jobStatus) {
|
|
67
73
|
case 'SUCCEEDED':
|
|
68
74
|
return { text: 'Succeeded', color: healthColors.neutral, level: 'neutral' }
|
|
@@ -77,15 +83,11 @@ export function getRayJobStatus(resource: any): StatusBadge {
|
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
switch (deploymentStatus) {
|
|
80
|
-
case 'Suspended':
|
|
81
|
-
return { text: 'Suspended', color: healthColors.neutral, level: 'neutral' }
|
|
82
86
|
case 'Complete':
|
|
83
87
|
return { text: 'Complete', color: healthColors.neutral, level: 'neutral' }
|
|
84
88
|
case 'Running':
|
|
85
89
|
return { text: 'Running', color: healthColors.healthy, level: 'healthy' }
|
|
86
90
|
case 'Initializing':
|
|
87
|
-
case 'Suspending':
|
|
88
|
-
case 'Retrying':
|
|
89
91
|
case 'Waiting':
|
|
90
92
|
return { text: deploymentStatus, color: healthColors.degraded, level: 'degraded' }
|
|
91
93
|
}
|
|
@@ -184,3 +186,11 @@ export function getRayCronJobLastSchedule(resource: any): string {
|
|
|
184
186
|
if (!lastSchedule) return '-'
|
|
185
187
|
return formatAge(lastSchedule)
|
|
186
188
|
}
|
|
189
|
+
|
|
190
|
+
export function rayJobIsLocal(resource: any): boolean {
|
|
191
|
+
return !resource.spec?.managedBy || resource.spec.managedBy === 'ray.io/kuberay-operator'
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function rayJobSubmissionMode(resource: any): string {
|
|
195
|
+
return resource.spec?.submissionMode || 'K8sJobMode'
|
|
196
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState, type DragEvent as ReactDragEvent } from 'react'
|
|
1
|
+
import { useCallback, useEffect, useRef, useState, type DragEvent as ReactDragEvent, useId } from 'react'
|
|
2
2
|
import { AlertTriangle, Check, Loader2, Upload, X } from 'lucide-react'
|
|
3
3
|
import { Collapse, CollapseChevron, useDisclosure } from '../ui/Collapse'
|
|
4
4
|
import { ConfirmDialog } from '../ui/ConfirmDialog'
|
|
@@ -69,6 +69,7 @@ export function CreateResourceDialog({
|
|
|
69
69
|
schemaLoader,
|
|
70
70
|
onCreated,
|
|
71
71
|
}: CreateResourceDialogProps) {
|
|
72
|
+
const titleId = useId()
|
|
72
73
|
const [yaml, setYaml] = useState(initialYaml)
|
|
73
74
|
const [mode, setMode] = useState<'apply' | 'create'>('apply')
|
|
74
75
|
const [dryRun, setDryRun] = useState(false)
|
|
@@ -363,6 +364,7 @@ export function CreateResourceDialog({
|
|
|
363
364
|
|
|
364
365
|
return (
|
|
365
366
|
<DialogPortal
|
|
367
|
+
ariaLabelledBy={titleId}
|
|
366
368
|
open={open}
|
|
367
369
|
onClose={handleClose}
|
|
368
370
|
closable={!pending}
|
|
@@ -374,6 +376,7 @@ export function CreateResourceDialog({
|
|
|
374
376
|
>
|
|
375
377
|
{preview ? (
|
|
376
378
|
<YamlReview
|
|
379
|
+
titleId={titleId}
|
|
377
380
|
submittedYaml={preview.yaml}
|
|
378
381
|
documents={preview.documents}
|
|
379
382
|
nonAtomic={preview.nonAtomic}
|
|
@@ -393,11 +396,12 @@ export function CreateResourceDialog({
|
|
|
393
396
|
) : (
|
|
394
397
|
<>
|
|
395
398
|
<div className="flex shrink-0 items-center justify-between border-b border-theme-border px-5 py-3.5">
|
|
396
|
-
<h2 className="text-sm font-semibold text-theme-text-primary">{dialogTitle}</h2>
|
|
399
|
+
<h2 id={titleId} className="text-sm font-semibold text-theme-text-primary">{dialogTitle}</h2>
|
|
397
400
|
<Tooltip content="Close">
|
|
398
401
|
<button
|
|
399
402
|
type="button"
|
|
400
403
|
onClick={handleClose}
|
|
404
|
+
aria-label="Close"
|
|
401
405
|
className="rounded p-1 text-theme-text-secondary transition-colors hover:bg-theme-hover"
|
|
402
406
|
>
|
|
403
407
|
<X className="h-4 w-4" />
|
|
@@ -88,6 +88,16 @@ describe('DrainPlanContent', () => {
|
|
|
88
88
|
)
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
it('says a drain plan needs a newer Radar when the cluster predates it', () => {
|
|
92
|
+
const html = render({
|
|
93
|
+
planSupported: false,
|
|
94
|
+
planUpgrade: { feature: 'Drain plans', minimumVersion: 'v1.14.0', currentVersion: 'v1.13.1' },
|
|
95
|
+
})
|
|
96
|
+
expect(html).toContain('Drain plans')
|
|
97
|
+
expect(html).toContain('need Radar v1.14 or newer on this cluster.')
|
|
98
|
+
expect(html).not.toContain('Computing the plan')
|
|
99
|
+
})
|
|
100
|
+
|
|
91
101
|
it('shows per-pod outcomes with reasons and calls the result an estimate', () => {
|
|
92
102
|
const html = render({ plan: plan([pod('web', 'may-block', { pdb: 'shop/web', reason: 'PodDisruptionBudget shop/web currently allows no disruptions' }), pod('agent', 'skip')]) })
|
|
93
103
|
expect(html).toContain('may block')
|
|
@@ -3,6 +3,8 @@ import { ConfirmDialog } from '../ui/ConfirmDialog'
|
|
|
3
3
|
import { Badge, type BadgeSeverity } from '../ui/Badge'
|
|
4
4
|
import { AlertBanner } from '../ui/drawer-components'
|
|
5
5
|
import { Tooltip } from '../ui/Tooltip'
|
|
6
|
+
import { RadarUpgradeNote } from '../ui/RadarUpgradeNote'
|
|
7
|
+
import type { RadarUpgradeRequirement } from '../../types/fetch-error'
|
|
6
8
|
import { pluralize } from '../../utils/pluralize'
|
|
7
9
|
|
|
8
10
|
// Shapes returned by POST /api/nodes/{name}/drain-plan.
|
|
@@ -102,6 +104,8 @@ interface DrainPlanContentProps {
|
|
|
102
104
|
options: DrainDialogOptions
|
|
103
105
|
onOptionsChange: (options: DrainDialogOptions) => void
|
|
104
106
|
planSupported: boolean
|
|
107
|
+
/** Why there is no plan, when the connected Radar predates drain plans. */
|
|
108
|
+
planUpgrade?: RadarUpgradeRequirement | null
|
|
105
109
|
/** Recompute the plan with the current options; also the retry after a failed plan. */
|
|
106
110
|
onRefreshPlan?: () => void
|
|
107
111
|
}
|
|
@@ -112,7 +116,7 @@ interface DrainPlanContentProps {
|
|
|
112
116
|
* react-dom/server); the enclosing dialog owns the confirm gating.
|
|
113
117
|
*/
|
|
114
118
|
export function DrainPlanContent({
|
|
115
|
-
nodeName, plan, loading, error, options, onOptionsChange, planSupported, onRefreshPlan,
|
|
119
|
+
nodeName, plan, loading, error, options, onOptionsChange, planSupported, planUpgrade, onRefreshPlan,
|
|
116
120
|
}: DrainPlanContentProps) {
|
|
117
121
|
const current = planMatches(plan, nodeName, options) ? plan : null
|
|
118
122
|
const atRisk = current ? emptyDirPodsAtRisk(current) : []
|
|
@@ -140,6 +144,8 @@ export function DrainPlanContent({
|
|
|
140
144
|
</label>
|
|
141
145
|
</div>
|
|
142
146
|
|
|
147
|
+
{!planSupported && planUpgrade && <RadarUpgradeNote requirement={planUpgrade} />}
|
|
148
|
+
|
|
143
149
|
{planSupported && loading && (
|
|
144
150
|
<div className="flex items-center gap-2 text-theme-text-secondary">
|
|
145
151
|
<Loader2 className="w-4 h-4 animate-spin" /> Computing the plan…
|
|
@@ -259,12 +265,14 @@ interface DrainPlanDialogProps {
|
|
|
259
265
|
isDraining: boolean
|
|
260
266
|
/** Whether the host can compute plans. Without it the dialog confirms without one. */
|
|
261
267
|
planSupported: boolean
|
|
268
|
+
/** Why there is no plan, when the connected Radar predates drain plans. */
|
|
269
|
+
planUpgrade?: RadarUpgradeRequirement | null
|
|
262
270
|
/** Recompute the plan with the current options; also the retry after a failed plan. */
|
|
263
271
|
onRefreshPlan?: () => void
|
|
264
272
|
}
|
|
265
273
|
|
|
266
274
|
export function DrainPlanDialog({
|
|
267
|
-
open, nodeName, plan, loading, error, options, onOptionsChange, onConfirm, onClose, isDraining, planSupported, onRefreshPlan,
|
|
275
|
+
open, nodeName, plan, loading, error, options, onOptionsChange, onConfirm, onClose, isDraining, planSupported, planUpgrade, onRefreshPlan,
|
|
268
276
|
}: DrainPlanDialogProps) {
|
|
269
277
|
const confirmEnabled = canConfirmDrain({ plan, nodeName, options, loading, error, planSupported })
|
|
270
278
|
|
|
@@ -274,7 +282,9 @@ export function DrainPlanDialog({
|
|
|
274
282
|
onClose={onClose}
|
|
275
283
|
onConfirm={() => onConfirm(options)}
|
|
276
284
|
title="Drain Node"
|
|
277
|
-
message={
|
|
285
|
+
message={planSupported
|
|
286
|
+
? `Cordon "${nodeName}" and evict the pods listed below. The list is an estimate: the drain re-lists live state when it runs.`
|
|
287
|
+
: `Cordon "${nodeName}" and evict its pods.`}
|
|
278
288
|
confirmLabel={isDraining ? 'Draining...' : 'Drain'}
|
|
279
289
|
variant="danger"
|
|
280
290
|
isLoading={isDraining}
|
|
@@ -290,6 +300,7 @@ export function DrainPlanDialog({
|
|
|
290
300
|
options={options}
|
|
291
301
|
onOptionsChange={onOptionsChange}
|
|
292
302
|
planSupported={planSupported}
|
|
303
|
+
planUpgrade={planUpgrade}
|
|
293
304
|
onRefreshPlan={onRefreshPlan}
|
|
294
305
|
/>
|
|
295
306
|
</ConfirmDialog>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useState, useRef } from 'react'
|
|
1
|
+
import { useEffect, useState, useRef, useId } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
RefreshCw,
|
|
4
4
|
Terminal,
|
|
@@ -25,6 +25,7 @@ import { ConfirmDialog } from '../ui/ConfirmDialog'
|
|
|
25
25
|
import { AlertBanner } from '../ui/drawer-components'
|
|
26
26
|
import { DialogPortal } from '../ui/DialogPortal'
|
|
27
27
|
import type { SelectedResource, WorkloadRevision } from '../../types'
|
|
28
|
+
import type { RadarUpgradeRequirement } from '../../types/fetch-error'
|
|
28
29
|
import { displayKindName } from '../ui/drawer-components'
|
|
29
30
|
import { getDefaultContainerName } from '../resources/resource-utils'
|
|
30
31
|
import { SetImageDialog, type ManagedImageSource } from './SetImageDialog'
|
|
@@ -143,10 +144,12 @@ interface ResourceActionsBarProps {
|
|
|
143
144
|
drainPlan?: DrainPlan | null
|
|
144
145
|
isPlanningDrain?: boolean
|
|
145
146
|
drainPlanError?: string | null
|
|
146
|
-
// The connected
|
|
147
|
-
//
|
|
148
|
-
// acknowledgement-only mode instead of keeping Drain disabled on a dead
|
|
149
|
-
|
|
147
|
+
// The connected Radar predates the drain-plan endpoint (a newer frontend
|
|
148
|
+
// against an older Radar). The dialog falls back to the plan-less
|
|
149
|
+
// acknowledgement-only mode instead of keeping Drain disabled on a dead
|
|
150
|
+
// request, and names the Radar version that serves plans so the user knows a
|
|
151
|
+
// preview exists.
|
|
152
|
+
drainPlanUpgrade?: RadarUpgradeRequirement | null
|
|
150
153
|
}
|
|
151
154
|
|
|
152
155
|
export function ResourceActionsBar({
|
|
@@ -177,7 +180,7 @@ export function ResourceActionsBar({
|
|
|
177
180
|
onCordonNode, isCordoningNode,
|
|
178
181
|
onUncordonNode, isUncordoningNode,
|
|
179
182
|
onDrainNode, isDrainingNode,
|
|
180
|
-
onPlanDrain, onPlanDrainReset, drainPlan, isPlanningDrain, drainPlanError,
|
|
183
|
+
onPlanDrain, onPlanDrainReset, drainPlan, isPlanningDrain, drainPlanError, drainPlanUpgrade,
|
|
181
184
|
}: ResourceActionsBarProps) {
|
|
182
185
|
const kind = resource.kind.toLowerCase()
|
|
183
186
|
const coreBatchJob = isCoreBatchJob(kind, resource.group)
|
|
@@ -202,13 +205,14 @@ export function ResourceActionsBar({
|
|
|
202
205
|
// open: the host reports it through a mutation error, which the next request clears,
|
|
203
206
|
// so without latching every option change would refire a request known to 404 and
|
|
204
207
|
// bounce the dialog out of its fallback mode.
|
|
205
|
-
const [
|
|
208
|
+
const [planUpgrade, setPlanUpgrade] = useState<RadarUpgradeRequirement | null>(null)
|
|
206
209
|
useEffect(() => {
|
|
207
|
-
if (
|
|
208
|
-
}, [
|
|
210
|
+
if (drainPlanUpgrade) setPlanUpgrade(drainPlanUpgrade)
|
|
211
|
+
}, [drainPlanUpgrade])
|
|
209
212
|
useEffect(() => {
|
|
210
|
-
if (!showDrainConfirm)
|
|
213
|
+
if (!showDrainConfirm) setPlanUpgrade(null)
|
|
211
214
|
}, [showDrainConfirm])
|
|
215
|
+
const planUnsupported = planUpgrade !== null
|
|
212
216
|
const planSupported = Boolean(onPlanDrain) && !planUnsupported
|
|
213
217
|
|
|
214
218
|
// Fetch (and refetch on option changes) the read-only plan while the drain dialog is open.
|
|
@@ -719,6 +723,7 @@ export function ResourceActionsBar({
|
|
|
719
723
|
options={drainOptions}
|
|
720
724
|
onOptionsChange={setDrainOptions}
|
|
721
725
|
planSupported={planSupported}
|
|
726
|
+
planUpgrade={planUpgrade}
|
|
722
727
|
onRefreshPlan={planSupported ? () => onPlanDrain?.({ name: resource.name, options: drainOptions }) : undefined}
|
|
723
728
|
isDraining={Boolean(isDrainingNode)}
|
|
724
729
|
onClose={() => {
|
|
@@ -1053,6 +1058,7 @@ export function RevisionHistoryDialog({ kind, namespace, name, open, onClose, re
|
|
|
1053
1058
|
isRollingBack?: boolean
|
|
1054
1059
|
onRolloutPromoteFull?: (params: { namespace: string; name: string }) => void | Promise<unknown>
|
|
1055
1060
|
}) {
|
|
1061
|
+
const titleId = useId()
|
|
1056
1062
|
const [confirmRevision, setConfirmRevision] = useState<number | null>(null)
|
|
1057
1063
|
const [diffRevision, setDiffRevision] = useState<number | null>(null)
|
|
1058
1064
|
const [promoteAfterRollback, setPromoteAfterRollback] = useState(false)
|
|
@@ -1144,6 +1150,7 @@ export function RevisionHistoryDialog({ kind, namespace, name, open, onClose, re
|
|
|
1144
1150
|
|
|
1145
1151
|
return (
|
|
1146
1152
|
<DialogPortal
|
|
1153
|
+
ariaLabelledBy={titleId}
|
|
1147
1154
|
open={open}
|
|
1148
1155
|
onClose={handleClose}
|
|
1149
1156
|
closable={!busy}
|
|
@@ -1152,7 +1159,7 @@ export function RevisionHistoryDialog({ kind, namespace, name, open, onClose, re
|
|
|
1152
1159
|
<div className="flex items-center justify-between p-4 border-b border-theme-border shrink-0">
|
|
1153
1160
|
<div className="flex items-center gap-2">
|
|
1154
1161
|
<History className="w-5 h-5 text-amber-500" />
|
|
1155
|
-
<h3 className="text-lg font-semibold text-theme-text-primary">Revision History</h3>
|
|
1162
|
+
<h3 id={titleId} className="text-lg font-semibold text-theme-text-primary">Revision History</h3>
|
|
1156
1163
|
{diffRevision && currentRevision && (
|
|
1157
1164
|
<span className="badge ml-2 bg-blue-500/15 text-blue-400">
|
|
1158
1165
|
<GitCompare className="w-3 h-3" />
|
|
@@ -315,11 +315,9 @@ describe('getResourceStatus — colliding plurals', () => {
|
|
|
315
315
|
// fine; being scored with Argo's vocabulary is not, because that attaches a
|
|
316
316
|
// HealthLevel derived from a phase Katib never reports.
|
|
317
317
|
it('does not score a Katib Experiment with the Argo vocabulary', () => {
|
|
318
|
-
const katib =
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
})
|
|
322
|
-
expect(katib?.level).toBeUndefined()
|
|
318
|
+
const katib = { apiVersion: 'kubeflow.org/v1beta1', status: { conditions: [{ type: 'Running', status: 'True' }] } }
|
|
319
|
+
// Read the way any unrelated CRD with the same status would be.
|
|
320
|
+
expect(getResourceStatus('experiments', katib)).toEqual(getResourceStatus('trainingruns', katib))
|
|
323
321
|
})
|
|
324
322
|
|
|
325
323
|
it('fabricates no engine status for a third-party backups CRD', () => {
|
|
@@ -1080,3 +1078,12 @@ it('uses native RayCluster detail only for ray.io/v1', () => {
|
|
|
1080
1078
|
if (apiVersion !== 'ray.io/v1') expect(html).toContain('2.55.0')
|
|
1081
1079
|
}
|
|
1082
1080
|
})
|
|
1081
|
+
|
|
1082
|
+
it('uses RayJob native detail only for the supported exact API', () => {
|
|
1083
|
+
for (const apiVersion of ['ray.io/v1', 'foreign.io/v1', 'ray.io/v99', 'ray.io/v1alpha1']) {
|
|
1084
|
+
const data = { apiVersion, kind: 'RayJob', metadata: { name: 'train', namespace: 'ml' }, spec: { customField: 'native-fallback' } }
|
|
1085
|
+
const html = renderKind('rayjobs', data, 'ml')
|
|
1086
|
+
expect(html.includes('RayJob execution')).toBe(apiVersion === 'ray.io/v1')
|
|
1087
|
+
if (apiVersion !== 'ray.io/v1') { expect(html).toContain('native-fallback'); expect(getResourceStatus('rayjobs', data)).toBeNull() }
|
|
1088
|
+
}
|
|
1089
|
+
})
|