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
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:
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
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 { apiJSON, apiMutate, waitForJob } from '$lib/api/client.js';
|
||||||
import type {
|
import type {
|
||||||
BirdStatus,
|
BirdStatus,
|
||||||
@@ -107,6 +107,8 @@
|
|||||||
let jobDetailDialog = $state(false);
|
let jobDetailDialog = $state(false);
|
||||||
let jobDetail = $state<JobRow | null>(null);
|
let jobDetail = $state<JobRow | null>(null);
|
||||||
let expandedJobIds = new SvelteSet<string>();
|
let expandedJobIds = new SvelteSet<string>();
|
||||||
|
let jobDetailsById = new SvelteMap<string, JobRow>();
|
||||||
|
let jobDetailsLoading = new SvelteSet<string>();
|
||||||
|
|
||||||
type JobLogEntry = {
|
type JobLogEntry = {
|
||||||
kind: string;
|
kind: string;
|
||||||
@@ -327,11 +329,32 @@
|
|||||||
return expandedJobIds.has(jobId);
|
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)) {
|
if (expandedJobIds.has(jobId)) {
|
||||||
expandedJobIds.delete(jobId);
|
expandedJobIds.delete(jobId);
|
||||||
} else {
|
} else {
|
||||||
expandedJobIds.add(jobId);
|
expandedJobIds.add(jobId);
|
||||||
|
await ensureJobDetails(job);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -643,7 +666,7 @@
|
|||||||
aria-expanded={isJobExpanded(job.job_id)}
|
aria-expanded={isJobExpanded(job.job_id)}
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleJobExpanded(job.job_id);
|
void toggleJobExpanded(job);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ChevronDown class={cn('size-3.5 transition-transform', isJobExpanded(job.job_id) && 'rotate-180')} />
|
<ChevronDown class={cn('size-3.5 transition-transform', isJobExpanded(job.job_id) && 'rotate-180')} />
|
||||||
@@ -687,35 +710,40 @@
|
|||||||
{#if isJobExpanded(job.job_id)}
|
{#if isJobExpanded(job.job_id)}
|
||||||
<TableRow class="bg-muted/20 hover:bg-muted/30">
|
<TableRow class="bg-muted/20 hover:bg-muted/30">
|
||||||
<TableCell colspan={5}>
|
<TableCell colspan={5}>
|
||||||
{@const logEntries = getJobLogEntries(job)}
|
{@const detailedJob = jobDetailsById.get(job.job_id) ?? job}
|
||||||
{@const logTotal = getJobLogTotal(job, logEntries)}
|
{@const isDetailsLoading = jobDetailsLoading.has(job.job_id)}
|
||||||
|
{@const logEntries = getJobLogEntries(detailedJob)}
|
||||||
|
{@const logTotal = getJobLogTotal(detailedJob, logEntries)}
|
||||||
<div class="space-y-3 py-2">
|
<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="grid gap-2 text-sm sm:grid-cols-2 lg:grid-cols-3">
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<p class="text-muted-foreground text-xs">job_id</p>
|
<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>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<p class="text-muted-foreground text-xs">status</p>
|
<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>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<p class="text-muted-foreground text-xs">created</p>
|
<p class="text-muted-foreground text-xs">created</p>
|
||||||
<p>{formatDate(job.created_at)}</p>
|
<p>{formatDate(detailedJob.created_at)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<p class="text-muted-foreground text-xs">started</p>
|
<p class="text-muted-foreground text-xs">started</p>
|
||||||
<p>{formatDate(job.started_at)}</p>
|
<p>{formatDate(detailedJob.started_at)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<p class="text-muted-foreground text-xs">finished</p>
|
<p class="text-muted-foreground text-xs">finished</p>
|
||||||
<p>{formatDate(job.finished_at)}</p>
|
<p>{formatDate(detailedJob.finished_at)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if job.error}
|
{#if detailedJob.error}
|
||||||
<div class="rounded-md border border-destructive/30 bg-destructive/5 p-3">
|
<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-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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if logEntries.length > 0}
|
{#if logEntries.length > 0}
|
||||||
@@ -752,7 +780,7 @@
|
|||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<p class="text-muted-foreground text-xs">Meta (raw JSON)</p>
|
<p class="text-muted-foreground text-xs">Meta (raw JSON)</p>
|
||||||
<ScrollArea class="max-h-56 rounded-md border bg-muted/30 p-3">
|
<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>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user