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,
|
Link2,
|
||||||
Loader2,
|
Loader2,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
|
XCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { useLocale } from "next-intl";
|
import { useLocale } from "next-intl";
|
||||||
@@ -205,6 +206,12 @@ export default function RenderPage() {
|
|||||||
} else if (data.status === "failed") {
|
} else if (data.status === "failed") {
|
||||||
setPhase("failed");
|
setPhase("failed");
|
||||||
setErrorMessage(data.errorMessage ?? "Render 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 {
|
} catch {
|
||||||
setPhase("failed");
|
setPhase("failed");
|
||||||
@@ -279,6 +286,26 @@ export default function RenderPage() {
|
|||||||
}
|
}
|
||||||
}, [projectId, resolution, fps, locale]);
|
}, [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 backToStudio = `/studio/video/${projectId}`;
|
||||||
const isBusy = phase === "submitting" || phase === "polling";
|
const isBusy = phase === "submitting" || phase === "polling";
|
||||||
|
|
||||||
@@ -393,9 +420,19 @@ export default function RenderPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : isBusy ? (
|
) : isBusy ? (
|
||||||
<p className="text-sm text-gray-400">
|
<div className="flex w-full max-w-md flex-col items-center gap-3">
|
||||||
میتوانید این صفحه را ببندید؛ رندر در پسزمینه ادامه مییابد و از هر صفحهای قابل پیگیری است.
|
<p className="text-center text-sm text-gray-400">
|
||||||
</p>
|
میتوانید این صفحه را ببندید؛ رندر در پسزمینه ادامه مییابد و از هر صفحهای قابل پیگیری است.
|
||||||
|
</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
|
// Config
|
||||||
<div className="w-full max-w-md space-y-5">
|
<div className="w-full max-w-md space-y-5">
|
||||||
|
|||||||
@@ -103,16 +103,25 @@ export function GlobalRenderProgress({ authed }: { authed: boolean }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dismiss (hides until job id changes) */}
|
{/* Cancel — fully stops the render server-side (works from any page). */}
|
||||||
<span
|
<span
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
onClick={(e) => {
|
onClick={async (e) => {
|
||||||
e.stopPropagation();
|
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"
|
className="shrink-0 rounded p-1 text-gray-500 transition-colors hover:bg-red-500/20 hover:text-red-300"
|
||||||
aria-label="پنهان کردن"
|
aria-label="لغو رندر"
|
||||||
|
title="لغو رندر"
|
||||||
>
|
>
|
||||||
<X className="h-3.5 w-3.5" />
|
<X className="h-3.5 w-3.5" />
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user