Track cancelled deployments separately.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
keyhan
2026-05-18 22:15:43 +03:30
parent d5e6ab0f68
commit 68d1ccb196
10 changed files with 34 additions and 9 deletions
@@ -0,0 +1,10 @@
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_type
WHERE typname = 'deployments_status_enum'
) THEN
ALTER TYPE deployments_status_enum ADD VALUE IF NOT EXISTS 'cancelled';
END IF;
END $$;
+3 -3
View File
@@ -32,7 +32,7 @@ interface ActiveBuildSession {
} }
export interface BuildProgress { export interface BuildProgress {
phase: 'uploading' | 'building' | 'deploying' | 'done' | 'failed'; phase: 'uploading' | 'building' | 'deploying' | 'done' | 'failed' | 'cancelled';
percent: number; percent: number;
bytesUploaded?: number; bytesUploaded?: number;
totalBytes?: number; totalBytes?: number;
@@ -98,7 +98,7 @@ export class BuildService {
async cancelBuild(deploymentId: string): Promise<void> { async cancelBuild(deploymentId: string): Promise<void> {
const session = this.activeBuilds.get(deploymentId); const session = this.activeBuilds.get(deploymentId);
if (!session) { if (!session) {
this.setProgress(deploymentId, { phase: 'failed', percent: 0, message: 'Cancelled by user' }); this.setProgress(deploymentId, { phase: 'cancelled', percent: 0, message: 'Cancelled by user' });
return; return;
} }
@@ -139,7 +139,7 @@ export class BuildService {
this.logger.log(`Cleaned up K8s build resources for deployment ${deploymentId}`); this.logger.log(`Cleaned up K8s build resources for deployment ${deploymentId}`);
} }
this.setProgress(deploymentId, { phase: 'failed', percent: 0, message: 'Cancelled by user' }); this.setProgress(deploymentId, { phase: 'cancelled', percent: 0, message: 'Cancelled by user' });
this.activeBuilds.delete(deploymentId); this.activeBuilds.delete(deploymentId);
} }
+1
View File
@@ -72,6 +72,7 @@ export enum DeploymentStatus {
DEPLOYING = 'deploying', DEPLOYING = 'deploying',
RUNNING = 'running', RUNNING = 'running',
FAILED = 'failed', FAILED = 'failed',
CANCELLED = 'cancelled',
STOPPED = 'stopped', STOPPED = 'stopped',
DELETING = 'deleting', DELETING = 'deleting',
} }
@@ -117,7 +117,7 @@ export class DeploymentsService {
if (error instanceof BuildCancelledError || error?.name === 'BuildCancelledError') { if (error instanceof BuildCancelledError || error?.name === 'BuildCancelledError') {
this.logger.log(`Deployment ${deploymentId} cancelled by user`); this.logger.log(`Deployment ${deploymentId} cancelled by user`);
await this.deploymentsRepository.update(deploymentId, { await this.deploymentsRepository.update(deploymentId, {
status: DeploymentStatus.FAILED, status: DeploymentStatus.CANCELLED,
errorMessage: 'Cancelled by user', errorMessage: 'Cancelled by user',
finishedAt: new Date(), finishedAt: new Date(),
}); });
@@ -209,6 +209,9 @@ export class DeploymentsService {
if (latest.status === DeploymentStatus.FAILED) { if (latest.status === DeploymentStatus.FAILED) {
return { phase: 'failed', percent: 0, message: latest.errorMessage || 'Deployment failed' }; return { phase: 'failed', percent: 0, message: latest.errorMessage || 'Deployment failed' };
} }
if (latest.status === DeploymentStatus.CANCELLED) {
return { phase: 'cancelled', percent: 0, message: latest.errorMessage || 'Cancelled by user' };
}
if (latest.status === DeploymentStatus.BUILDING) { if (latest.status === DeploymentStatus.BUILDING) {
return { phase: 'building', percent: 0, message: 'Building...' }; return { phase: 'building', percent: 0, message: 'Building...' };
} }
@@ -242,7 +245,7 @@ export class DeploymentsService {
await this.buildService.cancelBuild(latest.id); await this.buildService.cancelBuild(latest.id);
await this.buildService.cleanupBuildResourcesForApp(app); await this.buildService.cleanupBuildResourcesForApp(app);
latest.status = DeploymentStatus.FAILED; latest.status = DeploymentStatus.CANCELLED;
latest.errorMessage = 'Cancelled by user'; latest.errorMessage = 'Cancelled by user';
latest.finishedAt = new Date(); latest.finishedAt = new Date();
return this.deploymentsRepository.save(latest); return this.deploymentsRepository.save(latest);
@@ -17,6 +17,7 @@ const statusColors: Record<string, string> = {
deploying: 'badge-blue', deploying: 'badge-blue',
failed: 'badge-red', failed: 'badge-red',
build_failed: 'badge-red', build_failed: 'badge-red',
cancelled: 'badge-gray',
stopped: 'badge-gray', stopped: 'badge-gray',
}; };
@@ -22,6 +22,7 @@ const statusColors: Record<string, string> = {
failed: 'badge-red', failed: 'badge-red',
build_failed: 'badge-red', build_failed: 'badge-red',
stopped: 'badge-gray', stopped: 'badge-gray',
cancelled: 'badge-gray',
}; };
/** /**
+1
View File
@@ -15,6 +15,7 @@ const statusColors: Record<string, string> = {
deploying: 'badge-blue', deploying: 'badge-blue',
failed: 'badge-red', failed: 'badge-red',
build_failed: 'badge-red', build_failed: 'badge-red',
cancelled: 'badge-gray',
stopped: 'badge-gray', stopped: 'badge-gray',
}; };
+2
View File
@@ -16,6 +16,7 @@ const statusColors: Record<string, string> = {
deploying: 'bg-blue-100 text-blue-700', deploying: 'bg-blue-100 text-blue-700',
failed: 'bg-red-100 text-red-700', failed: 'bg-red-100 text-red-700',
build_failed: 'bg-red-100 text-red-700', build_failed: 'bg-red-100 text-red-700',
cancelled: 'bg-gray-100 text-gray-600',
stopped: 'bg-gray-100 text-gray-600', stopped: 'bg-gray-100 text-gray-600',
}; };
@@ -26,6 +27,7 @@ const statusIcons: Record<string, ReactNode> = {
deploying: <Circle className="w-3 h-3 fill-blue-500 text-blue-500" />, deploying: <Circle className="w-3 h-3 fill-blue-500 text-blue-500" />,
failed: <Circle className="w-3 h-3 fill-red-500 text-red-500" />, failed: <Circle className="w-3 h-3 fill-red-500 text-red-500" />,
build_failed: <Circle className="w-3 h-3 fill-red-500 text-red-500" />, build_failed: <Circle className="w-3 h-3 fill-red-500 text-red-500" />,
cancelled: <Circle className="w-3 h-3 fill-gray-400 text-gray-400" />,
stopped: <Circle className="w-3 h-3 fill-gray-400 text-gray-400" />, stopped: <Circle className="w-3 h-3 fill-gray-400 text-gray-400" />,
}; };
@@ -7,7 +7,7 @@ import { Loader2, Upload, Hammer, Rocket, CheckCircle, XCircle, X } from 'lucide
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
export interface BuildProgress { export interface BuildProgress {
phase: 'uploading' | 'building' | 'deploying' | 'done' | 'failed'; phase: 'uploading' | 'building' | 'deploying' | 'done' | 'failed' | 'cancelled';
percent: number; percent: number;
bytesUploaded?: number; bytesUploaded?: number;
totalBytes?: number; totalBytes?: number;
@@ -26,6 +26,7 @@ const phaseConfig = {
deploying: { label: 'Deploying to Kubernetes', icon: Rocket, bg: 'bg-purple-500' }, deploying: { label: 'Deploying to Kubernetes', icon: Rocket, bg: 'bg-purple-500' },
done: { label: 'Deployment complete', icon: CheckCircle, bg: 'bg-green-500' }, done: { label: 'Deployment complete', icon: CheckCircle, bg: 'bg-green-500' },
failed: { label: 'Deployment failed', icon: XCircle, bg: 'bg-red-500' }, failed: { label: 'Deployment failed', icon: XCircle, bg: 'bg-red-500' },
cancelled: { label: 'Deployment cancelled', icon: XCircle, bg: 'bg-gray-500' },
}; };
export function BuildProgressModal({ appId, enabled }: { appId: string; enabled: boolean }) { export function BuildProgressModal({ appId, enabled }: { appId: string; enabled: boolean }) {
@@ -60,7 +61,7 @@ export function BuildProgressModal({ appId, enabled }: { appId: string; enabled:
const cfg = phaseConfig[progress.phase]; const cfg = phaseConfig[progress.phase];
const PhaseIcon = cfg.icon; const PhaseIcon = cfg.icon;
const isActive = progress.phase !== 'failed'; const isActive = progress.phase !== 'failed' && progress.phase !== 'cancelled';
const showBytes = progress.phase === 'uploading' && progress.totalBytes; const showBytes = progress.phase === 'uploading' && progress.totalBytes;
const isCancelling = cancelMutation.isPending; const isCancelling = cancelMutation.isPending;
@@ -70,7 +71,7 @@ export function BuildProgressModal({ appId, enabled }: { appId: string; enabled:
<button <button
type="button" type="button"
onClick={() => cancelMutation.mutate()} onClick={() => cancelMutation.mutate()}
disabled={isCancelling || progress.phase === 'failed'} disabled={isCancelling || progress.phase === 'failed' || progress.phase === 'cancelled'}
className="absolute top-4 right-4 p-1.5 rounded-lg text-gray-400 hover:text-gray-700 hover:bg-gray-100 transition-colors disabled:opacity-50" className="absolute top-4 right-4 p-1.5 rounded-lg text-gray-400 hover:text-gray-700 hover:bg-gray-100 transition-colors disabled:opacity-50"
aria-label="Cancel and close" aria-label="Cancel and close"
title="Cancel deployment" title="Cancel deployment"
@@ -82,7 +83,7 @@ export function BuildProgressModal({ appId, enabled }: { appId: string; enabled:
{isActive ? ( {isActive ? (
<Loader2 className="w-12 h-12 text-primary-600 mx-auto mb-3 animate-spin" /> <Loader2 className="w-12 h-12 text-primary-600 mx-auto mb-3 animate-spin" />
) : ( ) : (
<PhaseIcon className="w-12 h-12 text-red-500 mx-auto mb-3" /> <PhaseIcon className={`w-12 h-12 ${progress.phase === 'cancelled' ? 'text-gray-500' : 'text-red-500'} mx-auto mb-3`} />
)} )}
<h3 className="text-lg font-semibold text-gray-900">{cfg.label}</h3> <h3 className="text-lg font-semibold text-gray-900">{cfg.label}</h3>
{progress.message && ( {progress.message && (
@@ -114,6 +115,10 @@ export function BuildProgressModal({ appId, enabled }: { appId: string; enabled:
<p className="text-sm text-red-600 text-center">{progress.message}</p> <p className="text-sm text-red-600 text-center">{progress.message}</p>
)} )}
{progress.phase === 'cancelled' && progress.message && (
<p className="text-sm text-gray-600 text-center">{progress.message}</p>
)}
{isActive && ( {isActive && (
<p className="text-xs text-gray-400 text-center"> <p className="text-xs text-gray-400 text-center">
Click the close button to cancel and remove cluster build resources. Click the close button to cancel and remove cluster build resources.
+1
View File
@@ -139,6 +139,7 @@ export type DeploymentStatus =
| 'deploying' | 'deploying'
| 'running' | 'running'
| 'failed' | 'failed'
| 'cancelled'
| 'stopped' | 'stopped'
| 'deleting'; | 'deleting';