feat(render): always-available, fully-cancel render controls
The backend cancel was solid (CancelJob/StopJob; the dev worker abandons a cancelled job, a real node kills its process) — but the UI couldn't reach it: the render page had NO cancel button, and the global progress pill's X only HID the pill (the job kept running). So a render couldn't actually be stopped from where you watch it. - Render page: a prominent «لغو رندر» button while a render is in flight (Queued or Running); cancelRender() calls /renders/:id/cancel and returns to config optimistically. The poll now also handles a `cancelled` status (when stopped from another surface). - Global pill: the X now CANCELS the render (with confirm) instead of just hiding it — so any in-flight render is cancellable from any page. - (Dashboard MyRenders already had a working cancel.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
Link2,
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
|
||||
import { useLocale } from "next-intl";
|
||||
@@ -205,6 +206,12 @@ export default function RenderPage() {
|
||||
} else if (data.status === "failed") {
|
||||
setPhase("failed");
|
||||
setErrorMessage(data.errorMessage ?? "Render failed.");
|
||||
} else if (data.status?.toLowerCase() === "cancelled") {
|
||||
// Cancelled here or from another surface (pill / dashboard) — reflect it.
|
||||
setPhase("config");
|
||||
setProgress(0);
|
||||
setEtaSec(null);
|
||||
setErrorMessage("رندر لغو شد.");
|
||||
}
|
||||
} catch {
|
||||
setPhase("failed");
|
||||
@@ -279,6 +286,26 @@ export default function RenderPage() {
|
||||
}
|
||||
}, [projectId, resolution, fps, locale]);
|
||||
|
||||
/** Fully cancel the active render — works in any state (Queued or Running). The
|
||||
* server marks it Cancelled (the dev worker abandons it, a real node kills its
|
||||
* process), so it stops fully. Optimistically return to config right away. */
|
||||
const cancelRender = useCallback(async () => {
|
||||
if (!jobId) return;
|
||||
const id = jobId;
|
||||
setPhase("config");
|
||||
setProgress(0);
|
||||
setEtaSec(null);
|
||||
setProgressMessage("");
|
||||
setPreviewB64(null);
|
||||
setErrorMessage("رندر لغو شد.");
|
||||
setJobId(null);
|
||||
try {
|
||||
await apiFetch(`/api/renders/${id}/cancel`, { method: "POST" });
|
||||
} catch {
|
||||
/* server is the source of truth; the dashboard can also cancel */
|
||||
}
|
||||
}, [jobId]);
|
||||
|
||||
const backToStudio = `/studio/video/${projectId}`;
|
||||
const isBusy = phase === "submitting" || phase === "polling";
|
||||
|
||||
@@ -393,9 +420,19 @@ export default function RenderPage() {
|
||||
</button>
|
||||
</div>
|
||||
) : isBusy ? (
|
||||
<p className="text-sm text-gray-400">
|
||||
میتوانید این صفحه را ببندید؛ رندر در پسزمینه ادامه مییابد و از هر صفحهای قابل پیگیری است.
|
||||
</p>
|
||||
<div className="flex w-full max-w-md flex-col items-center gap-3">
|
||||
<p className="text-center text-sm text-gray-400">
|
||||
میتوانید این صفحه را ببندید؛ رندر در پسزمینه ادامه مییابد و از هر صفحهای قابل پیگیری است.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={cancelRender}
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg border border-red-900/60 bg-red-950/40 px-5 py-2.5 text-sm font-medium text-red-300 transition-colors hover:bg-red-900/40"
|
||||
>
|
||||
<XCircle className="h-4 w-4" />
|
||||
لغو رندر
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
// Config
|
||||
<div className="w-full max-w-md space-y-5">
|
||||
|
||||
@@ -103,16 +103,25 @@ export function GlobalRenderProgress({ authed }: { authed: boolean }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dismiss (hides until job id changes) */}
|
||||
{/* Cancel — fully stops the render server-side (works from any page). */}
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
onClick={(e) => {
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
setDismissed(active.id);
|
||||
if (!window.confirm("این رندر لغو شود؟")) return;
|
||||
const id = active.id;
|
||||
setActive(null);
|
||||
setDismissed(id);
|
||||
try {
|
||||
await fetch(`/api/renders/${id}/cancel`, { method: "POST" });
|
||||
} catch {
|
||||
/* server is the source of truth; ignore transient errors */
|
||||
}
|
||||
}}
|
||||
className="shrink-0 rounded p-1 text-gray-600 hover:text-gray-300"
|
||||
aria-label="پنهان کردن"
|
||||
className="shrink-0 rounded p-1 text-gray-500 transition-colors hover:bg-red-500/20 hover:text-red-300"
|
||||
aria-label="لغو رندر"
|
||||
title="لغو رندر"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user