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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user