add optinal apps
This commit is contained in:
@@ -48,6 +48,10 @@ export default function DeployPage() {
|
||||
replicas: 1,
|
||||
port: 3000,
|
||||
dbStorageSize: '1',
|
||||
appStorageSize: '2',
|
||||
enableRedis: false,
|
||||
enableRabbitmq: false,
|
||||
enableElasticsearch: false,
|
||||
});
|
||||
const [envKey, setEnvKey] = useState('');
|
||||
const [envVal, setEnvVal] = useState('');
|
||||
@@ -84,7 +88,7 @@ export default function DeployPage() {
|
||||
|
||||
// Cost calculation for the review step
|
||||
const { data: costData, isLoading: costLoading } = useQuery<CostBreakdown>({
|
||||
queryKey: ['deploy-cost', form.runtime, form.databaseType, form.cpuLimit, form.memoryLimit, form.replicas, form.dbStorageSize],
|
||||
queryKey: ['deploy-cost', form.runtime, form.databaseType, form.cpuLimit, form.memoryLimit, form.replicas, form.dbStorageSize, form.appStorageSize, form.enableRedis, form.enableRabbitmq, form.enableElasticsearch],
|
||||
queryFn: () => api.post('/billing/calculate', {
|
||||
runtime: form.runtime,
|
||||
databaseType: form.databaseType,
|
||||
@@ -92,6 +96,10 @@ export default function DeployPage() {
|
||||
memoryLimit: form.memoryLimit,
|
||||
replicas: form.replicas,
|
||||
dbStorageSize: form.databaseType !== 'none' ? `${parseInt(form.dbStorageSize || '1', 10) || 1}` : undefined,
|
||||
appStorageSize: `${parseInt(form.appStorageSize || '2', 10) || 2}`,
|
||||
enableRedis: form.enableRedis,
|
||||
enableRabbitmq: form.enableRabbitmq,
|
||||
enableElasticsearch: form.enableElasticsearch,
|
||||
}).then((r) => r.data),
|
||||
enabled: step === 3,
|
||||
});
|
||||
@@ -115,6 +123,9 @@ export default function DeployPage() {
|
||||
if (payload.databaseType !== 'none' && payload.dbStorageSize) {
|
||||
payload.dbStorageSize = `${parseInt(payload.dbStorageSize, 10) || 1}Gi`;
|
||||
}
|
||||
if (payload.appStorageSize) {
|
||||
payload.appStorageSize = `${parseInt(payload.appStorageSize, 10) || 2}Gi`;
|
||||
}
|
||||
const res = await api.post('/applications', payload);
|
||||
const appId = res.data.id;
|
||||
|
||||
@@ -188,6 +199,9 @@ export default function DeployPage() {
|
||||
if (payload.databaseType !== 'none' && payload.dbStorageSize) {
|
||||
payload.dbStorageSize = `${parseInt(payload.dbStorageSize, 10) || 1}Gi`;
|
||||
}
|
||||
if (payload.appStorageSize) {
|
||||
payload.appStorageSize = `${parseInt(payload.appStorageSize, 10) || 2}Gi`;
|
||||
}
|
||||
const res = await api.post('/applications', payload);
|
||||
const appId = res.data.id;
|
||||
|
||||
@@ -312,6 +326,10 @@ export default function DeployPage() {
|
||||
if (payload.databaseType !== 'none' && payload.dbStorageSize) {
|
||||
payload.dbStorageSize = `${parseInt(payload.dbStorageSize, 10) || 1}Gi`;
|
||||
}
|
||||
// Format appStorageSize with Gi suffix (all app types)
|
||||
if (payload.appStorageSize) {
|
||||
payload.appStorageSize = `${parseInt(payload.appStorageSize, 10) || 2}Gi`;
|
||||
}
|
||||
createMutation.mutate(payload);
|
||||
};
|
||||
|
||||
@@ -361,6 +379,10 @@ export default function DeployPage() {
|
||||
return;
|
||||
}
|
||||
setWpContentFile(file);
|
||||
// Auto-suggest app storage size based on file size (add 50% buffer, minimum 2GB)
|
||||
const fileSizeGb = file.size / (1024 * 1024 * 1024);
|
||||
const suggestedSize = Math.max(2, Math.ceil(fileSizeGb * 1.5));
|
||||
setForm((prev) => ({ ...prev, appStorageSize: String(suggestedSize) }));
|
||||
toast.success(`WordPress files selected: ${file.name}`);
|
||||
}, []);
|
||||
|
||||
@@ -451,30 +473,40 @@ export default function DeployPage() {
|
||||
{/* Application Runtime */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3">Application Type</label>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
{[
|
||||
{ value: 'nodejs', label: 'Node.js', icon: <Hexagon className="w-6 h-6 text-green-500" />, desc: 'Express, NestJS, Fastify...' },
|
||||
{ value: 'laravel', label: 'Laravel', icon: <Hexagon className="w-6 h-6 text-orange-500" />, desc: 'PHP, Composer, Artisan' },
|
||||
{ value: 'wordpress', label: 'WordPress', icon: <Hexagon className="w-6 h-6 text-blue-600" />, desc: 'Official image, wp-content' },
|
||||
{ value: 'nodejs', label: 'Node.js', icon: <Hexagon className="w-5 h-5 text-green-500" />, desc: 'Express, NestJS, Fastify' },
|
||||
{ value: 'laravel', label: 'Laravel', icon: <Hexagon className="w-5 h-5 text-orange-500" />, desc: 'PHP, Composer, Artisan' },
|
||||
{ value: 'wordpress', label: 'WordPress', icon: <Hexagon className="w-5 h-5 text-blue-600" />, desc: 'Official image' },
|
||||
{ value: 'go', label: 'Go', icon: <Hexagon className="w-5 h-5 text-cyan-500" />, desc: 'Gin, Echo, Fiber' },
|
||||
{ value: 'python', label: 'Python', icon: <Hexagon className="w-5 h-5 text-yellow-500" />, desc: 'Flask, FastAPI' },
|
||||
{ value: 'django', label: 'Django', icon: <Hexagon className="w-5 h-5 text-green-700" />, desc: 'Python web framework' },
|
||||
{ value: 'php', label: 'PHP', icon: <Hexagon className="w-5 h-5 text-indigo-500" />, desc: 'Plain PHP apps' },
|
||||
{ value: 'dotnet', label: '.NET', icon: <Hexagon className="w-5 h-5 text-purple-500" />, desc: 'ASP.NET Core' },
|
||||
].map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const updates: any = { runtime: opt.value as any, phpVersion: '' };
|
||||
const updates: any = { runtime: opt.value as any, phpVersion: '', runtimeVersion: '' };
|
||||
if (opt.value === 'nodejs') { updates.port = 3000; updates.runtimeVersion = '20'; }
|
||||
else if (opt.value === 'laravel') { updates.port = 8000; updates.runtimeVersion = ''; updates.phpVersion = '8.3'; }
|
||||
else if (opt.value === 'laravel') { updates.port = 8000; updates.phpVersion = '8.3'; }
|
||||
else if (opt.value === 'wordpress') { updates.port = 80; updates.databaseType = 'mysql'; updates.runtimeVersion = '6.7'; updates.phpVersion = '8.3'; }
|
||||
else if (opt.value === 'go') { updates.port = 8080; updates.runtimeVersion = '1.22'; }
|
||||
else if (opt.value === 'python') { updates.port = 8000; updates.runtimeVersion = '3.12'; }
|
||||
else if (opt.value === 'django') { updates.port = 8000; updates.runtimeVersion = '3.12'; }
|
||||
else if (opt.value === 'php') { updates.port = 80; updates.phpVersion = '8.3'; }
|
||||
else if (opt.value === 'dotnet') { updates.port = 5000; updates.runtimeVersion = '8.0'; }
|
||||
setForm({ ...form, ...updates });
|
||||
// Reset WordPress-specific state when switching types
|
||||
if (opt.value !== 'wordpress') { setWpMode('fresh'); setWpContentFile(null); }
|
||||
}}
|
||||
className={`p-4 rounded-xl border-2 text-left transition-colors ${
|
||||
className={`p-3 rounded-xl border-2 text-left transition-colors ${
|
||||
form.runtime === opt.value ? 'border-primary-500 bg-primary-50' : 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
{opt.icon}
|
||||
<p className="mt-2 font-semibold text-gray-900">{opt.label}</p>
|
||||
<p className="mt-1.5 font-semibold text-sm text-gray-900">{opt.label}</p>
|
||||
<p className="text-xs text-gray-500">{opt.desc}</p>
|
||||
</button>
|
||||
))}
|
||||
@@ -898,29 +930,97 @@ export default function DeployPage() {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{form.runtime === 'go' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Go Version</label>
|
||||
<select
|
||||
className="input-field"
|
||||
value={form.runtimeVersion || '1.22'}
|
||||
onChange={(e) => setForm({ ...form, runtimeVersion: e.target.value })}
|
||||
>
|
||||
<option value="1.23">Go 1.23 (Latest)</option>
|
||||
<option value="1.22">Go 1.22 (Recommended)</option>
|
||||
<option value="1.21">Go 1.21</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{(form.runtime === 'python' || form.runtime === 'django') && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Python Version</label>
|
||||
<select
|
||||
className="input-field"
|
||||
value={form.runtimeVersion || '3.12'}
|
||||
onChange={(e) => setForm({ ...form, runtimeVersion: e.target.value })}
|
||||
>
|
||||
<option value="3.13">Python 3.13 (Latest)</option>
|
||||
<option value="3.12">Python 3.12 (Recommended)</option>
|
||||
<option value="3.11">Python 3.11</option>
|
||||
<option value="3.10">Python 3.10</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{form.runtime === 'php' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">PHP Version</label>
|
||||
<select
|
||||
className="input-field"
|
||||
value={form.phpVersion || '8.3'}
|
||||
onChange={(e) => setForm({ ...form, phpVersion: e.target.value })}
|
||||
>
|
||||
<option value="8.4">PHP 8.4 (Latest)</option>
|
||||
<option value="8.3">PHP 8.3 (Recommended)</option>
|
||||
<option value="8.2">PHP 8.2</option>
|
||||
<option value="8.1">PHP 8.1</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{form.runtime === 'dotnet' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">.NET Version</label>
|
||||
<select
|
||||
className="input-field"
|
||||
value={form.runtimeVersion || '8.0'}
|
||||
onChange={(e) => setForm({ ...form, runtimeVersion: e.target.value })}
|
||||
>
|
||||
<option value="9.0">.NET 9.0 (Latest)</option>
|
||||
<option value="8.0">.NET 8.0 LTS (Recommended)</option>
|
||||
<option value="7.0">.NET 7.0</option>
|
||||
<option value="6.0">.NET 6.0 LTS</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||
Database
|
||||
{form.runtime === 'wordpress' && (
|
||||
<span className="text-xs text-blue-500 mr-2"> — وردپرس به MySQL نیاز دارد</span>
|
||||
<span className="text-xs text-blue-500 mr-2"> — WordPress requires MySQL/MariaDB</span>
|
||||
)}
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-3 sm:gap-4">
|
||||
<div className="grid grid-cols-3 sm:grid-cols-5 gap-3 sm:gap-4">
|
||||
{[
|
||||
{ value: 'none', label: 'None', icon: <XCircle className="w-6 h-6 text-gray-400" /> },
|
||||
{ value: 'postgresql', label: 'PostgreSQL', icon: <svg className="w-6 h-6 text-blue-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5V19C3 20.66 7.03 22 12 22C16.97 22 21 20.66 21 19V5"/><path d="M3 12C3 13.66 7.03 15 12 15C16.97 15 21 13.66 21 12"/></svg> },
|
||||
{ value: 'mysql', label: 'MySQL', icon: <svg className="w-6 h-6 text-orange-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5V19C3 20.66 7.03 22 12 22C16.97 22 21 20.66 21 19V5"/><path d="M3 12C3 13.66 7.03 15 12 15C16.97 15 21 13.66 21 12"/></svg> },
|
||||
{ value: 'mariadb', label: 'MariaDB', icon: <svg className="w-6 h-6 text-teal-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5V19C3 20.66 7.03 22 12 22C16.97 22 21 20.66 21 19V5"/><path d="M3 12C3 13.66 7.03 15 12 15C16.97 15 21 13.66 21 12"/></svg> },
|
||||
{ value: 'mongodb', label: 'MongoDB', icon: <svg className="w-6 h-6 text-green-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 2L12 22M12 2C7.58 4 5 8 5 12C5 16 7.58 20 12 22M12 2C16.42 4 19 8 19 12C19 16 16.42 20 12 22"/></svg> },
|
||||
].map((opt) => {
|
||||
const isWordPress = form.runtime === 'wordpress';
|
||||
const disabled = isWordPress && opt.value !== 'mysql';
|
||||
const disabled = isWordPress && !['mysql', 'mariadb'].includes(opt.value);
|
||||
return (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => setForm({ ...form, databaseType: opt.value as any, dbVersion: opt.value === 'postgresql' ? '16' : opt.value === 'mysql' ? '8.0' : '' })}
|
||||
onClick={() => {
|
||||
let dbVersion = '';
|
||||
if (opt.value === 'postgresql') dbVersion = '16';
|
||||
else if (opt.value === 'mysql') dbVersion = '8.0';
|
||||
else if (opt.value === 'mariadb') dbVersion = '11.4';
|
||||
else if (opt.value === 'mongodb') dbVersion = '7.0';
|
||||
setForm({ ...form, databaseType: opt.value as any, dbVersion });
|
||||
}}
|
||||
className={`p-4 rounded-xl border-2 text-center transition-colors ${
|
||||
form.databaseType === opt.value ? 'border-primary-500 bg-primary-50' : 'border-gray-200 hover:border-gray-300'
|
||||
} ${disabled ? 'opacity-40 cursor-not-allowed' : ''}`}
|
||||
@@ -937,11 +1037,11 @@ export default function DeployPage() {
|
||||
{form.databaseType !== 'none' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{form.databaseType === 'postgresql' ? 'PostgreSQL' : 'MySQL'} Version
|
||||
{form.databaseType === 'postgresql' ? 'PostgreSQL' : form.databaseType === 'mysql' ? 'MySQL' : form.databaseType === 'mariadb' ? 'MariaDB' : 'MongoDB'} Version
|
||||
</label>
|
||||
<select
|
||||
className="input-field max-w-xs"
|
||||
value={form.dbVersion || (form.databaseType === 'postgresql' ? '16' : '8.0')}
|
||||
value={form.dbVersion || (form.databaseType === 'postgresql' ? '16' : form.databaseType === 'mysql' ? '8.0' : form.databaseType === 'mariadb' ? '11.4' : '7.0')}
|
||||
onChange={(e) => setForm({ ...form, dbVersion: e.target.value })}
|
||||
>
|
||||
{form.databaseType === 'postgresql' ? (
|
||||
@@ -951,13 +1051,26 @@ export default function DeployPage() {
|
||||
<option value="15">PostgreSQL 15</option>
|
||||
<option value="14">PostgreSQL 14</option>
|
||||
</>
|
||||
) : (
|
||||
) : form.databaseType === 'mysql' ? (
|
||||
<>
|
||||
<option value="9.0">MySQL 9.0 (Latest)</option>
|
||||
<option value="8.4">MySQL 8.4 (LTS)</option>
|
||||
<option value="8.0">MySQL 8.0</option>
|
||||
<option value="5.7">MySQL 5.7</option>
|
||||
</>
|
||||
) : form.databaseType === 'mariadb' ? (
|
||||
<>
|
||||
<option value="11.4">MariaDB 11.4 (LTS)</option>
|
||||
<option value="11.3">MariaDB 11.3</option>
|
||||
<option value="10.11">MariaDB 10.11 (LTS)</option>
|
||||
<option value="10.6">MariaDB 10.6</option>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<option value="7.0">MongoDB 7.0 (Latest)</option>
|
||||
<option value="6.0">MongoDB 6.0</option>
|
||||
<option value="5.0">MongoDB 5.0</option>
|
||||
</>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
@@ -1126,14 +1239,177 @@ export default function DeployPage() {
|
||||
<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)
|
||||
Suggested based on dump size ({(dbDumpFile.size / (1024 * 1024 * 1024)).toFixed(2)} GB)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-gray-400">حداقل ۱ گیگابایت • بعد از ساخت فقط امکان افزایش حجم وجود دارد</p>
|
||||
<p className="mt-1 text-xs text-gray-400">Minimum 1GB • Only expansion is allowed after creation</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Optional Services Section */}
|
||||
<div className="bg-purple-50 rounded-xl p-5 border border-purple-200">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Server className="w-5 h-5 text-purple-600" />
|
||||
<h3 className="font-semibold text-gray-900">Optional Services</h3>
|
||||
<span className="text-xs text-gray-400">(Enable additional services for your app)</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
{/* Redis */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setForm({ ...form, enableRedis: !form.enableRedis })}
|
||||
className={`p-4 rounded-xl border-2 text-left transition-all ${
|
||||
form.enableRedis
|
||||
? 'border-red-400 bg-red-50 shadow-sm'
|
||||
: 'border-gray-200 bg-white hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${form.enableRedis ? 'bg-red-100' : 'bg-gray-100'}`}>
|
||||
<svg className={`w-6 h-6 ${form.enableRedis ? 'text-red-500' : 'text-gray-400'}`} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2L2 7L12 12L22 7L12 2ZM2 17L12 22L22 17M2 12L12 17L22 12" stroke="currentColor" strokeWidth="2" fill="none"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">Redis</p>
|
||||
<p className="text-xs text-gray-500">In-memory cache & store</p>
|
||||
</div>
|
||||
</div>
|
||||
{form.enableRedis && (
|
||||
<div className="mt-3 pt-3 border-t border-red-200 text-xs text-red-600">
|
||||
<p>REDIS_HOST, REDIS_PASSWORD, REDIS_URL will be available</p>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* RabbitMQ */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setForm({ ...form, enableRabbitmq: !form.enableRabbitmq })}
|
||||
className={`p-4 rounded-xl border-2 text-left transition-all ${
|
||||
form.enableRabbitmq
|
||||
? 'border-orange-400 bg-orange-50 shadow-sm'
|
||||
: 'border-gray-200 bg-white hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${form.enableRabbitmq ? 'bg-orange-100' : 'bg-gray-100'}`}>
|
||||
<svg className={`w-6 h-6 ${form.enableRabbitmq ? 'text-orange-500' : 'text-gray-400'}`} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M21 3H3v18h18V3zM8 17H5v-3h3v3zm5-4h-3v-3h3v3zm5 0h-3v-3h3v3zm0-4h-8V6h8v3z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">RabbitMQ</p>
|
||||
<p className="text-xs text-gray-500">Message broker</p>
|
||||
</div>
|
||||
</div>
|
||||
{form.enableRabbitmq && (
|
||||
<div className="mt-3 pt-3 border-t border-orange-200 text-xs text-orange-600">
|
||||
<p>RABBITMQ_HOST, RABBITMQ_USER, AMQP_URL will be available</p>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Elasticsearch */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setForm({ ...form, enableElasticsearch: !form.enableElasticsearch })}
|
||||
className={`p-4 rounded-xl border-2 text-left transition-all ${
|
||||
form.enableElasticsearch
|
||||
? 'border-yellow-400 bg-yellow-50 shadow-sm'
|
||||
: 'border-gray-200 bg-white hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${form.enableElasticsearch ? 'bg-yellow-100' : 'bg-gray-100'}`}>
|
||||
<svg className={`w-6 h-6 ${form.enableElasticsearch ? 'text-yellow-500' : 'text-gray-400'}`} viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="3" stroke="currentColor" strokeWidth="2" fill="none"/>
|
||||
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" stroke="currentColor" strokeWidth="2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">Elasticsearch</p>
|
||||
<p className="text-xs text-gray-500">Logging & search</p>
|
||||
</div>
|
||||
</div>
|
||||
{form.enableElasticsearch && (
|
||||
<div className="mt-3 pt-3 border-t border-yellow-200 text-xs text-yellow-600">
|
||||
<p>Logs collected via Fluent Bit sidecar</p>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{(form.enableRedis || form.enableRabbitmq || form.enableElasticsearch) && (
|
||||
<p className="mt-4 text-xs text-gray-500">
|
||||
Each enabled service adds to the monthly cost. Services are deployed in your namespace and not shared.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* App Storage Size (all app types) */}
|
||||
<div className="bg-green-50 rounded-xl p-5 border border-green-200">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<FolderUp className="w-5 h-5 text-green-600" />
|
||||
<h3 className="font-semibold text-gray-900">
|
||||
{form.runtime === 'wordpress' ? 'Upload Storage (wp-content)'
|
||||
: form.runtime === 'laravel' ? 'Storage Directory'
|
||||
: 'Application Data Storage'}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
{form.runtime === 'wordpress'
|
||||
? 'This space is used for uploads, plugins, themes, and other WordPress files.'
|
||||
: form.runtime === 'laravel'
|
||||
? 'This space is used for uploads, logs, cache, and other Laravel storage files.'
|
||||
: 'This space is used for persistent application data, uploads, and files.'}
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center border border-gray-300 rounded-lg overflow-hidden bg-white">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const current = parseInt(form.appStorageSize || '2', 10);
|
||||
if (current > 1) setForm({ ...form, appStorageSize: String(current - 1) });
|
||||
}}
|
||||
disabled={parseInt(form.appStorageSize || '2', 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.appStorageSize || '2'}
|
||||
onChange={(e) => {
|
||||
const val = Math.max(1, Math.min(100, parseInt(e.target.value, 10) || 2));
|
||||
setForm({ ...form, appStorageSize: 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.appStorageSize || '2', 10);
|
||||
if (current < 100) setForm({ ...form, appStorageSize: String(current + 1) });
|
||||
}}
|
||||
disabled={parseInt(form.appStorageSize || '2', 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>
|
||||
{wpContentFile && form.runtime === 'wordpress' && (
|
||||
<span className="text-xs text-green-600">
|
||||
Suggested based on wp-content ({(wpContentFile.size / (1024 * 1024 * 1024)).toFixed(2)} GB)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-gray-500">Minimum 1GB • Recommended: 2GB or more</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1509,6 +1785,18 @@ export default function DeployPage() {
|
||||
<span className="text-sm text-gray-500">Port</span>
|
||||
<span className="text-sm font-medium">{form.port}</span>
|
||||
</div>
|
||||
{(form.enableRedis || form.enableRabbitmq || form.enableElasticsearch) && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-gray-500">Optional Services</span>
|
||||
<span className="text-sm font-medium">
|
||||
{[
|
||||
form.enableRedis && 'Redis',
|
||||
form.enableRabbitmq && 'RabbitMQ',
|
||||
form.enableElasticsearch && 'Elasticsearch',
|
||||
].filter(Boolean).join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{Object.keys(form.envVars || {}).length > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-gray-500">Env Vars</span>
|
||||
|
||||
Reference in New Issue
Block a user