427 lines
17 KiB
TypeScript
427 lines
17 KiB
TypeScript
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>
|
|
)
|
|
}
|