Files
multiphp/src/renderer/src/components/ServerTuning.tsx
T

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>
)
}