feat(ui): allow uploading DB dump at app creation — deploy page accepts optional SQL dump and uploads it after app creation

This commit is contained in:
keyhan
2026-04-06 22:53:47 +03:30
parent 9e3347cb71
commit ea86acf2ab
2 changed files with 75 additions and 1 deletions
@@ -41,6 +41,9 @@ export default function DeployPage() {
const [clusterMode, setClusterMode] = useState<'default' | 'manual' | 'pool'>('default');
const [showDbPassword, setShowDbPassword] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const [dbDumpFile, setDbDumpFile] = useState<File | null>(null);
const dbDumpInputRef = useRef<HTMLInputElement>(null);
const [dbUploadProgress, setDbUploadProgress] = useState(0);
const { data: clusters = [] } = useQuery<ClusterPublic[]>({
queryKey: ['clusters-public'],
@@ -71,6 +74,18 @@ export default function DeployPage() {
});
}
// Upload DB dump if provided and a database was requested
if (data.databaseType && data.databaseType !== 'none' && dbDumpFile) {
const formData = new FormData();
formData.append('file', dbDumpFile);
await api.post(`/applications/${appId}/db-upload`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (e) => {
if (e.total) setDbUploadProgress(Math.round((e.loaded * 100) / e.total));
},
});
}
return res;
},
onSuccess: (res) => {
@@ -448,6 +463,65 @@ export default function DeployPage() {
<p className="text-xs text-gray-400">
These credentials are used for internal cluster communication only. The database is not exposed externally.
</p>
{/* Optional DB dump upload at creation time */}
<div className="pt-2">
<label className="block text-xs text-gray-500 mb-2">Optional: Upload DB dump to restore at creation</label>
<div
onDrop={(e) => {
e.preventDefault();
setIsDragging(false);
const f = e.dataTransfer.files[0];
if (f) {
if (!f.name.endsWith('.sql') && !f.name.endsWith('.gz') && !f.name.endsWith('.dump')) {
toast.error('Allowed: .sql, .gz, .dump');
} else if (f.size > 500 * 1024 * 1024) {
toast.error('Max 500MB');
} else {
setDbDumpFile(f);
}
}
}}
onDragOver={(e) => e.preventDefault()}
onClick={() => dbDumpInputRef.current?.click()}
className={`border-2 border-dashed rounded-xl p-3 text-center cursor-pointer transition-colors ${dbDumpFile ? 'border-blue-400 bg-blue-50' : 'border-gray-300 hover:border-blue-400 hover:bg-gray-50'}`}
>
<input
ref={dbDumpInputRef}
type="file"
accept=".sql,.gz,.dump"
className="hidden"
onChange={(e) => {
const f = e.target.files?.[0];
if (f) {
if (!f.name.endsWith('.sql') && !f.name.endsWith('.gz') && !f.name.endsWith('.dump')) {
toast.error('Allowed: .sql, .gz, .dump');
} else if (f.size > 500 * 1024 * 1024) {
toast.error('Max 500MB');
} else {
setDbDumpFile(f);
}
}
e.target.value = '';
}}
/>
{dbDumpFile ? (
<div className="flex items-center justify-between">
<div className="text-sm text-left">
<p className="font-medium text-gray-800">{dbDumpFile.name}</p>
<p className="text-xs text-gray-500">{(dbDumpFile.size / (1024 * 1024)).toFixed(2)} MB</p>
</div>
<div className="flex items-center gap-2">
<button type="button" onClick={() => setDbDumpFile(null)} className="text-sm text-red-500">Remove</button>
</div>
</div>
) : (
<div>
<p className="text-sm text-gray-700">Upload a SQL dump to be restored after the database is created</p>
<p className="text-xs text-gray-400">Optional Max 500MB</p>
</div>
)}
</div>
</div>
</div>
)}
</div>