@skyhook-io/radar-app 1.14.5 → 1.14.6
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/README.md +2 -9
- package/package.json +6 -6
- package/src/App.tsx +91 -328
- package/src/RadarApp.tsx +3 -8
- package/src/api/client.jobset.test.ts +18 -0
- package/src/api/client.resource-identity.test.ts +44 -0
- package/src/api/client.rightsizing.test.ts +7 -3
- package/src/api/client.ts +233 -78
- package/src/api/preferences.test.ts +54 -0
- package/src/api/preferences.ts +38 -0
- package/src/components/CloudFunnelButton.test.tsx +155 -0
- package/src/components/CloudFunnelButton.tsx +14 -8
- package/src/components/ContextSwitcher.tsx +13 -132
- package/src/components/applications/ApplicationsView.identity.test.ts +39 -0
- package/src/components/applications/ApplicationsView.tsx +5 -3
- package/src/components/audit/AuditSettingsDialog.tsx +15 -11
- package/src/components/diagnose/AgentControls.tsx +26 -3
- package/src/components/diagnose/DiagnoseContext.tsx +1 -0
- package/src/components/diagnose/agentCatalog.ts +6 -0
- package/src/components/diagnose/launch.test.ts +23 -0
- package/src/components/diagnose/launch.ts +4 -0
- package/src/components/diagnose/parts.test.tsx +18 -0
- package/src/components/dock/NodeTerminalTab.test.tsx +34 -0
- package/src/components/dock/NodeTerminalTab.tsx +5 -3
- package/src/components/dock/WorkloadLogsTab.test.tsx +66 -0
- package/src/components/dock/WorkloadLogsTab.tsx +2 -1
- package/src/components/execution/BatchExecutionView.render.test.tsx +2 -1
- package/src/components/execution/BatchExecutionView.test.ts +52 -1
- package/src/components/execution/BatchExecutionView.tsx +324 -65
- package/src/components/execution/JobSetAdmission.test.tsx +42 -0
- package/src/components/execution/JobSetAdmission.tsx +17 -0
- package/src/components/execution/JobSetMemberComparison.test.tsx +30 -0
- package/src/components/execution/JobSetMemberComparison.tsx +88 -0
- package/src/components/execution/batch-run-actions.test.ts +1 -0
- package/src/components/execution/batch-timeline.test.ts +3 -0
- package/src/components/execution/execution-definition.test.ts +15 -1
- package/src/components/execution/execution-definition.ts +4 -0
- package/src/components/execution/member-collection.render.test.tsx +142 -0
- package/src/components/gitops/GitOpsView.tsx +33 -2
- package/src/components/gitops/destination-toast.test.ts +33 -0
- package/src/components/gitops/destination-toast.ts +46 -0
- package/src/components/gitops/useDestinationCluster.ts +30 -0
- package/src/components/helm/TrackChartSourceDialog.tsx +20 -11
- package/src/components/home/MCPSetupDialog.tsx +10 -0
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +75 -22
- package/src/components/logs/WorkloadLogsViewer.tsx +7 -4
- package/src/components/nav/PrimaryNavRail.tsx +2 -5
- package/src/components/resource/PVCUsageBar.render.test.tsx +168 -0
- package/src/components/resource/PVCUsageBar.tsx +62 -16
- package/src/components/resource/PrometheusChartsGrid.render.test.tsx +1 -1
- package/src/components/resources/ResourcesView.tsx +3 -3
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +8 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.test.tsx +39 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +37 -0
- package/src/components/resources/renderers/RayClusterRenderer.test.tsx +18 -0
- package/src/components/resources/renderers/RayClusterRenderer.tsx +17 -0
- package/src/components/resources/renderers/RayServiceRenderer.test.tsx +69 -0
- package/src/components/resources/renderers/RayServiceRenderer.tsx +38 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +34 -22
- package/src/components/rightsizing/copy.test.ts +7 -0
- package/src/components/rightsizing/model.test.ts +2 -1
- package/src/components/rightsizing/notices.test.tsx +4 -3
- package/src/components/settings/OperatorManagedNotice.tsx +17 -0
- package/src/components/settings/SettingsDialog.tsx +157 -42
- package/src/components/settings/settings-state.test.ts +18 -0
- package/src/components/settings/settings-state.ts +18 -0
- package/src/components/timeline/TimelineView.tsx +9 -3
- package/src/components/timeline/TimelineView.urlparams.test.ts +18 -1
- package/src/components/ui/command-items.ts +0 -3
- package/src/components/useContextSwitchFlow.tsx +147 -0
- package/src/components/workload/WorkloadView.test.ts +17 -1
- package/src/components/workload/WorkloadView.tsx +36 -18
- package/src/context/ContextSwitchContext.tsx +18 -2
- package/src/context/NavCustomization.tsx +4 -58
- package/src/context/ThemeContext.tsx +3 -11
- package/src/hooks/useClusterLoadState.ts +2 -2
- package/src/hooks/useFavorites.ts +3 -5
- package/src/utils/auditBadges.ts +1 -1
- package/src/utils/navigation.test.ts +18 -1
- package/src/utils/navigation.ts +12 -5
- package/src/utils/topology-selection.test.ts +56 -0
- package/src/utils/topology-selection.ts +9 -9
- package/src/components/ui/CommandPalette.tsx +0 -261
|
@@ -1,18 +1,37 @@
|
|
|
1
|
-
import { useEffect, useMemo, useState, type ComponentType, type ReactNode } from 'react'
|
|
1
|
+
import { useEffect, useMemo, useRef, useState, type ComponentType, type ReactNode } from 'react'
|
|
2
2
|
import { Activity, GitBranch, Terminal } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
Collapse,
|
|
6
|
+
Disclosure,
|
|
7
|
+
useDebouncedValue,
|
|
8
|
+
CollapseChevron,
|
|
9
|
+
EmptyState,
|
|
10
|
+
FetchResult,
|
|
11
|
+
StatusDot,
|
|
12
|
+
mapHealthToTone,
|
|
13
|
+
podDotClass,
|
|
14
|
+
podStatusClass,
|
|
15
|
+
podStatusLabel,
|
|
16
|
+
workloadPodDetail,
|
|
17
|
+
} from '@skyhook-io/k8s-ui'
|
|
5
18
|
import { buildWorkflowExecutionModel, flattenWorkflowExecution, type WorkflowExecutionActivity, type WorkflowExecutionModel, type WorkflowExecutionNode, type WorkflowExecutionRow, type WorkflowTemplateReference } from '@skyhook-io/k8s-ui/utils/workflow-execution'
|
|
6
19
|
import { midTruncate } from '@skyhook-io/k8s-ui/utils/format'
|
|
7
|
-
import {
|
|
20
|
+
import { isJobSetV1Alpha2 } from '@skyhook-io/k8s-ui/components/resources/resource-utils-jobset-lws'
|
|
21
|
+
import { JobSetRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/JobSetRenderer'
|
|
22
|
+
import { useResource, useWorkloadPods, useWorkloadRuns, useJobSetResources, type WorkloadRun } from '../../api/client'
|
|
23
|
+
import type { ResourceNavigationTarget } from '../../utils/navigation'
|
|
24
|
+
import { JobSetMemberComparison } from './JobSetMemberComparison'
|
|
25
|
+
import type { WorkloadPodInfo } from '../../types'
|
|
8
26
|
import { getScaledJobStatus } from '../resources/resource-utils-keda'
|
|
9
27
|
import { Tooltip } from '../ui/Tooltip'
|
|
10
28
|
import { ImageFilesystemModal } from '../resources/ImageFilesystemModal'
|
|
11
|
-
import {
|
|
29
|
+
import { executionDefinitionDiffers, executionDefinitionSummary, type ExecutionDefinitionSummary, type ExecutionUnitSummary } from './execution-definition'
|
|
12
30
|
import { batchRunHasContainerOutcome, batchRunNextStep, isFailedRunPhase, type BatchRunNextStep } from './batch-run-actions'
|
|
31
|
+
import { JobSetAdmission } from './JobSetAdmission'
|
|
13
32
|
|
|
14
33
|
const EMPTY_RUNS: WorkloadRun[] = []
|
|
15
|
-
const SCHEDULED_KINDS = new Set(['CronJob', 'CronWorkflow', 'WorkflowTemplate', 'ClusterWorkflowTemplate', 'ScaledJob'])
|
|
34
|
+
const SCHEDULED_KINDS = new Set(['CronJob', 'CronWorkflow', 'WorkflowTemplate', 'ClusterWorkflowTemplate', 'ScaledJob', 'JobSet'])
|
|
16
35
|
const RUN_KIND_LABEL: Record<string, string> = {
|
|
17
36
|
jobs: 'Job',
|
|
18
37
|
workflows: 'Workflow',
|
|
@@ -22,6 +41,10 @@ export function workloadRunKey(run: Pick<WorkloadRun, 'kind' | 'namespace' | 'na
|
|
|
22
41
|
return `${run.kind}/${run.namespace}/${run.name}`
|
|
23
42
|
}
|
|
24
43
|
|
|
44
|
+
export function selectedRunMissing(runs: WorkloadRun[], key: string): boolean {
|
|
45
|
+
return Boolean(key) && !runs.some((run) => workloadRunKey(run) === key)
|
|
46
|
+
}
|
|
47
|
+
|
|
25
48
|
function isTemplateKind(kind: string): boolean {
|
|
26
49
|
return kind === 'WorkflowTemplate' || kind === 'ClusterWorkflowTemplate'
|
|
27
50
|
}
|
|
@@ -32,6 +55,7 @@ function configurationTitle(kind: string): string {
|
|
|
32
55
|
if (kind === 'ScaledJob') return 'Trigger & job definition'
|
|
33
56
|
if (isTemplateKind(kind)) return 'Current definition'
|
|
34
57
|
if (kind === 'Job') return 'Job definition'
|
|
58
|
+
if (kind === 'JobSet') return 'JobSet details'
|
|
35
59
|
return 'Workflow definition'
|
|
36
60
|
}
|
|
37
61
|
|
|
@@ -91,17 +115,39 @@ interface BatchExecutionProps {
|
|
|
91
115
|
onSelectRun?: (runKey: string) => void
|
|
92
116
|
onSwitchToLogs?: () => void
|
|
93
117
|
onSwitchToTimeline?: () => void
|
|
94
|
-
onNavigateToResource?: (resource:
|
|
118
|
+
onNavigateToResource?: (resource: ResourceNavigationTarget) => void
|
|
95
119
|
}
|
|
96
120
|
|
|
97
121
|
export function BatchExecutionFullscreen({ kind, apiKind, namespace, name, resource, selectedRunKey = '', canViewLogs = false, onSelectRun, onSwitchToLogs, onSwitchToTimeline, onNavigateToResource }: BatchExecutionProps) {
|
|
98
122
|
const scheduled = SCHEDULED_KINDS.has(kind)
|
|
99
123
|
const clusterScoped = kind === 'ClusterWorkflowTemplate'
|
|
100
|
-
const
|
|
101
|
-
const runs = runsQuery.data?.runs ?? EMPTY_RUNS
|
|
102
|
-
const defaultRun = useMemo(() => pickDefaultRun(runs), [runs])
|
|
124
|
+
const jobSetRoot = isJobSetV1Alpha2(resource)
|
|
103
125
|
const [runFilter, setRunFilter] = useState<'all' | 'active' | 'failed'>('all')
|
|
104
126
|
const [runSearch, setRunSearch] = useState('')
|
|
127
|
+
const debouncedSearch = useDebouncedValue(runSearch, 250, value => value === '')
|
|
128
|
+
const [roleFilter, setRoleFilter] = useState('')
|
|
129
|
+
const memberOptions = { role: roleFilter, search: debouncedSearch, state: runFilter, selected: selectedRunKey }
|
|
130
|
+
const runsQuery = useWorkloadRuns(apiKind, namespace, name, true, { refetchActive: true, clusterScoped, ...(jobSetRoot ? memberOptions : {}) })
|
|
131
|
+
const resourcesQuery = useJobSetResources(namespace, name, jobSetRoot ? resource.metadata?.uid : undefined, memberOptions)
|
|
132
|
+
const memberCollection = runsQuery.data?.collection === 'members'
|
|
133
|
+
const memberShell = memberCollection || (!runsQuery.data && jobSetRoot)
|
|
134
|
+
const runs = runsQuery.data?.runs ?? EMPTY_RUNS
|
|
135
|
+
const selectedRunFromResponse = runsQuery.data?.selected
|
|
136
|
+
const resolvedRuns = useMemo(
|
|
137
|
+
() => selectedRunFromResponse && !runs.some(run => workloadRunKey(run) === workloadRunKey(selectedRunFromResponse))
|
|
138
|
+
? [...runs, selectedRunFromResponse]
|
|
139
|
+
: runs,
|
|
140
|
+
[runs, selectedRunFromResponse],
|
|
141
|
+
)
|
|
142
|
+
const defaultRun = useMemo(() => memberCollection ? runs[0] : pickDefaultRun(runs), [memberCollection, runs])
|
|
143
|
+
const previousSelection = useRef(selectedRunKey)
|
|
144
|
+
const selectedDetail = useRef<HTMLElement>(null)
|
|
145
|
+
const scrollToSelection = useRef(false)
|
|
146
|
+
const selectMember = (key: string) => {
|
|
147
|
+
if (key === selectedRunKey && selectedDetail.current) { selectedDetail.current.scrollIntoView({ behavior: 'smooth', block: 'start' }); return }
|
|
148
|
+
scrollToSelection.current = true
|
|
149
|
+
onSelectRun?.(key)
|
|
150
|
+
}
|
|
105
151
|
const referencedDefinitionTarget = workflowDefinitionTarget(kind, resource)
|
|
106
152
|
const referencedDefinitionQuery = useResource<any>(
|
|
107
153
|
referencedDefinitionTarget?.kind ?? '',
|
|
@@ -115,36 +161,76 @@ export function BatchExecutionFullscreen({ kind, apiKind, namespace, name, resou
|
|
|
115
161
|
[kind, resource, referencedDefinitionQuery.data],
|
|
116
162
|
)
|
|
117
163
|
|
|
164
|
+
const selectionMissing = memberCollection && selectedRunMissing(resolvedRuns, selectedRunKey)
|
|
165
|
+
|
|
118
166
|
useEffect(() => {
|
|
119
|
-
if (!runsQuery.data) return
|
|
120
|
-
if (
|
|
167
|
+
if (!runsQuery.data || runsQuery.isPlaceholderData || selectionMissing) return
|
|
168
|
+
if (resolvedRuns.length === 0) {
|
|
121
169
|
if (selectedRunKey) onSelectRun?.('')
|
|
122
170
|
return
|
|
123
171
|
}
|
|
124
|
-
if (!
|
|
172
|
+
if (!resolvedRuns.some((run) => workloadRunKey(run) === selectedRunKey)) {
|
|
125
173
|
onSelectRun?.(workloadRunKey(defaultRun ?? runs[0]))
|
|
126
174
|
}
|
|
127
|
-
}, [runsQuery.data, runs, selectedRunKey, defaultRun, onSelectRun])
|
|
175
|
+
}, [runsQuery.data, runsQuery.isPlaceholderData, runs, resolvedRuns, selectedRunKey, defaultRun, onSelectRun, selectionMissing])
|
|
128
176
|
|
|
129
|
-
const selectedRun =
|
|
130
|
-
|
|
177
|
+
const selectedRun = selectionMissing ? undefined : resolvedRuns.find((run) => workloadRunKey(run) === selectedRunKey) ?? defaultRun
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
if (!selectedRun) return
|
|
180
|
+
const selectionChanged = previousSelection.current !== selectedRunKey
|
|
181
|
+
previousSelection.current = selectedRunKey
|
|
182
|
+
if (selectionChanged && roleFilter && selectedRun?.jobset?.replicatedJob !== roleFilter) setRoleFilter('')
|
|
183
|
+
}, [selectedRunKey, roleFilter, selectedRun])
|
|
184
|
+
const shownMemberCounts = useMemo(() => {
|
|
185
|
+
const counts = new Map<string, number>()
|
|
186
|
+
for (const run of runs) {
|
|
187
|
+
if (run.jobset?.replicatedJob) counts.set(run.jobset.replicatedJob, (counts.get(run.jobset.replicatedJob) ?? 0) + 1)
|
|
188
|
+
}
|
|
189
|
+
return counts
|
|
190
|
+
}, [runs])
|
|
191
|
+
const selectRole = (role: string) => {
|
|
192
|
+
setRoleFilter(role)
|
|
193
|
+
setRunFilter('all')
|
|
194
|
+
setRunSearch('')
|
|
195
|
+
onSelectRun?.('')
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const shouldResolveLivePods = Boolean(
|
|
199
|
+
selectedRun && (
|
|
200
|
+
memberCollection ||
|
|
201
|
+
(canViewLogs && isFailedRunPhase(selectedRun.phase) && batchRunHasContainerOutcome(selectedRun))
|
|
202
|
+
),
|
|
203
|
+
)
|
|
131
204
|
const selectedRunPodsQuery = useWorkloadPods(
|
|
132
205
|
shouldResolveLivePods ? selectedRun?.kind ?? '' : '',
|
|
133
206
|
selectedRun?.namespace ?? '',
|
|
134
207
|
selectedRun?.name ?? '',
|
|
208
|
+
{
|
|
209
|
+
limit: memberCollection ? 20 : 1,
|
|
210
|
+
refetchInterval: memberCollection && selectedRun?.active ? 5000 : false,
|
|
211
|
+
},
|
|
135
212
|
)
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
if (scrollToSelection.current && selectedRun && selectedDetail.current) {
|
|
215
|
+
selectedDetail.current.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
216
|
+
scrollToSelection.current = false
|
|
217
|
+
}
|
|
218
|
+
}, [selectedRun, selectedRunKey])
|
|
136
219
|
const hasLivePods = shouldResolveLivePods && selectedRunPodsQuery.isLoading
|
|
137
220
|
? undefined
|
|
138
221
|
: Boolean(selectedRunPodsQuery.data?.pods.length)
|
|
139
|
-
const nextStep = selectedRun
|
|
140
|
-
|
|
222
|
+
const nextStep = selectedRun
|
|
223
|
+
? memberCollection
|
|
224
|
+
? canViewLogs && hasLivePods ? 'logs' : null
|
|
225
|
+
: batchRunNextStep(selectedRun, canViewLogs, hasLivePods)
|
|
226
|
+
: null
|
|
227
|
+
const visibleRuns = useMemo(() => memberCollection ? runs : runs.filter((run) => {
|
|
141
228
|
if (runFilter === 'active' && !run.active) return false
|
|
142
229
|
if (runFilter === 'failed' && run.phase !== 'Failed' && run.phase !== 'Error') return false
|
|
143
230
|
return !runSearch || run.name.toLowerCase().includes(runSearch.toLowerCase())
|
|
144
|
-
}), [runs, runFilter, runSearch])
|
|
145
|
-
const source = sourceFacts(kind, definitionResource, runs)
|
|
231
|
+
}), [runs, runFilter, runSearch, memberCollection])
|
|
146
232
|
const phaseCounts = countPhases(runs)
|
|
147
|
-
const retentionCopy = retentionHistoryCopy(kind, resource, phaseCounts)
|
|
233
|
+
const retentionCopy = memberCollection ? null : retentionHistoryCopy(kind, resource, phaseCounts)
|
|
148
234
|
const fetchTarget = selectedRun && scheduled ? resourceTargetForRun(selectedRun) : null
|
|
149
235
|
const selectedResourceQuery = useResource<any>(
|
|
150
236
|
fetchTarget?.kind ?? '',
|
|
@@ -158,28 +244,42 @@ export function BatchExecutionFullscreen({ kind, apiKind, namespace, name, resou
|
|
|
158
244
|
() => selectedResource && selectedRun?.kind === 'workflows' ? buildWorkflowExecutionModel(selectedResource) : null,
|
|
159
245
|
[selectedResource, selectedRun?.kind],
|
|
160
246
|
)
|
|
247
|
+
const runsErrorMessage = runsQuery.error instanceof Error
|
|
248
|
+
? runsQuery.error.message
|
|
249
|
+
: memberShell
|
|
250
|
+
? 'Radar could not load this JobSet’s child Jobs.'
|
|
251
|
+
: 'Radar could not load retained runs.'
|
|
161
252
|
|
|
162
|
-
if (runsQuery.isLoading) {
|
|
253
|
+
if (runsQuery.isLoading && !jobSetRoot) {
|
|
163
254
|
return <FetchResult loading className="h-full" />
|
|
164
255
|
}
|
|
165
256
|
|
|
166
|
-
if (runsQuery.error) {
|
|
257
|
+
if (runsQuery.error && !jobSetRoot) {
|
|
167
258
|
return (
|
|
168
259
|
<div className="p-4">
|
|
169
|
-
<EmptyState
|
|
260
|
+
<EmptyState
|
|
261
|
+
tone="neutral"
|
|
262
|
+
variant="card"
|
|
263
|
+
headline="Run history unavailable"
|
|
264
|
+
body={runsErrorMessage}
|
|
265
|
+
/>
|
|
170
266
|
</div>
|
|
171
267
|
)
|
|
172
268
|
}
|
|
173
269
|
|
|
174
270
|
return (
|
|
175
271
|
<div className="flex h-full min-h-0 bg-theme-base">
|
|
176
|
-
{scheduled && (
|
|
272
|
+
{scheduled && !memberShell && (
|
|
177
273
|
<aside className="flex w-72 shrink-0 flex-col border-r border-theme-border bg-theme-surface">
|
|
178
274
|
<div className="border-b border-theme-border px-3 py-3">
|
|
179
275
|
<div className="flex items-center justify-between gap-2">
|
|
180
276
|
<div>
|
|
181
|
-
<div className="text-xs font-medium uppercase tracking-wide text-theme-text-tertiary">
|
|
182
|
-
|
|
277
|
+
<div className="text-xs font-medium uppercase tracking-wide text-theme-text-tertiary">
|
|
278
|
+
{isTemplateKind(kind) ? 'Workflows using this definition' : 'Run history'}
|
|
279
|
+
</div>
|
|
280
|
+
<div className="mt-1 text-sm font-semibold text-theme-text-primary">
|
|
281
|
+
{pluralizeRuns(runs.length)}
|
|
282
|
+
</div>
|
|
183
283
|
</div>
|
|
184
284
|
</div>
|
|
185
285
|
<div className="mt-2 flex flex-wrap gap-1 text-[10px] text-theme-text-tertiary">
|
|
@@ -187,7 +287,9 @@ export function BatchExecutionFullscreen({ kind, apiKind, namespace, name, resou
|
|
|
187
287
|
{phaseCounts.failed > 0 && <span className="rounded bg-theme-hover px-1.5 py-0.5">{phaseCounts.failed} failed</span>}
|
|
188
288
|
{phaseCounts.succeeded > 0 && <span className="rounded bg-theme-hover px-1.5 py-0.5">{phaseCounts.succeeded} succeeded</span>}
|
|
189
289
|
</div>
|
|
190
|
-
<p className="mt-2 text-[10px] leading-4 text-theme-text-tertiary">
|
|
290
|
+
<p className="mt-2 text-[10px] leading-4 text-theme-text-tertiary">
|
|
291
|
+
Retained Kubernetes objects, not all-time history.
|
|
292
|
+
</p>
|
|
191
293
|
{retentionCopy && <p className="mt-1 text-[10px] leading-4 text-theme-text-secondary">{retentionCopy}</p>}
|
|
192
294
|
{(runs.length > 8 || phaseCounts.failed > 0) && (
|
|
193
295
|
<div className="mt-3 space-y-2">
|
|
@@ -212,15 +314,20 @@ export function BatchExecutionFullscreen({ kind, apiKind, namespace, name, resou
|
|
|
212
314
|
/>
|
|
213
315
|
</div>
|
|
214
316
|
) : visibleRuns.length === 0 ? (
|
|
215
|
-
<EmptyState
|
|
317
|
+
<EmptyState
|
|
318
|
+
tone="filtered"
|
|
319
|
+
variant="card"
|
|
320
|
+
headline="No runs match these filters"
|
|
321
|
+
body="Change the status filter or run-name search."
|
|
322
|
+
/>
|
|
216
323
|
) : (
|
|
217
324
|
<div className="space-y-1">
|
|
218
325
|
{visibleRuns.map((run) => (
|
|
219
326
|
<RunRailButton
|
|
220
|
-
key={
|
|
327
|
+
key={workloadRunKey(run)}
|
|
221
328
|
run={run}
|
|
222
329
|
showNamespace={clusterScoped}
|
|
223
|
-
selected={
|
|
330
|
+
selected={Boolean(selectedRun && workloadRunKey(selectedRun) === workloadRunKey(run))}
|
|
224
331
|
onClick={() => onSelectRun?.(workloadRunKey(run))}
|
|
225
332
|
/>
|
|
226
333
|
))}
|
|
@@ -232,19 +339,49 @@ export function BatchExecutionFullscreen({ kind, apiKind, namespace, name, resou
|
|
|
232
339
|
|
|
233
340
|
<main className="min-w-0 flex-1 overflow-auto">
|
|
234
341
|
<div className="space-y-4 p-4">
|
|
342
|
+
{jobSetRoot && (
|
|
343
|
+
<section className="rounded-lg border border-theme-border bg-theme-surface p-4" aria-label="JobSet overview">
|
|
344
|
+
<JobSetRenderer data={resource} mode="overview" shownMemberCounts={shownMemberCounts} onSelectRole={memberCollection ? selectRole : undefined} />
|
|
345
|
+
</section>
|
|
346
|
+
)}
|
|
347
|
+
{jobSetRoot && <JobSetAdmission resource={resource} namespace={namespace} name={name} onNavigate={onNavigateToResource} />}
|
|
348
|
+
{memberShell && <JobSetMemberComparison
|
|
349
|
+
runs={runs} total={runsQuery.data?.total ?? 0} filteredTotal={runsQuery.data?.filteredTotal ?? runs.length}
|
|
350
|
+
truncated={runsQuery.data?.truncated ?? false} loading={runsQuery.isLoading} error={runsQuery.error}
|
|
351
|
+
role={roleFilter} onRoleChange={setRoleFilter} roles={resource.spec?.replicatedJobs?.map((role: { name: string }) => role.name) ?? []}
|
|
352
|
+
search={runSearch} onSearchChange={setRunSearch} state={runFilter} onStateChange={setRunFilter}
|
|
353
|
+
selected={selectedRunKey} onSelect={selectMember}
|
|
354
|
+
resources={resourcesQuery.data?.uid === resource.metadata?.uid ? resourcesQuery.data : undefined}
|
|
355
|
+
resourceError={resourcesQuery.error} resourcesLoading={resourcesQuery.isLoading} durationFor={formatRunDuration}
|
|
356
|
+
/>}
|
|
235
357
|
{scheduled && selectedResourceQuery.error && (
|
|
236
|
-
<EmptyState
|
|
358
|
+
<EmptyState
|
|
359
|
+
tone="neutral"
|
|
360
|
+
variant="card"
|
|
361
|
+
headline={memberCollection ? 'Selected Job unavailable' : 'Selected run unavailable'}
|
|
362
|
+
body={
|
|
363
|
+
selectedResourceQuery.error instanceof Error
|
|
364
|
+
? selectedResourceQuery.error.message
|
|
365
|
+
: memberCollection
|
|
366
|
+
? 'Radar could not load this child Job.'
|
|
367
|
+
: 'Radar could not load this retained run.'
|
|
368
|
+
}
|
|
369
|
+
/>
|
|
237
370
|
)}
|
|
238
|
-
<div className=
|
|
371
|
+
<div className={clsx('grid items-start gap-4', jobSetRoot ? '2xl:grid-cols-[minmax(0,1.7fr)_minmax(320px,0.9fr)]' : 'lg:grid-cols-[minmax(0,1.7fr)_minmax(320px,0.9fr)]')}>
|
|
239
372
|
<section className="min-w-0 space-y-4">
|
|
240
|
-
{
|
|
241
|
-
<
|
|
373
|
+
{memberShell && runsQuery.isLoading ? (
|
|
374
|
+
<FetchResult loading className="min-h-40 rounded-lg border border-theme-border bg-theme-surface" />
|
|
375
|
+
) : memberShell && runsQuery.error ? (
|
|
376
|
+
<EmptyState tone="neutral" variant="card" headline="Member Jobs unavailable" body={runsErrorMessage} />
|
|
377
|
+
) : selectedRun ? (
|
|
378
|
+
<section ref={selectedDetail} className="scroll-mt-4 rounded-lg border border-theme-border bg-theme-surface">
|
|
242
379
|
<div className="flex items-start justify-between gap-3 border-b border-theme-border px-4 py-3">
|
|
243
380
|
<div className="min-w-0">
|
|
244
381
|
<div className="flex items-center gap-2">
|
|
245
382
|
<StatusDot tone={mapHealthToTone(phaseHealth(selectedRun.phase))} />
|
|
246
383
|
<div className="min-w-0">
|
|
247
|
-
<div className="text-[10px] font-medium uppercase tracking-wide text-theme-text-tertiary">Selected run</div>
|
|
384
|
+
<div className="text-[10px] font-medium uppercase tracking-wide text-theme-text-tertiary">{memberCollection ? 'Selected Job' : 'Selected run'}</div>
|
|
248
385
|
{onNavigateToResource ? (
|
|
249
386
|
<button type="button" className="block max-w-full truncate text-base font-semibold text-accent-text hover:underline" onClick={() => onNavigateToResource(resourceTargetForRun(selectedRun))}>{selectedRun.name}</button>
|
|
250
387
|
) : <h3 className="truncate text-base font-semibold text-theme-text-primary">{selectedRun.name}</h3>}
|
|
@@ -259,9 +396,14 @@ export function BatchExecutionFullscreen({ kind, apiKind, namespace, name, resou
|
|
|
259
396
|
{selectedResourceQuery.isFetching && <span>refreshing</span>}
|
|
260
397
|
</div>
|
|
261
398
|
</div>
|
|
262
|
-
<span className={clsx('badge', phaseBadgeClass(selectedRun.phase))}>{selectedRun.phase}</span>
|
|
399
|
+
<span className={clsx('badge', phaseBadgeClass(selectedRun.phase))}>{selectedRun.phase}{selectedRun.deleting ? ' · deleting' : ''}</span>
|
|
263
400
|
</div>
|
|
264
|
-
<
|
|
401
|
+
{memberCollection && <div className="flex flex-wrap items-center gap-3 border-b border-theme-border px-4 py-2 text-xs">
|
|
402
|
+
{!runs.some(run => workloadRunKey(run) === selectedRunKey) && <span className="text-theme-text-secondary">Selected Job is outside the current filter or shown window.</span>}
|
|
403
|
+
{onNavigateToResource && <><button type="button" className="text-accent-text hover:underline" onClick={() => onNavigateToResource(resourceTargetForRun(selectedRun))}>Open Job</button><button type="button" className="text-accent-text hover:underline" onClick={() => onNavigateToResource({ ...resourceTargetForRun(selectedRun), tab: 'metrics' })}>Job metrics</button></>}
|
|
404
|
+
{canViewLogs && <button type="button" className="text-accent-text hover:underline" onClick={onSwitchToLogs}>Job logs</button>}
|
|
405
|
+
</div>}
|
|
406
|
+
<RunDetailList run={selectedRun} resource={selectedResource} workflowExecution={workflowExecution} scheduledParent={scheduled} memberCollection={memberCollection} />
|
|
265
407
|
<RunContext run={selectedRun} resource={selectedResource} definitionResource={definitionResource} workflowExecution={workflowExecution} currentWorkload={{ kind, namespace, name }} onNavigateToResource={onNavigateToResource} />
|
|
266
408
|
{(selectedRun.message || isFailedRunPhase(selectedRun.phase)) && (
|
|
267
409
|
<RunMessageDetails
|
|
@@ -275,11 +417,33 @@ export function BatchExecutionFullscreen({ kind, apiKind, namespace, name, resou
|
|
|
275
417
|
<EmptyState
|
|
276
418
|
tone="neutral"
|
|
277
419
|
variant="card"
|
|
278
|
-
headline=
|
|
279
|
-
body={
|
|
420
|
+
headline={selectionMissing ? 'Selected Job is currently unavailable' : memberCollection ? 'No selected Job' : 'No selected run'}
|
|
421
|
+
body={
|
|
422
|
+
selectionMissing
|
|
423
|
+
? 'This Job is not currently retained. It may have been removed or be waiting for recreation. Your selection is preserved if it reappears; choose another shown Job to switch.'
|
|
424
|
+
: memberCollection
|
|
425
|
+
? 'This JobSet has no readable child Jobs to inspect.'
|
|
426
|
+
: `There are no retained ${runKindPluralForSchedule(kind)} to inspect.`
|
|
427
|
+
}
|
|
280
428
|
/>
|
|
281
429
|
)}
|
|
282
430
|
|
|
431
|
+
{memberCollection && selectedRun && (
|
|
432
|
+
<JobSetMemberPods
|
|
433
|
+
pods={selectedRunPodsQuery.data?.pods ?? []}
|
|
434
|
+
total={selectedRunPodsQuery.data?.total ?? 0}
|
|
435
|
+
truncated={selectedRunPodsQuery.data?.truncated ?? false}
|
|
436
|
+
loading={selectedRunPodsQuery.isLoading}
|
|
437
|
+
error={selectedRunPodsQuery.error}
|
|
438
|
+
namespace={selectedRun.namespace}
|
|
439
|
+
onNavigateToResource={onNavigateToResource}
|
|
440
|
+
/>
|
|
441
|
+
)}
|
|
442
|
+
|
|
443
|
+
{memberCollection && selectedRun && (
|
|
444
|
+
<RunActivityPanel run={selectedRun} resource={selectedResource} workflowExecution={workflowExecution} />
|
|
445
|
+
)}
|
|
446
|
+
|
|
283
447
|
{selectedRun?.kind === 'workflows' && (
|
|
284
448
|
<RunExecutionPanel run={selectedRun} workflowExecution={workflowExecution} loading={selectedResourceQuery.isLoading} onNavigateToResource={onNavigateToResource} />
|
|
285
449
|
)}
|
|
@@ -291,16 +455,20 @@ export function BatchExecutionFullscreen({ kind, apiKind, namespace, name, resou
|
|
|
291
455
|
<h3 className="text-sm font-semibold text-theme-text-primary">{configurationTitle(kind)}</h3>
|
|
292
456
|
</div>
|
|
293
457
|
<div className="space-y-3 p-4">
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
458
|
+
{jobSetRoot ? (
|
|
459
|
+
<JobSetRenderer data={resource} mode="configuration" />
|
|
460
|
+
) : (
|
|
461
|
+
<SourceFacts
|
|
462
|
+
source={sourceFacts(kind, definitionResource, runs)}
|
|
463
|
+
namespace={selectedRun?.namespace || namespace}
|
|
464
|
+
definitionLoading={Boolean(referencedDefinitionTarget) && referencedDefinitionQuery.isLoading}
|
|
465
|
+
definitionError={referencedDefinitionTarget ? referencedDefinitionQuery.error : undefined}
|
|
466
|
+
/>
|
|
467
|
+
)}
|
|
300
468
|
</div>
|
|
301
469
|
</section>
|
|
302
470
|
|
|
303
|
-
{selectedRun && (
|
|
471
|
+
{selectedRun && !memberCollection && (
|
|
304
472
|
<RunActivityPanel run={selectedRun} resource={selectedResource} workflowExecution={workflowExecution} />
|
|
305
473
|
)}
|
|
306
474
|
</section>
|
|
@@ -638,14 +806,23 @@ function parameterValue(parameter: WorkflowParameter): string {
|
|
|
638
806
|
return 'Required'
|
|
639
807
|
}
|
|
640
808
|
|
|
641
|
-
function RunDetailList({ run, resource, workflowExecution, scheduledParent }: { run: WorkloadRun; resource: any; workflowExecution: WorkflowExecutionModel | null; scheduledParent: boolean }) {
|
|
809
|
+
function RunDetailList({ run, resource, workflowExecution, scheduledParent, memberCollection }: { run: WorkloadRun; resource: any; workflowExecution: WorkflowExecutionModel | null; scheduledParent: boolean; memberCollection: boolean }) {
|
|
642
810
|
const isWorkflowRun = run.kind === 'workflows' || !!workflowExecution
|
|
811
|
+
const advancedRows: Array<[string, string]> = [
|
|
812
|
+
...(run.jobset?.jobIndex ? [['Job index', formatIndexedCount(run.jobset?.jobIndex, run.jobset?.replicatedJobReplicas)]] as Array<[string, string]> : []),
|
|
813
|
+
...(run.jobset?.globalIndex ? [['Global index', formatIndexedCount(run.jobset?.globalIndex, run.jobset?.globalReplicas)]] as Array<[string, string]> : []),
|
|
814
|
+
...(run.jobset?.groupName ? [['Group', run.jobset?.groupName]] as Array<[string, string]> : []),
|
|
815
|
+
...(run.jobset?.groupIndex ? [['Group index', formatIndexedCount(run.jobset?.groupIndex, run.jobset?.groupReplicas)]] as Array<[string, string]> : []),
|
|
816
|
+
]
|
|
643
817
|
const rows: Array<[string, string]> = [
|
|
818
|
+
...(run.jobset?.replicatedJob ? [['Role', run.jobset?.replicatedJob]] as Array<[string, string]> : []),
|
|
819
|
+
...(run.jobset?.restartAttempt ? [['JobSet restart attempt', `#${run.jobset?.restartAttempt}`]] as Array<[string, string]> : []),
|
|
820
|
+
...(run.jobset?.jobRestartAttempt ? [['Job restart attempt', `#${run.jobset?.jobRestartAttempt}`]] as Array<[string, string]> : []),
|
|
644
821
|
['Started', run.startedAt ? formatAge(run.startedAt) : '-'],
|
|
645
822
|
['Finished', run.finishedAt ? formatAge(run.finishedAt) : run.active ? 'Running' : '-'],
|
|
646
823
|
['Duration', formatRunDuration(run) || '-'],
|
|
647
824
|
...(run.progress ? [['Progress', run.progress]] as Array<[string, string]> : []),
|
|
648
|
-
...(scheduledParent || run.trigger || run.scheduledAt ? [
|
|
825
|
+
...(!memberCollection && (scheduledParent || run.trigger || run.scheduledAt) ? [
|
|
649
826
|
['Trigger', run.trigger === 'manual' ? 'Manual' : run.trigger === 'event' ? 'Event' : run.scheduledAt ? 'Cron schedule' : '-'],
|
|
650
827
|
] as Array<[string, string]> : []),
|
|
651
828
|
...(run.scheduledAt ? [
|
|
@@ -662,27 +839,32 @@ function RunDetailList({ run, resource, workflowExecution, scheduledParent }: {
|
|
|
662
839
|
] as Array<[string, string]>),
|
|
663
840
|
]
|
|
664
841
|
return (
|
|
665
|
-
<div className="grid gap-x-8 gap-y-2 p-4 xl:grid-cols-2">
|
|
842
|
+
<div><div className="grid gap-x-8 gap-y-2 p-4 xl:grid-cols-2">
|
|
666
843
|
{rows.map(([label, value]) => (
|
|
667
844
|
<div key={label} className="flex items-start justify-between gap-3 text-sm">
|
|
668
845
|
<span className="text-theme-text-tertiary">{label}</span>
|
|
669
846
|
<span className="min-w-0 break-words text-right text-theme-text-primary">{value}</span>
|
|
670
847
|
</div>
|
|
671
848
|
))}
|
|
672
|
-
</div>
|
|
849
|
+
</div>{advancedRows.length > 0 && <Disclosure summary="Member indices" className="px-4 pb-3" summaryClassName="text-xs text-theme-text-tertiary"><div className="flex flex-wrap gap-x-4 gap-y-1 pt-2 text-xs text-theme-text-secondary">{advancedRows.map(([label, value]) => <span key={label}>{label}: {value}</span>)}</div></Disclosure>}</div>
|
|
673
850
|
)
|
|
674
851
|
}
|
|
675
852
|
|
|
853
|
+
function formatIndexedCount(index: string, total?: string): string {
|
|
854
|
+
return total ? `#${index} / ${total}` : `#${index}`
|
|
855
|
+
}
|
|
856
|
+
|
|
676
857
|
function RunMessageDetails({ run, nextStep, onNextStep }: { run: WorkloadRun; nextStep: BatchRunNextStep | null; onNextStep?: () => void }) {
|
|
677
858
|
const [open, setOpen] = useState(false)
|
|
678
859
|
const failed = isFailedRunPhase(run.phase)
|
|
679
860
|
const action = nextStep && onNextStep ? <RunNextStep step={nextStep} onClick={onNextStep} /> : null
|
|
680
861
|
const message = run.message ?? ''
|
|
862
|
+
const messageLabel = run.launcher?.kind === 'JobSet' ? 'Job message' : 'Run message'
|
|
681
863
|
if (!runMessageNeedsDisclosure(message)) {
|
|
682
864
|
return (
|
|
683
865
|
<div className="flex flex-wrap items-start gap-x-3 gap-y-2 border-t border-theme-border px-4 py-3 text-sm">
|
|
684
866
|
<div className="flex min-w-0 flex-1 items-start gap-3">
|
|
685
|
-
<span className={clsx('shrink-0 font-medium', failed ? 'text-red-700 dark:text-red-300' : 'text-theme-text-primary')}>{failed ? 'Failure' :
|
|
867
|
+
<span className={clsx('shrink-0 font-medium', failed ? 'text-red-700 dark:text-red-300' : 'text-theme-text-primary')}>{failed ? 'Failure' : messageLabel}</span>
|
|
686
868
|
{message && <span className="min-w-0 break-words text-theme-text-tertiary">{message}</span>}
|
|
687
869
|
</div>
|
|
688
870
|
{action}
|
|
@@ -695,7 +877,7 @@ function RunMessageDetails({ run, nextStep, onNextStep }: { run: WorkloadRun; ne
|
|
|
695
877
|
<button type="button" aria-expanded={open} onClick={() => setOpen((value) => !value)} className="flex min-w-0 flex-1 items-center gap-2 px-4 py-3 text-left">
|
|
696
878
|
<CollapseChevron open={open} className="h-4 w-4" />
|
|
697
879
|
<span className={clsx('shrink-0 text-sm font-medium', failed ? 'text-red-700 dark:text-red-300' : 'text-theme-text-primary')}>
|
|
698
|
-
{failed ? 'Failure details' :
|
|
880
|
+
{failed ? 'Failure details' : messageLabel}
|
|
699
881
|
</span>
|
|
700
882
|
<span className="min-w-0 truncate text-xs text-theme-text-tertiary">{message}</span>
|
|
701
883
|
</button>
|
|
@@ -790,9 +972,9 @@ function RunActivityPanel({ run, resource, workflowExecution }: { run: WorkloadR
|
|
|
790
972
|
const defaultItems = activity.length <= 10 ? activity : activityPreviewItems(activity)
|
|
791
973
|
const overflowItems = activity.slice(defaultItems.length)
|
|
792
974
|
return (
|
|
793
|
-
<Panel title=
|
|
975
|
+
<Panel title={run.launcher?.kind === 'JobSet' ? 'Job activity' : 'Run activity'} icon={Activity} detail={activity.length ? `${activity.length} events` : undefined}>
|
|
794
976
|
{activity.length === 0 ? (
|
|
795
|
-
<EmptyState tone="neutral" variant="card" headline="No activity yet" body=
|
|
977
|
+
<EmptyState tone="neutral" variant="card" headline="No activity yet" body={run.launcher?.kind === 'JobSet' ? 'This Job has not reported timing details yet.' : 'This run has not reported timing details yet.'} />
|
|
796
978
|
) : (
|
|
797
979
|
<div className="divide-y divide-theme-border border-y border-theme-border">
|
|
798
980
|
{defaultItems.map((item) => <RunActivityRow key={item.id} item={item} runMessage={run.message} />)}
|
|
@@ -845,7 +1027,7 @@ function RunContext({ run, resource, definitionResource, workflowExecution, curr
|
|
|
845
1027
|
const currentDefinition = executionDefinitionSummary(currentWorkload.kind, definitionResource)
|
|
846
1028
|
const parentDefinesRun = !isDirectRunKind(currentWorkload.kind, run.kind)
|
|
847
1029
|
const showRunConfiguration = parentDefinesRun && Boolean(runDefinition)
|
|
848
|
-
const definitionDiffers = showRunConfiguration &&
|
|
1030
|
+
const definitionDiffers = showRunConfiguration && executionDefinitionDiffers(currentDefinition, runDefinition)
|
|
849
1031
|
if (!launcher && !definition && uses.length === 0 && arguments_.length === 0 && outputs.length === 0 && !showRunConfiguration) return null
|
|
850
1032
|
return (
|
|
851
1033
|
<div className="border-t border-theme-border p-4">
|
|
@@ -853,7 +1035,7 @@ function RunContext({ run, resource, definitionResource, workflowExecution, curr
|
|
|
853
1035
|
<div>
|
|
854
1036
|
<h4 className="mb-3 text-xs font-medium uppercase tracking-wide text-theme-text-tertiary">Run context</h4>
|
|
855
1037
|
<div className="space-y-3">
|
|
856
|
-
{launcher && <ContextRow label={launcher.kind === 'ScaledJob' ? 'Triggered by' : 'Scheduled by'}><GenericResourceButton refInfo={launcher} onNavigateToResource={onNavigateToResource} /></ContextRow>}
|
|
1038
|
+
{launcher && <ContextRow label={launcher.kind === 'ScaledJob' ? 'Triggered by' : launcher.kind === 'JobSet' ? 'Member of' : 'Scheduled by'}><GenericResourceButton refInfo={launcher} selectedMember={launcher.kind === 'JobSet' ? workloadRunKey(run) : undefined} onNavigateToResource={onNavigateToResource} /></ContextRow>}
|
|
857
1039
|
{definition && <ContextRow label="Definition"><ResourceButton refInfo={definition} onNavigateToResource={onNavigateToResource} /></ContextRow>}
|
|
858
1040
|
{uses.length > 0 && (
|
|
859
1041
|
<ContextRow label="Uses">
|
|
@@ -911,10 +1093,10 @@ function sameResource(ref: { kind: string; namespace?: string; name: string }, c
|
|
|
911
1093
|
return ref.kind === current.kind && (ref.namespace ?? '') === current.namespace && ref.name === current.name
|
|
912
1094
|
}
|
|
913
1095
|
|
|
914
|
-
function GenericResourceButton({ refInfo, onNavigateToResource }: { refInfo: NonNullable<WorkloadRun['launcher']>; onNavigateToResource?: BatchExecutionProps['onNavigateToResource'] }) {
|
|
1096
|
+
function GenericResourceButton({ refInfo, selectedMember, onNavigateToResource }: { selectedMember?: string; refInfo: NonNullable<WorkloadRun['launcher']>; onNavigateToResource?: BatchExecutionProps['onNavigateToResource'] }) {
|
|
915
1097
|
const label = `${refInfo.kind} · ${refInfo.namespace ? `${refInfo.namespace}/` : ''}${refInfo.name}`
|
|
916
1098
|
if (!onNavigateToResource) return <span>{label}</span>
|
|
917
|
-
return <button type="button" className="text-accent-text hover:underline" onClick={() => onNavigateToResource({ kind: pluralKind(refInfo.kind), namespace: refInfo.namespace ?? '', name: refInfo.name, group: refInfo.group })}>{label}</button>
|
|
1099
|
+
return <button type="button" className="text-accent-text hover:underline" onClick={() => onNavigateToResource({ kind: pluralKind(refInfo.kind), namespace: refInfo.namespace ?? '', name: refInfo.name, group: refInfo.group, run: selectedMember })}>{label}</button>
|
|
918
1100
|
}
|
|
919
1101
|
|
|
920
1102
|
function dedupeResourceRefs(refs: WorkflowTemplateReference[]): WorkflowTemplateReference[] {
|
|
@@ -942,6 +1124,81 @@ function Panel({ title, icon: Icon, detail, children }: { title: string; icon: C
|
|
|
942
1124
|
)
|
|
943
1125
|
}
|
|
944
1126
|
|
|
1127
|
+
function JobSetMemberPods({
|
|
1128
|
+
pods,
|
|
1129
|
+
total,
|
|
1130
|
+
truncated,
|
|
1131
|
+
loading,
|
|
1132
|
+
error,
|
|
1133
|
+
namespace,
|
|
1134
|
+
onNavigateToResource,
|
|
1135
|
+
}: {
|
|
1136
|
+
pods: WorkloadPodInfo[]
|
|
1137
|
+
total: number
|
|
1138
|
+
truncated: boolean
|
|
1139
|
+
loading: boolean
|
|
1140
|
+
error: unknown
|
|
1141
|
+
namespace: string
|
|
1142
|
+
onNavigateToResource?: BatchExecutionProps['onNavigateToResource']
|
|
1143
|
+
}) {
|
|
1144
|
+
return (
|
|
1145
|
+
<Panel
|
|
1146
|
+
title="Pods for selected Job"
|
|
1147
|
+
icon={GitBranch}
|
|
1148
|
+
detail={total ? `${total} ${total === 1 ? 'Pod' : 'Pods'}` : undefined}
|
|
1149
|
+
>
|
|
1150
|
+
{loading ? (
|
|
1151
|
+
<FetchResult loading />
|
|
1152
|
+
) : error ? (
|
|
1153
|
+
<EmptyState
|
|
1154
|
+
tone="neutral"
|
|
1155
|
+
variant="card"
|
|
1156
|
+
headline="Pods unavailable"
|
|
1157
|
+
body={error instanceof Error ? error.message : 'Radar could not load Pods for this Job.'}
|
|
1158
|
+
/>
|
|
1159
|
+
) : pods.length === 0 ? (
|
|
1160
|
+
<EmptyState
|
|
1161
|
+
tone="neutral"
|
|
1162
|
+
variant="card"
|
|
1163
|
+
headline="No Pods currently retained"
|
|
1164
|
+
body="No readable Pods owned by this Job are currently available. They may not exist yet or may already have been cleaned up."
|
|
1165
|
+
/>
|
|
1166
|
+
) : (
|
|
1167
|
+
<div className="divide-y divide-theme-border rounded-md border border-theme-border">
|
|
1168
|
+
{pods.map((pod) => (
|
|
1169
|
+
<div key={pod.name} className="flex items-center gap-3 px-3 py-2">
|
|
1170
|
+
<span className={clsx('h-2 w-2 shrink-0 rounded-full', podDotClass(pod.healthLevel, pod.ready))} />
|
|
1171
|
+
<div className="min-w-0 flex-1">
|
|
1172
|
+
{onNavigateToResource ? (
|
|
1173
|
+
<button
|
|
1174
|
+
type="button"
|
|
1175
|
+
className="block max-w-full truncate text-sm font-medium text-accent-text hover:underline"
|
|
1176
|
+
onClick={() => onNavigateToResource({ kind: 'pods', namespace, name: pod.name })}
|
|
1177
|
+
>
|
|
1178
|
+
{pod.name}
|
|
1179
|
+
</button>
|
|
1180
|
+
) : (
|
|
1181
|
+
<div className="truncate text-sm font-medium text-theme-text-primary">{pod.name}</div>
|
|
1182
|
+
)}
|
|
1183
|
+
<div className="truncate text-xs text-theme-text-tertiary">{workloadPodDetail(pod)}</div>
|
|
1184
|
+
{pod.nodeName && <div className="truncate text-xs text-theme-text-tertiary">Node {pod.nodeName}</div>}
|
|
1185
|
+
</div>
|
|
1186
|
+
<span className={clsx('badge-sm shrink-0', podStatusClass(pod.healthLevel, pod.ready))}>
|
|
1187
|
+
{podStatusLabel(pod.healthLevel, pod.ready)}
|
|
1188
|
+
</span>
|
|
1189
|
+
</div>
|
|
1190
|
+
))}
|
|
1191
|
+
{truncated && (
|
|
1192
|
+
<div className="px-3 py-2 text-xs text-theme-text-tertiary">
|
|
1193
|
+
Showing {pods.length} of {total} Pods, with problems first.
|
|
1194
|
+
</div>
|
|
1195
|
+
)}
|
|
1196
|
+
</div>
|
|
1197
|
+
)}
|
|
1198
|
+
</Panel>
|
|
1199
|
+
)
|
|
1200
|
+
}
|
|
1201
|
+
|
|
945
1202
|
function ExecutionNodeRow({ node, depth, showMessage, namespace, onNavigateToResource }: { node: WorkflowExecutionNode; depth: number; showMessage: boolean; namespace: string; onNavigateToResource?: BatchExecutionProps['onNavigateToResource'] }) {
|
|
946
1203
|
return (
|
|
947
1204
|
<div className="flex items-start gap-3 px-3 py-2" style={{ paddingLeft: `${12 + Math.min(depth, 8) * 18}px` }}>
|
|
@@ -995,9 +1252,11 @@ function RunRailButton({ run, selected, showNamespace, onClick }: { run: Workloa
|
|
|
995
1252
|
<Tooltip content={run.name} delay={300} wrapperClassName="block min-w-0">
|
|
996
1253
|
<span className="block truncate text-xs font-medium text-theme-text-primary">{showNamespace ? `${run.namespace}/` : ''}{midTruncate(run.name, 34)}</span>
|
|
997
1254
|
</Tooltip>
|
|
998
|
-
<span className="mt-0.5 block truncate text-[10px] text-theme-text-tertiary">
|
|
1255
|
+
<span className="mt-0.5 block truncate text-[10px] text-theme-text-tertiary">
|
|
1256
|
+
{formatRunTime(run) || 'time unknown'}{formatRunDuration(run) ? ` · ${formatRunDuration(run)}` : ''} · {workCount(run)}
|
|
1257
|
+
</span>
|
|
999
1258
|
</span>
|
|
1000
|
-
<span className={clsx('badge-sm shrink-0', phaseBadgeClass(run.phase))}>{shortPhase(run.phase)}</span>
|
|
1259
|
+
<span className={clsx('badge-sm shrink-0', phaseBadgeClass(run.phase))}>{shortPhase(run.phase)}{run.deleting ? ' · deleting' : ''}</span>
|
|
1001
1260
|
</button>
|
|
1002
1261
|
)
|
|
1003
1262
|
}
|
|
@@ -1122,12 +1381,12 @@ function retentionLimitCopy(limit: number | undefined, phase: string): string {
|
|
|
1122
1381
|
return limit === 0 ? `no ${phase}` : `latest ${limit} ${phase}`
|
|
1123
1382
|
}
|
|
1124
1383
|
|
|
1125
|
-
function resourceTargetForRun(run: WorkloadRun) {
|
|
1384
|
+
export function resourceTargetForRun(run: WorkloadRun) {
|
|
1126
1385
|
return {
|
|
1127
1386
|
kind: run.kind,
|
|
1128
1387
|
namespace: run.namespace,
|
|
1129
1388
|
name: run.name,
|
|
1130
|
-
group: run.
|
|
1389
|
+
group: run.group,
|
|
1131
1390
|
}
|
|
1132
1391
|
}
|
|
1133
1392
|
|
|
@@ -1147,10 +1406,10 @@ function pluralKind(kind: string): string {
|
|
|
1147
1406
|
|
|
1148
1407
|
function workCount(run: WorkloadRun): string {
|
|
1149
1408
|
if (run.podTotal) {
|
|
1150
|
-
if (run.podRunning) return `${run.podRunning} running ${pluralize('pod', run.podRunning)}`
|
|
1409
|
+
if (run.podRunning) return `${run.podRunning} ${run.kind === 'jobs' ? 'active' : 'running'} ${pluralize('pod', run.podRunning)}`
|
|
1151
1410
|
if (run.podPending && !run.podSucceeded && !run.podFailed) return `${run.podPending} pending ${pluralize('pod', run.podPending)}`
|
|
1152
1411
|
if (run.podFailed && run.phase !== 'Succeeded') return `${run.podFailed}/${run.podTotal} pods failed`
|
|
1153
|
-
return `${run.podSucceeded ?? 0}/${run.podTotal} pods`
|
|
1412
|
+
return `${run.podSucceeded ?? 0}/${run.podTotal} completed pods`
|
|
1154
1413
|
}
|
|
1155
1414
|
if (run.desired) return `${run.succeeded ?? 0}/${run.desired} completions`
|
|
1156
1415
|
return 'work unknown'
|
|
@@ -1167,7 +1426,7 @@ function podBreakdown(run: WorkloadRun, workflowExecution?: WorkflowExecutionMod
|
|
|
1167
1426
|
const podTotal = counts?.podTotal ?? run.podTotal
|
|
1168
1427
|
if (!podTotal) return null
|
|
1169
1428
|
const parts = [
|
|
1170
|
-
(counts?.podRunning ?? run.podRunning) ? `${counts?.podRunning ?? run.podRunning} running` : '',
|
|
1429
|
+
(counts?.podRunning ?? run.podRunning) ? `${counts?.podRunning ?? run.podRunning} ${run.kind === 'jobs' ? 'active' : 'running'}` : '',
|
|
1171
1430
|
(counts?.podSucceeded ?? run.podSucceeded) ? `${counts?.podSucceeded ?? run.podSucceeded} succeeded` : '',
|
|
1172
1431
|
(counts?.podFailed ?? run.podFailed) ? `${counts?.podFailed ?? run.podFailed} failed` : '',
|
|
1173
1432
|
(counts?.podPending ?? run.podPending) ? `${counts?.podPending ?? run.podPending} pending` : '',
|