Localize database-config, snapshots panel and new-service wizard.

Add components.dbConfig / components.snapshots and dashboard.servicesNew
dictionaries; move the managed-database config (engine, credentials, dump
upload, storage), the snapshots panel and the three-step new-service
wizard onto them. validateDbDumpStorage now takes a localized template.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
keyhan
2026-06-11 19:22:37 +03:30
parent 63e368f464
commit 96d24b50a1
6 changed files with 334 additions and 114 deletions
@@ -3,6 +3,7 @@
import { useRef, useState } from 'react';
import { Database, Eye, EyeOff, RefreshCw, AlertCircle } from 'lucide-react';
import { toast } from 'react-toastify';
import { useT } from '@/i18n/I18nProvider';
import { ONE_GIB, minGiToFitFileBytes } from '@/lib/storage-utils';
export type DatabaseEngine = 'postgresql' | 'mysql' | 'mariadb' | 'mongodb';
@@ -45,6 +46,7 @@ export function ManagedDatabaseConfig({
dbDumpFile: File | null;
onDbDumpFileChange: (file: File | null) => void;
}) {
const c = useT().components.dbConfig;
const dbDumpInputRef = useRef<HTMLInputElement>(null);
const [showDbPassword, setShowDbPassword] = useState(false);
const [isDragging, setIsDragging] = useState(false);
@@ -54,11 +56,11 @@ export function ManagedDatabaseConfig({
const acceptDump = (f: File) => {
if (!f.name.endsWith('.sql') && !f.name.endsWith('.gz') && !f.name.endsWith('.dump')) {
toast.error('Allowed: .sql, .gz, .dump');
toast.error(c.allowedFormats);
return;
}
if (f.size > 500 * 1024 * 1024) {
toast.error('Max 500MB');
toast.error(c.maxSize);
return;
}
onDbDumpFileChange(f);
@@ -70,7 +72,7 @@ export function ManagedDatabaseConfig({
return (
<div className="space-y-5">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Database engine</label>
<label className="block text-sm font-medium text-gray-700 mb-2">{c.engine}</label>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
{DB_OPTIONS.map((opt) => (
<button
@@ -98,7 +100,7 @@ export function ManagedDatabaseConfig({
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Version</label>
<label className="block text-sm font-medium text-gray-700 mb-1">{c.version}</label>
<select
className="input-field max-w-xs"
value={form.dbVersion}
@@ -115,30 +117,30 @@ export function ManagedDatabaseConfig({
<div className="p-4 bg-blue-50/50 border border-blue-200 rounded-xl space-y-4">
<div className="flex items-center gap-2">
<Database className="w-5 h-5 text-blue-500" />
<h3 className="text-sm font-semibold text-gray-800">Database credentials</h3>
<span className="text-xs text-gray-400">(optional auto-generated if empty)</span>
<h3 className="text-sm font-semibold text-gray-800">{c.credentials}</h3>
<span className="text-xs text-gray-400">{c.credentialsOptional}</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-xs text-gray-500 mb-1">Username</label>
<label className="block text-xs text-gray-500 mb-1">{c.username}</label>
<input
className="input-field"
placeholder="appuser"
placeholder={c.usernamePlaceholder}
value={form.dbUsername}
onChange={(e) => onChange({ dbUsername: e.target.value })}
/>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">Password</label>
<label className="block text-xs text-gray-500 mb-1">{c.password}</label>
<div className="relative">
<input
className="input-field pr-20"
className="input-field pr-20 rtl:pr-3 rtl:pl-20"
type={showDbPassword ? 'text' : 'password'}
placeholder="Auto-generated"
placeholder={c.passwordPlaceholder}
value={form.dbPassword}
onChange={(e) => onChange({ dbPassword: e.target.value })}
/>
<div className="absolute inset-y-0 right-0 flex items-center gap-1 pr-2">
<div className="absolute inset-y-0 right-0 rtl:right-auto rtl:left-0 flex items-center gap-1 pr-2 rtl:pr-0 rtl:pl-2">
<button
type="button"
onClick={() => {
@@ -149,7 +151,7 @@ export function ManagedDatabaseConfig({
setShowDbPassword(true);
}}
className="p-1 text-gray-400 hover:text-blue-500 transition-colors"
title="Generate random password"
title={c.generatePassword}
>
<RefreshCw className="w-4 h-4" />
</button>
@@ -164,12 +166,10 @@ export function ManagedDatabaseConfig({
</div>
</div>
</div>
<p className="text-xs text-gray-400">
Used for internal cluster access. Use temporary or permanent external access below after deploy.
</p>
<p className="text-xs text-gray-400">{c.credentialsNote}</p>
<div className="pt-2">
<label className="block text-xs text-gray-500 mb-2">Optional: upload DB dump to restore at creation</label>
<label className="block text-xs text-gray-500 mb-2">{c.uploadDumpLabel}</label>
<div
onDrop={(e) => {
e.preventDefault();
@@ -204,7 +204,7 @@ export function ManagedDatabaseConfig({
/>
{dbDumpFile ? (
<div className="flex items-center justify-between">
<div className="text-sm text-left">
<div className="text-sm text-left rtl:text-right">
<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>
@@ -216,20 +216,20 @@ export function ManagedDatabaseConfig({
}}
className="text-sm text-red-500"
>
Remove
{c.remove}
</button>
</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 .sql, .gz, .dump</p>
<p className="text-sm text-gray-700">{c.uploadHint}</p>
<p className="text-xs text-gray-400">{c.uploadConstraints}</p>
</div>
)}
</div>
</div>
<div className="pt-2">
<label className="block text-xs text-gray-500 mb-2">Database storage size</label>
<label className="block text-xs text-gray-500 mb-2">{c.storageSize}</label>
<div className="flex items-center gap-3 flex-wrap">
<div className="flex items-center border border-gray-300 rounded-lg overflow-hidden">
<button
@@ -274,13 +274,13 @@ export function ManagedDatabaseConfig({
<span className="text-sm font-medium text-gray-700">GB</span>
{dbDumpFile && (
<span className="text-xs text-blue-500">
Suggested from dump ({(dbDumpFile.size / ONE_GIB).toFixed(2)} GiB min fit)
{c.suggestedFromDump.replace('{gi}', (dbDumpFile.size / ONE_GIB).toFixed(2))}
</span>
)}
</div>
<p className="mt-1 text-xs text-gray-400">
Minimum {minDbGiFromRestoreDump} GB
{dbDumpFile ? ' (must fit the uploaded dump)' : ''} Only expansion allowed after creation
{c.minimumGb.replace('{n}', String(minDbGiFromRestoreDump))}
{dbDumpFile ? c.mustFitDump : ''}{c.expansionOnly}
</p>
</div>
</div>
@@ -288,14 +288,22 @@ export function ManagedDatabaseConfig({
);
}
/**
* Validates that the chosen DB storage fits the uploaded dump. Pass the localized
* message template (components.dbConfig.storageTooSmallDetail) with {dumpGi}/{need}/{selected}.
*/
export function validateDbDumpStorage(
dbDumpFile: File | null,
dbStorageSizeGi: number,
detailTemplate: string,
): string | null {
if (!dbDumpFile) return null;
const need = minGiToFitFileBytes(dbDumpFile.size);
if (dbStorageSizeGi < need) {
return `Your database dump is about ${(dbDumpFile.size / ONE_GIB).toFixed(2)} GiB. Database storage must be at least ${need} GiB (you selected ${dbStorageSizeGi} GiB). Increase database storage, then try again.`;
return detailTemplate
.replace('{dumpGi}', (dbDumpFile.size / ONE_GIB).toFixed(2))
.replace('{need}', String(need))
.replace('{selected}', String(dbStorageSizeGi));
}
return null;
}
@@ -309,6 +317,7 @@ export function RestoreStorageErrorModal({
message: string;
onClose: () => void;
}) {
const c = useT().components.dbConfig;
if (!open) return null;
return (
<div className="fixed inset-0 z-[9999] flex items-center justify-center p-4">
@@ -322,12 +331,12 @@ export function RestoreStorageErrorModal({
<div className="w-12 h-12 rounded-xl bg-red-100 flex items-center justify-center mb-4">
<AlertCircle className="w-6 h-6 text-red-600" />
</div>
<h3 className="text-lg font-bold text-gray-900 mb-2">Storage too small</h3>
<h3 className="text-lg font-bold text-gray-900 mb-2">{c.storageTooSmallTitle}</h3>
<p className="text-sm text-gray-600 leading-relaxed">{message}</p>
</div>
<div className="flex items-center justify-end gap-3 p-6">
<button type="button" onClick={onClose} className="btn-primary">
OK
{c.ok}
</button>
</div>
</div>