169 lines
4.9 KiB
Svelte
169 lines
4.9 KiB
Svelte
<script lang="ts">
|
|
import { formatDistanceToNow, format } from 'date-fns';
|
|
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
|
import JobStatusBadge from './JobStatusBadge.svelte';
|
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
|
import { cn } from '$lib/utils';
|
|
|
|
interface MetadataEntry {
|
|
key: string;
|
|
value: string;
|
|
}
|
|
|
|
interface ChildJob {
|
|
id: string;
|
|
jobType: string;
|
|
displayName: string;
|
|
status: string;
|
|
errorMessage: string | null;
|
|
metadata: MetadataEntry[] | null;
|
|
createdTime: string;
|
|
lastUpdatedTime: string;
|
|
}
|
|
|
|
interface JobNode {
|
|
id: string;
|
|
parentJobId: string | null;
|
|
jobType: string;
|
|
displayName: string;
|
|
status: string;
|
|
errorMessage: string | null;
|
|
metadata: MetadataEntry[] | null;
|
|
createdTime: string;
|
|
lastUpdatedTime: string;
|
|
childJobs: ChildJob[] | null;
|
|
}
|
|
|
|
interface Props {
|
|
job: JobNode;
|
|
expanded: boolean;
|
|
onToggle: () => void;
|
|
columnCount: number;
|
|
}
|
|
|
|
let { job, expanded, onToggle, columnCount }: Props = $props();
|
|
|
|
const children = $derived(job.childJobs ?? []);
|
|
const hasChildren = $derived(children.length > 0);
|
|
const metadata = $derived(job.metadata ?? []);
|
|
|
|
function formatTime(iso: string): string {
|
|
try {
|
|
return formatDistanceToNow(new Date(iso), { addSuffix: true });
|
|
} catch {
|
|
return iso;
|
|
}
|
|
}
|
|
|
|
function formatTimeFull(iso: string): string {
|
|
try {
|
|
return format(new Date(iso), 'yyyy-MM-dd HH:mm:ss');
|
|
} catch {
|
|
return iso;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<!-- Main row -->
|
|
<tr class={cn("border-b hover:bg-muted/50 transition-colors", expanded && "bg-muted/30")}>
|
|
<td class="w-10 px-3 py-3">
|
|
{#if hasChildren}
|
|
<button onclick={onToggle} class={cn("hover:bg-accent rounded p-1 transition-transform", expanded && "rotate-90")}>
|
|
<ChevronRight class="h-4 w-4" />
|
|
</button>
|
|
{/if}
|
|
</td>
|
|
<td class="px-3 py-3 font-medium">{job.displayName}</td>
|
|
<td class="px-3 py-3 text-muted-foreground text-sm">{job.jobType}</td>
|
|
<td class="px-3 py-3"><JobStatusBadge status={job.status} /></td>
|
|
<td class="px-3 py-3 text-sm text-muted-foreground">
|
|
<Tooltip.Provider>
|
|
<Tooltip.Root>
|
|
<Tooltip.Trigger class="cursor-default">{formatTime(job.createdTime)}</Tooltip.Trigger>
|
|
<Tooltip.Content>{formatTimeFull(job.createdTime)}</Tooltip.Content>
|
|
</Tooltip.Root>
|
|
</Tooltip.Provider>
|
|
</td>
|
|
<td class="px-3 py-3 text-sm text-muted-foreground">
|
|
{#if hasChildren}
|
|
{children.length} sub-job{children.length !== 1 ? 's' : ''}
|
|
{/if}
|
|
</td>
|
|
</tr>
|
|
|
|
<!-- Expanded detail -->
|
|
{#if expanded}
|
|
<tr class="border-b bg-muted/20">
|
|
<td colspan={columnCount} class="px-6 py-4">
|
|
<div class="space-y-4">
|
|
<!-- Job details -->
|
|
<div class="grid grid-cols-2 gap-x-8 gap-y-2 text-sm max-w-lg">
|
|
<span class="text-muted-foreground">Last Updated</span>
|
|
<span>{formatTimeFull(job.lastUpdatedTime)}</span>
|
|
</div>
|
|
|
|
{#if job.errorMessage}
|
|
<div class="text-sm">
|
|
<span class="text-muted-foreground font-medium">Error: </span>
|
|
<span class="text-destructive">{job.errorMessage}</span>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Metadata -->
|
|
{#if metadata.length > 0}
|
|
<div>
|
|
<h4 class="text-sm font-medium text-muted-foreground mb-2">Metadata</h4>
|
|
<div class="grid grid-cols-2 gap-x-8 gap-y-1 text-sm max-w-lg">
|
|
{#each metadata as entry (entry.key)}
|
|
<span class="text-muted-foreground">{entry.key}</span>
|
|
<span class="break-all">{entry.value}</span>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Sub-jobs table -->
|
|
{#if hasChildren}
|
|
<div>
|
|
<h4 class="text-sm font-medium text-muted-foreground mb-2">Sub-jobs</h4>
|
|
<table class="w-full text-sm">
|
|
<thead>
|
|
<tr class="border-b text-left text-muted-foreground">
|
|
<th class="px-3 py-2 font-medium">Name</th>
|
|
<th class="px-3 py-2 font-medium">Type</th>
|
|
<th class="px-3 py-2 font-medium">Status</th>
|
|
<th class="px-3 py-2 font-medium">Created</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each children as child (child.id)}
|
|
<tr class="border-b last:border-0">
|
|
<td class="px-3 py-2">{child.displayName}</td>
|
|
<td class="px-3 py-2 text-muted-foreground">{child.jobType}</td>
|
|
<td class="px-3 py-2"><JobStatusBadge status={child.status} /></td>
|
|
<td class="px-3 py-2 text-muted-foreground">
|
|
<Tooltip.Provider>
|
|
<Tooltip.Root>
|
|
<Tooltip.Trigger class="cursor-default">{formatTime(child.createdTime)}</Tooltip.Trigger>
|
|
<Tooltip.Content>{formatTimeFull(child.createdTime)}</Tooltip.Content>
|
|
</Tooltip.Root>
|
|
</Tooltip.Provider>
|
|
</td>
|
|
</tr>
|
|
{#if child.errorMessage}
|
|
<tr class="border-b last:border-0">
|
|
<td colspan="4" class="px-3 pb-2">
|
|
<span class="text-destructive text-xs">{child.errorMessage}</span>
|
|
</td>
|
|
</tr>
|
|
{/if}
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{/if}
|