@skyhook-io/radar-app 1.14.4 → 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.
Files changed (98) hide show
  1. package/README.md +2 -9
  2. package/package.json +6 -6
  3. package/src/App.tsx +91 -328
  4. package/src/RadarApp.tsx +3 -8
  5. package/src/api/client.jobset.test.ts +18 -0
  6. package/src/api/client.resource-identity.test.ts +44 -0
  7. package/src/api/client.rightsizing.test.ts +7 -3
  8. package/src/api/client.ts +233 -78
  9. package/src/api/preferences.test.ts +54 -0
  10. package/src/api/preferences.ts +38 -0
  11. package/src/components/CloudFunnelButton.test.tsx +155 -0
  12. package/src/components/CloudFunnelButton.tsx +14 -8
  13. package/src/components/ContextSwitcher.tsx +13 -132
  14. package/src/components/applications/ApplicationsView.identity.test.ts +39 -0
  15. package/src/components/applications/ApplicationsView.tsx +5 -3
  16. package/src/components/audit/AuditSettingsDialog.tsx +15 -11
  17. package/src/components/diagnose/AIMarkdown.tsx +8 -3
  18. package/src/components/diagnose/AgentControls.tsx +26 -3
  19. package/src/components/diagnose/AssessmentCard.tsx +249 -237
  20. package/src/components/diagnose/DiagnoseContext.tsx +1 -0
  21. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +47 -0
  22. package/src/components/diagnose/InvestigationEvidencePane.tsx +15 -2
  23. package/src/components/diagnose/InvestigationEvidenceSections.tsx +1 -1
  24. package/src/components/diagnose/InvestigationView.tsx +4 -1
  25. package/src/components/diagnose/agentCatalog.ts +6 -0
  26. package/src/components/diagnose/investigationCase.test.tsx +56 -0
  27. package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +60 -0
  28. package/src/components/diagnose/investigationEvidence/adapters/logs.ts +7 -3
  29. package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +1 -1
  30. package/src/components/diagnose/investigationEvidence/observations.ts +8 -5
  31. package/src/components/diagnose/investigationEvidence/types.ts +1 -1
  32. package/src/components/diagnose/launch.test.ts +23 -0
  33. package/src/components/diagnose/launch.ts +4 -0
  34. package/src/components/diagnose/parts.test.tsx +159 -2
  35. package/src/components/dock/NodeTerminalTab.test.tsx +34 -0
  36. package/src/components/dock/NodeTerminalTab.tsx +5 -3
  37. package/src/components/dock/WorkloadLogsTab.test.tsx +66 -0
  38. package/src/components/dock/WorkloadLogsTab.tsx +2 -1
  39. package/src/components/execution/BatchExecutionView.render.test.tsx +2 -1
  40. package/src/components/execution/BatchExecutionView.test.ts +52 -1
  41. package/src/components/execution/BatchExecutionView.tsx +324 -65
  42. package/src/components/execution/JobSetAdmission.test.tsx +42 -0
  43. package/src/components/execution/JobSetAdmission.tsx +17 -0
  44. package/src/components/execution/JobSetMemberComparison.test.tsx +30 -0
  45. package/src/components/execution/JobSetMemberComparison.tsx +88 -0
  46. package/src/components/execution/batch-run-actions.test.ts +1 -0
  47. package/src/components/execution/batch-timeline.test.ts +3 -0
  48. package/src/components/execution/execution-definition.test.ts +15 -1
  49. package/src/components/execution/execution-definition.ts +4 -0
  50. package/src/components/execution/member-collection.render.test.tsx +142 -0
  51. package/src/components/gitops/GitOpsView.tsx +33 -2
  52. package/src/components/gitops/destination-toast.test.ts +33 -0
  53. package/src/components/gitops/destination-toast.ts +46 -0
  54. package/src/components/gitops/useDestinationCluster.ts +30 -0
  55. package/src/components/helm/TrackChartSourceDialog.tsx +20 -11
  56. package/src/components/home/MCPSetupDialog.tsx +10 -0
  57. package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +75 -22
  58. package/src/components/logs/WorkloadLogsViewer.tsx +7 -4
  59. package/src/components/nav/PrimaryNavRail.tsx +2 -5
  60. package/src/components/resource/PVCUsageBar.render.test.tsx +168 -0
  61. package/src/components/resource/PVCUsageBar.tsx +62 -16
  62. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +1 -1
  63. package/src/components/resources/ResourcesView.tsx +3 -3
  64. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +8 -0
  65. package/src/components/resources/renderers/KueueWorkloadRenderer.test.tsx +39 -0
  66. package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +37 -0
  67. package/src/components/resources/renderers/RayClusterRenderer.test.tsx +18 -0
  68. package/src/components/resources/renderers/RayClusterRenderer.tsx +17 -0
  69. package/src/components/resources/renderers/RayServiceRenderer.test.tsx +69 -0
  70. package/src/components/resources/renderers/RayServiceRenderer.tsx +38 -0
  71. package/src/components/rightsizing/RightsizingScanView.tsx +207 -83
  72. package/src/components/rightsizing/copy.test.ts +7 -0
  73. package/src/components/rightsizing/empty-state.test.tsx +67 -0
  74. package/src/components/rightsizing/model.test.ts +120 -2
  75. package/src/components/rightsizing/model.ts +36 -13
  76. package/src/components/rightsizing/notices.test.tsx +176 -0
  77. package/src/components/settings/OperatorManagedNotice.tsx +17 -0
  78. package/src/components/settings/SettingsDialog.tsx +157 -42
  79. package/src/components/settings/settings-state.test.ts +18 -0
  80. package/src/components/settings/settings-state.ts +18 -0
  81. package/src/components/timeline/TimelineView.tsx +9 -3
  82. package/src/components/timeline/TimelineView.urlparams.test.ts +18 -1
  83. package/src/components/ui/Markdown.tsx +33 -6
  84. package/src/components/ui/command-items.ts +0 -3
  85. package/src/components/useContextSwitchFlow.tsx +147 -0
  86. package/src/components/workload/WorkloadView.test.ts +17 -1
  87. package/src/components/workload/WorkloadView.tsx +36 -18
  88. package/src/context/ContextSwitchContext.tsx +18 -2
  89. package/src/context/NavCustomization.tsx +4 -58
  90. package/src/context/ThemeContext.tsx +3 -11
  91. package/src/hooks/useClusterLoadState.ts +2 -2
  92. package/src/hooks/useFavorites.ts +3 -5
  93. package/src/utils/auditBadges.ts +1 -1
  94. package/src/utils/navigation.test.ts +18 -1
  95. package/src/utils/navigation.ts +12 -5
  96. package/src/utils/topology-selection.test.ts +56 -0
  97. package/src/utils/topology-selection.ts +9 -9
  98. 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 { Collapse, CollapseChevron, EmptyState, FetchResult, StatusDot, mapHealthToTone } from '@skyhook-io/k8s-ui'
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 { useResource, useWorkloadPods, useWorkloadRuns, type WorkloadRun } from '../../api/client'
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 { executionDefinitionFingerprint, executionDefinitionSummary, type ExecutionDefinitionSummary, type ExecutionUnitSummary } from './execution-definition'
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: { kind: string; namespace: string; name: string; group?: string }) => void
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 runsQuery = useWorkloadRuns(apiKind, namespace, name, true, { refetchActive: true, clusterScoped })
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 (runs.length === 0) {
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 (!runs.some((run) => workloadRunKey(run) === selectedRunKey)) {
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 = runs.find((run) => workloadRunKey(run) === selectedRunKey) ?? defaultRun
130
- const shouldResolveLivePods = Boolean(selectedRun && canViewLogs && isFailedRunPhase(selectedRun.phase) && batchRunHasContainerOutcome(selectedRun))
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 ? batchRunNextStep(selectedRun, canViewLogs, hasLivePods) : null
140
- const visibleRuns = useMemo(() => runs.filter((run) => {
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 tone="neutral" variant="card" headline="Run history unavailable" body={runsQuery.error instanceof Error ? runsQuery.error.message : 'Radar could not load retained runs.'} />
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">{isTemplateKind(kind) ? 'Workflows using this definition' : 'Run history'}</div>
182
- <div className="mt-1 text-sm font-semibold text-theme-text-primary">{pluralizeRuns(runs.length)}</div>
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">Retained Kubernetes objects, not all-time history.</p>
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 tone="filtered" variant="card" headline="No runs match these filters" body="Change the status filter or run-name search." />
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={`${run.kind}/${run.namespace}/${run.name}`}
327
+ key={workloadRunKey(run)}
221
328
  run={run}
222
329
  showNamespace={clusterScoped}
223
- selected={workloadRunKey(selectedRun ?? run) === workloadRunKey(run)}
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 tone="neutral" variant="card" headline="Selected run unavailable" body={selectedResourceQuery.error instanceof Error ? selectedResourceQuery.error.message : 'Radar could not load this retained run.'} />
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="grid items-start gap-4 lg:grid-cols-[minmax(0,1.7fr)_minmax(320px,0.9fr)]">
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
- {selectedRun ? (
241
- <section className="rounded-lg border border-theme-border bg-theme-surface">
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
- <RunDetailList run={selectedRun} resource={selectedResource} workflowExecution={workflowExecution} scheduledParent={scheduled} />
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="No selected run"
279
- body={`There are no retained ${runKindPluralForSchedule(kind)} to inspect.`}
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
- <SourceFacts
295
- source={source}
296
- namespace={selectedRun?.namespace || namespace}
297
- definitionLoading={Boolean(referencedDefinitionTarget) && referencedDefinitionQuery.isLoading}
298
- definitionError={referencedDefinitionTarget ? referencedDefinitionQuery.error : undefined}
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' : 'Run message'}</span>
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' : 'Run message'}
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="Run activity" icon={Activity} detail={activity.length ? `${activity.length} events` : undefined}>
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="This run has not reported timing details yet." />
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 && executionDefinitionFingerprint(runDefinition) !== executionDefinitionFingerprint(currentDefinition)
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">{formatRunTime(run) || 'time unknown'}{formatRunDuration(run) ? ` · ${formatRunDuration(run)}` : ''} · {workCount(run)}</span>
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.kind === 'workflows' ? 'argoproj.io' : undefined,
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` : '',