feat: implement code editors for PHP, Nginx, and Apache, and add IPC handlers for service configuration management
This commit is contained in:
@@ -1068,6 +1068,36 @@ export function registerIpcHandlers(): void {
|
||||
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 () => {
|
||||
try {
|
||||
serviceEventLogger.clear()
|
||||
|
||||
@@ -190,10 +190,20 @@ export class PhpManagerService {
|
||||
if (fs.existsSync(iniPath)) {
|
||||
const content = fs.readFileSync(iniPath, 'utf8')
|
||||
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) {
|
||||
// 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')
|
||||
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 Editor from 'react-simple-code-editor'
|
||||
import SearchableCodeEditor from './SearchableCodeEditor'
|
||||
import Prism from 'prismjs'
|
||||
import 'prismjs/components/prism-apacheconf'
|
||||
import 'prismjs/themes/prism-tomorrow.css'
|
||||
@@ -20,7 +20,7 @@ const ApacheCodeEditor: React.FC<ApacheCodeEditorProps> = ({ value, onChange })
|
||||
fontSize: '14px',
|
||||
minHeight: '300px'
|
||||
}}>
|
||||
<Editor
|
||||
<SearchableCodeEditor
|
||||
value={value}
|
||||
onValueChange={code => onChange(code)}
|
||||
highlight={code => Prism.highlight(code, Prism.languages.apacheconf, 'apacheconf')}
|
||||
@@ -32,7 +32,7 @@ const ApacheCodeEditor: React.FC<ApacheCodeEditorProps> = ({ value, onChange })
|
||||
outline: 'none'
|
||||
}}
|
||||
textareaClassName="apache-editor-textarea"
|
||||
preClassName="apache-editor-pre"
|
||||
languageLabel="Apache"
|
||||
/>
|
||||
<style>{`
|
||||
.apache-editor-textarea {
|
||||
|
||||
@@ -40,6 +40,7 @@ import 'prismjs/components/prism-nginx'
|
||||
import 'prismjs/components/prism-apacheconf'
|
||||
import 'prismjs/components/prism-ini'
|
||||
import Editor from 'react-simple-code-editor'
|
||||
import SearchableCodeEditor from '../SearchableCodeEditor'
|
||||
import Prism from 'prismjs'
|
||||
import Swal from 'sweetalert2'
|
||||
|
||||
@@ -254,19 +255,19 @@ export default function DashboardDetails({
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Box sx={{ mb: 1.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>
|
||||
</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 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 }}>
|
||||
{memory ? `${Math.round(memory / (1024 * 1024))} MB` : '0 MB'}
|
||||
</Typography>
|
||||
</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>
|
||||
);
|
||||
@@ -283,13 +284,15 @@ export default function DashboardDetails({
|
||||
{serviceType === 'mariadb' && <DbIcon color="primary" sx={{ fontSize: 40 }} />}
|
||||
</Paper>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography variant="h4" sx={{ fontWeight: 800, letterSpacing: '-1px' }}>{serviceId.toUpperCase()}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<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' }} />
|
||||
{isStarting && <Box component="span" sx={{ color: 'warning.light', fontWeight: 700 }}>{t('common.starting').toUpperCase()}</Box>}
|
||||
{isStopping && <Box component="span" sx={{ color: 'warning.light', fontWeight: 700 }}>{t('common.stopping').toUpperCase()}</Box>}
|
||||
{isRunning && <Box component="span" sx={{ color: 'success.main', fontWeight: 700 }}>{t('common.running').toUpperCase()}</Box>}
|
||||
{!isRunning && !isStarting && !isStopping && <Box component="span" sx={{ opacity: 0.5, fontWeight: 700 }}>{t('common.stopped').toUpperCase()}</Box>}
|
||||
<Typography variant="h5" sx={{ fontWeight: 800, letterSpacing: '-0.5px', color: 'primary.main' }}>
|
||||
{serviceId.replace(':', ' ').replace('php', 'PHP').replace('mariadb', 'MariaDB').replace('nginx', 'Nginx').replace('apache', 'Apache')}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 0.5 }}>
|
||||
<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' }} />
|
||||
{isStarting && <Box component="span" sx={{ color: 'warning.light', fontWeight: 700, letterSpacing: 0.5 }}>{t('common.starting')}</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>
|
||||
</Box>
|
||||
<Stack direction="row" spacing={2}>
|
||||
@@ -314,15 +317,17 @@ export default function DashboardDetails({
|
||||
</Box>
|
||||
|
||||
<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">
|
||||
<Tab label={t('dashboard.overview')} value="overview" sx={{ fontWeight: 700, py: 2.5 }} />
|
||||
<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 }} />}
|
||||
<Tabs value={activeTab} onChange={(_e, v) => setActiveTab(v)} textColor="primary" indicatorColor="primary" sx={{
|
||||
'.MuiTab-root': { textTransform: 'none', minHeight: 64, fontWeight: 700, fontSize: '0.95rem' }
|
||||
}}>
|
||||
<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) && (
|
||||
<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('projects.title')} value="projects" sx={{ fontWeight: 700, py: 2.5 }} />
|
||||
<Tab label={t('diagnostics.logs')} value="logs" />
|
||||
<Tab label={t('projects.title')} value="projects" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
@@ -331,7 +336,7 @@ export default function DashboardDetails({
|
||||
<Grid container spacing={4}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<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>
|
||||
<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} />
|
||||
@@ -339,12 +344,12 @@ export default function DashboardDetails({
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<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>
|
||||
<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}>
|
||||
<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)' }}>
|
||||
<Typography variant="body1" sx={{ fontWeight: 800, color: 'primary.main', fontFamily: 'monospace' }}>
|
||||
{getPort()}
|
||||
@@ -353,9 +358,9 @@ export default function DashboardDetails({
|
||||
</Box>
|
||||
<Divider sx={{ opacity: 0.05 }} />
|
||||
<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
|
||||
label={(isRunning ? t('common.running') : t('common.stopped')).toUpperCase()}
|
||||
label={isRunning ? t('common.running', 'Çalışıyor') : t('common.stopped', 'Durduruldu')}
|
||||
color={isRunning ? 'success' : 'error'}
|
||||
sx={{ fontWeight: 900, borderRadius: 2 }}
|
||||
/>
|
||||
@@ -369,8 +374,8 @@ export default function DashboardDetails({
|
||||
{(activeTab === 'settings_template' || activeTab === 'settings_live') && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||
<Typography variant="overline" sx={{ fontWeight: 900, color: 'primary.main', letterSpacing: 2 }}>
|
||||
{activeTab === 'settings_live' ? 'php.ini (ACTIVE)' : getTemplateName().toUpperCase()}
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: 'primary.main', letterSpacing: 1 }}>
|
||||
{activeTab === 'settings_live' ? 'php.ini (Aktif Konfigürasyon)' : getTemplateName()}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -388,12 +393,12 @@ export default function DashboardDetails({
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flexGrow: 1 }}><CircularProgress /></Box>
|
||||
) : (
|
||||
<Box sx={{ flexGrow: 1, overflow: 'auto' }}>
|
||||
<Editor
|
||||
<SearchableCodeEditor
|
||||
value={activeTab === 'settings_live' ? liveConfigContent : configContent}
|
||||
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()))}
|
||||
padding={25}
|
||||
style={{ fontFamily: '"Fira Code", monospace', fontSize: '15px', minHeight: '100%', outline: 'none' }}
|
||||
style={{ fontFamily: '"Fira Code", monospace', fontSize: '15px' }}
|
||||
textareaClassName="editor-textarea"
|
||||
/>
|
||||
</Box>
|
||||
@@ -405,8 +410,8 @@ export default function DashboardDetails({
|
||||
{activeTab === 'projects_template' && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||
<Typography variant="overline" sx={{ fontWeight: 900, color: 'primary.main', letterSpacing: 2 }}>
|
||||
{getProjectTemplateNames()[0]?.toUpperCase()}
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: 'primary.main', letterSpacing: 1 }}>
|
||||
{getProjectTemplateNames()[0]}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -423,12 +428,12 @@ export default function DashboardDetails({
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flexGrow: 1 }}><CircularProgress /></Box>
|
||||
) : (
|
||||
<Box sx={{ flexGrow: 1, overflow: 'auto' }}>
|
||||
<Editor
|
||||
<SearchableCodeEditor
|
||||
value={projectConfigContent}
|
||||
onValueChange={setProjectConfigContent}
|
||||
highlight={code => Prism.highlight(code, getHighlightLanguage(getProjectTemplateNames()[0]), getHighlightLabel(getProjectTemplateNames()[0]))}
|
||||
padding={25}
|
||||
style={{ fontFamily: '"Fira Code", monospace', fontSize: '15px', minHeight: '100%', outline: 'none' }}
|
||||
style={{ fontFamily: '"Fira Code", monospace', fontSize: '15px' }}
|
||||
textareaClassName="editor-textarea"
|
||||
/>
|
||||
</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={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1.5, color: 'rgba(255,255,255,0.3)' }}>
|
||||
<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>
|
||||
{logs.length === 0 ? (
|
||||
<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 }}>
|
||||
<Icon sx={{ color: selectedServiceId === id ? 'primary.main' : 'rgba(255,255,255,0.4)', fontSize: '1.2rem' }} />
|
||||
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||
<Typography variant="body2" noWrap sx={{ fontWeight: 600 }}>
|
||||
<Typography variant="body2" noWrap sx={{ fontWeight: 600, fontSize: '0.85rem' }}>
|
||||
{label}
|
||||
</Typography>
|
||||
{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
|
||||
</Typography>
|
||||
)}
|
||||
@@ -93,7 +93,7 @@ export default function DashboardSidebar({
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
width: 280,
|
||||
width: 320,
|
||||
borderRight: '1px solid rgba(255, 255, 255, 0.05)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -102,24 +102,24 @@ export default function DashboardSidebar({
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
<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')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="overline" sx={{ px: 1.5, color: 'rgba(255,255,255,0.3)', fontWeight: 800, letterSpacing: 1 }}>
|
||||
WEB SERVERS
|
||||
<Box sx={{ p: 1.5 }}>
|
||||
<Typography variant="caption" sx={{ px: 1, color: 'primary.main', fontWeight: 600, mb: 1, display: 'block', opacity: 0.8 }}>
|
||||
Web Sunucuları
|
||||
</Typography>
|
||||
<List dense disablePadding sx={{ mb: 2 }}>
|
||||
<List dense disablePadding sx={{ mb: 3 }}>
|
||||
<ServiceItem id="nginx" label="Nginx" icon={WebIcon} serviceKey="nginx" />
|
||||
<ServiceItem id="apache" label="Apache" icon={ServerIcon} serviceKey="apache" />
|
||||
</List>
|
||||
|
||||
<Typography variant="overline" sx={{ px: 1.5, color: 'rgba(255,255,255,0.3)', fontWeight: 800, letterSpacing: 1 }}>
|
||||
PHP VERSIONS
|
||||
<Typography variant="caption" sx={{ px: 1, color: 'primary.main', fontWeight: 600, mb: 1, display: 'block', opacity: 0.8 }}>
|
||||
PHP Sürümleri
|
||||
</Typography>
|
||||
<List dense disablePadding sx={{ mb: 2 }}>
|
||||
<List dense disablePadding sx={{ mb: 3 }}>
|
||||
{installedPhp.map(v => (
|
||||
<ServiceItem
|
||||
key={v.id}
|
||||
@@ -131,8 +131,8 @@ export default function DashboardSidebar({
|
||||
))}
|
||||
</List>
|
||||
|
||||
<Typography variant="overline" sx={{ px: 1.5, color: 'rgba(255,255,255,0.3)', fontWeight: 800, letterSpacing: 1 }}>
|
||||
MARIADB VERSIONS
|
||||
<Typography variant="caption" sx={{ px: 1, color: 'primary.main', fontWeight: 600, mb: 1, display: 'block', opacity: 0.8 }}>
|
||||
Veritabanı Sürümleri
|
||||
</Typography>
|
||||
<List dense disablePadding>
|
||||
{installedMariaDb.map(v => (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import Editor from 'react-simple-code-editor'
|
||||
import SearchableCodeEditor from './SearchableCodeEditor'
|
||||
import Prism from 'prismjs'
|
||||
import 'prismjs/components/prism-nginx'
|
||||
import 'prismjs/themes/prism-tomorrow.css'
|
||||
@@ -20,7 +20,7 @@ const NginxCodeEditor: React.FC<NginxCodeEditorProps> = ({ value, onChange }) =>
|
||||
fontSize: '14px',
|
||||
minHeight: '300px'
|
||||
}}>
|
||||
<Editor
|
||||
<SearchableCodeEditor
|
||||
value={value}
|
||||
onValueChange={code => onChange(code)}
|
||||
highlight={code => Prism.highlight(code, Prism.languages.nginx, 'nginx')}
|
||||
@@ -32,7 +32,7 @@ const NginxCodeEditor: React.FC<NginxCodeEditorProps> = ({ value, onChange }) =>
|
||||
outline: 'none'
|
||||
}}
|
||||
textareaClassName="nginx-editor-textarea"
|
||||
preClassName="nginx-editor-pre"
|
||||
languageLabel="Nginx"
|
||||
/>
|
||||
<style>{`
|
||||
.nginx-editor-textarea {
|
||||
|
||||
@@ -1,217 +1,43 @@
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react'
|
||||
import Editor from 'react-simple-code-editor'
|
||||
import React from 'react'
|
||||
import SearchableCodeEditor from './SearchableCodeEditor'
|
||||
import Prism from 'prismjs'
|
||||
import 'prismjs/components/prism-ini'
|
||||
import 'prismjs/themes/prism-tomorrow.css'
|
||||
import { IconButton, Box, Typography, Tooltip } from '@mui/material'
|
||||
import { KeyboardArrowDown as DownIcon, KeyboardArrowUp as UpIcon } from '@mui/icons-material'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Box } from '@mui/material'
|
||||
|
||||
interface PhpIniCodeEditorProps {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
searchTerm?: string
|
||||
}
|
||||
|
||||
const PhpIniCodeEditor: React.FC<PhpIniCodeEditorProps> = ({ value, onChange, searchTerm }) => {
|
||||
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])
|
||||
|
||||
const PhpIniCodeEditor: React.FC<PhpIniCodeEditorProps> = ({ value, onChange }) => {
|
||||
return (
|
||||
<Box sx={{ position: 'relative', display: 'flex', flexDirection: 'column', height: '450px', bgcolor: '#1a1a1a' }}>
|
||||
{/* Search Navigation Bar */}
|
||||
{cleanSearchTerm.length > 1 && (
|
||||
<Box sx={{
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
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}
|
||||
<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)' }}>
|
||||
<SearchableCodeEditor
|
||||
value={value}
|
||||
onValueChange={code => onChange(code)}
|
||||
highlight={code => Prism.highlight(code, Prism.languages.ini, 'ini')}
|
||||
padding={20}
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
overflow: 'auto',
|
||||
fontFamily: '"Fira code", "Fira Mono", monospace',
|
||||
fontSize: '14px',
|
||||
scrollBehavior: 'smooth'
|
||||
fontSize: 14,
|
||||
minHeight: '100%',
|
||||
outline: 'none'
|
||||
}}
|
||||
>
|
||||
<Editor
|
||||
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>
|
||||
textareaClassName="ini-editor-textarea"
|
||||
languageLabel="php.ini"
|
||||
/>
|
||||
<style>{`
|
||||
.ini-editor-textarea {
|
||||
outline: none !important;
|
||||
caret-color: #fff !important;
|
||||
}
|
||||
.ini-editor-pre {
|
||||
pointer-events: none;
|
||||
}
|
||||
.token.comment { color: #6a9955; }
|
||||
.token.section { color: #569cd6; font-weight: bold; }
|
||||
.token.attr-name { color: #9cdcfe; }
|
||||
.token.attr-value { color: #ce9178; }
|
||||
.token.punctuation { color: #d4d4d4; }
|
||||
.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>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user