feat: add MariaDB tuning support and internationalization for application UI

This commit is contained in:
Ümit Tunç
2026-04-02 14:35:19 +03:00
parent f059ebb83b
commit b28c088558
7 changed files with 550 additions and 3 deletions
+53 -1
View File
@@ -484,11 +484,13 @@ export function registerIpcHandlers(): void {
const sanitizedServiceName = serviceName.replace(/:/g, '_')
const logFile = path.join(configService.getBasePath(), 'logs', `${sanitizedServiceName}_error.log`)
const tuning = settings.mariaDbTuning?.[dbId] || ''
const configPath = await configService.generateConfig('mariadb.ini.template', `mariadb_${dbId || 'default'}.ini`, {
PORT: dbPort,
DATADIR: dataDir.replace(/\\/g, '/'),
SOCKET: `/tmp/${sanitizedServiceName}.sock`,
LOG_FILE: logFile.replace(/\\/g, '/')
LOG_FILE: logFile.replace(/\\/g, '/'),
EXTRA_CONFIG: tuning
})
const success = await processManager.startService(serviceName, binPath, ['--defaults-file=' + configPath], false)
@@ -627,6 +629,56 @@ export function registerIpcHandlers(): void {
return { success: false, message: `MariaDB hatası: ${error.message}` }
}
})
ipcMain.handle('mariadb:config:get', async (_event, versionId: string) => {
const settings = configService.getSettings()
const tuning = settings.mariaDbTuning?.[versionId] || ''
return { content: tuning }
})
ipcMain.handle('mariadb:config:save', async (_event, { versionId, content }) => {
const settings = configService.getSettings()
const mariaDbTuning = settings.mariaDbTuning || {}
mariaDbTuning[versionId] = content
configService.updateSettings({ ...settings, mariaDbTuning })
const serviceName = `mariadb:${versionId.replace('mariadb-', '')}`
if (processManager.isServiceRunning(serviceName)) {
await processManager.stopService(serviceName)
}
return { success: true, message: 'Tuning configuration saved.' }
})
ipcMain.handle('php:versions:all', async () => {
return phpManagerService.getVersions()
})
ipcMain.handle('php:config:get', async (_event, version: string) => {
const phpIniPath = path.join(configService.getBasePath(), 'generated_configs', `php_${version}.ini`)
if (fs.existsSync(phpIniPath)) {
return { content: fs.readFileSync(phpIniPath, 'utf8') }
}
return { content: '' }
})
ipcMain.handle('php:config:save', async (_event, { version, content }) => {
const phpIniPath = path.join(configService.getBasePath(), 'generated_configs', `php_${version}.ini`)
// Add manual edit marker to prevent overwriting
let finalContent = content
if (!finalContent.includes('; MANUAL_EDIT')) {
finalContent = `; MANUAL_EDIT\n${finalContent}`
}
fs.writeFileSync(phpIniPath, finalContent, 'utf8')
// Restart PHP if running
const serviceName = `php:${version}`
if (processManager.isServiceRunning(serviceName)) {
await processManager.stopService(serviceName)
// It will be started again later or manually
}
return { success: true }
})
// Apache Versions
ipcMain.handle('apache:versions', async () => {
+2 -1
View File
@@ -17,7 +17,8 @@ export class ConfigService {
language: 'tr',
cliEnvEnabled: true,
defaultPhpVersion: '',
defaultMariaDbVersion: ''
defaultMariaDbVersion: '',
mariaDbTuning: {} as Record<string, string>
}
constructor() {
+10 -1
View File
@@ -78,7 +78,8 @@ import {
Launch as LaunchIcon,
Handyman as ToolsIcon,
Restore as ResetIcon,
Search as SearchIcon
Search as SearchIcon,
Tune as TuningIcon
} from '@mui/icons-material'
import Swal from 'sweetalert2'
import { useTranslation } from 'react-i18next'
@@ -88,6 +89,7 @@ import MariaDbExportWizard from './components/MariaDbExportWizard'
import NginxCodeEditor from './components/NginxCodeEditor'
import ApacheCodeEditor from './components/ApacheCodeEditor'
import PhpExtensionManager from './components/PhpExtensionManager'
import ServerTuning from './components/ServerTuning'
declare global {
interface Window {
@@ -1105,6 +1107,12 @@ function App(): JSX.Element {
<ListItemText primary={t('projects.title')} />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton selected={activeTab === 'tuning'} onClick={() => setActiveTab('tuning')}>
<ListItemIcon><TuningIcon color={activeTab === 'tuning' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('tuning.title')} />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton selected={activeTab === 'tools'} onClick={() => setActiveTab('tools')}>
<ListItemIcon><ToolsIcon color={activeTab === 'tools' ? 'primary' : 'inherit'} /></ListItemIcon>
@@ -1133,6 +1141,7 @@ function App(): JSX.Element {
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
<Toolbar />
<Container maxWidth={false} sx={{ py: 2 }}>
{activeTab === 'tuning' && <ServerTuning />}
{activeTab === 'settings' && (
<Box>
@@ -0,0 +1,426 @@
import { useState, useEffect } from 'react'
import {
Box,
Typography,
Grid,
Paper,
Button,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Slider,
TextField,
Stack,
IconButton,
Tooltip,
Alert,
CircularProgress,
Card,
CardContent,
CardActionArea,
Chip
} from '@mui/material'
import {
Tune as TuneIcon,
Close as CloseIcon,
Save as SaveIcon,
Code as PhpIcon,
Storage as DbIcon,
InfoOutlined as InfoIcon
} from '@mui/icons-material'
import { useTranslation } from 'react-i18next'
interface TuningParam {
key: string
label: string
description: string
min: number
max: number
step: number
unit: string
type: 'php' | 'mariadb'
transform?: (val: number) => string
parse?: (val: string) => number
}
const PHP_PARAMS: TuningParam[] = [
{
key: 'memory_limit',
label: 'memory_limit',
description: 'tuning.parameters.memory_limit_desc',
min: 64,
max: 4096,
step: 64,
unit: 'M',
type: 'php',
transform: (v) => `${v}M`,
parse: (v) => parseInt(v.replace('M', '')) || 128
},
{
key: 'max_execution_time',
label: 'max_execution_time',
description: 'tuning.parameters.max_execution_time_desc',
min: 30,
max: 3600,
step: 30,
unit: 's',
type: 'php',
transform: (v) => `${v}`,
parse: (v) => parseInt(v) || 30
},
{
key: 'post_max_size',
label: 'post_max_size',
description: 'tuning.parameters.post_max_size_desc',
min: 8,
max: 2048,
step: 8,
unit: 'M',
type: 'php',
transform: (v) => `${v}M`,
parse: (v) => parseInt(v.replace('M', '')) || 8
},
{
key: 'upload_max_filesize',
label: 'upload_max_filesize',
description: 'tuning.parameters.upload_max_filesize_desc',
min: 2,
max: 2048,
step: 2,
unit: 'M',
type: 'php',
transform: (v) => `${v}M`,
parse: (v) => parseInt(v.replace('M', '')) || 2
},
{
key: 'max_input_vars',
label: 'max_input_vars',
description: 'tuning.parameters.max_input_vars_desc',
min: 1000,
max: 100000,
step: 1000,
unit: '',
type: 'php',
transform: (v) => `${v}`,
parse: (v) => parseInt(v) || 1000
}
]
const MARIADB_PARAMS: TuningParam[] = [
{
key: 'max_connections',
label: 'max_connections',
description: 'tuning.parameters.max_connections_desc',
min: 10,
max: 5000,
step: 10,
unit: '',
type: 'mariadb',
transform: (v) => `${v}`,
parse: (v) => parseInt(v) || 151
},
{
key: 'innodb_buffer_pool_size',
label: 'innodb_buffer_pool_size',
description: 'tuning.parameters.innodb_buffer_pool_size_desc',
min: 128,
max: 16384,
step: 128,
unit: 'M',
type: 'mariadb',
transform: (v) => `${v}M`,
parse: (v) => {
if (v.endsWith('G')) return parseInt(v) * 1024
return parseInt(v) || 128
}
},
{
key: 'key_buffer_size',
label: 'key_buffer_size',
description: 'tuning.parameters.key_buffer_size_desc',
min: 8,
max: 1024,
step: 8,
unit: 'M',
type: 'mariadb',
transform: (v) => `${v}M`,
parse: (v) => parseInt(v) || 16
},
{
key: 'max_allowed_packet',
label: 'max_allowed_packet',
description: 'tuning.parameters.max_allowed_packet_desc',
min: 1,
max: 1024,
step: 1,
unit: 'M',
type: 'mariadb',
transform: (v) => `${v}M`,
parse: (v) => {
if (v.endsWith('G')) return parseInt(v) * 1024
return parseInt(v) || 16
}
}
]
export default function ServerTuning() {
const { t } = useTranslation()
const [phpVersions, setPhpVersions] = useState<any[]>([])
const [mariaDbVersions, setMariaDbVersions] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const [selectedVersion, setSelectedVersion] = useState<any>(null)
const [configContent, setConfigContent] = useState('')
const [isModalOpen, setIsModalOpen] = useState(false)
const [saving, setSaving] = useState(false)
const [tuningValues, setTuningValues] = useState<Record<string, number>>({})
const fetchVersions = async () => {
setLoading(true)
if (window.api) {
const phps = await window.api.getPhpVersions()
const dbs = await window.api.getMariaDbVersions()
setPhpVersions(phps.filter((v: any) => v.status === 'installed'))
setMariaDbVersions(dbs.filter((v: any) => v.status === 'installed'))
}
setLoading(false)
}
useEffect(() => {
fetchVersions()
}, [])
const handleOpenTuning = async (version: any, type: 'php' | 'mariadb') => {
setSelectedVersion({ ...version, type })
let content = ''
if (type === 'php') {
const res = await window.api.invoke('php:config:get', version.version)
content = res.content
} else {
const res = await window.api.invoke('mariadb:config:get', version.id)
content = res.content
}
setConfigContent(content)
// Parse current values
const params = type === 'php' ? PHP_PARAMS : MARIADB_PARAMS
const values: Record<string, number> = {}
params.forEach(p => {
const regex = type === 'php'
? new RegExp(`^\\s*${p.key}\\s*=\\s*(.+)$`, 'm')
: new RegExp(`^\\s*${p.key}\\s*=\\s*(.+)$`, 'm')
const match = content.match(regex)
if (match && p.parse) {
values[p.key] = p.parse(match[1].trim())
} else {
values[p.key] = p.min // Default to min if not found
}
})
setTuningValues(values)
setIsModalOpen(true)
}
const handleSave = async () => {
setSaving(true)
let updatedContent = configContent
const params = selectedVersion.type === 'php' ? PHP_PARAMS : MARIADB_PARAMS
params.forEach(p => {
const newValue = p.transform ? p.transform(tuningValues[p.key]) : tuningValues[p.key].toString()
const regex = new RegExp(`^(\\s*${p.key}\\s*=\\s*)(.+)$`, 'm')
if (updatedContent.match(regex)) {
updatedContent = updatedContent.replace(regex, `$1${newValue}`)
} else {
updatedContent += `\n${p.key}=${newValue}`
}
})
if (selectedVersion.type === 'php') {
await window.api.invoke('php:config:save', { version: selectedVersion.version, content: updatedContent })
} else {
await window.api.invoke('mariadb:config:save', { versionId: selectedVersion.id, content: updatedContent })
}
setSaving(false)
setIsModalOpen(false)
}
if (loading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 10 }}>
<CircularProgress />
</Box>
)
}
return (
<Box>
<Box sx={{ mb: 4 }}>
<Typography variant="h4" sx={{ fontWeight: 800, mb: 1, color: 'primary.light' }}>
{t('tuning.title')}
</Typography>
<Typography variant="body1" sx={{ color: 'rgba(255,255,255,0.6)' }}>
{t('tuning.desc')}
</Typography>
</Box>
<Grid container spacing={4}>
{/* PHP Section */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, height: '100%', bgcolor: 'rgba(255,255,255,0.02)', borderRadius: 4, border: '1px solid rgba(255,255,255,0.05)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3, gap: 2 }}>
<Box sx={{ p: 1.5, borderRadius: 2, bgcolor: 'primary.main', display: 'flex' }}>
<PhpIcon sx={{ color: 'white' }} />
</Box>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>PHP Tuning</Typography>
<Chip label={phpVersions.length} size="small" variant="outlined" />
</Box>
<Stack spacing={2}>
{phpVersions.map(v => (
<Card key={v.id} sx={{ bgcolor: 'rgba(255,255,255,0.03)', backgroundImage: 'none', border: '1px solid rgba(255,255,255,0.05)' }}>
<CardActionArea onClick={() => handleOpenTuning(v, 'php')}>
<CardContent sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>PHP {v.version}</Typography>
<Typography variant="caption" color="text.secondary">php.ini optimization</Typography>
</Box>
<Button variant="contained" size="small" startIcon={<TuneIcon />}>
{t('tuning.tune_btn')}
</Button>
</CardContent>
</CardActionArea>
</Card>
))}
</Stack>
</Paper>
</Grid>
{/* MariaDB Section */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, height: '100%', bgcolor: 'rgba(255,255,255,0.02)', borderRadius: 4, border: '1px solid rgba(255,255,255,0.05)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3, gap: 2 }}>
<Box sx={{ p: 1.5, borderRadius: 2, bgcolor: 'secondary.main', display: 'flex' }}>
<DbIcon sx={{ color: 'white' }} />
</Box>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>MariaDB Tuning</Typography>
<Chip label={mariaDbVersions.length} size="small" variant="outlined" />
</Box>
<Stack spacing={2}>
{mariaDbVersions.map(v => (
<Card key={v.id} sx={{ bgcolor: 'rgba(255,255,255,0.03)', backgroundImage: 'none', border: '1px solid rgba(255,255,255,0.05)' }}>
<CardActionArea onClick={() => handleOpenTuning(v, 'mariadb')}>
<CardContent sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>MariaDB {v.version}</Typography>
<Typography variant="caption" color="text.secondary">Performance variables</Typography>
</Box>
<Button variant="contained" color="secondary" size="small" startIcon={<TuneIcon />}>
{t('tuning.tune_btn')}
</Button>
</CardContent>
</CardActionArea>
</Card>
))}
</Stack>
</Paper>
</Grid>
</Grid>
{/* Tuning Modal */}
<Dialog
open={isModalOpen}
onClose={() => !saving && setIsModalOpen(false)}
maxWidth="sm"
fullWidth
PaperProps={{
sx: {
bgcolor: '#1a1d21',
backgroundImage: 'none',
color: '#fff',
borderRadius: 4,
border: '1px solid rgba(255,255,255,0.1)'
}
}}
>
<DialogTitle sx={{ borderBottom: '1px solid rgba(255,255,255,0.1)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<TuneIcon color={selectedVersion?.type === 'php' ? 'primary' : 'secondary'} />
<Typography variant="h6" sx={{ fontWeight: 800 }}>
{selectedVersion?.type === 'php' ? `PHP ${selectedVersion?.version}` : `MariaDB ${selectedVersion?.version}`} Tuning
</Typography>
</Box>
<IconButton onClick={() => setIsModalOpen(false)} sx={{ color: 'rgba(255,255,255,0.5)' }}>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent>
<Box sx={{ pt: 2 }}>
<Alert severity="info" sx={{ mb: 4, bgcolor: 'rgba(2, 136, 209, 0.1)', color: 'info.light', border: '1px solid rgba(2, 136, 209, 0.2)' }}>
{t('tuning.restart_notice')}
</Alert>
<Stack spacing={5}>
{(selectedVersion?.type === 'php' ? PHP_PARAMS : MARIADB_PARAMS).map(p => (
<Box key={p.key}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1, alignItems: 'center' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold' }}>{p.label}</Typography>
<Tooltip title={t(p.description)}>
<InfoIcon sx={{ fontSize: 16, color: 'rgba(255,255,255,0.3)', cursor: 'help' }} />
</Tooltip>
</Box>
<TextField
size="small"
value={tuningValues[p.key]}
onChange={(e) => setTuningValues({ ...tuningValues, [p.key]: parseInt(e.target.value) || p.min })}
InputProps={{
endAdornment: <Typography variant="caption" sx={{ ml: 1, opacity: 0.5 }}>{p.unit}</Typography>
} as any}
sx={{ width: 120, '& .MuiOutlinedInput-root': { height: 32 } }}
/>
</Box>
<Slider
value={tuningValues[p.key]}
min={p.min}
max={p.max}
step={p.step}
onChange={(_e, v) => setTuningValues({ ...tuningValues, [p.key]: v as number })}
sx={{
color: selectedVersion?.type === 'php' ? 'primary.main' : 'secondary.main',
'& .MuiSlider-thumb': {
width: 14,
height: 14,
}
}}
/>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', mt: -1, display: 'block' }}>
{t(p.description)}
</Typography>
</Box>
))}
</Stack>
</Box>
</DialogContent>
<DialogActions sx={{ p: 3, borderTop: '1px solid rgba(255,255,255,0.1)' }}>
<Button onClick={() => setIsModalOpen(false)} disabled={saving}>
{t('common.cancel')}
</Button>
<Button
variant="contained"
color={selectedVersion?.type === 'php' ? 'primary' : 'secondary'}
onClick={handleSave}
startIcon={saving ? <CircularProgress size={20} color="inherit" /> : <SaveIcon />}
disabled={saving}
sx={{ px: 4, borderRadius: 2 }}
>
{saving ? t('tuning.saving') : t('common.save')}
</Button>
</DialogActions>
</Dialog>
</Box>
)
}
+29
View File
@@ -276,5 +276,34 @@
"info": "Info",
"progress": "Progress"
}
},
"tuning": {
"title": "Server Tuning",
"desc": "Optimize performance of PHP and MariaDB servers with key resource limits.",
"php_total": "PHP Instances",
"mariadb_total": "MariaDB Instances",
"tune_btn": "TUNE",
"saving": "Applying changes...",
"restart_notice": "Some changes require a service restart to take effect.",
"parameters": {
"memory_limit": "Memory Limit",
"memory_limit_desc": "Maximum amount of memory a script may consume.",
"max_execution_time": "Max Execution Time",
"max_execution_time_desc": "Maximum time in seconds a script is allowed to run.",
"post_max_size": "Post Max Size",
"post_max_size_desc": "Maximum size of POST data that PHP will accept.",
"upload_max_filesize": "Upload Max Filesize",
"upload_max_filesize_desc": "Maximum size of an uploaded file.",
"max_input_vars": "Max Input Vars",
"max_input_vars_desc": "How many input variables may be accepted.",
"max_connections": "Max Connections",
"max_connections_desc": "Maximum number of simultaneous client connections.",
"innodb_buffer_pool_size": "InnoDB Buffer Pool",
"innodb_buffer_pool_size_desc": "Crucial for performance. Holds indexes and data from memory.",
"key_buffer_size": "Key Buffer Size",
"key_buffer_size_desc": "Index blocks for MyISAM tables (shared by all threads).",
"max_allowed_packet": "Max Allowed Packet",
"max_allowed_packet_desc": "Maximum size of one packet or any generated/intermediate string."
}
}
}
+29
View File
@@ -271,5 +271,34 @@
"info": "Bilgi",
"progress": "İlerleme"
}
},
"tuning": {
"title": "Sunucu Optimizasyonu",
"desc": "PHP ve MariaDB sunucularının performansını kritik kaynak sınırlarıyla optimize edin.",
"php_total": "PHP Örneği",
"mariadb_total": "MariaDB Örneği",
"tune_btn": "AYARLA",
"saving": "Değişiklikler uygulanıyor...",
"restart_notice": "Bazı değişikliklerin geçerli olması için servislerin yeniden başlatılması gerekir.",
"parameters": {
"memory_limit": "Bellek Limiti",
"memory_limit_desc": "Bir betiğin tüketebileceği maksimum bellek miktarı.",
"max_execution_time": "Maksimum Çalışma Süresi",
"max_execution_time_desc": "Bir betiğin çalışmasına izin verilen maksimum süre (saniye).",
"post_max_size": "Post Maksimum Boyutu",
"post_max_size_desc": "PHP'nin kabul edeceği maksimum POST verisi boyutu.",
"upload_max_filesize": "Dosya Yükleme Limiti",
"upload_max_filesize_desc": "Yüklenebilecek maksimum dosya boyutu.",
"max_input_vars": "Maksimum Girdi Değişkeni",
"max_input_vars_desc": "Kaç tane girdi değişkeninin (GET/POST/Cookie) kabul edilebileceği.",
"max_connections": "Maksimum Bağlantı",
"max_connections_desc": "Eşzamanlı maksimum istemci bağlantısı sayısı.",
"innodb_buffer_pool_size": "InnoDB Tampon Havuzu",
"innodb_buffer_pool_size_desc": "Performans için kritiktir. İndeksleri ve verileri bellekte tutar.",
"key_buffer_size": "Anahtar Tampon Boyutu",
"key_buffer_size_desc": "MyISAM tabloları için indeks blokları (tüm iş parçacıkları tarafından paylaşılır).",
"max_allowed_packet": "Maksimum Paket Boyutu",
"max_allowed_packet_desc": "Bir paketin veya oluşturulan herhangi bir dizenin maksimum boyutu."
}
}
}