feat: enhance job detail handling in UI with caching and loading states. Introduce SvelteMap for job details and SvelteSet for loading states, improving performance and user experience when expanding job entries. Update UI to reflect detailed job information dynamically.
CI / changes (push) Successful in 6s
CI / openapi (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m10s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m4s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped

This commit is contained in:
Denozordec
2026-04-06 17:41:40 +07:00
parent c48cc4e2d4
commit 05bc5c435e
+41 -13
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { SvelteSet } from 'svelte/reactivity';
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import { apiJSON, apiMutate, waitForJob } from '$lib/api/client.js';
import type {
BirdStatus,
@@ -107,6 +107,8 @@
let jobDetailDialog = $state(false);
let jobDetail = $state<JobRow | null>(null);
let expandedJobIds = new SvelteSet<string>();
let jobDetailsById = new SvelteMap<string, JobRow>();
let jobDetailsLoading = new SvelteSet<string>();
type JobLogEntry = {
kind: string;
@@ -327,11 +329,32 @@
return expandedJobIds.has(jobId);
}
function toggleJobExpanded(jobId: string) {
async function ensureJobDetails(job: JobRow) {
const jobId = job.job_id;
if (jobDetailsById.has(jobId)) {
return;
}
if (jobDetailsLoading.has(jobId)) {
return;
}
jobDetailsLoading.add(jobId);
try {
const freshJob = await apiJSON<JobRow>(`/v1/jobs/${jobId}`);
jobDetailsById.set(jobId, freshJob);
} catch (e) {
toast.error(e instanceof Error ? e.message : String(e));
} finally {
jobDetailsLoading.delete(jobId);
}
}
async function toggleJobExpanded(job: JobRow) {
const jobId = job.job_id;
if (expandedJobIds.has(jobId)) {
expandedJobIds.delete(jobId);
} else {
expandedJobIds.add(jobId);
await ensureJobDetails(job);
}
}
@@ -643,7 +666,7 @@
aria-expanded={isJobExpanded(job.job_id)}
onclick={(e) => {
e.stopPropagation();
toggleJobExpanded(job.job_id);
void toggleJobExpanded(job);
}}
>
<ChevronDown class={cn('size-3.5 transition-transform', isJobExpanded(job.job_id) && 'rotate-180')} />
@@ -687,35 +710,40 @@
{#if isJobExpanded(job.job_id)}
<TableRow class="bg-muted/20 hover:bg-muted/30">
<TableCell colspan={5}>
{@const logEntries = getJobLogEntries(job)}
{@const logTotal = getJobLogTotal(job, logEntries)}
{@const detailedJob = jobDetailsById.get(job.job_id) ?? job}
{@const isDetailsLoading = jobDetailsLoading.has(job.job_id)}
{@const logEntries = getJobLogEntries(detailedJob)}
{@const logTotal = getJobLogTotal(detailedJob, logEntries)}
<div class="space-y-3 py-2">
{#if isDetailsLoading}
<p class="text-muted-foreground text-xs">Догружаем свежие детали задачи…</p>
{/if}
<div class="grid gap-2 text-sm sm:grid-cols-2 lg:grid-cols-3">
<div class="space-y-1">
<p class="text-muted-foreground text-xs">job_id</p>
<p class="font-mono break-all text-xs">{job.job_id}</p>
<p class="font-mono break-all text-xs">{detailedJob.job_id}</p>
</div>
<div class="space-y-1">
<p class="text-muted-foreground text-xs">status</p>
<Badge variant={jobStatusVariant(job.status)}>{job.status}</Badge>
<Badge variant={jobStatusVariant(detailedJob.status)}>{detailedJob.status}</Badge>
</div>
<div class="space-y-1">
<p class="text-muted-foreground text-xs">created</p>
<p>{formatDate(job.created_at)}</p>
<p>{formatDate(detailedJob.created_at)}</p>
</div>
<div class="space-y-1">
<p class="text-muted-foreground text-xs">started</p>
<p>{formatDate(job.started_at)}</p>
<p>{formatDate(detailedJob.started_at)}</p>
</div>
<div class="space-y-1">
<p class="text-muted-foreground text-xs">finished</p>
<p>{formatDate(job.finished_at)}</p>
<p>{formatDate(detailedJob.finished_at)}</p>
</div>
</div>
{#if job.error}
{#if detailedJob.error}
<div class="rounded-md border border-destructive/30 bg-destructive/5 p-3">
<p class="text-muted-foreground mb-1 text-xs">error</p>
<p class="text-destructive break-words text-sm">{job.error}</p>
<p class="text-destructive break-words text-sm">{detailedJob.error}</p>
</div>
{/if}
{#if logEntries.length > 0}
@@ -752,7 +780,7 @@
<div class="space-y-1">
<p class="text-muted-foreground text-xs">Meta (raw JSON)</p>
<ScrollArea class="max-h-56 rounded-md border bg-muted/30 p-3">
<pre class="font-mono text-xs whitespace-pre-wrap">{JSON.stringify(job.meta ?? {}, null, 2)}</pre>
<pre class="font-mono text-xs whitespace-pre-wrap">{JSON.stringify(detailedJob.meta ?? {}, null, 2)}</pre>
</ScrollArea>
</div>
</div>