feat: dynamic database storage size with PVC expansion
- Add dbStorageSize column to Application entity (default: 1Gi) - Add dbStorageSize to CreateApplicationDto, frontend types - Use dynamic storage size in K8s deployDatabase instead of hardcoded 5Gi - Deploy page: storage size selector with +/- buttons (min 1GB, max 100GB) - Auto-suggest storage based on DB dump file size (3x dump size, min 1GB) - Show DB storage in Review step - App detail: Database Storage section with expand button - GET /applications/:id/db-storage — read current PVC size from K8s - PATCH /applications/:id/db-storage — expand PVC (only increase, no shrink) - PVC resize uses JSON patch on K8s API
This commit is contained in:
@@ -34,6 +34,7 @@ export default function DeployPage() {
|
||||
memoryLimit: '512Mi',
|
||||
replicas: 1,
|
||||
port: 3000,
|
||||
dbStorageSize: '1',
|
||||
});
|
||||
const [envKey, setEnvKey] = useState('');
|
||||
const [envVal, setEnvVal] = useState('');
|
||||
@@ -117,7 +118,12 @@ export default function DeployPage() {
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
createMutation.mutate(form);
|
||||
const payload = { ...form };
|
||||
// Format dbStorageSize with Gi suffix
|
||||
if (payload.databaseType !== 'none' && payload.dbStorageSize) {
|
||||
payload.dbStorageSize = `${parseInt(payload.dbStorageSize, 10) || 1}Gi`;
|
||||
}
|
||||
createMutation.mutate(payload);
|
||||
};
|
||||
|
||||
const handleFileSelect = useCallback((file: File) => {
|
||||
@@ -613,6 +619,8 @@ export default function DeployPage() {
|
||||
toast.error('Max 500MB');
|
||||
} else {
|
||||
setDbDumpFile(f);
|
||||
const sizeGb = Math.max(1, Math.ceil((f.size / (1024 * 1024 * 1024)) * 3));
|
||||
setForm((prev) => ({ ...prev, dbStorageSize: String(sizeGb) }));
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -634,6 +642,8 @@ export default function DeployPage() {
|
||||
toast.error('Max 500MB');
|
||||
} else {
|
||||
setDbDumpFile(f);
|
||||
const sizeGb = Math.max(1, Math.ceil((f.size / (1024 * 1024 * 1024)) * 3));
|
||||
setForm((prev) => ({ ...prev, dbStorageSize: String(sizeGb) }));
|
||||
}
|
||||
}
|
||||
e.target.value = '';
|
||||
@@ -657,6 +667,55 @@ export default function DeployPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Database Storage Size */}
|
||||
<div className="pt-2">
|
||||
<label className="block text-xs text-gray-500 mb-2">Database Storage Size</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center border border-gray-300 rounded-lg overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const current = parseInt(form.dbStorageSize || '1', 10);
|
||||
if (current > 1) setForm({ ...form, dbStorageSize: String(current - 1) });
|
||||
}}
|
||||
disabled={parseInt(form.dbStorageSize || '1', 10) <= 1}
|
||||
className="px-3 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={100}
|
||||
value={form.dbStorageSize || '1'}
|
||||
onChange={(e) => {
|
||||
const val = Math.max(1, Math.min(100, parseInt(e.target.value, 10) || 1));
|
||||
setForm({ ...form, dbStorageSize: String(val) });
|
||||
}}
|
||||
className="w-16 text-center py-2 border-x border-gray-300 text-sm font-semibold focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const current = parseInt(form.dbStorageSize || '1', 10);
|
||||
if (current < 100) setForm({ ...form, dbStorageSize: String(current + 1) });
|
||||
}}
|
||||
disabled={parseInt(form.dbStorageSize || '1', 10) >= 100}
|
||||
className="px-3 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-700">GB</span>
|
||||
{dbDumpFile && (
|
||||
<span className="text-xs text-blue-500">
|
||||
پیشنهاد بر اساس حجم دامپ ({(dbDumpFile.size / (1024 * 1024 * 1024)).toFixed(2)} GB)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-gray-400">حداقل ۱ گیگابایت • بعد از ساخت فقط امکان افزایش حجم وجود دارد</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -970,6 +1029,16 @@ export default function DeployPage() {
|
||||
<span className="text-sm text-gray-500">DB Password</span>
|
||||
<span className="text-sm font-medium">{form.dbPassword ? '••••••••' : 'Auto-generated'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-gray-500">DB Storage</span>
|
||||
<span className="text-sm font-medium">{form.dbStorageSize || '1'} GB</span>
|
||||
</div>
|
||||
{dbDumpFile && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-gray-500">DB Dump</span>
|
||||
<span className="text-sm font-medium">{dbDumpFile.name} ({(dbDumpFile.size / (1024 * 1024)).toFixed(1)} MB)</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="flex justify-between">
|
||||
|
||||
Reference in New Issue
Block a user