Track cancelled deployments separately.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 $$;
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ export type DeploymentStatus =
|
|||||||
| 'deploying'
|
| 'deploying'
|
||||||
| 'running'
|
| 'running'
|
||||||
| 'failed'
|
| 'failed'
|
||||||
|
| 'cancelled'
|
||||||
| 'stopped'
|
| 'stopped'
|
||||||
| 'deleting';
|
| 'deleting';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user