Skip to content

Commit 45ba20e

Browse files
committed
Clarify batch run hierarchy
1 parent edc8755 commit 45ba20e

5 files changed

Lines changed: 89 additions & 21 deletions

File tree

internal/server/workload_logs.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ type WorkloadRun struct {
5959
StartedAt string `json:"startedAt,omitempty"`
6060
FinishedAt string `json:"finishedAt,omitempty"`
6161
ScheduledAt string `json:"scheduledAt,omitempty"`
62+
Trigger string `json:"trigger,omitempty"`
6263
Message string `json:"message,omitempty"`
6364

6465
Succeeded int32 `json:"succeeded,omitempty"`
@@ -644,14 +645,25 @@ func jobRunInfo(job *batchv1.Job) WorkloadRun {
644645
phase = "Failed"
645646
}
646647

648+
annotations := job.Annotations
649+
scheduledAt := ""
650+
trigger := ""
651+
if annotations["cronjob.kubernetes.io/instantiate"] == "manual" {
652+
trigger = "manual"
653+
} else if v := annotations["batch.kubernetes.io/cronjob-scheduled-timestamp"]; v != "" {
654+
scheduledAt = v
655+
trigger = "schedule"
656+
}
657+
647658
run := WorkloadRun{
648659
Kind: "jobs",
649660
Namespace: job.Namespace,
650661
Name: job.Name,
651662
Phase: phase,
652663
Active: phase == "Running" || phase == "Pending",
653664
StartedAt: formatMetaTime(job.Status.StartTime),
654-
ScheduledAt: job.Annotations["batch.kubernetes.io/cronjob-scheduled-timestamp"],
665+
ScheduledAt: scheduledAt,
666+
Trigger: trigger,
655667
Succeeded: job.Status.Succeeded,
656668
Failed: job.Status.Failed,
657669
Running: job.Status.Active,

internal/server/workload_logs_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,38 @@ func TestJobRunInfoTreatsPendingAsActive(t *testing.T) {
198198
}
199199
}
200200

201+
func TestJobRunInfoDistinguishesManualAndScheduledCronRuns(t *testing.T) {
202+
startedAt := metav1.NewTime(time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC))
203+
manual := jobRunInfo(&batchv1.Job{
204+
ObjectMeta: metav1.ObjectMeta{
205+
Name: "nightly-manual-1",
206+
Namespace: "ci",
207+
Annotations: map[string]string{
208+
"cronjob.kubernetes.io/instantiate": "manual",
209+
"batch.kubernetes.io/cronjob-scheduled-timestamp": "2026-01-02T02:00:00Z",
210+
},
211+
},
212+
Status: batchv1.JobStatus{StartTime: &startedAt},
213+
})
214+
if manual.Trigger != "manual" || manual.ScheduledAt != "" {
215+
t.Fatalf("manual run trigger/schedule = %q/%q, want manual/empty", manual.Trigger, manual.ScheduledAt)
216+
}
217+
218+
scheduled := jobRunInfo(&batchv1.Job{
219+
ObjectMeta: metav1.ObjectMeta{
220+
Name: "nightly-123",
221+
Namespace: "ci",
222+
Annotations: map[string]string{
223+
"batch.kubernetes.io/cronjob-scheduled-timestamp": "2026-01-02T02:00:00Z",
224+
},
225+
},
226+
Status: batchv1.JobStatus{StartTime: &startedAt},
227+
})
228+
if scheduled.Trigger != "schedule" || scheduled.ScheduledAt != "2026-01-02T02:00:00Z" {
229+
t.Fatalf("scheduled run trigger/schedule = %q/%q, want schedule/timestamp", scheduled.Trigger, scheduled.ScheduledAt)
230+
}
231+
}
232+
201233
func TestFormatMetaTime(t *testing.T) {
202234
timestamp := metav1.NewTime(time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC))
203235
if got := formatMetaTime(&timestamp); got != "2026-01-02T03:04:05Z" {

packages/k8s-ui/src/components/applications/ApplicationDetail.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ export function ApplicationDetail({ app, onBack, renderWorkload, topology, topol
256256
// the app graph rather than silently rendering a different workload under a
257257
// URL that names the missing one.
258258
const selected = rawSelected !== null && !selectedWorkload ? null : rawSelected
259+
const showBatchActivityPanel = batchActivity.length > 0 && (workloads.length === 1 || (appGraphAvailable && selected === null))
259260
useEffect(() => {
260261
if (selectedWorkloadKey !== undefined && selectedWorkloadKey !== null && !selectedWorkload) {
261262
onSelectWorkload?.(null)
@@ -398,7 +399,7 @@ export function ApplicationDetail({ app, onBack, renderWorkload, topology, topol
398399
)}
399400
</div>
400401

401-
{batchActivity.length > 0 && (
402+
{showBatchActivityPanel && (
402403
<BatchActivityPanel
403404
activity={batchActivity}
404405
onSelect={(workload) => setSelected(workloadKey(workload))}

web/src/api/client.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2156,6 +2156,17 @@ export function useApplyResource() {
21562156
// CronJob operations
21572157
// ============================================================================
21582158

2159+
function invalidateCronJobOperationQueries(queryClient: ReturnType<typeof useQueryClient>, namespace: string, name: string) {
2160+
queryClient.invalidateQueries({ queryKey: ['resources', 'cronjobs'] })
2161+
queryClient.invalidateQueries({ queryKey: ['resources', 'jobs'] })
2162+
queryClient.invalidateQueries({ queryKey: ['resource', 'cronjobs', namespace, name] })
2163+
queryClient.invalidateQueries({ queryKey: ['workload-runs', 'cronjobs', namespace, name] })
2164+
queryClient.invalidateQueries({ queryKey: ['applications'] })
2165+
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
2166+
queryClient.invalidateQueries({ queryKey: ['resource-counts'] })
2167+
queryClient.invalidateQueries({ queryKey: ['topology'] })
2168+
}
2169+
21592170
// Trigger a CronJob (create a Job from it)
21602171
export function useTriggerCronJob() {
21612172
const queryClient = useQueryClient()
@@ -2175,10 +2186,8 @@ export function useTriggerCronJob() {
21752186
errorMessage: 'Failed to trigger CronJob',
21762187
successMessage: 'CronJob triggered',
21772188
},
2178-
onSuccess: () => {
2179-
queryClient.invalidateQueries({ queryKey: ['resources', 'cronjobs'] })
2180-
queryClient.invalidateQueries({ queryKey: ['resources', 'jobs'] })
2181-
queryClient.invalidateQueries({ queryKey: ['topology'] })
2189+
onSuccess: (_, variables) => {
2190+
invalidateCronJobOperationQueries(queryClient, variables.namespace, variables.name)
21822191
},
21832192
})
21842193
}
@@ -2202,9 +2211,8 @@ export function useSuspendCronJob() {
22022211
errorMessage: 'Failed to suspend CronJob',
22032212
successMessage: 'CronJob suspended',
22042213
},
2205-
onSuccess: () => {
2206-
queryClient.invalidateQueries({ queryKey: ['resources', 'cronjobs'] })
2207-
queryClient.invalidateQueries({ queryKey: ['topology'] })
2214+
onSuccess: (_, variables) => {
2215+
invalidateCronJobOperationQueries(queryClient, variables.namespace, variables.name)
22082216
},
22092217
})
22102218
}
@@ -2228,9 +2236,8 @@ export function useResumeCronJob() {
22282236
errorMessage: 'Failed to resume CronJob',
22292237
successMessage: 'CronJob resumed',
22302238
},
2231-
onSuccess: () => {
2232-
queryClient.invalidateQueries({ queryKey: ['resources', 'cronjobs'] })
2233-
queryClient.invalidateQueries({ queryKey: ['topology'] })
2239+
onSuccess: (_, variables) => {
2240+
invalidateCronJobOperationQueries(queryClient, variables.namespace, variables.name)
22342241
},
22352242
})
22362243
}
@@ -3559,6 +3566,7 @@ export interface WorkloadRun {
35593566
startedAt?: string
35603567
finishedAt?: string
35613568
scheduledAt?: string
3569+
trigger?: 'manual' | 'schedule' | string
35623570
message?: string
35633571
succeeded?: number
35643572
failed?: number

web/src/components/execution/BatchExecutionView.tsx

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useMemo, useState, type ComponentType, type ReactNode } from 'react'
1+
import { useEffect, useMemo, useRef, useState, type ComponentType, type ReactNode } from 'react'
22
import { Activity, AlertTriangle, GitBranch, Layers3, Loader2, Terminal } from 'lucide-react'
33
import { clsx } from 'clsx'
44
import { EmptyState, FetchResult, StatusDot, mapHealthToTone } from '@skyhook-io/k8s-ui'
@@ -69,14 +69,22 @@ export function BatchExecutionFullscreen({ kind, apiKind, namespace, name, resou
6969
const runs = scheduledRunsFor(kind, runsQuery.data?.runs, resource, apiKind, namespace, name)
7070
const defaultRun = useMemo(() => pickDefaultRun(runs), [runs])
7171
const [selectedRunName, setSelectedRunName] = useState('')
72+
const userSelectedRunRef = useRef(false)
7273

7374
useEffect(() => {
7475
if (runs.length === 0) {
76+
userSelectedRunRef.current = false
7577
setSelectedRunName('')
7678
return
7779
}
80+
const nextDefaultRunName = defaultRun?.name ?? runs[0].name
7881
if (!runs.some((run) => run.name === selectedRunName)) {
79-
setSelectedRunName(defaultRun?.name ?? runs[0].name)
82+
userSelectedRunRef.current = false
83+
setSelectedRunName(nextDefaultRunName)
84+
return
85+
}
86+
if (!userSelectedRunRef.current && selectedRunName !== nextDefaultRunName) {
87+
setSelectedRunName(nextDefaultRunName)
8088
}
8189
}, [runs, selectedRunName, defaultRun])
8290

@@ -136,7 +144,10 @@ export function BatchExecutionFullscreen({ kind, apiKind, namespace, name, resou
136144
key={`${run.kind}/${run.namespace}/${run.name}`}
137145
run={run}
138146
selected={selectedRun?.name === run.name}
139-
onClick={() => setSelectedRunName(run.name)}
147+
onClick={() => {
148+
userSelectedRunRef.current = true
149+
setSelectedRunName(run.name)
150+
}}
140151
/>
141152
))}
142153
</div>
@@ -178,7 +189,8 @@ export function BatchExecutionFullscreen({ kind, apiKind, namespace, name, resou
178189
<div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-theme-text-tertiary">
179190
<span>{RUN_KIND_LABEL[selectedRun.kind] ?? selectedRun.kind}</span>
180191
{selectedRun.startedAt && <span>started {formatAge(selectedRun.startedAt)}</span>}
181-
{selectedRun.scheduledAt && <span>scheduled {formatAge(selectedRun.scheduledAt)}</span>}
192+
{selectedRun.trigger === 'manual' && <span>manual trigger</span>}
193+
{selectedRun.scheduledAt && <span>cron scheduled {formatAge(selectedRun.scheduledAt)}</span>}
182194
{selectedRun.template && <span>template {selectedRun.template}</span>}
183195
{selectedResourceQuery.isFetching && <span>refreshing</span>}
184196
</div>
@@ -449,11 +461,14 @@ function SourceFacts({ source }: { source: ReturnType<typeof sourceFacts> }) {
449461
}
450462

451463
function RunDetailList({ run, resource, workflowExecution }: { run: WorkloadRun; resource: any; workflowExecution: WorkflowExecutionModel | null }) {
452-
const rows = [
464+
const rows: Array<[string, string]> = [
453465
['Started', run.startedAt ? formatAge(run.startedAt) : '-'],
454466
['Finished', run.finishedAt ? formatAge(run.finishedAt) : run.active ? 'Running' : '-'],
455-
['Scheduled', run.scheduledAt ? formatAge(run.scheduledAt) : '-'],
456-
['Schedule delay', formatScheduleDelay(run) || '-'],
467+
['Trigger', run.trigger === 'manual' ? 'Manual' : run.scheduledAt ? 'Cron schedule' : '-'],
468+
...(run.scheduledAt ? [
469+
['Cron scheduled', formatAge(run.scheduledAt)],
470+
['Start delay', formatScheduleDelay(run) || '-'],
471+
] as Array<[string, string]> : []),
457472
['Parallelism', run.parallelism ? String(run.parallelism) : '-'],
458473
['Pods', podBreakdown(run, workflowExecution) || '-'],
459474
['Steps', stepBreakdown(run, workflowExecution) || '-'],
@@ -482,7 +497,7 @@ function RunMetrics({ run, resource, workflowExecution, retainedRuns }: { run: W
482497
['Active retained', activeRuns ? String(activeRuns) : '0'],
483498
['Failed retained', failedRuns ? String(failedRuns) : '0'],
484499
['Current duration', formatRunDuration(run) || '-'],
485-
['Schedule delay', formatScheduleDelay(run) || '-'],
500+
['Start delay', run.scheduledAt ? formatScheduleDelay(run) || '-' : '-'],
486501
['Completions', jobSpec.completions != null ? String(jobSpec.completions) : run.desired ? String(run.desired) : '-'],
487502
['Resource duration', resourceDuration || '-'],
488503
]
@@ -893,7 +908,7 @@ function jobPseudoSteps(run: WorkloadRun, resource: any): WorkflowExecutionNode[
893908

894909
function jobActivity(run: WorkloadRun, resource: any): WorkflowExecutionActivity[] {
895910
const items: WorkflowExecutionActivity[] = []
896-
if (run.scheduledAt) items.push({ id: 'scheduled', at: run.scheduledAt, label: 'Scheduled', tone: 'info' })
911+
if (run.scheduledAt) items.push({ id: 'scheduled', at: run.scheduledAt, label: 'Cron scheduled', tone: 'info' })
897912
if (run.startedAt) items.push({ id: 'started', at: run.startedAt, label: 'Job started', tone: 'info' })
898913
const conditions = Array.isArray(resource?.status?.conditions) ? resource.status.conditions : []
899914
for (const condition of conditions) {

0 commit comments

Comments
 (0)