feat: implement code editors for PHP, Nginx, and Apache, and add IPC handlers for service configuration management

This commit is contained in:
Ümit Tunç
2026-04-03 08:49:00 +03:00
parent 1086a2c22a
commit 499fdef661
9 changed files with 479 additions and 242 deletions
+157
View File
@@ -1,4 +1,161 @@
[ [
{
"service": "php:5.6.40",
"action": "start",
"status": "success",
"id": "lm8lauin4",
"timestamp": 1775194498184
},
{
"service": "php:7.4.33",
"action": "start",
"status": "success",
"id": "fv2jk93kp",
"timestamp": 1775194498166
},
{
"service": "php:8.2.30",
"action": "start",
"status": "success",
"id": "ajlbnlqlt",
"timestamp": 1775194498146
},
{
"service": "php:8.5.4",
"action": "start",
"status": "success",
"id": "7ky21n7ej",
"timestamp": 1775194498128
},
{
"service": "mariadb:11.4.2",
"action": "start",
"status": "success",
"id": "ivnjwq509",
"timestamp": 1775194497945
},
{
"service": "mariadb:10.11.8",
"action": "start",
"status": "success",
"id": "xkegjk3qq",
"timestamp": 1775194497925
},
{
"service": "apache",
"action": "start",
"status": "success",
"id": "cs9w16ouo",
"timestamp": 1775194497146
},
{
"service": "nginx",
"action": "start",
"status": "success",
"id": "9njsd2gqb",
"timestamp": 1775194497081
},
{
"service": "php:5.6.40",
"action": "start",
"status": "success",
"id": "5je86oerx",
"timestamp": 1775194323027
},
{
"service": "php:7.4.33",
"action": "start",
"status": "success",
"id": "2mhrdvjef",
"timestamp": 1775194323008
},
{
"service": "php:8.2.30",
"action": "start",
"status": "success",
"id": "o32ayujie",
"timestamp": 1775194322987
},
{
"service": "php:8.5.4",
"action": "start",
"status": "success",
"id": "ypehqtkno",
"timestamp": 1775194322969
},
{
"service": "mariadb:10.11.8",
"action": "start",
"status": "success",
"id": "lo6hi46hi",
"timestamp": 1775194322796
},
{
"service": "mariadb:11.4.2",
"action": "start",
"status": "success",
"id": "583vl27oq",
"timestamp": 1775194322774
},
{
"service": "apache",
"action": "start",
"status": "success",
"id": "sg7xaxnph",
"timestamp": 1775194321996
},
{
"service": "nginx",
"action": "start",
"status": "success",
"id": "y3kdqv4im",
"timestamp": 1775194321881
},
{
"service": "nginx",
"action": "start",
"status": "success",
"id": "j3che13xf",
"timestamp": 1775194289720
},
{
"service": "nginx",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "kky7b35zb",
"timestamp": 1775194287876
},
{
"service": "nginx",
"action": "start",
"status": "success",
"id": "gge4posew",
"timestamp": 1775194278036
},
{
"service": "nginx",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "ttyn4sulo",
"timestamp": 1775194276079
},
{
"service": "nginx",
"action": "start",
"status": "success",
"id": "cq91o7h9b",
"timestamp": 1775194257169
},
{
"service": "nginx",
"action": "exit",
"status": "failure",
"details": "Exit code: 1",
"id": "yyp3j341e",
"timestamp": 1775194255191
},
{ {
"service": "php:7.4.33", "service": "php:7.4.33",
"action": "start", "action": "start",
+30
View File
@@ -1068,6 +1068,36 @@ export function registerIpcHandlers(): void {
return { success: true, settings } return { success: true, settings }
}) })
ipcMain.handle('config:service-file:get', async (_event, serviceId: string) => {
try {
if (serviceId && serviceId.startsWith('php:')) {
const version = serviceId.split(':')[1]
const filePath = path.join(configService.getBasePath(), 'bin', `php-${version}`, 'php.ini')
if (fs.existsSync(filePath)) {
return { success: true, content: fs.readFileSync(filePath, 'utf8'), path: filePath }
}
return { success: false, message: 'php.ini dosyası bulunamadı' }
}
return { success: false, message: 'Bu servis türü için doğrudan dosya düzenleme desteklenmiyor' }
} catch (error: any) {
return { success: false, message: error.message }
}
})
ipcMain.handle('config:service-file:save', async (_event, { serviceId, content }) => {
try {
if (serviceId && serviceId.startsWith('php:')) {
const version = serviceId.split(':')[1]
const filePath = path.join(configService.getBasePath(), 'bin', `php-${version}`, 'php.ini')
fs.writeFileSync(filePath, content, 'utf8')
return { success: true, message: 'Ayarlar kaydedildi, bir sonraki başlatmada geçerli olacak.' }
}
return { success: false, message: 'Servis türü desteklenmiyor' }
} catch (error: any) {
return { success: false, message: error.message }
}
})
ipcMain.handle('services:clear-events', async () => { ipcMain.handle('services:clear-events', async () => {
try { try {
serviceEventLogger.clear() serviceEventLogger.clear()
+11 -1
View File
@@ -190,10 +190,20 @@ export class PhpManagerService {
if (fs.existsSync(iniPath)) { if (fs.existsSync(iniPath)) {
const content = fs.readFileSync(iniPath, 'utf8') const content = fs.readFileSync(iniPath, 'utf8')
const lines = content.split(/\r?\n/) const lines = content.split(/\r?\n/)
const essentialExts = ['openssl', 'curl', 'mbstring', 'fileinfo', 'zip', 'gd', 'pdo_mysql', 'mysqli']
let changed = false
for (const ext of extensions) { for (const ext of extensions) {
// Check for extension=php_NAME.dll or extension=NAME (newer PHP) // Check if already enabled
const pattern = new RegExp(`^\\s*extension\\s*=\\s*(?:php_)?${ext.name}(?:\\.dll)?\\s*$`, 'i') const pattern = new RegExp(`^\\s*extension\\s*=\\s*(?:php_)?${ext.name}(?:\\.dll)?\\s*$`, 'i')
ext.enabled = lines.some(line => pattern.test(line.trim())) ext.enabled = lines.some(line => pattern.test(line.trim()))
// Auto-enable essential ones if found but not enabled
if (!ext.enabled && essentialExts.includes(ext.name.toLowerCase())) {
await this.toggleExtension(version, ext.name, true)
ext.enabled = true
changed = true
}
} }
} }
@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import Editor from 'react-simple-code-editor' import SearchableCodeEditor from './SearchableCodeEditor'
import Prism from 'prismjs' import Prism from 'prismjs'
import 'prismjs/components/prism-apacheconf' import 'prismjs/components/prism-apacheconf'
import 'prismjs/themes/prism-tomorrow.css' import 'prismjs/themes/prism-tomorrow.css'
@@ -20,7 +20,7 @@ const ApacheCodeEditor: React.FC<ApacheCodeEditorProps> = ({ value, onChange })
fontSize: '14px', fontSize: '14px',
minHeight: '300px' minHeight: '300px'
}}> }}>
<Editor <SearchableCodeEditor
value={value} value={value}
onValueChange={code => onChange(code)} onValueChange={code => onChange(code)}
highlight={code => Prism.highlight(code, Prism.languages.apacheconf, 'apacheconf')} highlight={code => Prism.highlight(code, Prism.languages.apacheconf, 'apacheconf')}
@@ -32,7 +32,7 @@ const ApacheCodeEditor: React.FC<ApacheCodeEditorProps> = ({ value, onChange })
outline: 'none' outline: 'none'
}} }}
textareaClassName="apache-editor-textarea" textareaClassName="apache-editor-textarea"
preClassName="apache-editor-pre" languageLabel="Apache"
/> />
<style>{` <style>{`
.apache-editor-textarea { .apache-editor-textarea {
@@ -40,6 +40,7 @@ import 'prismjs/components/prism-nginx'
import 'prismjs/components/prism-apacheconf' import 'prismjs/components/prism-apacheconf'
import 'prismjs/components/prism-ini' import 'prismjs/components/prism-ini'
import Editor from 'react-simple-code-editor' import Editor from 'react-simple-code-editor'
import SearchableCodeEditor from '../SearchableCodeEditor'
import Prism from 'prismjs' import Prism from 'prismjs'
import Swal from 'sweetalert2' import Swal from 'sweetalert2'
@@ -254,19 +255,19 @@ export default function DashboardDetails({
<Box sx={{ mt: 2 }}> <Box sx={{ mt: 2 }}>
<Box sx={{ mb: 1.5 }}> <Box sx={{ mb: 1.5 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', fontWeight: 800 }}>CPU USAGE</Typography> <Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', fontWeight: 600, letterSpacing: 0.5 }}>İşlemci Kullanımı</Typography>
<Typography variant="caption" sx={{ color: 'primary.main', fontWeight: 800 }}>{cpu || 0}%</Typography> <Typography variant="caption" sx={{ color: 'primary.main', fontWeight: 800 }}>{cpu || 0}%</Typography>
</Box> </Box>
<LinearProgress variant="determinate" value={cpu || 0} sx={{ height: 8, borderRadius: 4, bgcolor: 'rgba(255,255,255,0.05)', '.MuiLinearProgress-bar': { borderRadius: 4, background: 'linear-gradient(90deg, #2196F3, #21CBF3)' } }} /> <LinearProgress variant="determinate" value={cpu || 0} sx={{ height: 6, borderRadius: 3, bgcolor: 'rgba(255,255,255,0.05)', '.MuiLinearProgress-bar': { borderRadius: 3, background: 'linear-gradient(90deg, #2196F3, #21CBF3)' } }} />
</Box> </Box>
<Box> <Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', fontWeight: 800 }}>RAM USAGE</Typography> <Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', fontWeight: 600, letterSpacing: 0.5 }}>Bellek Kullanımı</Typography>
<Typography variant="caption" sx={{ color: 'secondary.main', fontWeight: 800 }}> <Typography variant="caption" sx={{ color: 'secondary.main', fontWeight: 800 }}>
{memory ? `${Math.round(memory / (1024 * 1024))} MB` : '0 MB'} {memory ? `${Math.round(memory / (1024 * 1024))} MB` : '0 MB'}
</Typography> </Typography>
</Box> </Box>
<LinearProgress variant="determinate" value={Math.min(((memory || 0) / (1024 * 1024) / 1024) * 100, 100)} sx={{ height: 8, borderRadius: 4, bgcolor: 'rgba(255,255,255,0.05)', '.MuiLinearProgress-bar': { borderRadius: 4, background: 'linear-gradient(90deg, #9C27B0, #E91E63)' } }} /> <LinearProgress variant="determinate" value={Math.min(((memory || 0) / (1024 * 1024) / 1024) * 100, 100)} sx={{ height: 6, borderRadius: 3, bgcolor: 'rgba(255,255,255,0.05)', '.MuiLinearProgress-bar': { borderRadius: 3, background: 'linear-gradient(90deg, #9C27B0, #E91E63)' } }} />
</Box> </Box>
</Box> </Box>
); );
@@ -283,13 +284,15 @@ export default function DashboardDetails({
{serviceType === 'mariadb' && <DbIcon color="primary" sx={{ fontSize: 40 }} />} {serviceType === 'mariadb' && <DbIcon color="primary" sx={{ fontSize: 40 }} />}
</Paper> </Paper>
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1 }}>
<Typography variant="h4" sx={{ fontWeight: 800, letterSpacing: '-1px' }}>{serviceId.toUpperCase()}</Typography> <Typography variant="h5" sx={{ fontWeight: 800, letterSpacing: '-0.5px', color: 'primary.main' }}>
<Typography variant="subtitle2" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> {serviceId.replace(':', ' ').replace('php', 'PHP').replace('mariadb', 'MariaDB').replace('nginx', 'Nginx').replace('apache', 'Apache')}
<Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: isRunning ? 'success.main' : 'error.main', boxShadow: isRunning ? '0 0 10px rgba(76, 175, 80, 0.5)' : 'none' }} /> </Typography>
{isStarting && <Box component="span" sx={{ color: 'warning.light', fontWeight: 700 }}>{t('common.starting').toUpperCase()}</Box>} <Typography variant="caption" sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 0.5 }}>
{isStopping && <Box component="span" sx={{ color: 'warning.light', fontWeight: 700 }}>{t('common.stopping').toUpperCase()}</Box>} <Box sx={{ width: 6, height: 6, borderRadius: '50%', bgcolor: isRunning ? 'success.main' : 'error.main', boxShadow: isRunning ? '0 0 10px rgba(76, 175, 80, 0.5)' : 'none' }} />
{isRunning && <Box component="span" sx={{ color: 'success.main', fontWeight: 700 }}>{t('common.running').toUpperCase()}</Box>} {isStarting && <Box component="span" sx={{ color: 'warning.light', fontWeight: 700, letterSpacing: 0.5 }}>{t('common.starting')}</Box>}
{!isRunning && !isStarting && !isStopping && <Box component="span" sx={{ opacity: 0.5, fontWeight: 700 }}>{t('common.stopped').toUpperCase()}</Box>} {isStopping && <Box component="span" sx={{ color: 'warning.light', fontWeight: 700, letterSpacing: 0.5 }}>{t('common.stopping')}</Box>}
{isRunning && <Box component="span" sx={{ color: 'success.main', fontWeight: 700, letterSpacing: 0.5 }}>{t('common.running')}</Box>}
{!isRunning && !isStarting && !isStopping && <Box component="span" sx={{ opacity: 0.5, fontWeight: 700, letterSpacing: 0.5 }}>{t('common.stopped')}</Box>}
</Typography> </Typography>
</Box> </Box>
<Stack direction="row" spacing={2}> <Stack direction="row" spacing={2}>
@@ -314,15 +317,17 @@ export default function DashboardDetails({
</Box> </Box>
<Box sx={{ borderBottom: 1, borderColor: 'rgba(255,255,255,0.05)', px: 4, bgcolor: 'rgba(255,255,255,0.01)' }}> <Box sx={{ borderBottom: 1, borderColor: 'rgba(255,255,255,0.05)', px: 4, bgcolor: 'rgba(255,255,255,0.01)' }}>
<Tabs value={activeTab} onChange={(_e, v) => setActiveTab(v)} textColor="primary" indicatorColor="primary"> <Tabs value={activeTab} onChange={(_e, v) => setActiveTab(v)} textColor="primary" indicatorColor="primary" sx={{
<Tab label={t('dashboard.overview')} value="overview" sx={{ fontWeight: 700, py: 2.5 }} /> '.MuiTab-root': { textTransform: 'none', minHeight: 64, fontWeight: 700, fontSize: '0.95rem' }
<Tab label={isPhp ? 'php.ini (Şablon)' : t('dashboard.service_settings')} value="settings_template" sx={{ fontWeight: 700, py: 2.5 }} /> }}>
{isPhp && <Tab label="php.ini" value="settings_live" sx={{ fontWeight: 700, py: 2.5 }} />} <Tab label={t('dashboard.overview')} value="overview" />
<Tab label={isPhp ? 'php.ini (Şablon)' : t('dashboard.service_settings')} value="settings_template" />
{isPhp && <Tab label="php.ini" value="settings_live" />}
{(serviceId === 'nginx' || serviceId === 'apache' || isPhp) && ( {(serviceId === 'nginx' || serviceId === 'apache' || isPhp) && (
<Tab label={t('dashboard.project_settings')} value="projects_template" sx={{ fontWeight: 700, py: 2.5 }} /> <Tab label={t('dashboard.project_settings')} value="projects_template" />
)} )}
<Tab label={t('diagnostics.logs')} value="logs" sx={{ fontWeight: 700, py: 2.5 }} /> <Tab label={t('diagnostics.logs')} value="logs" />
<Tab label={t('projects.title')} value="projects" sx={{ fontWeight: 700, py: 2.5 }} /> <Tab label={t('projects.title')} value="projects" />
</Tabs> </Tabs>
</Box> </Box>
@@ -331,7 +336,7 @@ export default function DashboardDetails({
<Grid container spacing={4}> <Grid container spacing={4}>
<Grid item xs={12} md={6}> <Grid item xs={12} md={6}>
<Typography variant="h6" sx={{ fontWeight: 800, mb: 3, color: 'primary.main', display: 'flex', alignItems: 'center', gap: 1.5 }}> <Typography variant="h6" sx={{ fontWeight: 800, mb: 3, color: 'primary.main', display: 'flex', alignItems: 'center', gap: 1.5 }}>
<StatusIcon /> {t('dashboard.system_stats')} <StatusIcon /> {t('dashboard.system_stats', 'Sistem İstatistikleri')}
</Typography> </Typography>
<Paper sx={{ p: 4, bgcolor: 'rgba(255,255,255,0.02)', borderRadius: 4, border: '1px solid rgba(255,255,255,0.05)' }}> <Paper sx={{ p: 4, bgcolor: 'rgba(255,255,255,0.02)', borderRadius: 4, border: '1px solid rgba(255,255,255,0.05)' }}>
<ResourceMonitor cpu={currentStatus.cpu} memory={currentStatus.memory} /> <ResourceMonitor cpu={currentStatus.cpu} memory={currentStatus.memory} />
@@ -339,12 +344,12 @@ export default function DashboardDetails({
</Grid> </Grid>
<Grid item xs={12} md={6}> <Grid item xs={12} md={6}>
<Typography variant="h6" sx={{ fontWeight: 800, mb: 3, color: 'primary.main', display: 'flex', alignItems: 'center', gap: 1.5 }}> <Typography variant="h6" sx={{ fontWeight: 800, mb: 3, color: 'primary.main', display: 'flex', alignItems: 'center', gap: 1.5 }}>
<SettingsIcon /> {t('dashboard.service_info')} <SettingsIcon /> {t('dashboard.service_info', 'Servis Bilgileri')}
</Typography> </Typography>
<Paper sx={{ p: 4, bgcolor: 'rgba(255,255,255,0.02)', borderRadius: 4, border: '1px solid rgba(255,255,255,0.05)' }}> <Paper sx={{ p: 4, bgcolor: 'rgba(255,255,255,0.02)', borderRadius: 4, border: '1px solid rgba(255,255,255,0.05)' }}>
<Stack spacing={3}> <Stack spacing={3}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body1" sx={{ opacity: 0.5, fontWeight: 700 }}>{t('common.port').toUpperCase()}</Typography> <Typography variant="body1" sx={{ opacity: 0.5, fontWeight: 700 }}>{t('common.port', 'Port')}</Typography>
<Paper sx={{ px: 2, py: 0.5, borderRadius: 2, bgcolor: 'rgba(57, 167, 255, 0.1)', border: '1px solid rgba(57, 167, 255, 0.2)' }}> <Paper sx={{ px: 2, py: 0.5, borderRadius: 2, bgcolor: 'rgba(57, 167, 255, 0.1)', border: '1px solid rgba(57, 167, 255, 0.2)' }}>
<Typography variant="body1" sx={{ fontWeight: 800, color: 'primary.main', fontFamily: 'monospace' }}> <Typography variant="body1" sx={{ fontWeight: 800, color: 'primary.main', fontFamily: 'monospace' }}>
{getPort()} {getPort()}
@@ -353,9 +358,9 @@ export default function DashboardDetails({
</Box> </Box>
<Divider sx={{ opacity: 0.05 }} /> <Divider sx={{ opacity: 0.05 }} />
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body1" sx={{ opacity: 0.5, fontWeight: 700 }}>{t('common.status').toUpperCase()}</Typography> <Typography variant="body1" sx={{ opacity: 0.5, fontWeight: 700 }}>{t('common.status', 'Durum')}</Typography>
<Chip <Chip
label={(isRunning ? t('common.running') : t('common.stopped')).toUpperCase()} label={isRunning ? t('common.running', 'Çalışıyor') : t('common.stopped', 'Durduruldu')}
color={isRunning ? 'success' : 'error'} color={isRunning ? 'success' : 'error'}
sx={{ fontWeight: 900, borderRadius: 2 }} sx={{ fontWeight: 900, borderRadius: 2 }}
/> />
@@ -369,8 +374,8 @@ export default function DashboardDetails({
{(activeTab === 'settings_template' || activeTab === 'settings_live') && ( {(activeTab === 'settings_template' || activeTab === 'settings_live') && (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}> <Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="overline" sx={{ fontWeight: 900, color: 'primary.main', letterSpacing: 2 }}> <Typography variant="caption" sx={{ fontWeight: 700, color: 'primary.main', letterSpacing: 1 }}>
{activeTab === 'settings_live' ? 'php.ini (ACTIVE)' : getTemplateName().toUpperCase()} {activeTab === 'settings_live' ? 'php.ini (Aktif Konfigürasyon)' : getTemplateName()}
</Typography> </Typography>
<Button <Button
variant="contained" variant="contained"
@@ -388,12 +393,12 @@ export default function DashboardDetails({
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flexGrow: 1 }}><CircularProgress /></Box> <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flexGrow: 1 }}><CircularProgress /></Box>
) : ( ) : (
<Box sx={{ flexGrow: 1, overflow: 'auto' }}> <Box sx={{ flexGrow: 1, overflow: 'auto' }}>
<Editor <SearchableCodeEditor
value={activeTab === 'settings_live' ? liveConfigContent : configContent} value={activeTab === 'settings_live' ? liveConfigContent : configContent}
onValueChange={activeTab === 'settings_live' ? setLiveConfigContent : setConfigContent} onValueChange={activeTab === 'settings_live' ? setLiveConfigContent : setConfigContent}
highlight={code => Prism.highlight(code, getHighlightLanguage(activeTab === 'settings_live' ? 'php.ini' : getTemplateName()), getHighlightLabel(activeTab === 'settings_live' ? 'php.ini' : getTemplateName()))} highlight={code => Prism.highlight(code, getHighlightLanguage(activeTab === 'settings_live' ? 'php.ini' : getTemplateName()), getHighlightLabel(activeTab === 'settings_live' ? 'php.ini' : getTemplateName()))}
padding={25} padding={25}
style={{ fontFamily: '"Fira Code", monospace', fontSize: '15px', minHeight: '100%', outline: 'none' }} style={{ fontFamily: '"Fira Code", monospace', fontSize: '15px' }}
textareaClassName="editor-textarea" textareaClassName="editor-textarea"
/> />
</Box> </Box>
@@ -405,8 +410,8 @@ export default function DashboardDetails({
{activeTab === 'projects_template' && ( {activeTab === 'projects_template' && (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}> <Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="overline" sx={{ fontWeight: 900, color: 'primary.main', letterSpacing: 2 }}> <Typography variant="caption" sx={{ fontWeight: 700, color: 'primary.main', letterSpacing: 1 }}>
{getProjectTemplateNames()[0]?.toUpperCase()} {getProjectTemplateNames()[0]}
</Typography> </Typography>
<Button <Button
variant="contained" variant="contained"
@@ -423,12 +428,12 @@ export default function DashboardDetails({
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flexGrow: 1 }}><CircularProgress /></Box> <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flexGrow: 1 }}><CircularProgress /></Box>
) : ( ) : (
<Box sx={{ flexGrow: 1, overflow: 'auto' }}> <Box sx={{ flexGrow: 1, overflow: 'auto' }}>
<Editor <SearchableCodeEditor
value={projectConfigContent} value={projectConfigContent}
onValueChange={setProjectConfigContent} onValueChange={setProjectConfigContent}
highlight={code => Prism.highlight(code, getHighlightLanguage(getProjectTemplateNames()[0]), getHighlightLabel(getProjectTemplateNames()[0]))} highlight={code => Prism.highlight(code, getHighlightLanguage(getProjectTemplateNames()[0]), getHighlightLabel(getProjectTemplateNames()[0]))}
padding={25} padding={25}
style={{ fontFamily: '"Fira Code", monospace', fontSize: '15px', minHeight: '100%', outline: 'none' }} style={{ fontFamily: '"Fira Code", monospace', fontSize: '15px' }}
textareaClassName="editor-textarea" textareaClassName="editor-textarea"
/> />
</Box> </Box>
@@ -441,7 +446,7 @@ export default function DashboardDetails({
<Box sx={{ bgcolor: '#0b0e11', borderRadius: 4, p: 3, height: '65vh', overflowY: 'auto', border: '1px solid rgba(255,255,255,0.05)', boxShadow: 'inset 0 0 20px rgba(0,0,0,0.5)' }}> <Box sx={{ bgcolor: '#0b0e11', borderRadius: 4, p: 3, height: '65vh', overflowY: 'auto', border: '1px solid rgba(255,255,255,0.05)', boxShadow: 'inset 0 0 20px rgba(0,0,0,0.5)' }}>
<Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1.5, color: 'rgba(255,255,255,0.3)' }}> <Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1.5, color: 'rgba(255,255,255,0.3)' }}>
<LogIcon fontSize="small" /> <LogIcon fontSize="small" />
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 1 }}>REALTIME SERVICE LOGS</Typography> <Typography variant="caption" sx={{ fontWeight: 700, letterSpacing: 1 }}>Gerçek Zamanlı Servis Logları</Typography>
</Box> </Box>
{logs.length === 0 ? ( {logs.length === 0 ? (
<Box sx={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', opacity: 0.2 }}> <Box sx={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', opacity: 0.2 }}>
@@ -58,11 +58,11 @@ export default function DashboardSidebar({
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%', gap: 1.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', width: '100%', gap: 1.5 }}>
<Icon sx={{ color: selectedServiceId === id ? 'primary.main' : 'rgba(255,255,255,0.4)', fontSize: '1.2rem' }} /> <Icon sx={{ color: selectedServiceId === id ? 'primary.main' : 'rgba(255,255,255,0.4)', fontSize: '1.2rem' }} />
<Box sx={{ flexGrow: 1, minWidth: 0 }}> <Box sx={{ flexGrow: 1, minWidth: 0 }}>
<Typography variant="body2" noWrap sx={{ fontWeight: 600 }}> <Typography variant="body2" noWrap sx={{ fontWeight: 600, fontSize: '0.85rem' }}>
{label} {label}
</Typography> </Typography>
{isRunning && status[serviceKey] && ( {isRunning && status[serviceKey] && (
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', display: 'block', fontSize: '0.65rem', mt: -0.5 }}> <Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', display: 'block', fontSize: '0.6rem', mt: -0.5 }}>
Port: {status[serviceKey].port || '?'} | CPU: {Math.round(status[serviceKey].cpu || 0)}% | RAM: {Math.round((status[serviceKey].memory || 0) / (1024 * 1024))}MB Port: {status[serviceKey].port || '?'} | CPU: {Math.round(status[serviceKey].cpu || 0)}% | RAM: {Math.round((status[serviceKey].memory || 0) / (1024 * 1024))}MB
</Typography> </Typography>
)} )}
@@ -93,7 +93,7 @@ export default function DashboardSidebar({
return ( return (
<Box sx={{ <Box sx={{
width: 280, width: 320,
borderRight: '1px solid rgba(255, 255, 255, 0.05)', borderRight: '1px solid rgba(255, 255, 255, 0.05)',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@@ -102,24 +102,24 @@ export default function DashboardSidebar({
overflowY: 'auto' overflowY: 'auto'
}}> }}>
<Box sx={{ p: 2, borderBottom: '1px solid rgba(255, 255, 255, 0.05)' }}> <Box sx={{ p: 2, borderBottom: '1px solid rgba(255, 255, 255, 0.05)' }}>
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: '1.1rem' }}> <Typography variant="h6" sx={{ fontWeight: 700, fontSize: '1rem' }}>
{t('dashboard.services', 'Servisler')} {t('dashboard.services', 'Servisler')}
</Typography> </Typography>
</Box> </Box>
<Box sx={{ p: 1 }}> <Box sx={{ p: 1.5 }}>
<Typography variant="overline" sx={{ px: 1.5, color: 'rgba(255,255,255,0.3)', fontWeight: 800, letterSpacing: 1 }}> <Typography variant="caption" sx={{ px: 1, color: 'primary.main', fontWeight: 600, mb: 1, display: 'block', opacity: 0.8 }}>
WEB SERVERS Web Sunucuları
</Typography> </Typography>
<List dense disablePadding sx={{ mb: 2 }}> <List dense disablePadding sx={{ mb: 3 }}>
<ServiceItem id="nginx" label="Nginx" icon={WebIcon} serviceKey="nginx" /> <ServiceItem id="nginx" label="Nginx" icon={WebIcon} serviceKey="nginx" />
<ServiceItem id="apache" label="Apache" icon={ServerIcon} serviceKey="apache" /> <ServiceItem id="apache" label="Apache" icon={ServerIcon} serviceKey="apache" />
</List> </List>
<Typography variant="overline" sx={{ px: 1.5, color: 'rgba(255,255,255,0.3)', fontWeight: 800, letterSpacing: 1 }}> <Typography variant="caption" sx={{ px: 1, color: 'primary.main', fontWeight: 600, mb: 1, display: 'block', opacity: 0.8 }}>
PHP VERSIONS PHP Sürümleri
</Typography> </Typography>
<List dense disablePadding sx={{ mb: 2 }}> <List dense disablePadding sx={{ mb: 3 }}>
{installedPhp.map(v => ( {installedPhp.map(v => (
<ServiceItem <ServiceItem
key={v.id} key={v.id}
@@ -131,8 +131,8 @@ export default function DashboardSidebar({
))} ))}
</List> </List>
<Typography variant="overline" sx={{ px: 1.5, color: 'rgba(255,255,255,0.3)', fontWeight: 800, letterSpacing: 1 }}> <Typography variant="caption" sx={{ px: 1, color: 'primary.main', fontWeight: 600, mb: 1, display: 'block', opacity: 0.8 }}>
MARIADB VERSIONS Veritabanı Sürümleri
</Typography> </Typography>
<List dense disablePadding> <List dense disablePadding>
{installedMariaDb.map(v => ( {installedMariaDb.map(v => (
@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import Editor from 'react-simple-code-editor' import SearchableCodeEditor from './SearchableCodeEditor'
import Prism from 'prismjs' import Prism from 'prismjs'
import 'prismjs/components/prism-nginx' import 'prismjs/components/prism-nginx'
import 'prismjs/themes/prism-tomorrow.css' import 'prismjs/themes/prism-tomorrow.css'
@@ -20,7 +20,7 @@ const NginxCodeEditor: React.FC<NginxCodeEditorProps> = ({ value, onChange }) =>
fontSize: '14px', fontSize: '14px',
minHeight: '300px' minHeight: '300px'
}}> }}>
<Editor <SearchableCodeEditor
value={value} value={value}
onValueChange={code => onChange(code)} onValueChange={code => onChange(code)}
highlight={code => Prism.highlight(code, Prism.languages.nginx, 'nginx')} highlight={code => Prism.highlight(code, Prism.languages.nginx, 'nginx')}
@@ -32,7 +32,7 @@ const NginxCodeEditor: React.FC<NginxCodeEditorProps> = ({ value, onChange }) =>
outline: 'none' outline: 'none'
}} }}
textareaClassName="nginx-editor-textarea" textareaClassName="nginx-editor-textarea"
preClassName="nginx-editor-pre" languageLabel="Nginx"
/> />
<style>{` <style>{`
.nginx-editor-textarea { .nginx-editor-textarea {
+16 -190
View File
@@ -1,217 +1,43 @@
import React, { useState, useEffect, useRef, useMemo } from 'react' import React from 'react'
import Editor from 'react-simple-code-editor' import SearchableCodeEditor from './SearchableCodeEditor'
import Prism from 'prismjs' import Prism from 'prismjs'
import 'prismjs/components/prism-ini' import 'prismjs/components/prism-ini'
import 'prismjs/themes/prism-tomorrow.css' import 'prismjs/themes/prism-tomorrow.css'
import { IconButton, Box, Typography, Tooltip } from '@mui/material' import { Box } from '@mui/material'
import { KeyboardArrowDown as DownIcon, KeyboardArrowUp as UpIcon } from '@mui/icons-material'
import { useTranslation } from 'react-i18next'
interface PhpIniCodeEditorProps { interface PhpIniCodeEditorProps {
value: string value: string
onChange: (value: string) => void onChange: (value: string) => void
searchTerm?: string
} }
const PhpIniCodeEditor: React.FC<PhpIniCodeEditorProps> = ({ value, onChange, searchTerm }) => { const PhpIniCodeEditor: React.FC<PhpIniCodeEditorProps> = ({ value, onChange }) => {
const { t } = useTranslation()
const editorRef = useRef<HTMLDivElement>(null)
const [matches, setMatches] = useState<number[]>([])
const [currentIndex, setCurrentIndex] = useState(0)
const cleanSearchTerm = useMemo(() => searchTerm?.trim().toLowerCase() || '', [searchTerm])
// Find all matches when searchTerm or value changes
useEffect(() => {
if (cleanSearchTerm.length > 1) {
const indices: number[] = []
let pos = value.toLowerCase().indexOf(cleanSearchTerm)
while (pos !== -1) {
indices.push(pos)
pos = value.toLowerCase().indexOf(cleanSearchTerm, pos + 1)
}
setMatches(indices)
setCurrentIndex(indices.length > 0 ? 0 : -1)
} else {
setMatches([])
setCurrentIndex(-1)
}
}, [cleanSearchTerm, value])
const scrollToMatch = (index: number) => {
if (index >= 0 && index < matches.length && editorRef.current) {
const startPos = matches[index]
const linesBefore = value.substring(0, startPos).split('\n').length
const lineHeight = 21
const container = editorRef.current
container.scrollTop = (linesBefore - 5) * lineHeight
}
}
useEffect(() => {
if (currentIndex !== -1) {
scrollToMatch(currentIndex)
}
}, [currentIndex])
const handleNext = () => {
if (matches.length > 0) {
setCurrentIndex(prev => (prev + 1) % matches.length)
}
}
const handlePrev = () => {
if (matches.length > 0) {
setCurrentIndex(prev => (prev - 1 + matches.length) % matches.length)
}
}
const highlightWithSearch = (code: string) => {
let highlighted = Prism.highlight(code, Prism.languages.ini, 'ini')
if (cleanSearchTerm.length > 1) {
const escapedSearch = cleanSearchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const regex = new RegExp(`(${escapedSearch})(?![^<]*>)`, 'gi')
let matchCounter = 0
highlighted = highlighted.replace(regex, (match) => {
const isCurrent = matchCounter === currentIndex
matchCounter++
return `<mark class="search-match ${isCurrent ? 'active-match' : ''}">${match}</mark>`
})
}
return highlighted
}
// Keyboard support
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && cleanSearchTerm.length > 1) {
e.preventDefault()
if (e.shiftKey) handlePrev()
else handleNext()
}
}
const totalLines = useMemo(() => value.split('\n').length, [value])
return ( return (
<Box sx={{ position: 'relative', display: 'flex', flexDirection: 'column', height: '450px', bgcolor: '#1a1a1a' }}> <Box sx={{ position: 'relative', display: 'flex', flexDirection: 'column', height: '100%', bgcolor: '#1a1a1a', borderRadius: 1, overflow: 'hidden', border: '1px solid rgba(255,255,255,0.05)' }}>
{/* Search Navigation Bar */} <SearchableCodeEditor
{cleanSearchTerm.length > 1 && ( value={value}
<Box sx={{ onValueChange={code => onChange(code)}
position: 'absolute', highlight={code => Prism.highlight(code, Prism.languages.ini, 'ini')}
top: 10, padding={20}
right: 30, // Leave space for scrollbar/markers
zIndex: 10,
bgcolor: '#333',
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
px: 1,
py: 0.5,
boxShadow: '0 4px 12px rgba(0,0,0,0.5)',
border: '1px solid rgba(255,255,255,0.1)'
}}>
<Typography variant="caption" sx={{ color: '#aaa', mr: 1, minWidth: '40px', textAlign: 'center', userSelect: 'none' }}>
{matches.length > 0 ? `${currentIndex + 1} / ${matches.length}` : '0 / 0'}
</Typography>
<Tooltip title={t('php.extensions.prev_match')}>
<IconButton size="small" onClick={handlePrev} sx={{ color: '#fff' }}>
<UpIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title={t('php.extensions.next_match')}>
<IconButton size="small" onClick={handleNext} sx={{ color: '#fff' }}>
<DownIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
)}
{/* Scrollbar Markers (Minimap style) */}
<Box sx={{
position: 'absolute',
top: 0,
right: 0,
width: '12px',
height: '100%',
zIndex: 5,
pointerEvents: 'none',
bgcolor: 'rgba(255,255,255,0.02)'
}}>
{matches.map((pos, i) => {
const line = value.substring(0, pos).split('\n').length
const topPercent = (line / totalLines) * 100
return (
<Box
key={i}
sx={{
position: 'absolute',
top: `${topPercent}%`,
left: 2,
width: '8px',
height: '2px',
bgcolor: i === currentIndex ? '#ff9800' : '#ffcf00',
boxShadow: i === currentIndex ? '0 0 4px #ff9800' : 'none'
}}
/>
)
})}
</Box>
<div
ref={editorRef}
onKeyDown={handleKeyDown}
style={{ style={{
flexGrow: 1,
overflow: 'auto',
fontFamily: '"Fira code", "Fira Mono", monospace', fontFamily: '"Fira code", "Fira Mono", monospace',
fontSize: '14px', fontSize: 14,
scrollBehavior: 'smooth' minHeight: '100%',
outline: 'none'
}} }}
> textareaClassName="ini-editor-textarea"
<Editor languageLabel="php.ini"
key={cleanSearchTerm + currentIndex} // Key for re-highlighting active match />
value={value}
onValueChange={code => onChange(code)}
highlight={highlightWithSearch}
padding={15}
style={{
fontFamily: '"Fira code", "Fira Mono", monospace',
fontSize: 14,
minHeight: '100%',
outline: 'none'
}}
textareaClassName="ini-editor-textarea"
preClassName="ini-editor-pre"
/>
</div>
<style>{` <style>{`
.ini-editor-textarea { .ini-editor-textarea {
outline: none !important; outline: none !important;
caret-color: #fff !important; caret-color: #fff !important;
} }
.ini-editor-pre {
pointer-events: none;
}
.token.comment { color: #6a9955; } .token.comment { color: #6a9955; }
.token.section { color: #569cd6; font-weight: bold; } .token.section { color: #569cd6; font-weight: bold; }
.token.attr-name { color: #9cdcfe; } .token.attr-name { color: #9cdcfe; }
.token.attr-value { color: #ce9178; } .token.attr-value { color: #ce9178; }
.token.punctuation { color: #d4d4d4; } .token.punctuation { color: #d4d4d4; }
.token.important { color: #569cd6; } .token.important { color: #569cd6; }
.search-match {
background: rgba(255, 207, 0, 0.3) !important;
color: inherit !important;
border-radius: 1px;
}
.active-match {
background: #ff9800 !important;
color: #000 !important;
box-shadow: 0 0 8px rgba(255, 152, 0, 0.8);
font-weight: bold;
}
`}</style> `}</style>
</Box> </Box>
) )
@@ -0,0 +1,209 @@
import {
Box,
TextField,
IconButton,
Typography,
Paper,
Stack,
Tooltip
} from '@mui/material'
import {
Close as CloseIcon,
KeyboardArrowDown as NextIcon,
KeyboardArrowUp as PrevIcon,
Search as SearchIcon
} from '@mui/icons-material'
import { useState, useRef, useEffect, useCallback } from 'react'
import Editor from 'react-simple-code-editor'
interface SearchableCodeEditorProps {
value: string;
onValueChange: (v: string) => void;
highlight: (code: string) => string | JSX.Element;
padding?: number;
style?: React.CSSProperties;
textareaClassName?: string;
languageLabel?: string;
}
export default function SearchableCodeEditor({
value,
onValueChange,
highlight,
padding = 20,
style,
textareaClassName = "editor-textarea",
languageLabel
}: SearchableCodeEditorProps) {
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [searchIndex, setSearchIndex] = useState(-1);
const [searchResults, setSearchResults] = useState<number[]>([]);
const editorRef = useRef<HTMLDivElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null);
const performSearch = useCallback((query: string) => {
if (!query) {
setSearchResults([]);
setSearchIndex(-1);
return;
}
const indices: number[] = [];
let pos = value.toLowerCase().indexOf(query.toLowerCase());
while (pos !== -1) {
indices.push(pos);
pos = value.toLowerCase().indexOf(query.toLowerCase(), pos + 1);
}
setSearchResults(indices);
setSearchIndex(indices.length > 0 ? 0 : -1);
}, [value]);
useEffect(() => {
if (isSearchOpen && searchQuery) {
performSearch(searchQuery);
}
}, [searchQuery, isSearchOpen, performSearch]);
const scrollToMatch = (index: number) => {
if (index === -1 || searchResults.length === 0) return;
const offset = searchResults[index];
const textarea = editorRef.current?.querySelector('textarea');
if (textarea) {
textarea.focus();
textarea.setSelectionRange(offset, offset + searchQuery.length);
// Basic line height estimate to scroll correctly
const lineCount = value.substring(0, offset).split('\n').length;
const lineHeight = 22; // Typical line height for 15px mono font
textarea.scrollTop = (lineCount - 5) * lineHeight;
}
};
useEffect(() => {
if (searchIndex !== -1) {
scrollToMatch(searchIndex);
}
}, [searchIndex]);
const handleKeyDown = (e: React.KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
e.preventDefault();
setIsSearchOpen(true);
setTimeout(() => searchInputRef.current?.focus(), 100);
}
if (e.key === 'Escape') {
setIsSearchOpen(false);
setSearchQuery('');
}
};
const nextMatch = () => {
if (searchResults.length === 0) return;
setSearchIndex((prev) => (prev + 1) % searchResults.length);
};
const prevMatch = () => {
if (searchResults.length === 0) return;
setSearchIndex((prev) => (prev - 1 + searchResults.length) % searchResults.length);
};
return (
<Box
onKeyDown={handleKeyDown}
sx={{
position: 'relative',
height: '100%',
display: 'flex',
flexDirection: 'column',
'& .editor-container': {
flexGrow: 1,
overflow: 'auto',
'& textarea': {
outline: 'none !important'
}
}
}}
>
{isSearchOpen && (
<Paper
elevation={8}
sx={{
position: 'absolute',
top: 20,
right: 40,
zIndex: 1000,
p: 1,
display: 'flex',
alignItems: 'center',
gap: 1,
bgcolor: '#2d2d2d',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 2,
minWidth: 300,
boxShadow: '0 8px 32px rgba(0,0,0,0.5)'
}}
>
<SearchIcon sx={{ ml: 1, opacity: 0.5, fontSize: 18 }} />
<TextField
size="small"
placeholder="Bul..."
value={searchQuery}
inputRef={searchInputRef}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') nextMatch();
}}
autoComplete="off"
sx={{
flexGrow: 1,
'& .MuiInputBase-root': {
fontSize: '0.85rem',
color: '#fff'
},
'& .MuiOutlinedInput-notchedOutline': {
border: 'none'
}
}}
/>
{searchResults.length > 0 && (
<Typography variant="caption" sx={{ opacity: 0.5, minWidth: 40, fontWeight: 700 }}>
{searchIndex + 1} / {searchResults.length}
</Typography>
)}
<Stack direction="row">
<IconButton size="small" onClick={prevMatch} disabled={searchResults.length === 0} sx={{ color: 'primary.main' }}>
<PrevIcon />
</IconButton>
<IconButton size="small" onClick={nextMatch} disabled={searchResults.length === 0} sx={{ color: 'primary.main' }}>
<NextIcon />
</IconButton>
<IconButton size="small" onClick={() => { setIsSearchOpen(false); setSearchQuery(''); }} sx={{ color: 'error.main', opacity: 0.8 }}>
<CloseIcon />
</IconButton>
</Stack>
</Paper>
)}
<Box className="editor-container" ref={editorRef}>
<Editor
value={value}
onValueChange={onValueChange}
highlight={highlight}
padding={padding}
style={{
fontFamily: '"Fira Code", monospace',
fontSize: '15px',
minHeight: '100%',
...style
}}
textareaClassName={textareaClassName}
/>
</Box>
</Box>
);
}