fix: reliable source upload, build cancel, WordPress port 80 default

- Replace port-forward/netcat PVC upload with kubectl cp for integrity
- Add build cancellation API and session cleanup; deploy catches cancel
- Default port 80 for WordPress, PHP, and Laravel on create
- Build progress modal with cancel; Helm/K8s adjustments for deployments
- Update build and kubernetes specs

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
keyhan
2026-05-14 16:03:45 +03:30
parent 3d56a2cc5d
commit 0c0a6cd5be
12 changed files with 637 additions and 93 deletions
@@ -0,0 +1,125 @@
'use client';
import { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import api from '@/lib/api';
import { Loader2, Upload, Hammer, Rocket, CheckCircle, XCircle, X } from 'lucide-react';
import { toast } from 'react-toastify';
export interface BuildProgress {
phase: 'uploading' | 'building' | 'deploying' | 'done' | 'failed';
percent: number;
bytesUploaded?: number;
totalBytes?: number;
message?: string;
}
function formatBytes(bytes?: number): string {
if (!bytes) return '';
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
const phaseConfig = {
uploading: { label: 'Uploading source to cluster', icon: Upload, bg: 'bg-blue-500' },
building: { label: 'Building Docker image', icon: Hammer, bg: 'bg-amber-500' },
deploying: { label: 'Deploying to Kubernetes', icon: Rocket, bg: 'bg-purple-500' },
done: { label: 'Deployment complete', icon: CheckCircle, bg: 'bg-green-500' },
failed: { label: 'Deployment failed', icon: XCircle, bg: 'bg-red-500' },
};
export function BuildProgressModal({ appId, enabled }: { appId: string; enabled: boolean }) {
const queryClient = useQueryClient();
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
if (enabled) setDismissed(false);
}, [enabled]);
const { data } = useQuery<{ progress: BuildProgress | null }>({
queryKey: ['build-progress', appId],
queryFn: () => api.get(`/deployments/applications/${appId}/build-progress`).then((r) => r.data),
enabled: enabled && !dismissed,
refetchInterval: enabled && !dismissed ? 1500 : false,
});
const cancelMutation = useMutation({
mutationFn: () => api.post(`/deployments/applications/${appId}/cancel`),
onSuccess: () => {
toast.success('Deployment cancelled');
setDismissed(true);
queryClient.invalidateQueries({ queryKey: ['application', appId] });
queryClient.invalidateQueries({ queryKey: ['deployments', appId] });
queryClient.invalidateQueries({ queryKey: ['build-progress', appId] });
},
onError: () => toast.error('Failed to cancel deployment'),
});
const progress = data?.progress;
if (!enabled || dismissed || !progress || progress.phase === 'done') return null;
const cfg = phaseConfig[progress.phase];
const PhaseIcon = cfg.icon;
const isActive = progress.phase !== 'failed';
const showBytes = progress.phase === 'uploading' && progress.totalBytes;
const isCancelling = cancelMutation.isPending;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="relative bg-white rounded-2xl shadow-2xl p-8 max-w-md w-full mx-4 space-y-5">
<button
type="button"
onClick={() => cancelMutation.mutate()}
disabled={isCancelling || progress.phase === 'failed'}
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"
title="Cancel deployment"
>
{isCancelling ? <Loader2 className="w-5 h-5 animate-spin" /> : <X className="w-5 h-5" />}
</button>
<div className="text-center">
{isActive ? (
<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" />
)}
<h3 className="text-lg font-semibold text-gray-900">{cfg.label}</h3>
{progress.message && (
<p className="text-sm text-gray-500 mt-1">{progress.message}</p>
)}
</div>
{isActive && (
<div className="space-y-2">
<div className="flex justify-between text-sm text-gray-600">
<span className="flex items-center gap-1.5">
<PhaseIcon className="w-4 h-4" />
{progress.percent}%
</span>
{showBytes && (
<span>{formatBytes(progress.bytesUploaded)} / {formatBytes(progress.totalBytes)}</span>
)}
</div>
<div className="h-3 bg-gray-100 rounded-full overflow-hidden">
<div
className={`h-full ${cfg.bg} rounded-full transition-all duration-500 ease-out`}
style={{ width: `${progress.percent}%` }}
/>
</div>
</div>
)}
{progress.phase === 'failed' && progress.message && (
<p className="text-sm text-red-600 text-center">{progress.message}</p>
)}
{isActive && (
<p className="text-xs text-gray-400 text-center">
Click the close button to cancel and remove cluster build resources.
</p>
)}
</div>
</div>
);
}