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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user