Files
stripstream-librarian/apps/backoffice/app/components/JobRow.tsx
Froidefond Julien e64848a216 feat: implement thumbnail generation and management
- Remove unused image dependencies from Cargo.lock.
- Update API to handle thumbnail generation and checkup processes.
- Introduce new routes for rebuilding and regenerating thumbnails.
- Enhance job tracking with progress indicators for thumbnail jobs.
- Update front-end components to display thumbnail job status and progress.
- Add backend logic for managing thumbnail jobs and integrating with the API.
- Refactor existing code to accommodate new thumbnail functionalities.
2026-03-08 20:55:12 +01:00

181 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import Link from "next/link";
import { JobProgress } from "./JobProgress";
import { StatusBadge, Button, MiniProgressBar } from "./ui";
interface JobRowProps {
job: {
id: string;
library_id: string | null;
type: string;
status: string;
created_at: string;
started_at: string | null;
finished_at: string | null;
error_opt: string | null;
stats_json: {
scanned_files: number;
indexed_files: number;
removed_files: number;
errors: number;
} | null;
progress_percent: number | null;
processed_files: number | null;
total_files: number | null;
};
libraryName: string | undefined;
highlighted?: boolean;
onCancel: (id: string) => void;
formatDate: (date: string) => string;
formatDuration: (start: string, end: string | null) => string;
}
export function JobRow({ job, libraryName, highlighted, onCancel, formatDate, formatDuration }: JobRowProps) {
const isActive = job.status === "running" || job.status === "pending" || job.status === "generating_thumbnails";
const [showProgress, setShowProgress] = useState(highlighted || isActive);
const handleComplete = () => {
setShowProgress(false);
window.location.reload();
};
// Calculate duration
const duration = job.started_at
? formatDuration(job.started_at, job.finished_at)
: "-";
// Get file stats
const scanned = job.stats_json?.scanned_files ?? 0;
const indexed = job.stats_json?.indexed_files ?? 0;
const removed = job.stats_json?.removed_files ?? 0;
const errors = job.stats_json?.errors ?? 0;
const isThumbnailPhase = job.status === "generating_thumbnails";
const isThumbnailJob = job.type === "thumbnail_rebuild" || job.type === "thumbnail_regenerate";
const hasThumbnailPhase = isThumbnailPhase || isThumbnailJob;
// Files column: index-phase stats only
const filesDisplay =
job.status === "running" && !isThumbnailPhase
? job.total_files != null
? `${job.processed_files ?? 0}/${job.total_files}`
: scanned > 0
? `${scanned} scanned`
: "-"
: job.status === "success" && (indexed > 0 || removed > 0 || errors > 0)
? null // rendered below as ✓ / / ⚠
: scanned > 0
? `${scanned} scanned`
: "—";
// Thumbnails column
const thumbInProgress = hasThumbnailPhase && (job.status === "running" || isThumbnailPhase);
const thumbDisplay =
thumbInProgress && job.total_files != null
? `${job.processed_files ?? 0}/${job.total_files}`
: job.status === "success" && job.total_files != null && hasThumbnailPhase
? `${job.total_files}`
: "—";
return (
<>
<tr className={highlighted ? 'bg-primary/10' : 'hover:bg-muted/50'}>
<td className="px-4 py-3">
<Link
href={`/jobs/${job.id}`}
className="text-primary hover:text-primary/80 hover:underline font-mono text-sm"
>
<code>{job.id.slice(0, 8)}</code>
</Link>
</td>
<td className="px-4 py-3 text-sm text-foreground">
{job.library_id ? libraryName || job.library_id.slice(0, 8) : "—"}
</td>
<td className="px-4 py-3 text-sm text-foreground">{job.type}</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2 flex-wrap">
<StatusBadge status={job.status} />
{job.error_opt && (
<span
className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-error text-white text-xs font-bold cursor-help"
title={job.error_opt}
>
!
</span>
)}
{isActive && (
<button
className="text-xs text-primary hover:text-primary/80 hover:underline"
onClick={() => setShowProgress(!showProgress)}
>
{showProgress ? "Hide" : "Show"} progress
</button>
)}
</div>
</td>
<td className="px-4 py-3">
<div className="flex flex-col gap-1">
{filesDisplay !== null ? (
<span className="text-sm text-foreground">{filesDisplay}</span>
) : (
<div className="flex items-center gap-2 text-xs">
<span className="text-success"> {indexed}</span>
{removed > 0 && <span className="text-warning"> {removed}</span>}
{errors > 0 && <span className="text-error"> {errors}</span>}
</div>
)}
{job.status === "running" && !isThumbnailPhase && job.total_files != null && (
<MiniProgressBar value={job.processed_files ?? 0} max={job.total_files} className="w-24" />
)}
</div>
</td>
<td className="px-4 py-3">
<div className="flex flex-col gap-1">
<span className="text-sm text-foreground">{thumbDisplay}</span>
{thumbInProgress && job.total_files != null && (
<MiniProgressBar value={job.processed_files ?? 0} max={job.total_files} className="w-24" />
)}
</div>
</td>
<td className="px-4 py-3 text-sm text-muted-foreground">
{duration}
</td>
<td className="px-4 py-3 text-sm text-muted-foreground">
{formatDate(job.created_at)}
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<Link
href={`/jobs/${job.id}`}
className="inline-flex items-center px-3 py-1.5 text-xs font-medium rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors"
>
View
</Link>
{(job.status === "pending" || job.status === "running" || job.status === "generating_thumbnails") && (
<Button
variant="danger"
size="sm"
onClick={() => onCancel(job.id)}
>
Cancel
</Button>
)}
</div>
</td>
</tr>
{showProgress && isActive && (
<tr>
<td colSpan={9} className="px-4 py-3 bg-muted/50">
<JobProgress
jobId={job.id}
onComplete={handleComplete}
/>
</td>
</tr>
)}
</>
);
}