job detail header nit (#7786)

* do not truncate worker

* align button right + breadscrum

* remove job arg header

* nit

* Revert "remove job arg header"

This reverts commit b68ee6d2e5.

* improve mem peak formatting

* improve permissioned as layout

* Fix job preview spacing inconsistency

* nit spacing
This commit is contained in:
Guilhem
2026-02-04 16:44:35 +00:00
committed by GitHub
parent c760a33ccd
commit e6ae87ecc1
5 changed files with 125 additions and 74 deletions

View File

@@ -1,5 +1,6 @@
import type { Job } from '$lib/gen'
import { triggerIconMap } from '$lib/components/triggers/utils'
import { formatMemory } from '$lib/utils'
import { Calendar, Bot } from 'lucide-svelte'
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
@@ -22,6 +23,7 @@ export type JobField =
| 'created_at'
| 'started_at'
| 'created_by'
| 'permissioned_as'
| 'memory_peak'
| 'run_id'
| 'trigger_info'
@@ -179,10 +181,20 @@ export const fieldConfigs: Record<JobField, FieldConfig> = {
getValue: (job) => job.created_by || 'unknown'
},
permissioned_as: {
field: 'permissioned_as',
label: 'Permissioned as',
getValue: (job) => job.permissioned_as || null
},
memory_peak: {
field: 'memory_peak',
label: 'Mem peak',
getValue: (job) => (job.mem_peak ? `${(job.mem_peak / 1024).toPrecision(5)}MB` : null)
getValue: (job) => {
if (!job.mem_peak) return null
const formatted = formatMemory(job.mem_peak, true)
return `${formatted.display}|${formatted.tooltip}`
}
},
run_id: {
@@ -305,6 +317,7 @@ export const categoryFieldPresence: FieldPresenceConfig = {
script: {
created_at: true,
created_by: true,
permissioned_as: false, // Will be conditionally shown
worker: true,
run_id: true,
started_at: true,
@@ -326,6 +339,7 @@ export const categoryFieldPresence: FieldPresenceConfig = {
created_at: true,
started_at: true,
created_by: true,
permissioned_as: false, // Will be conditionally shown
worker: true,
run_id: true,
script_path: true,
@@ -345,6 +359,7 @@ export const categoryFieldPresence: FieldPresenceConfig = {
dependencies: {
created_at: true,
created_by: true,
permissioned_as: false, // Will be conditionally shown
worker: true,
run_id: true,
started_at: true,
@@ -365,6 +380,7 @@ export const categoryFieldPresence: FieldPresenceConfig = {
flow_node: {
created_at: true,
created_by: true,
permissioned_as: false, // Will be conditionally shown
worker: true,
run_id: true,
started_at: true,
@@ -385,6 +401,7 @@ export const categoryFieldPresence: FieldPresenceConfig = {
ai_agent: {
created_at: true,
created_by: true,
permissioned_as: false, // Will be conditionally shown
worker: true,
run_id: true,
started_at: true,
@@ -405,6 +422,7 @@ export const categoryFieldPresence: FieldPresenceConfig = {
system: {
created_at: true,
created_by: true,
permissioned_as: false, // Will be conditionally shown
worker: true,
run_id: true,
started_at: true,
@@ -425,6 +443,7 @@ export const categoryFieldPresence: FieldPresenceConfig = {
trigger_job: {
created_at: true,
created_by: true,
permissioned_as: false, // Will be conditionally shown
worker: true,
run_id: true,
started_at: true,
@@ -451,11 +470,16 @@ export function getRelevantFields(job: Job): FieldConfig[] {
const category = getJobCategory(job)
const fieldsPresence = categoryFieldPresence[category]
// Define the field ordering: created_by, started_at, worker, then others
// Conditionally show permissioned_as field when different from created_by
const shouldShowPermissionedAs =
job.permissioned_as !== `u/${job.created_by}` && job.permissioned_as !== job.created_by
// Define the field ordering: created_by, permissioned_as, started_at, worker, then others
const fieldOrder: JobField[] = [
'created_at',
'started_at',
'created_by',
'permissioned_as',
'worker',
'run_id',
'memory_peak',
@@ -474,7 +498,12 @@ export function getRelevantFields(job: Job): FieldConfig[] {
// Return fields in the specified order if they are present for this category
return fieldOrder
.filter((fieldName) => fieldsPresence[fieldName])
.filter((fieldName) => {
if (fieldName === 'permissioned_as') {
return shouldShowPermissionedAs
}
return fieldsPresence[fieldName]
})
.map((fieldName) => fieldConfigs[fieldName])
.filter((config) => config) // Safety check
}

View File

@@ -91,12 +91,13 @@
return truncateRev(fullValue, 8)
case 'script_hash':
return truncateHash(fullValue.toString())
case 'worker':
return truncateRev(fullValue, compact ? 8 : 12)
case 'parent_job':
return truncateRev(fullValue, 6)
case 'schedule_path':
return truncateRev(fullValue, 20)
case 'memory_peak':
// For memory, only show the display part (before the |)
return fullValue.split('|')[0] || fullValue
default:
return fullValue
}
@@ -156,31 +157,6 @@
<TimeAgo agoOnlyIfRecent date={job.started_at ?? ''} />
</span>
</Tooltip>
{:else if config.field === 'created_by'}
<div class="flex items-center gap-1 min-w-0">
{#if job.permissioned_as !== `u/${job.created_by}` && job.permissioned_as != job.created_by}
<Tooltip small class="truncate" style="direction: rtl;">
{#snippet text()}
{#if (job?.created_by?.length ?? 0) > 30}
Created by: {job.created_by}<br />
{/if}
But permissioned as {job.permissioned_as}
{/snippet}
<span
class="text-secondary flex-shrink whitespace-nowrap overflow-hidden"
style="direction: rtl;"
>
<span class="text-primary">{displayValue}</span> ({job.permissioned_as})
</span>
</Tooltip>
{:else}
<Tooltip small>
{#snippet text()}{job.created_by}{/snippet}
<span>{displayValue}</span>
</Tooltip>
{/if}
</div>
{:else if config.field === 'worker'}
<span>
{#if displayValue === 'no value'}
@@ -223,12 +199,20 @@
href={`${base}/runs/?job_kinds=all&worker=${job?.worker}`}
class="flex items-center gap-1 text-primary"
>
{displayValue}
<span class="truncate flex-shrink min-w-0">{displayValue}</span>
<ExternalLink size={12} class="flex-shrink-0" />
</a>
</Tooltip>
{/if}
</span>
{:else if config.field === 'memory_peak'}
{@const memoryParts = fullValue.split('|')}
{@const displayMem = memoryParts[0] || fullValue}
{@const tooltipMem = memoryParts[1] || fullValue}
<Tooltip small>
{#snippet text()}{tooltipMem}{/snippet}
<span>{displayMem}</span>
</Tooltip>
{:else if config.field === 'schedule_path' && job.schedule_path}
<span class="whitespace-nowrap" title={fullValue}>
<button
@@ -301,7 +285,7 @@
<ExternalLink size={12} class="flex-shrink-0" />
</a>
{:else}
<span title={fullValue} class="truncate">{displayValue}</span>
<div title={fullValue} class="truncate">{displayValue}</div>
{/if}
{/snippet}
@@ -449,7 +433,7 @@
{@const fields = relevantFields()}
<div class="bg-surface flex items-center" style="flex: 2 1 100px; min-width: 350px;">
<div
class="grid gap-x-6 gap-y-1.5 w-full h-full px-8 py-4 bg-surface-secondary/30"
class="grid gap-x-12 gap-y-1.5 w-full h-full px-8 py-4 bg-surface-secondary/30"
style="grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));"
>
{#if job}
@@ -459,7 +443,7 @@
{@const href = config.getHref?.(job, $workspaceStore || '')}
<div class="flex items-baseline gap-3 text-xs">
<span class="text-secondary min-w-[70px] flex-shrink-0">
<span class="text-secondary min-w-[110px] flex-shrink-0">
{config.label}
</span>
<span
@@ -558,7 +542,7 @@
{@const href = config.getHref?.(job, $workspaceStore || '')}
<div class="flex items-baseline gap-3 text-xs">
<span class="text-secondary min-w-[70px] flex-shrink-0">
<span class="text-secondary min-w-[110px] flex-shrink-0">
{#if config.field === 'created_at'}
{renderFieldValue(config, job)}
{:else}

View File

@@ -110,7 +110,7 @@
/>
<div class="h-full overflow-y-auto">
<div class="flex flex-col gap-2 items-start p-4 pb-8 min-h-full">
<div class="flex flex-col items-start p-4 pb-8 min-h-full">
{#if job}
{@const isFlow = job?.job_kind == 'flow' || isFlowPreview(job?.job_kind)}
<JobDetailHeader
@@ -123,13 +123,15 @@
<!-- Workflow timeline -->
{#if job?.workflow_as_code_status}
<WorkflowTimeline
flow_status={asWorkflowStatus(job.workflow_as_code_status)}
flowDone={job.type == 'CompletedJob'}
/>
<div class="py-2">
<WorkflowTimeline
flow_status={asWorkflowStatus(job.workflow_as_code_status)}
flowDone={job.type == 'CompletedJob'}
/>
</div>
{/if}
{#if isFlow}
<div class="w-full">
<div class="w-full mt-2">
{#if isFlow}
<FlowExecutionStatus
{job}
workspaceId={job?.workspace_id}
@@ -137,11 +139,11 @@
innerModules={job?.flow_status?.modules}
{suspendStatus}
/>
</div>
{/if}
{/if}
</div>
<!-- Job inputs -->
<div class="w-full mt-6">
<div class="w-full mt-4">
<div class="text-xs text-emphasis font-semibold mb-1">Inputs</div>
<JobArgs
id={job?.id}
@@ -162,7 +164,7 @@
/>
{:else if job?.type === 'CompletedJob'}
<!-- Result Section (moved outside tabs) -->
<div class="w-full mt-6 mb-6">
<div class="w-full mb-6">
<h3 class="text-xs font-semibold text-emphasis mb-1">Result</h3>
<div class="border rounded-md bg-surface-tertiary p-4 overflow-auto max-h-[400px]">
{#if job.result_stream || (job.type == 'CompletedJob' && job.result !== undefined)}

View File

@@ -1985,3 +1985,36 @@ export function pick<T extends object, K extends keyof T>(obj: T, keys: readonly
}
return result
}
/**
* Formats memory size in KB to human-readable format with appropriate units
* @param sizeInKb - Memory size in kilobytes
* @param includeTooltip - Whether to return tooltip data with precise values
* @returns Formatted string with appropriate unit (KB, MB, GB) or object with display and tooltip
*/
export function formatMemory(sizeInKb: number): string
export function formatMemory(sizeInKb: number, includeTooltip: true): { display: string; tooltip: string }
export function formatMemory(sizeInKb: number, includeTooltip = false): string | { display: string; tooltip: string } {
const precise = `${sizeInKb.toLocaleString()}KB`;
let display: string;
if (sizeInKb >= 1024 * 1024) {
// Convert to GB for values >= 1GB
display = `${(sizeInKb / (1024 * 1024)).toFixed(0)}GB`
} else if (sizeInKb >= 1024) {
// Convert to MB for values >= 1MB
display = `${(sizeInKb / 1024).toFixed(0)}MB`
} else {
// Keep as KB for smaller values
display = `${sizeInKb.toFixed(0)}KB`
}
if (includeTooltip) {
return {
display,
tooltip: `${precise} (${(sizeInKb / 1024).toFixed(2)}MB)`
}
}
return display
}

View File

@@ -444,7 +444,7 @@
</div>
{/each}
<div>
<Button href="{base}/runs" unifiedSize="md" variant="default">Go to runs page</Button>
<Button href="{base}/runs" unifiedSize="md" variant="accent">Go to runs page</Button>
</div>
</div>
</div>
@@ -470,36 +470,39 @@
/>
<ActionRow class="max-w-7xl px-4 mx-auto w-full">
{#snippet left()}
{@const isScript = job?.job_kind === 'script'}
{@const runsHref = `/runs/${job?.script_path}${!isScript ? '?jobKind=flow' : ''}`}
<div class="flex gap-2 items-center">
{#if job && 'deleted' in job && !job?.deleted && ($superadmin || ($userStore?.is_admin ?? false))}
<Dropdown
items={[
{
displayName: 'Delete result, logs and args (admin only)',
action: () => {
job?.id && deleteCompletedJob(job.id)
},
type: 'delete'
}
]}
>
{#snippet buttonReplacement()}
<Button nonCaptureEvent variant="default" size="sm" startIcon={{ icon: Trash }} />
{/snippet}
</Dropdown>
{#if job?.job_kind === 'script' || job?.job_kind === 'flow'}
<Button href={runsHref} variant="default" size="sm" startIcon={{ icon: List }}>
View runs
</Button>
{/if}
{/if}
</div>
<h1 class="text-sm font-semibold text-primary">run/{page.params.run}</h1>
{/snippet}
{#snippet right()}
{@const stem = job?.job_kind === 'script_hub' ? '/scripts' : `/${job?.job_kind}s`}
{@const isScript = job?.job_kind === 'script'}
{@const runsHref = `/runs/${job?.script_path}${!isScript ? '?jobKind=flow' : ''}`}
{#if job && 'deleted' in job && !job?.deleted && ($superadmin || ($userStore?.is_admin ?? false))}
<Dropdown
items={[
{
displayName: 'Delete result, logs and args (admin only)',
action: () => {
job?.id && deleteCompletedJob(job.id)
},
type: 'delete'
}
]}
>
{#snippet buttonReplacement()}
<Button
nonCaptureEvent
variant="default"
unifiedSize="md"
startIcon={{ icon: Trash }}
/>
{/snippet}
</Dropdown>
{#if job?.job_kind === 'script' || job?.job_kind === 'flow'}
<Button href={runsHref} variant="default" unifiedSize="md" startIcon={{ icon: List }}>
View runs
</Button>
{/if}
{/if}
{@const stem = job?.job_kind === 'script_hub' ? '/scripts' : `/${job?.job_kind}s`}
{@const viewHref = `${stem}/get/${isScript ? job?.script_hash : job?.script_path}`}
{#if (job?.job_kind == 'flow' || isFlowPreview(job?.job_kind)) && job?.['running'] && job?.parent_job == undefined}
<div class="inline">