fix(update): add a timeout for deployment/rebuild and add a cancel button.

This commit is contained in:
KodekaTeam committed 2026-06-06 00:22:04 +07:00
1 parent c30f828d2c
commit 81622b5a4c
8 files changed
+443 -19

No files matched your search

@@ -234,6 +234,8 @@ export default function DeployContainerModal({
const [networksLoading, setNetworksLoading] = useState(false);
const [networksError, setNetworksError] = useState("");
const envFileInputRef = useRef<HTMLInputElement | null>(null);
const currentJobIdRef = useRef<string | null>(null);
const cancelRequestedRef = useRef(false);
const [uploadTargetEnvId, setUploadTargetEnvId] = useState<string | null>(
null,
);
@@ -779,6 +781,8 @@ export default function DeployContainerModal({
event.preventDefault();
setError("");
setLoading(true);
currentJobIdRef.current = null;
cancelRequestedRef.current = false;
let processLogLines = [
`[deploy] Starting deployment for ${form.name || "new container"}`,
];
@@ -946,6 +950,7 @@ export default function DeployContainerModal({
const jobResponse = await containersApi.createDeployJob(payload);
const job = jobResponse.data;
currentJobIdRef.current = job.id;
let finalStatus = job.status;
let finalError = job.error;
@@ -961,6 +966,33 @@ export default function DeployContainerModal({
}),
);
onProcessUpdate?.({
cancelAction: {
label: "Cancel",
loadingLabel: "Cancelling",
onClick: () => {
const jobId = currentJobIdRef.current;
if (!jobId || cancelRequestedRef.current) return;
cancelRequestedRef.current = true;
onProcessUpdate?.({
statusLabel: "Cancelling",
cancelAction: {
label: "Cancel",
loadingLabel: "Cancelling",
isLoading: true,
disabled: true,
onClick: () => undefined,
},
});
void containersApi
.cancelJob(jobId)
.catch(() => undefined);
},
},
});
await containersApi.streamJob(job.id, {
onLog: (entry) => {
updateProcessTerminal(
@@ -977,10 +1009,25 @@ export default function DeployContainerModal({
},
});
if (cancelRequestedRef.current && finalStatus !== "cancelled") {
const cancelledJob = currentJobIdRef.current
? await containersApi.getJob(currentJobIdRef.current)
: null;
finalStatus = cancelledJob?.data.status ?? "cancelled";
finalError =
cancelledJob?.data.error ??
cancelledJob?.data.cancelReason ??
"Deployment cancelled";
}
if (finalStatus === "error") {
throw new Error(finalError || "Deployment job failed");
}
if (finalStatus === "cancelled") {
throw new Error(finalError || "Deployment cancelled");
}
updateDeployTimelineModal({
onProcessUpdate,
activeStep: 3,
@@ -1003,6 +1050,7 @@ export default function DeployContainerModal({
"[deploy] Done",
].filter(Boolean),
});
onProcessUpdate?.({ cancelAction: undefined });
onClose();
} catch (err: unknown) {
@@ -1013,7 +1061,9 @@ export default function DeployContainerModal({
name: form.name,
message,
});
onProcessUpdate?.({ cancelAction: undefined });
} finally {
currentJobIdRef.current = null;
setLoading(false);
}
};
@@ -424,6 +424,8 @@ export default function AppContainerDetailPage() {
closeProcessLogs,
} = useProcessLogsModal();
const containerRecordRef = useRef<Container | null>(null);
const rebuildJobIdRef = useRef<string | null>(null);
const rebuildCancelRequestedRef = useRef(false);
const runtimeMetricsInFlightRef = useRef(false);
const environmentContainersHref = `/projects/${params.projectId}/environments/${params.environmentId}`;
const appDetailId = appDetail?.id;
@@ -796,6 +798,8 @@ export default function AppContainerDetailPage() {
setActing(true);
setActiveAction("rebuild");
setActionError("");
rebuildJobIdRef.current = null;
rebuildCancelRequestedRef.current = false;
const baseTerminalLines = [
`[rebuild] Starting rebuild for ${containerRecord.name}`,
`[rebuild] Source: ${containerRecord.sourceType ?? "unknown"}`,
@@ -841,6 +845,7 @@ export default function AppContainerDetailPage() {
params.containerId,
);
const job = jobResponse.data;
rebuildJobIdRef.current = job.id;
let finalStatus = job.status;
let finalError = job.error;
@@ -856,6 +861,31 @@ export default function AppContainerDetailPage() {
},
);
updateProcessLogs({
cancelAction: {
label: "Cancel",
loadingLabel: "Cancelling",
onClick: () => {
const jobId = rebuildJobIdRef.current;
if (!jobId || rebuildCancelRequestedRef.current) return;
rebuildCancelRequestedRef.current = true;
updateProcessLogs({
statusLabel: "Cancelling",
cancelAction: {
label: "Cancel",
loadingLabel: "Cancelling",
isLoading: true,
disabled: true,
onClick: () => undefined,
},
});
void containersApi.cancelJob(jobId).catch(() => undefined);
},
},
});
await containersApi.streamJob(job.id, {
onLog: (entry) => {
updateProcessTerminal([...latestTerminalLines, entry.message], {
@@ -869,10 +899,25 @@ export default function AppContainerDetailPage() {
},
});
if (rebuildCancelRequestedRef.current && finalStatus !== "cancelled") {
const cancelledJob = rebuildJobIdRef.current
? await containersApi.getJob(rebuildJobIdRef.current)
: null;
finalStatus = cancelledJob?.data.status ?? "cancelled";
finalError =
cancelledJob?.data.error ??
cancelledJob?.data.cancelReason ??
"Rebuild cancelled";
}
if (finalStatus === "error") {
throw new Error(finalError || "Rebuild job failed");
}
if (finalStatus === "cancelled") {
throw new Error(finalError || "Rebuild cancelled");
}
updateProcessLogs({
timelineLogs: createRebuildTimeline(2),
terminalLogs: [
@@ -898,6 +943,7 @@ export default function AppContainerDetailPage() {
].filter(Boolean),
statusLabel: "100%",
});
updateProcessLogs({ cancelAction: undefined });
} catch (rebuildError) {
const message =
rebuildError instanceof Error ? rebuildError.message : "Rebuild failed";
@@ -911,7 +957,9 @@ export default function AppContainerDetailPage() {
],
statusLabel: "Failed",
});
updateProcessLogs({ cancelAction: undefined });
} finally {
rebuildJobIdRef.current = null;
setActing(false);
setActiveAction(null);
}