@skyhook-io/k8s-ui 1.14.11 → 1.15.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 +2 -1
- package/src/components/compare/CompareResourcePicker.tsx +4 -3
- 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/insights/ArgoResourceDiff.tsx +1 -0
- 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/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/ResourceActionsBar.tsx +4 -2
- 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/YamlReview.tsx +4 -1
- package/src/components/ui/drawer-components.test.tsx +14 -1
- package/src/components/ui/drawer-components.tsx +50 -40
|
@@ -6243,6 +6243,7 @@ export function ResourcesView({
|
|
|
6243
6243
|
{onCreateResource && (
|
|
6244
6244
|
<Tooltip content={`Create ${selectedKind.kind || 'resource'}`}>
|
|
6245
6245
|
<button
|
|
6246
|
+
aria-label={`Create ${selectedKind.kind || 'resource'}`}
|
|
6246
6247
|
onClick={() => onCreateResource(selectedKind)}
|
|
6247
6248
|
className="p-2 hover:bg-theme-elevated rounded-lg text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
6248
6249
|
>
|
|
@@ -6846,6 +6847,7 @@ export function ResourcesView({
|
|
|
6846
6847
|
<div className="flex items-center justify-center gap-3">
|
|
6847
6848
|
<button
|
|
6848
6849
|
type="button"
|
|
6850
|
+
aria-label="Decrease replicas"
|
|
6849
6851
|
onClick={() => setBulkScaleReplicas(Math.max(0, bulkScaleReplicas - 1))}
|
|
6850
6852
|
disabled={bulkScaleReplicas <= 0}
|
|
6851
6853
|
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"
|
|
@@ -6857,12 +6859,14 @@ export function ResourcesView({
|
|
|
6857
6859
|
min="0"
|
|
6858
6860
|
max="10000"
|
|
6859
6861
|
value={bulkScaleReplicas}
|
|
6862
|
+
aria-label="Replicas"
|
|
6860
6863
|
onChange={(e) => setBulkScaleReplicas(Math.min(10000, Math.max(0, Number.parseInt(e.target.value, 10) || 0)))}
|
|
6861
6864
|
className="w-24 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-skyhook-500"
|
|
6862
6865
|
autoFocus
|
|
6863
6866
|
/>
|
|
6864
6867
|
<button
|
|
6865
6868
|
type="button"
|
|
6869
|
+
aria-label="Increase replicas"
|
|
6866
6870
|
onClick={() => setBulkScaleReplicas(Math.min(10000, bulkScaleReplicas + 1))}
|
|
6867
6871
|
disabled={bulkScaleReplicas >= 10000}
|
|
6868
6872
|
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"
|
|
@@ -113,7 +113,7 @@ describe('formatPrinterCell', () => {
|
|
|
113
113
|
describe('printerCellSortValue', () => {
|
|
114
114
|
// Sorting must use the value, not its rendered text, or 10 sorts before 9.
|
|
115
115
|
it('sorts numbers numerically', () => {
|
|
116
|
-
expect([10, 9, 100].map(printerCellSortValue).sort((a, b) => (a as number) - (b as number)))
|
|
116
|
+
expect([10, 9, 100].map(v => printerCellSortValue(v)).sort((a, b) => (a as number) - (b as number)))
|
|
117
117
|
.toEqual([9, 10, 100])
|
|
118
118
|
})
|
|
119
119
|
it('sorts booleans by truth and falls back to empty for anything else', () => {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
1
2
|
import { Clock } from 'lucide-react'
|
|
2
3
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
3
4
|
import { formatDuration } from '../resource-utils'
|
|
4
5
|
|
|
5
6
|
interface JobRendererProps {
|
|
6
7
|
data: any
|
|
8
|
+
admissionContent?: ReactNode
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
// Extract problems from Job status and conditions
|
|
@@ -42,7 +44,7 @@ function getJobProblems(data: any): string[] {
|
|
|
42
44
|
return problems
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
export function JobRenderer({ data }: JobRendererProps) {
|
|
47
|
+
export function JobRenderer({ data, admissionContent }: JobRendererProps) {
|
|
46
48
|
const status = data.status || {}
|
|
47
49
|
const spec = data.spec || {}
|
|
48
50
|
const conditions = status.conditions || []
|
|
@@ -82,6 +84,8 @@ export function JobRenderer({ data }: JobRendererProps) {
|
|
|
82
84
|
<AlertBanner variant="success" title="Job Completed Successfully" />
|
|
83
85
|
)}
|
|
84
86
|
|
|
87
|
+
{admissionContent}
|
|
88
|
+
|
|
85
89
|
<Section title="Status" icon={Clock}>
|
|
86
90
|
<PropertyList>
|
|
87
91
|
<Property label="Succeeded" value={status.succeeded || 0} />
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
1
2
|
import { Activity, Boxes, Network as NetworkIcon, ShieldCheck } from 'lucide-react'
|
|
2
3
|
import {
|
|
3
4
|
ConditionsSection,
|
|
@@ -13,6 +14,7 @@ import { getJobSetStatus } from '../resource-utils-jobset-lws'
|
|
|
13
14
|
|
|
14
15
|
interface JobSetRendererProps {
|
|
15
16
|
data: any
|
|
17
|
+
admissionContent?: ReactNode
|
|
16
18
|
mode?: 'detail' | 'overview' | 'configuration'
|
|
17
19
|
shownMemberCounts?: ReadonlyMap<string, number>
|
|
18
20
|
onSelectRole?: (role: string) => void
|
|
@@ -50,7 +52,7 @@ function sumCountArray(value: unknown): number | undefined {
|
|
|
50
52
|
return value.reduce((total: number, count: unknown) => total + (typeof count === 'number' ? count : 0), 0)
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
export function JobSetRenderer({ data, mode = 'detail', shownMemberCounts, onSelectRole }: JobSetRendererProps) {
|
|
55
|
+
export function JobSetRenderer({ data, admissionContent, mode = 'detail', shownMemberCounts, onSelectRole }: JobSetRendererProps) {
|
|
54
56
|
const spec = data?.spec || {}
|
|
55
57
|
const status = data?.status || {}
|
|
56
58
|
const conditions: any[] = Array.isArray(status.conditions) ? status.conditions : []
|
|
@@ -85,6 +87,7 @@ export function JobSetRenderer({ data, mode = 'detail', shownMemberCounts, onSel
|
|
|
85
87
|
return (
|
|
86
88
|
<>
|
|
87
89
|
{showSummary && <ProblemAlerts problems={problems} />}
|
|
90
|
+
{showSummary && admissionContent}
|
|
88
91
|
|
|
89
92
|
{showSummary && externalController && (
|
|
90
93
|
<div className="mb-3 rounded border border-theme-border bg-theme-elevated p-3 text-sm text-theme-text-secondary">
|
|
@@ -5,7 +5,7 @@ import { expect, it, vi } from 'vitest'
|
|
|
5
5
|
import { ClusterQueueRenderer, LocalQueueRenderer } from './KueueQueueRenderers'
|
|
6
6
|
|
|
7
7
|
it('navigates from a namespaced LocalQueue to exact cluster-scoped Kueue dependencies', async () => {
|
|
8
|
-
globalThis
|
|
8
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
|
|
9
9
|
const container = document.createElement('div')
|
|
10
10
|
const root = createRoot(container)
|
|
11
11
|
const onNavigate = vi.fn()
|
|
@@ -64,6 +64,6 @@ it('navigates from a namespaced LocalQueue to exact cluster-scoped Kueue depende
|
|
|
64
64
|
})
|
|
65
65
|
} finally {
|
|
66
66
|
await act(async () => root.unmount())
|
|
67
|
-
globalThis
|
|
67
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: false })
|
|
68
68
|
}
|
|
69
69
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
2
|
import { Cpu, Gauge, ListChecks } from 'lucide-react'
|
|
3
3
|
import { formatRelativeAgeTime } from '../../../utils/format'
|
|
4
|
-
import { Badge
|
|
4
|
+
import { Badge } from '../../ui/Badge'
|
|
5
5
|
import {
|
|
6
6
|
AlertBanner,
|
|
7
7
|
ConditionsSection,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type ConditionTone,
|
|
13
13
|
} from '../../ui/drawer-components'
|
|
14
14
|
import { formatResources } from '../resource-utils'
|
|
15
|
-
import { getKueueWorkloadStatus, getKueueWorkloadPriority, getKueueWorkloadStatusCondition, isKueueWorkloadFailureReason, isKueueConditionStale } from '../resource-utils-kueue'
|
|
15
|
+
import { admissionCheckSeverity, getKueueWorkloadStatus, getKueueWorkloadPriority, getKueueWorkloadStatusCondition, isKueueWorkloadFailureReason, isKueueConditionStale } from '../resource-utils-kueue'
|
|
16
16
|
|
|
17
17
|
const KUEUE_GROUP = 'kueue.x-k8s.io'
|
|
18
18
|
|
|
@@ -22,19 +22,6 @@ interface KueueWorkloadRendererProps {
|
|
|
22
22
|
onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function admissionCheckSeverity(state: string): BadgeSeverity {
|
|
26
|
-
switch (state) {
|
|
27
|
-
case 'Ready':
|
|
28
|
-
return 'success'
|
|
29
|
-
case 'Rejected':
|
|
30
|
-
return 'error'
|
|
31
|
-
case 'Retry':
|
|
32
|
-
return 'warning'
|
|
33
|
-
default:
|
|
34
|
-
return 'neutral'
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
25
|
function resourceList(resources: any): string | null {
|
|
39
26
|
if (!resources || typeof resources !== 'object' || Object.keys(resources).length === 0) return null
|
|
40
27
|
return formatResources(Object.fromEntries(Object.entries(resources).map(([name, quantity]) => [name, String(quantity)])))
|
|
@@ -41,9 +41,9 @@ export function RayClusterRenderer({ data, onNavigate, podEvidence, podsLoading,
|
|
|
41
41
|
const scope = selection.workerGroup != null ? `Worker group: ${selection.workerGroup}` : selection.nodeType === 'head' ? 'Head' : 'All runtime Pods'
|
|
42
42
|
const select = (value: RayClusterPodSelection) => { setExpanded(false); onSelectPods?.(value) }
|
|
43
43
|
const row = (pod: WorkloadPodInfo) => <PodRow key={pod.name} name={pod.name} namespace={data.metadata.namespace} ready={pod.ready} healthLevel={pod.healthLevel} detail={workloadPodDetail(pod)} onNavigate={onNavigate} />
|
|
44
|
-
return
|
|
44
|
+
return <div className="space-y-4">
|
|
45
45
|
{stale && <AlertBanner variant="info" title="Status describes an earlier generation" message={`Specification generation ${data.metadata.generation}; controller observed ${status.observedGeneration}. Counts and conditions below retain that observation.`} />}
|
|
46
|
-
<Section title="Runtime Health" icon={Activity} defaultExpanded>
|
|
46
|
+
<Section variant="card" title="Runtime Health" icon={Activity} defaultExpanded>
|
|
47
47
|
<PropertyList>
|
|
48
48
|
<Property label="Ray Version" value={spec.rayVersion || 'Not specified'} />
|
|
49
49
|
<Property label="Head" value={<Badge severity={stale ? 'neutral' : head?.status === 'True' ? 'success' : head?.status === 'False' ? 'warning' : 'neutral'}>{head?.status === 'True' ? 'Ready' : head?.status === 'False' ? 'Not ready' : 'Not reported'}{stale ? ' (stale)' : ''}</Badge>} />
|
|
@@ -58,7 +58,7 @@ export function RayClusterRenderer({ data, onNavigate, podEvidence, podsLoading,
|
|
|
58
58
|
{status.observedGeneration == null && <p className="mt-2 text-xs text-theme-text-tertiary">Observed generation is not reported.</p>}
|
|
59
59
|
{conditions.some((c: any) => c.type === 'RayClusterProvisioned' && c.status === 'True') && <p className="mt-2 text-xs text-theme-text-tertiary">Provisioned records initial provisioning, not continuing runtime health.</p>}
|
|
60
60
|
</Section>
|
|
61
|
-
{onSelectPods && <div ref={podsRef}><Section title="Runtime Pods" icon={Server} defaultExpanded>
|
|
61
|
+
{onSelectPods && <div ref={podsRef}><Section variant="card" title="Runtime Pods" icon={Server} defaultExpanded>
|
|
62
62
|
<SelectMenu ariaLabel="Pod scope" className="mb-3" searchPlaceholder="Find a worker group…"
|
|
63
63
|
value={selection.workerGroup != null ? `group:${selection.workerGroup}` : selection.nodeType === 'head' ? 'head' : 'all'}
|
|
64
64
|
options={[{ value: 'all', label: 'All runtime Pods' }, { value: 'head', label: 'Head' }, ...groups.map((g: any) => ({ value: `group:${g.groupName}`, label: `Worker group: ${g.groupName}` }))]}
|
|
@@ -70,7 +70,7 @@ export function RayClusterRenderer({ data, onNavigate, podEvidence, podsLoading,
|
|
|
70
70
|
<PodListFrame expanded={expanded} hasOverflow={podEvidence.pods.length > 20} overflow={podEvidence.pods.slice(20).map(row)} toggle={<PodListToggle expanded={expanded} hiddenCount={podEvidence.pods.length - 20} label="Pods" onToggle={() => setExpanded(!expanded)} />}>{podEvidence.pods.slice(0,20).map(row)}</PodListFrame>
|
|
71
71
|
</> : <p className="text-sm text-theme-text-secondary">Pod evidence is not available.</p>}
|
|
72
72
|
</Section></div>}
|
|
73
|
-
<Section title={`Worker Groups (${groups.length})`} icon={Boxes} defaultExpanded>
|
|
73
|
+
<Section variant="card" title={`Worker Groups (${groups.length})`} icon={Boxes} defaultExpanded>
|
|
74
74
|
<p className="mb-3 text-xs text-theme-text-tertiary">Declared sizing, not observed group health. Replicas can span multiple hosts; controller-reported worker counts above are Pods. In-tree autoscaling: {spec.enableInTreeAutoscaling === true ? 'enabled' : 'disabled'}.</p>
|
|
75
75
|
<div className="space-y-3">{groups.map((group: any) => <div className="card-inner min-w-0" key={group.groupName}>
|
|
76
76
|
<div className="mb-2 flex items-start justify-between gap-3"><span className="break-all text-sm font-medium text-theme-text-primary">{group.groupName}</span>{onSelectPods && <button className="shrink-0 text-xs text-accent-text hover:underline" onClick={() => { select({ nodeType: 'worker', workerGroup: group.groupName }); podsRef.current?.scrollIntoView({ block: 'start' }) }}>View Pods</button>}</div>
|
|
@@ -83,11 +83,11 @@ export function RayClusterRenderer({ data, onNavigate, podEvidence, podsLoading,
|
|
|
83
83
|
</div>)}</div>
|
|
84
84
|
{groups.length === 0 && <p className="text-sm text-theme-text-secondary">No worker groups declared.</p>}
|
|
85
85
|
</Section>
|
|
86
|
-
{(owners.length > 0 || status.head?.podName || status.head?.serviceName) && <Section title="References" defaultExpanded><PropertyList>
|
|
86
|
+
{(owners.length > 0 || status.head?.podName || status.head?.serviceName) && <Section variant="card" title="References" defaultExpanded><PropertyList>
|
|
87
87
|
{owners.map((owner: any) => <Property key={owner.uid} label="Controller" value={<ResourceLink kind={owner.kind === 'RayService' ? 'rayservices' : 'rayjobs'} group="ray.io" namespace={data.metadata.namespace} name={owner.name} onNavigate={onNavigate} />} />)}
|
|
88
88
|
{status.head?.podName && <Property label="Reported Head Pod" value={<ResourceLink kind="pods" namespace={data.metadata.namespace} name={status.head.podName} onNavigate={onNavigate} />} />}
|
|
89
89
|
{status.head?.serviceName && <Property label="Reported Head Service" value={<ResourceLink kind="services" namespace={data.metadata.namespace} name={status.head.serviceName} onNavigate={onNavigate} />} />}
|
|
90
90
|
</PropertyList></Section>}
|
|
91
|
-
<ConditionsSection conditions={conditions} getConditionTone={c => stale ? 'unknown' : rayClusterConditionTone(c)} defaultExpanded />
|
|
92
|
-
|
|
91
|
+
<ConditionsSection variant="card" conditions={conditions} getConditionTone={c => stale ? 'unknown' : rayClusterConditionTone(c)} defaultExpanded />
|
|
92
|
+
</div>
|
|
93
93
|
}
|
|
@@ -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" />
|