feat: implement multi-service management dashboard with project, database, and PHP extension support

This commit is contained in:
Ümit Tunç
2026-04-03 15:24:20 +03:00
parent 2413c19393
commit e7581d442a
11 changed files with 1066 additions and 333 deletions
+4
View File
@@ -719,6 +719,10 @@ export function registerIpcHandlers(): void {
return processManager.getStatuses()
})
ipcMain.handle('service:status', async (_event, name: string) => {
return processManager.getServiceState(name);
})
ipcMain.handle('services:logs', async (_event, serviceName: string) => {
return processManager.getLogs(serviceName)
})
+224 -125
View File
@@ -459,79 +459,130 @@ function App(): JSX.Element {
setSettings(prev => ({ ...prev, language: newLang }))
}
const handleStartAll = async () => {
if (!window.api) return
const criticalServices = ['nginx', 'apache']
for (const s of criticalServices) {
if (status[s]?.status === 'stopped') setStatus(prev => ({ ...prev, [s]: { status: 'starting' } }))
const runServiceOperation = async (title: string, services: string[], operation: 'start' | 'stop') => {
const results: Record<string, any> = {};
const getDisplayLabel = (srv: string) => {
if (srv === 'nginx') return 'Nginx';
if (srv === 'apache') return 'Apache';
if (srv.startsWith('php:')) return `PHP ${srv.split(':')[1]}`;
if (srv.startsWith('mariadb:')) return `MariaDB ${srv.split(':')[1]}`;
return srv;
};
const updateSwal = (currentSrv?: string) => {
const html = `
<div style="text-align: left; font-family: 'Inter', sans-serif; padding-top: 10px;">
${services.map(srv => {
const label = getDisplayLabel(srv);
const res = results[srv];
let icon = '<span style="color: rgba(255,255,255,0.1); margin-right: 12px; font-size: 0.8rem;">○</span>';
let statusColor = 'rgba(255,255,255,0.4)';
let note = operation === 'start' ? t('common.ready') : t('common.running');
if (res?.success) {
icon = '<span style="color: #33d9b2; margin-right: 12px; font-size: 0.8rem;">●</span>';
statusColor = '#33d9b2';
note = operation === 'start' ? t('common.started') : t('common.stopped');
} else if (res?.error) {
icon = '<span style="color: #ff5252; margin-right: 12px; font-size: 0.8rem;">●</span>';
statusColor = '#ff5252';
note = t('common.error');
} else if (srv === currentSrv) {
icon = '<span style="color: #39A7FF; margin-right: 12px; font-size: 0.8rem;" class="swal2-loading-dots">●</span>';
statusColor = '#39A7FF';
note = operation === 'start' ? t('common.starting') : t('common.stopping');
}
return '<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; padding: 8px 14px; background: rgba(0,0,0,0.2); border: 1px solid rgba(255,255,255,0.03); border-radius: 8px; font-size: 0.8rem;">' +
'<span style="font-weight: 600; display: flex; align-items: center;">' + icon + ' ' + label + '</span>' +
'<span style="font-size: 0.65rem; color: ' + statusColor + '; font-weight: 800; letter-spacing: 0.5px;">' + note.toUpperCase() + '</span>' +
'</div>';
}).join('')}
</div>
`;
Swal.update({ html });
};
Swal.fire({
title: title.toUpperCase(),
html: '<div></div>',
allowOutsideClick: false,
showConfirmButton: false,
background: '#1a1d21',
color: '#fff',
customClass: {
title: 'swal-title-small'
},
didOpen: () => {
updateSwal();
}
});
for (const srv of services) {
updateSwal(srv);
try {
const res = operation === 'start' ? await window.api.startService(srv) : await window.api.stopService(srv);
if (res.success) {
// Poll for status confirmation
let attempts = 0;
const maxAttempts = 15; // ~7.5 seconds
let confirmed = false;
while (attempts < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 500));
const statusRes = await window.api.invoke('service:status', srv);
if (operation === 'start' && statusRes === 'running') {
confirmed = true;
break;
}
if (operation === 'stop' && statusRes === 'stopped') {
confirmed = true;
break;
}
attempts++;
}
results[srv] = { success: confirmed, error: !confirmed ? t('common.timeout') : null };
} else {
results[srv] = { success: false, error: res.message };
}
} catch (err: any) {
results[srv] = { success: false, error: err.message };
}
updateSwal();
}
const phpToStart = phpVersions.filter(v => v.status === 'installed').map(v => `php:${v.version}`)
for (const s of phpToStart) {
if ((status[s]?.status || 'stopped') === 'stopped') setStatus(prev => ({ ...prev, [s]: { status: 'starting' } }))
}
const mariaDbToStart = mariaDbVersions.filter(v => v.status === 'installed').map(v => `mariadb:${v.version}`)
for (const s of mariaDbToStart) {
if ((status[s]?.status || 'stopped') === 'stopped') setStatus(prev => ({ ...prev, [s]: { status: 'starting' } }))
}
const anyError = Object.values(results).some(r => r.error);
Swal.update({
icon: anyError ? 'warning' : 'success',
showConfirmButton: true,
confirmButtonText: t('common.close').toUpperCase()
});
const results = await Promise.all([
...criticalServices.filter(s => status[s]?.status !== 'running').map(s => window.api?.startService(s).then((r: any) => ({ service: s, ...r }))),
...phpToStart.filter(s => (status[s]?.status || 'stopped') !== 'running').map(s => window.api?.startService(s).then((r: any) => ({ service: s, ...r }))),
...mariaDbToStart.filter(s => (status[s]?.status || 'stopped') !== 'running').map(s => window.api?.startService(s).then((r: any) => ({ service: s, ...r })))
])
const errors = results.filter(r => r && !r.success)
if (errors.length > 0) {
const errorList = errors.map(e => `<b>${e.service}:</b> ${e.message}`).join('<br/>')
Swal.fire({
title: t('common.error'),
html: `<div style="text-align: left; font-size: 0.9rem;">${errorList}</div>`,
icon: 'error',
background: '#1e1e1e',
color: '#fff'
})
} else {
setNotification({ open: true, message: t('dashboard.all_started'), severity: 'success' })
// If no error, auto-close after 3s
if (!anyError) {
setTimeout(() => {
if (Swal.isVisible()) Swal.close();
}, 3000);
}
fetchStatus()
}
fetchStatus();
};
const handleStopAll = async () => {
if (!window.api) return
// Show immediate "stopping" UI feedback for all running services
const updatedStatus = { ...status }
Object.keys(updatedStatus).forEach(s => {
if (updatedStatus[s].status === 'running') updatedStatus[s].status = 'stopping'
})
setStatus(updatedStatus)
const handleStartAll = () => {
const services = ['nginx', 'apache', ...phpVersions.filter(v => v.status === 'installed').map(v => `php:${v.version}`), ...mariaDbVersions.filter(v => v.status === 'installed').map(v => `mariadb:${v.version}`)];
runServiceOperation(t('dashboard.starting_all'), services, 'start');
};
const result = await window.api.invoke('services:stop-all')
if (!result.success) {
Swal.fire({
title: t('common.error'),
text: result.message,
icon: 'error',
background: '#1e1e1e',
color: '#fff'
})
} else {
setNotification({ open: true, message: t('dashboard.all_stopped'), severity: 'success' })
}
fetchStatus()
}
const formatMemory = (bytes?: number) => {
if (!bytes) return ''
const mb = bytes / (1024 * 1024)
if (mb > 1024) return `${(mb / 1024).toFixed(1)} GB`
return `${Math.round(mb)} MB`
}
const handleStopAll = () => {
const services = ['nginx', 'apache', ...phpVersions.filter(v => v.status === 'installed').map(v => `php:${v.version}`), ...mariaDbVersions.filter(v => v.status === 'installed').map(v => `mariadb:${v.version}`)];
runServiceOperation(t('dashboard.stopping_all'), services, 'stop');
};
const handleAddProject = async () => {
if (!window.api) return
@@ -716,7 +767,6 @@ function App(): JSX.Element {
setIsPhpExtensionsOpen(true)
}
const fetchProcessList = async (id: string) => {
if (!window.api) return
setIsProcessListLoading(true)
@@ -752,6 +802,7 @@ function App(): JSX.Element {
const installedPhpVersions = phpVersions.filter(v => v.status === 'installed')
return (
<>
<Dialog
@@ -841,6 +892,21 @@ function App(): JSX.Element {
'.swal2-container': {
zIndex: '9999 !important'
},
'.swal-title-small': {
fontSize: '1rem !important',
fontWeight: '800 !important',
letterSpacing: '1px !important',
color: '#fff !important',
marginTop: '10px !important'
},
'@keyframes swalLoadingDots': {
'0%': { opacity: 0.3, transform: 'scale(0.8)' },
'50%': { opacity: 1, transform: 'scale(1.2)' },
'100%': { opacity: 0.3, transform: 'scale(0.8)' }
},
'.swal2-loading-dots': {
animation: 'swalLoadingDots 1.4s infinite ease-in-out'
},
'.nginx-error-container': {
textAlign: 'left !important',
fontFamily: 'monospace !important',
@@ -863,51 +929,84 @@ function App(): JSX.Element {
backdropFilter: 'blur(10px)'
}}
>
<Toolbar sx={{ gap: 1 }}>
<Toolbar sx={{ gap: 1.5, minHeight: '56px !important' }}>
<Box sx={{ flexGrow: 1 }} />
<Tooltip title={t('dashboard.start_all')}>
<IconButton color="inherit" onClick={handleStartAll} sx={{ '&:hover': { color: 'success.main' } }}>
<StartIcon />
</IconButton>
</Tooltip>
<Tooltip title={t('dashboard.stop_all')}>
<IconButton color="inherit" onClick={handleStopAll} sx={{ '&:hover': { color: 'error.main' } }}>
<StopIcon />
</IconButton>
</Tooltip>
<Divider orientation="vertical" flexItem sx={{ mx: 1, borderColor: 'rgba(255,255,255,0.1)' }} />
<FormControl size="small" variant="outlined" sx={{ minWidth: 80 }}>
<Button
variant="outlined"
size="small"
color="success"
onClick={handleStartAll}
startIcon={<StartIcon sx={{ fontSize: '1rem !important' }} />}
sx={{
borderRadius: 1.5,
fontSize: '0.7rem',
fontWeight: 800,
textTransform: 'none',
borderColor: 'rgba(76, 175, 80, 0.3)',
px: 1.5,
height: 28
}}
>
{t('dashboard.start_all')}
</Button>
<Button
variant="outlined"
size="small"
color="error"
onClick={handleStopAll}
startIcon={<StopIcon sx={{ fontSize: '1rem !important' }} />}
sx={{
borderRadius: 1.5,
fontSize: '0.7rem',
fontWeight: 800,
textTransform: 'none',
borderColor: 'rgba(244, 67, 54, 0.3)',
px: 1.5,
height: 28
}}
>
{t('dashboard.stop_all')}
</Button>
<Divider orientation="vertical" flexItem sx={{ mx: 0.5, borderColor: 'rgba(255,255,255,0.1)', height: 20, mt: 1.5 }} />
<FormControl size="small" variant="outlined" sx={{ minWidth: 70 }}>
<Select
value={i18n.language}
onChange={handleLanguageChange}
displayEmpty
IconComponent={LanguageIcon}
size="small"
sx={{
color: 'white',
height: '35px',
fontSize: '0.8rem',
'.MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.2)' },
'&:hover .MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.4)' },
'&.Mui-focused .MuiOutlinedInput-notchedOutline': { borderColor: 'primary.main' },
'.MuiSelect-icon': { color: 'white' }
height: '28px',
fontSize: '0.7rem',
fontWeight: 700,
'.MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.1)' },
'&:hover .MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.2)' },
'.MuiSelect-icon': { color: 'rgba(255,255,255,0.5)', fontSize: '1rem' }
}}
>
{supportedLngs.map(lang => (
<MenuItem key={lang} value={lang}>
<MenuItem key={lang} value={lang} sx={{ fontSize: '0.75rem' }}>
{lang.toUpperCase()}
</MenuItem>
))}
))}
</Select>
</FormControl>
<Tooltip title={t('mariadb_process_monitor.title')}>
<IconButton color="inherit" onClick={() => setActiveTab('db_monitor')} sx={{ '&:hover': { color: 'primary.main' } }}>
<TerminalIcon />
<IconButton size="small" color="inherit" onClick={() => setActiveTab('db_monitor')} sx={{ p: 0.5, opacity: 0.7, '&:hover': { opacity: 1, color: 'primary.main' } }}>
<TerminalIcon sx={{ fontSize: '1.2rem' }} />
</IconButton>
</Tooltip>
<IconButton color="inherit" onClick={() => setActiveTab('settings')} sx={{ ml: 1 }}>
<SettingsIcon />
<IconButton size="small" color="inherit" onClick={() => setActiveTab('settings')} sx={{ p: 0.5, opacity: 0.7, '&:hover': { opacity: 1 } }}>
<SettingsIcon sx={{ fontSize: '1.2rem' }} />
</IconButton>
</Toolbar>
</AppBar>
<Drawer
@@ -925,60 +1024,60 @@ function App(): JSX.Element {
<Box sx={{ overflow: 'auto' }}>
<List>
<ListItem disablePadding>
<ListItemButton selected={activeTab === 'dashboard'} onClick={() => setActiveTab('dashboard')}>
<ListItemIcon><DashboardIcon color={activeTab === 'dashboard' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('dashboard.title')} />
<ListItemButton selected={activeTab === 'dashboard'} onClick={() => setActiveTab('dashboard')} sx={{ py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 40 }}><DashboardIcon fontSize="small" color={activeTab === 'dashboard' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('dashboard.title')} primaryTypographyProps={{ fontSize: '0.75rem', fontWeight: 500 }} />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton selected={activeTab === 'projects'} onClick={() => setActiveTab('projects')}>
<ListItemIcon><ProjectIcon color={activeTab === 'projects' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('projects.title')} />
<ListItemButton selected={activeTab === 'projects'} onClick={() => setActiveTab('projects')} sx={{ py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 40 }}><ProjectIcon fontSize="small" color={activeTab === 'projects' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('projects.title')} primaryTypographyProps={{ fontSize: '0.75rem', fontWeight: 500 }} />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton selected={activeTab === 'tuning'} onClick={() => setActiveTab('tuning')}>
<ListItemIcon><TuningIcon color={activeTab === 'tuning' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('tuning.title')} />
<ListItemButton selected={activeTab === 'tuning'} onClick={() => setActiveTab('tuning')} sx={{ py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 40 }}><TuningIcon fontSize="small" color={activeTab === 'tuning' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('tuning.title')} primaryTypographyProps={{ fontSize: '0.75rem', fontWeight: 500 }} />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton selected={activeTab === 'tools'} onClick={() => setActiveTab('tools')}>
<ListItemIcon><ToolsIcon color={activeTab === 'tools' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('common.tools')} />
<ListItemButton selected={activeTab === 'tools'} onClick={() => setActiveTab('tools')} sx={{ py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 40 }}><ToolsIcon fontSize="small" color={activeTab === 'tools' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('common.tools')} primaryTypographyProps={{ fontSize: '0.75rem', fontWeight: 500 }} />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton selected={activeTab === 'db_manager'} onClick={() => setActiveTab('db_manager')}>
<ListItemIcon><DbIcon color={activeTab === 'db_manager' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('mariadb_manager.title')} />
<ListItemButton selected={activeTab === 'db_manager'} onClick={() => setActiveTab('db_manager')} sx={{ py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 40 }}><DbIcon fontSize="small" color={activeTab === 'db_manager' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('mariadb_manager.title')} primaryTypographyProps={{ fontSize: '0.75rem', fontWeight: 500 }} />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton selected={activeTab === 'db_monitor'} onClick={() => setActiveTab('db_monitor')}>
<ListItemIcon><TerminalIcon color={activeTab === 'db_monitor' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('mariadb_process_monitor.title')} />
<ListItemButton selected={activeTab === 'db_monitor'} onClick={() => setActiveTab('db_monitor')} sx={{ py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 40 }}><TerminalIcon fontSize="small" color={activeTab === 'db_monitor' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('mariadb_process_monitor.title')} primaryTypographyProps={{ fontSize: '0.75rem', fontWeight: 500 }} />
</ListItemButton>
</ListItem>
</List>
<Divider />
<Divider sx={{ opacity: 0.05 }} />
<List>
<ListItem disablePadding>
<ListItemButton selected={activeTab === 'user_guide'} onClick={() => setActiveTab('user_guide')}>
<ListItemIcon><GuideIcon color={activeTab === 'user_guide' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('common.user_guide')} />
<ListItemButton selected={activeTab === 'user_guide'} onClick={() => setActiveTab('user_guide')} sx={{ py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 40 }}><GuideIcon fontSize="small" color={activeTab === 'user_guide' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('common.user_guide')} primaryTypographyProps={{ fontSize: '0.75rem', fontWeight: 500 }} />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton selected={activeTab === 'settings'} onClick={() => setActiveTab('settings')}>
<ListItemIcon><SettingsIcon color={activeTab === 'settings' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('settings.general')} />
<ListItemButton selected={activeTab === 'settings'} onClick={() => setActiveTab('settings')} sx={{ py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 40 }}><SettingsIcon fontSize="small" color={activeTab === 'settings' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('settings.general')} primaryTypographyProps={{ fontSize: '0.75rem', fontWeight: 500 }} />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton selected={activeTab === 'event_monitor'} onClick={() => setActiveTab('event_monitor')}>
<ListItemIcon><HistoryIcon color={activeTab === 'event_monitor' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('service_monitor.title')} />
<ListItemButton selected={activeTab === 'event_monitor'} onClick={() => setActiveTab('event_monitor')} sx={{ py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 40 }}><HistoryIcon fontSize="small" color={activeTab === 'event_monitor' ? 'primary' : 'inherit'} /></ListItemIcon>
<ListItemText primary={t('service_monitor.title')} primaryTypographyProps={{ fontSize: '0.75rem', fontWeight: 500 }} />
</ListItemButton>
</ListItem>
</List>
@@ -1402,9 +1501,11 @@ function App(): JSX.Element {
t={t}
onFetchStatus={fetchStatus}
onSelectProject={handleSelectProject}
runServiceOperation={runServiceOperation}
/>
)}
{activeTab === 'db_monitor' && (
<Box sx={{ mt: 2 }}>
<Paper sx={{ p: 3, bgcolor: 'rgba(255,255,255,0.03)', borderRadius: 2 }}>
@@ -1634,13 +1735,10 @@ function App(): JSX.Element {
status[`php:${p.phpVersion}`]?.status === 'running'
);
if (isRunning) {
await window.api.stopService(p.serverType === 'apache' ? 'apache' : 'nginx');
await window.api.stopService(`php:${p.phpVersion}`);
runServiceOperation(t('projects.stop_site'), [p.serverType === 'apache' ? 'apache' : 'nginx', `php:${p.phpVersion}`], 'stop');
} else {
await window.api.startService(p.serverType === 'apache' ? 'apache' : 'nginx');
await window.api.startService(`php:${p.phpVersion}`);
runServiceOperation(t('projects.start_site'), [p.serverType === 'apache' ? 'apache' : 'nginx', `php:${p.phpVersion}`], 'start');
}
fetchStatus();
}}
onOpenSite={(p: any, isHostMapped: boolean) => {
const port = p.serverType === 'apache' ? settings.apachePort : settings.nginxPort
@@ -1665,6 +1763,7 @@ function App(): JSX.Element {
}
}}
onManageDb={() => setActiveTab('db_manager')}
onRefreshProjects={fetchProjects}
/>
)}
@@ -284,22 +284,22 @@ export default function DashboardDetails({
{serviceType === 'mariadb' && <DbIcon color="primary" sx={{ fontSize: 40 }} />}
</Paper>
<Box sx={{ flexGrow: 1 }}>
<Typography variant="h5" sx={{ fontWeight: 800, letterSpacing: '-0.5px', color: 'primary.main' }}>
<Typography variant="h6" sx={{ fontWeight: 800, letterSpacing: '-0.5px', color: 'primary.main', fontSize: '1.25rem' }}>
{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 }}>
<Typography variant="caption" sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 0.2 }}>
<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>}
{isStarting && <Box component="span" sx={{ color: 'warning.light', fontWeight: 700, letterSpacing: 0.5, fontSize: '0.7rem' }}>{t('common.starting')}</Box>}
{isStopping && <Box component="span" sx={{ color: 'warning.light', fontWeight: 700, letterSpacing: 0.5, fontSize: '0.7rem' }}>{t('common.stopping')}</Box>}
{isRunning && <Box component="span" sx={{ color: 'success.main', fontWeight: 700, letterSpacing: 0.5, fontSize: '0.7rem' }}>{t('common.running')}</Box>}
{!isRunning && !isStarting && !isStopping && <Box component="span" sx={{ opacity: 0.5, fontWeight: 700, letterSpacing: 0.5, fontSize: '0.7rem' }}>{t('common.stopped')}</Box>}
</Typography>
</Box>
<Stack direction="row" spacing={2}>
<Stack direction="row" spacing={1.5}>
{isRunning && !isTransitioning && (
<Tooltip title={t('common.reload')}>
<IconButton onClick={() => onReloadService(serviceId)} sx={{ bgcolor: 'rgba(57, 167, 255, 0.05)', color: 'primary.main', border: '1px solid rgba(57, 167, 255, 0.1)', width: 48, height: 48, borderRadius: 3 }}>
<RefreshIcon />
<IconButton onClick={() => onReloadService(serviceId)} sx={{ bgcolor: 'rgba(57, 167, 255, 0.05)', color: 'primary.main', border: '1px solid rgba(57, 167, 255, 0.1)', width: 32, height: 32, borderRadius: 2 }}>
<RefreshIcon sx={{ fontSize: '1.1rem' }} />
</IconButton>
</Tooltip>
)}
@@ -307,9 +307,10 @@ export default function DashboardDetails({
variant="outlined"
color={isRunning ? 'error' : 'success'}
disabled={isTransitioning}
startIcon={isTransitioning ? <CircularProgress size={20} color="inherit" /> : isRunning ? <StopIcon /> : <StartIcon />}
size="small"
startIcon={isTransitioning ? <CircularProgress size={14} color="inherit" /> : isRunning ? <StopIcon sx={{ fontSize: '1.1rem' }} /> : <StartIcon sx={{ fontSize: '1.1rem' }} />}
onClick={() => onToggleService(serviceId)}
sx={{ borderRadius: 3, px: 4, height: 48, fontWeight: 800, fontSize: '1rem', boxShadow: 'none' }}
sx={{ borderRadius: 2, px: 2, height: 32, fontWeight: 800, fontSize: '0.75rem', boxShadow: 'none' }}
>
{isStarting ? t('common.starting') : (isStopping ? t('common.stopping') : (isRunning ? t('common.stop') : t('common.start')))}
</Button>
@@ -318,7 +319,7 @@ export default function DashboardDetails({
<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" sx={{
'.MuiTab-root': { textTransform: 'none', minHeight: 64, fontWeight: 700, fontSize: '0.95rem' }
'.MuiTab-root': { textTransform: 'none', minHeight: 48, fontWeight: 700, fontSize: '0.8rem' }
}}>
<Tab label={t('dashboard.overview')} value="overview" />
<Tab label={isPhp ? 'php.ini (Şablon)' : t('dashboard.service_settings')} value="settings_template" />
@@ -331,38 +332,37 @@ export default function DashboardDetails({
</Tabs>
</Box>
<Box sx={{ flexGrow: 1, overflowY: 'auto', p: 4 }}>
<Box sx={{ flexGrow: 1, overflowY: 'auto', p: 3 }}>
{activeTab === 'overview' && (
<Grid container spacing={4}>
<Grid container spacing={2}>
<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', 'Sistem İstatistikleri')}
<Typography variant="body2" sx={{ fontWeight: 800, mb: 1.5, color: 'primary.main', display: 'flex', alignItems: 'center', gap: 1, fontSize: '0.8rem' }}>
<StatusIcon sx={{ fontSize: '1rem' }} /> {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)' }}>
<Paper sx={{ p: 2, bgcolor: 'rgba(255,255,255,0.02)', borderRadius: 3, border: '1px solid rgba(255,255,255,0.05)' }}>
<ResourceMonitor cpu={currentStatus.cpu} memory={currentStatus.memory} />
</Paper>
</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', 'Servis Bilgileri')}
<Typography variant="body2" sx={{ fontWeight: 800, mb: 1.5, color: 'primary.main', display: 'flex', alignItems: 'center', gap: 1, fontSize: '0.8rem' }}>
<SettingsIcon sx={{ fontSize: '1rem' }} /> {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}>
<Paper sx={{ p: 2, bgcolor: 'rgba(255,255,255,0.02)', borderRadius: 3, border: '1px solid rgba(255,255,255,0.05)' }}>
<Stack spacing={1.5}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<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()}
</Typography>
</Paper>
<Typography variant="caption" sx={{ opacity: 0.5, fontWeight: 700, fontSize: '0.75rem' }}>{t('common.port', 'Port')}</Typography>
<Typography variant="body2" sx={{ fontWeight: 800, color: 'primary.main', fontFamily: 'monospace', fontSize: '0.75rem' }}>
{getPort()}
</Typography>
</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', 'Durum')}</Typography>
<Typography variant="caption" sx={{ opacity: 0.5, fontWeight: 700, fontSize: '0.75rem' }}>{t('common.status', 'Durum')}</Typography>
<Chip
label={isRunning ? t('common.running', 'Çalışıyor') : t('common.stopped', 'Durduruldu')}
size="small"
color={isRunning ? 'success' : 'error'}
sx={{ fontWeight: 900, borderRadius: 2 }}
sx={{ fontWeight: 900, borderRadius: 1.5, height: 20, fontSize: '0.65rem' }}
/>
</Box>
</Stack>
@@ -373,21 +373,22 @@ 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="caption" sx={{ fontWeight: 700, color: 'primary.main', letterSpacing: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: 'primary.main', letterSpacing: 1, fontSize: '0.7rem' }}>
{activeTab === 'settings_live' ? 'php.ini (Aktif Konfigürasyon)' : getTemplateName()}
</Typography>
<Button
variant="outlined"
size="large"
startIcon={isLoadingConfig ? <CircularProgress size={20} color="inherit" /> : <SaveIcon />}
size="small"
startIcon={isLoadingConfig ? <CircularProgress size={16} color="inherit" /> : <SaveIcon sx={{ fontSize: '1rem' }} />}
disabled={isLoadingConfig}
onClick={() => activeTab === 'settings_live' ? saveLiveConfig(serviceId!, liveConfigContent) : saveConfig(getTemplateName(), configContent)}
sx={{ borderRadius: 2.5, px: 4, fontWeight: 800 }}
sx={{ borderRadius: 2, px: 2, fontWeight: 800, fontSize: '0.75rem' }}
>
{t('common.save').toUpperCase()}
</Button>
</Box>
<Paper sx={{ flexGrow: 1, bgcolor: '#1a1a1a', borderRadius: 4, border: '1px solid rgba(255,255,255,0.05)', overflow: 'hidden', display: 'flex', flexDirection: 'column', minHeight: '60vh' }}>
{isLoadingConfig ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flexGrow: 1 }}><CircularProgress /></Box>
@@ -13,6 +13,7 @@ interface DashboardManagerProps {
t: any;
onFetchStatus: () => void;
onSelectProject: (projectId: string) => void;
runServiceOperation: (title: string, services: string[], operation: 'start' | 'stop') => void;
}
export default function DashboardManager({
@@ -23,53 +24,20 @@ export default function DashboardManager({
mariaDbVersions,
t,
onFetchStatus,
onSelectProject
onSelectProject,
runServiceOperation
}: DashboardManagerProps) {
const [selectedServiceId, setSelectedServiceId] = useState<string | null>('nginx');
const [isToggling, setIsToggling] = useState<string | null>(null);
const [toggledStates, setToggledStates] = useState<Record<string, string>>({});
const handleToggleService = async (service: string) => {
if (!window.api) return;
if (isToggling) return;
const currentStatus = status[service]?.status || 'stopped';
const isRunning = currentStatus === 'running';
const title = isRunning ? t('common.stopping') : t('common.starting');
const operation = isRunning ? 'stop' : 'start';
setIsToggling(service);
// Optimistically set transition state if not already there
setToggledStates(prev => ({ ...prev, [service]: isRunning ? 'stopping' : 'starting' }));
try {
const result = isRunning
? await window.api.stopService(service)
: await window.api.startService(service);
if (result && !result.success) {
Swal.fire({
title: t('common.error'),
text: result.message,
icon: 'error',
background: '#1e1e1e',
color: '#fff'
});
}
} finally {
setIsToggling(null);
setToggledStates(prev => {
const updated = { ...prev };
delete updated[service];
return updated;
});
onFetchStatus();
}
runServiceOperation(title, [service], operation);
};
// Merge actual status with our local transition states
const mergedStatus = { ...status };
Object.keys(toggledStates).forEach(service => {
mergedStatus[service] = { ...mergedStatus[service], status: toggledStates[service] };
});
const handleReloadService = async (service: string) => {
if (!window.api) return;
@@ -89,7 +57,7 @@ export default function DashboardManager({
return (
<Box sx={{ display: 'flex', height: 'calc(100vh - 120px)', bgcolor: 'background.default', overflow: 'hidden', borderRadius: 4, border: '1px solid rgba(255, 255, 255, 0.05)', boxShadow: '0 8px 32px rgba(0,0,0,0.2)' }}>
<DashboardSidebar
status={mergedStatus}
status={status}
phpVersions={phpVersions}
mariaDbVersions={mariaDbVersions}
selectedServiceId={selectedServiceId}
@@ -98,7 +66,7 @@ export default function DashboardManager({
/>
<DashboardDetails
serviceId={selectedServiceId}
status={mergedStatus}
status={status}
settings={settings}
projects={projects}
t={t}
@@ -190,64 +190,65 @@ const MariaDbDatabaseManager: React.FC<MariaDbDatabaseManagerProps> = ({ version
<DbIcon color="primary" />
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>{t('mariadb_manager.title')}</Typography>
</Box>
<Stack direction="row" spacing={2}>
<FormControl size="small" sx={{ minWidth: 200 }}>
<InputLabel id="version-select-label">{t('mariadb.version')}</InputLabel>
<Stack direction="row" spacing={1.5}>
<FormControl size="small" sx={{ minWidth: 160 }}>
<InputLabel id="version-select-label" sx={{ fontSize: '0.75rem' }}>{t('mariadb.version')}</InputLabel>
<Select
labelId="version-select-label"
value={selectedVersion}
label={t('mariadb.version')}
onChange={(e) => setSelectedVersion(e.target.value)}
sx={{ fontSize: '0.75rem', height: 32 }}
>
{installedVersions.map(v => (
<MenuItem key={v.id} value={v.id}>MariaDB {v.version}</MenuItem>
<MenuItem key={v.id} value={v.id} sx={{ fontSize: '0.75rem' }}>MariaDB {v.version}</MenuItem>
))}
</Select>
</FormControl>
<Button startIcon={<RefreshIcon />} onClick={fetchData} disabled={loading}>
<Button variant="outlined" size="small" startIcon={<RefreshIcon sx={{ fontSize: '1rem' }} />} onClick={fetchData} disabled={loading} sx={{ fontSize: '0.7rem', height: 32, borderRadius: 2 }}>
{t('common.refresh')}
</Button>
</Stack>
</Stack>
{!selectedVersion && (
<Alert severity="info" sx={{ mt: 2 }}>{t('mariadb_manager.no_active_version')}</Alert>
<Alert severity="info" sx={{ mt: 2, fontSize: '0.75rem' }}>{t('mariadb_manager.no_active_version')}</Alert>
)}
{selectedVersion && (
<Stack spacing={4}>
<Stack spacing={3}>
{/* Databases Section */}
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 800, color: 'primary.light' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 800, color: 'primary.light', fontSize: '0.8rem' }}>
{t('mariadb_manager.databases_title')}
</Typography>
<Button variant="outlined" size="small" startIcon={<AddIcon />} onClick={handleCreateDb}>
<Button variant="outlined" size="small" startIcon={<AddIcon sx={{ fontSize: '1rem' }} />} onClick={handleCreateDb} sx={{ fontSize: '0.7rem', height: 28, borderRadius: 1.5 }}>
{t('mariadb_manager.create_db_btn')}
</Button>
</Box>
<TableContainer component={Paper} variant="outlined" sx={{ bgcolor: 'rgba(0,0,0,0.2)', maxHeight: 400 }}>
<TableContainer component={Paper} variant="outlined" sx={{ bgcolor: 'rgba(0,0,0,0.2)', maxHeight: 300, borderRadius: 2 }}>
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 'bold' }}>{t('mariadb_manager.db_name')}</TableCell>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 'bold' }}>{t('mariadb_manager.db_size')}</TableCell>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 'bold', textAlign: 'right' }}>{t('common.actions')}</TableCell>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 800, fontSize: '0.7rem', py: 1 }}>{t('mariadb_manager.db_name')}</TableCell>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 800, fontSize: '0.7rem', py: 1 }}>{t('mariadb_manager.db_size')}</TableCell>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 800, fontSize: '0.7rem', py: 1, textAlign: 'right' }}>{t('common.actions')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{loading ? (
<TableRow><TableCell colSpan={3} align="center"><CircularProgress size={24} sx={{ my: 2 }} /></TableCell></TableRow>
<TableRow><TableCell colSpan={3} align="center"><CircularProgress size={20} sx={{ my: 1 }} /></TableCell></TableRow>
) : databases.length === 0 ? (
<TableRow><TableCell colSpan={3} align="center" sx={{ py: 3, color: 'text.secondary' }}>{t('common.no_data')}</TableCell></TableRow>
<TableRow><TableCell colSpan={3} align="center" sx={{ py: 2, color: 'text.secondary', fontSize: '0.7rem' }}>{t('common.no_data')}</TableCell></TableRow>
) : databases.map(db => (
<TableRow key={db.name}>
<TableCell sx={{ fontWeight: 'bold' }}>{db.name}</TableCell>
<TableCell>{db.size.toFixed(2)} MB</TableCell>
<TableCell align="right">
<TableCell sx={{ fontWeight: 600, fontSize: '0.75rem' }}>{db.name}</TableCell>
<TableCell sx={{ fontSize: '0.7rem', opacity: 0.8 }}>{db.size.toFixed(2)} MB</TableCell>
<TableCell align="right" sx={{ py: 0.5 }}>
<Tooltip title={t('common.delete')}>
<IconButton color="error" size="small" onClick={() => handleDeleteDb(db.name)}>
<DeleteIcon fontSize="small" />
<IconButton color="error" size="small" onClick={() => handleDeleteDb(db.name)} sx={{ p: 0.5 }}>
<DeleteIcon sx={{ fontSize: '1rem' }} />
</IconButton>
</Tooltip>
</TableCell>
@@ -258,50 +259,50 @@ const MariaDbDatabaseManager: React.FC<MariaDbDatabaseManagerProps> = ({ version
</TableContainer>
</Box>
<Divider sx={{ opacity: 0.1 }} />
<Divider sx={{ opacity: 0.05 }} />
{/* Users Section */}
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 800, color: 'secondary.light' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 800, color: 'secondary.light', fontSize: '0.8rem' }}>
{t('mariadb_manager.users_title')}
</Typography>
<Button variant="outlined" color="secondary" size="small" startIcon={<AddIcon />} onClick={handleCreateUser}>
<Button variant="outlined" color="secondary" size="small" startIcon={<AddIcon sx={{ fontSize: '1rem' }} />} onClick={handleCreateUser} sx={{ fontSize: '0.7rem', height: 28, borderRadius: 1.5 }}>
{t('mariadb_manager.create_user_btn')}
</Button>
</Box>
<TableContainer component={Paper} variant="outlined" sx={{ bgcolor: 'rgba(0,0,0,0.2)', maxHeight: 400 }}>
<TableContainer component={Paper} variant="outlined" sx={{ bgcolor: 'rgba(0,0,0,0.2)', maxHeight: 300, borderRadius: 2 }}>
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 'bold' }}>{t('mariadb_manager.user_label')}</TableCell>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 'bold' }}>Host</TableCell>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 'bold', textAlign: 'right' }}>{t('common.actions')}</TableCell>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 800, fontSize: '0.7rem', py: 1 }}>{t('mariadb_manager.user_label')}</TableCell>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 800, fontSize: '0.7rem', py: 1 }}>Host</TableCell>
<TableCell sx={{ bgcolor: '#1a1d21', fontWeight: 800, fontSize: '0.7rem', py: 1, textAlign: 'right' }}>{t('common.actions')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{loading ? (
<TableRow><TableCell colSpan={3} align="center"><CircularProgress size={24} sx={{ my: 2 }} /></TableCell></TableRow>
<TableRow><TableCell colSpan={3} align="center"><CircularProgress size={20} sx={{ my: 1 }} /></TableCell></TableRow>
) : users.length === 0 ? (
<TableRow><TableCell colSpan={3} align="center" sx={{ py: 3, color: 'text.secondary' }}>{t('common.no_data')}</TableCell></TableRow>
<TableRow><TableCell colSpan={3} align="center" sx={{ py: 2, color: 'text.secondary', fontSize: '0.7rem' }}>{t('common.no_data')}</TableCell></TableRow>
) : users.map(user => (
<TableRow key={`${user.user}-${user.host}`}>
<TableCell sx={{ fontWeight: 'bold' }}>
<TableCell sx={{ fontWeight: 600, fontSize: '0.75rem' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<UserIcon fontSize="inherit" sx={{ opacity: 0.5 }} />
<UserIcon sx={{ opacity: 0.5, fontSize: '0.9rem' }} />
{user.user}
</Box>
</TableCell>
<TableCell>
<TableCell sx={{ fontSize: '0.7rem', opacity: 0.8 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<HostIcon fontSize="inherit" sx={{ opacity: 0.5 }} />
<HostIcon sx={{ opacity: 0.5, fontSize: '0.9rem' }} />
{user.host}
</Box>
</TableCell>
<TableCell align="right">
<TableCell align="right" sx={{ py: 0.5 }}>
<Tooltip title={t('common.delete')}>
<IconButton color="error" size="small" onClick={() => handleDeleteUser(user.user, user.host)}>
<DeleteIcon fontSize="small" />
<IconButton color="error" size="small" onClick={() => handleDeleteUser(user.user, user.host)} sx={{ p: 0.5 }}>
<DeleteIcon sx={{ fontSize: '1rem' }} />
</IconButton>
</Tooltip>
</TableCell>
@@ -311,6 +312,7 @@ const MariaDbDatabaseManager: React.FC<MariaDbDatabaseManagerProps> = ({ version
</Table>
</TableContainer>
</Box>
</Stack>
)}
</Paper>
@@ -178,17 +178,17 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
}
}}
>
<DialogTitle sx={{ m: 0, p: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<DialogTitle sx={{ m: 0, p: 1.5, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Box>
<Typography variant="h6">{t('php.extensions.title', { version: phpVersion })}</Typography>
<Typography variant="caption" color="text.secondary">{t('php.extensions.desc')}</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 800 }}>{t('php.extensions.title', { version: phpVersion })}</Typography>
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.65rem' }}>{t('php.extensions.desc')}</Typography>
</Box>
<IconButton onClick={onClose} sx={{ color: 'text.secondary' }}>
<CloseIcon />
<IconButton onClick={onClose} size="small" sx={{ color: 'text.secondary' }}>
<CloseIcon fontSize="small" />
</IconButton>
</DialogTitle>
<DialogContent dividers sx={{ borderColor: 'rgba(255,255,255,0.1)', p: 0 }}>
<Box sx={{ p: 2, bgcolor: 'rgba(255,255,255,0.03)', display: 'flex', gap: 1 }}>
<DialogContent dividers sx={{ borderColor: 'rgba(255,255,255,0.05)', p: 0 }}>
<Box sx={{ p: 1.5, bgcolor: 'rgba(255,255,255,0.02)', display: 'flex', gap: 1 }}>
<TextField
fullWidth
size="small"
@@ -198,33 +198,34 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon sx={{ color: 'text.secondary' }} />
<SearchIcon sx={{ color: 'text.secondary', fontSize: '1rem' }} />
</InputAdornment>
),
sx: { color: '#fff', bgcolor: 'rgba(0,0,0,0.2)' }
sx: { color: '#fff', bgcolor: 'rgba(0,0,0,0.2)', fontSize: '0.75rem', height: 32 }
}}
/>
<Button
variant="outlined"
startIcon={<RawIcon />}
size="small"
startIcon={<RawIcon sx={{ fontSize: '1rem' }} />}
onClick={fetchRawConfig}
sx={{ whiteSpace: 'nowrap', borderColor: 'rgba(255,255,255,0.2)', color: '#fff' }}
sx={{ whiteSpace: 'nowrap', borderColor: 'rgba(255,255,255,0.1)', color: 'rgba(255,255,255,0.7)', fontSize: '0.7rem', height: 32, borderRadius: 1.5 }}
>
{t('php.extensions.raw_editor')}
</Button>
</Box>
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
<CircularProgress />
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress size={24} />
</Box>
) : (
<List sx={{ maxHeight: 400, overflow: 'auto' }}>
<List sx={{ maxHeight: 350, overflow: 'auto' }} dense>
{filteredExtensions.length === 0 ? (
<ListItem>
<ListItemText
primary={searchTerm ? t('php.extensions.not_found') : t('php.extensions.none')}
sx={{ textAlign: 'center', color: 'text.secondary' }}
primaryTypographyProps={{ sx: { textAlign: 'center', color: 'text.secondary', fontSize: '0.75rem' } }}
/>
</ListItem>
) : (
@@ -232,14 +233,15 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
<ListItem
key={ext.name}
divider
sx={{ borderColor: 'rgba(255,255,255,0.05)' }}
sx={{ borderColor: 'rgba(255,255,255,0.03)', py: 0.5 }}
secondaryAction={
saving === ext.name ? (
<CircularProgress size={24} />
<CircularProgress size={16} />
) : (
<Switch
edge="end"
checked={ext.enabled}
size="small"
onChange={(e) => handleToggle(ext.name, e.target.checked)}
color="primary"
/>
@@ -248,13 +250,13 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, fontSize: '0.75rem', fontWeight: 600 }}>
{ext.name}
{ext.enabled && <Chip label={t('php.extensions.active')} size="small" color="success" sx={{ height: 18, fontSize: '0.65rem' }} />}
{ext.enabled && <Chip label={t('php.extensions.active')} size="small" color="success" sx={{ height: 16, fontSize: '0.55rem', fontWeight: 900 }} />}
</Box>
}
secondary={ext.dll}
secondaryTypographyProps={{ sx: { color: 'text.secondary', fontSize: '0.75rem' } }}
secondaryTypographyProps={{ sx: { color: 'text.secondary', fontSize: '0.65rem', opacity: 0.5 } }}
/>
</ListItem>
))
@@ -262,11 +264,11 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
</List>
)}
</DialogContent>
<DialogActions sx={{ p: 2, bgcolor: 'rgba(255,255,255,0.02)' }}>
<Button onClick={fetchExtensions} startIcon={<RefreshIcon />} sx={{ mr: 'auto' }}>
<DialogActions sx={{ p: 1.5, bgcolor: 'rgba(255,255,255,0.01)' }}>
<Button onClick={fetchExtensions} size="small" startIcon={<RefreshIcon sx={{ fontSize: '0.9rem' }} />} sx={{ mr: 'auto', fontSize: '0.7rem' }}>
{t('common.refresh').toUpperCase()}
</Button>
<Button onClick={onClose} variant="outlined" color="primary">
<Button onClick={onClose} variant="outlined" size="small" sx={{ fontSize: '0.7rem', borderRadius: 1.5 }}>
{t('common.close').toUpperCase()}
</Button>
</DialogActions>
@@ -282,14 +284,16 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
bgcolor: '#1a1a1a',
color: '#fff',
borderRadius: 2,
backgroundImage: 'none',
border: '1px solid rgba(255,255,255,0.1)'
}
}}
>
<DialogTitle sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, p: 2 }}>
<DialogTitle sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, p: 1.5 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="h6">{t('php.extensions.ini_edit', { version: phpVersion })}</Typography>
<IconButton onClick={() => setIsRawEditorOpen(false)} sx={{ color: 'rgba(255,255,255,0.5)' }}>
<CloseIcon />
<Typography variant="subtitle2" sx={{ fontWeight: 800 }}>{t('php.extensions.ini_edit', { version: phpVersion })}</Typography>
<IconButton onClick={() => setIsRawEditorOpen(false)} size="small" sx={{ color: 'rgba(255,255,255,0.5)' }}>
<CloseIcon fontSize="small" />
</IconButton>
</Box>
<Typography
@@ -298,6 +302,8 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
color: 'primary.main',
wordBreak: 'break-all',
cursor: 'pointer',
fontSize: '0.6rem',
opacity: 0.7,
'&:hover': { textDecoration: 'underline', color: 'primary.light' }
}}
onClick={() => window.api.invoke('shell:open-path', configPath)}
@@ -306,7 +312,7 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
{configPath}
</Typography>
</DialogTitle>
<Box sx={{ px: 2, pb: 2, bgcolor: '#1a1a1a' }}>
<Box sx={{ px: 1.5, pb: 1.5, bgcolor: '#1a1a1a' }}>
<TextField
fullWidth
size="small"
@@ -316,37 +322,42 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon sx={{ color: 'text.secondary', fontSize: '1.2rem' }} />
<SearchIcon sx={{ color: 'text.secondary', fontSize: '1rem' }} />
</InputAdornment>
),
sx: {
color: '#fff',
bgcolor: 'rgba(255,255,255,0.05)',
'& fieldset': { borderColor: 'rgba(255,255,255,0.1)' }
bgcolor: 'rgba(255,255,255,0.03)',
fontSize: '0.75rem',
height: 32,
'& fieldset': { borderColor: 'rgba(255,255,255,0.05)' }
}
}}
/>
</Box>
<DialogContent sx={{ p: 0, minHeight: 400, borderTop: '1px solid rgba(255,255,255,0.1)' }}>
<DialogContent sx={{ p: 0, minHeight: 350, borderTop: '1px solid rgba(255,255,255,0.05)' }}>
<PhpIniCodeEditor
value={rawConfig}
onChange={setRawConfig}
/>
</DialogContent>
<DialogActions sx={{ p: 2, bgcolor: 'rgba(0,0,0,0.2)' }}>
<Button onClick={() => setIsRawEditorOpen(false)} sx={{ color: 'rgba(255,255,255,0.5)' }}>
<DialogActions sx={{ p: 1.5, bgcolor: 'rgba(0,0,0,0.1)' }}>
<Button onClick={() => setIsRawEditorOpen(false)} size="small" sx={{ color: 'rgba(255,255,255,0.4)', fontSize: '0.7rem' }}>
{t('common.cancel').toUpperCase()}
</Button>
<Button
onClick={handleSaveRaw}
variant="outlined"
color="primary"
startIcon={isSavingRaw ? <CircularProgress size={18} color="inherit" /> : <SaveIcon />}
size="small"
startIcon={isSavingRaw ? <CircularProgress size={14} color="inherit" /> : <SaveIcon sx={{ fontSize: '1rem' }} />}
disabled={isSavingRaw}
sx={{ fontSize: '0.7rem', borderRadius: 1.5, px: 2 }}
>
{isSavingRaw ? t('php.extensions.saving_btn') : t('php.extensions.save_restart_btn')}
</Button>
</DialogActions>
</Dialog>
</>
)
@@ -57,6 +57,7 @@ interface ProjectDetailsProps {
onOpenVSCode: () => void;
onManageDb: () => void;
onAddHost: (hostname: string) => void;
onRefreshProjects: () => void;
isTransitioning?: boolean;
setTransitioning?: (v: boolean) => void;
}
@@ -77,6 +78,7 @@ export default function ProjectDetails({
onOpenVSCode,
onManageDb,
onAddHost,
onRefreshProjects,
isTransitioning = false,
setTransitioning
}: ProjectDetailsProps) {
@@ -152,6 +154,7 @@ export default function ProjectDetails({
background: '#1e1e1e',
color: '#fff'
});
onRefreshProjects();
}
} catch (e: any) {
Swal.fire({
@@ -187,6 +190,7 @@ export default function ProjectDetails({
background: '#1e1e1e',
color: '#fff'
});
if (onRefreshProjects) onRefreshProjects();
}
} catch (error: any) {
Swal.fire({
@@ -243,9 +247,9 @@ export default function ProjectDetails({
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap' }}>
<Button
variant="outlined"
size="medium"
size="small"
disabled={isTransitioning}
startIcon={isTransitioning ? <CircularProgress size={18} color="inherit" /> : (isRunning ? <StopIcon /> : <StartIcon />)}
startIcon={isTransitioning ? <CircularProgress size={16} color="inherit" /> : (isRunning ? <StopIcon sx={{ fontSize: '1rem !important' }} /> : <StartIcon sx={{ fontSize: '1rem !important' }} />)}
color={isRunning ? 'error' : 'success'}
onClick={async () => {
if (setTransitioning) setTransitioning(true);
@@ -257,27 +261,27 @@ export default function ProjectDetails({
}
}
}}
sx={{ px: 3, height: 44, borderRadius: 2.5, fontWeight: 800, boxShadow: 'none' }}
sx={{ px: 2, height: 32, borderRadius: 1.5, fontWeight: 700, fontSize: '0.75rem', boxShadow: 'none' }}
>
{isTransitioning ? t('common.processing') : (isRunning ? t('projects.stop_site').toUpperCase() : t('projects.start_site').toUpperCase())}
</Button>
<Button
variant="outlined"
size="medium"
startIcon={<LaunchIcon />}
size="small"
startIcon={<LaunchIcon sx={{ fontSize: '1rem !important' }} />}
onClick={() => onOpenSite(isHostMapped)}
sx={{ px: 3, height: 44, borderRadius: 2.5, fontWeight: 700, borderColor: 'rgba(255,255,255,0.1)', '&:hover': { borderColor: 'primary.main', bgcolor: 'rgba(57, 167, 255, 0.05)' } }}
sx={{ px: 2, height: 32, borderRadius: 1.5, fontWeight: 600, fontSize: '0.75rem', borderColor: 'rgba(255,255,255,0.1)', '&:hover': { borderColor: 'primary.main', bgcolor: 'rgba(57, 167, 255, 0.05)' } }}
>
{t('projects.open_site').toUpperCase()}
</Button>
<Button
variant="outlined"
size="medium"
startIcon={<CodeIcon />}
size="small"
startIcon={<CodeIcon sx={{ fontSize: '1rem !important' }} />}
onClick={onOpenVSCode}
sx={{ px: 3, height: 44, borderRadius: 2.5, borderColor: 'rgba(255,255,255,0.1)', color: 'rgba(255,255,255,0.6)', fontWeight: 700, '&:hover': { borderColor: '#007ACC', color: '#007ACC', bgcolor: 'rgba(0, 122, 204, 0.05)' } }}
sx={{ px: 2, height: 32, borderRadius: 1.5, borderColor: 'rgba(255,255,255,0.1)', color: 'rgba(255,255,255,0.6)', fontWeight: 600, fontSize: '0.75rem', '&:hover': { borderColor: '#007ACC', color: '#007ACC', bgcolor: 'rgba(0, 122, 204, 0.05)' } }}
>
VS CODE
</Button>
@@ -285,13 +289,13 @@ export default function ProjectDetails({
{!isHostMapped && (
<Button
variant="outlined"
size="medium"
size="small"
color="warning"
onClick={() => {
const finalHost = p.host.includes('.') ? p.host : `${p.host}.local`;
onAddHost(finalHost);
}}
sx={{ px: 3, height: 44, borderRadius: 2.5, fontWeight: 800, boxShadow: 'none' }}
sx={{ px: 2, height: 32, borderRadius: 1.5, fontWeight: 700, fontSize: '0.75rem', boxShadow: 'none' }}
>
HOSTS DOSYASINA EKLE
</Button>
@@ -305,17 +309,17 @@ export default function ProjectDetails({
<IconButton
onClick={onDelete}
sx={{
width: 48,
height: 48,
width: 40,
height: 40,
bgcolor: 'rgba(255, 68, 68, 0.05)',
color: 'error.main',
border: '1px solid rgba(255, 68, 68, 0.1)',
borderRadius: 3,
borderRadius: 1.5,
'&:hover': { bgcolor: 'error.main', color: '#fff', transform: 'scale(1.05) rotate(5deg)', boxShadow: '0 4px 12px rgba(255, 68, 68, 0.3)' },
transition: 'all 0.2s'
}}
>
<DeleteIcon />
<DeleteIcon sx={{ fontSize: '1.25rem' }} />
</IconButton>
</Tooltip>
</Box>
@@ -324,9 +328,9 @@ export default function ProjectDetails({
{/* Tabs */}
<Box sx={{ borderBottom: 1, borderColor: 'rgba(255,255,255,0.05)', px: 4, bgcolor: 'rgba(255,255,255,0.01)' }}>
<Tabs value={projectDetailTab} onChange={(_e, v) => setProjectDetailTab(v)} textColor="primary" indicatorColor="primary">
<Tab label={t('projects.overview')} sx={{ fontWeight: 700, py: 2.5, minWidth: 120 }} />
<Tab label={t('projects.database')} sx={{ fontWeight: 700, py: 2.5, minWidth: 120 }} />
<Tab label={t('projects.server_settings')} sx={{ fontWeight: 700, py: 2.5, minWidth: 120 }} />
<Tab label={t('projects.overview')} sx={{ fontWeight: 700, py: 2.5, minWidth: 120, fontSize: '0.8rem' }} />
<Tab label={t('projects.database')} sx={{ fontWeight: 700, py: 2.5, minWidth: 120, fontSize: '0.8rem' }} />
<Tab label={t('projects.server_settings')} sx={{ fontWeight: 700, py: 2.5, minWidth: 120, fontSize: '0.8rem' }} />
</Tabs>
</Box>
@@ -338,8 +342,8 @@ export default function ProjectDetails({
{/* Editable Form Side */}
<Grid item xs={12} md={7}>
<Paper sx={{ p: 3, borderRadius: 4, bgcolor: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.05)' }}>
<Typography variant="h6" sx={{ mb: 3, fontWeight: 800, color: 'primary.main', display: 'flex', alignItems: 'center', gap: 1.5 }}>
<SettingsIcon /> {t('projects.edit_details', 'Proje Detaylarını Düzenle')}
<Typography variant="h6" sx={{ mb: 3, fontWeight: 800, color: 'primary.main', display: 'flex', alignItems: 'center', gap: 1.5, fontSize: '1rem' }}>
<SettingsIcon sx={{ fontSize: '1.25rem' }} /> {t('projects.edit_details', 'Proje Detaylarını Düzenle')}
</Typography>
<Stack spacing={3}>
@@ -347,60 +351,62 @@ export default function ProjectDetails({
label={t('projects.site_name')}
fullWidth
variant="filled"
size="small"
value={editForm.name}
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' } }}
sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' }, '& .MuiInputLabel-root': { fontSize: '0.85rem' }, '& .MuiInputBase-input': { fontSize: '0.85rem' } }}
/>
<TextField
label={t('projects.site_domain')}
fullWidth
variant="filled"
size="small"
value={editForm.host}
onChange={(e) => setEditForm({ ...editForm, host: e.target.value })}
helperText={t('projects.host_helper', 'Domain adı (örn: myproject)')}
sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' } }}
sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' }, '& .MuiInputLabel-root': { fontSize: '0.85rem' }, '& .MuiInputBase-input': { fontSize: '0.85rem' }, '& .MuiFormHelperText-root': { fontSize: '0.7rem' } }}
/>
<Grid container spacing={2}>
<Grid item xs={6}>
<FormControl fullWidth variant="filled">
<InputLabel>{t('projects.web_server')}</InputLabel>
<FormControl fullWidth variant="filled" size="small">
<InputLabel sx={{ fontSize: '0.85rem' }}>{t('projects.web_server')}</InputLabel>
<Select
value={editForm.serverType}
onChange={(e) => setEditForm({ ...editForm, serverType: e.target.value })}
sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' } }}
sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' }, fontSize: '0.85rem' }}
>
<MenuItem value="nginx">Nginx</MenuItem>
<MenuItem value="apache">Apache</MenuItem>
<MenuItem value="nginx" sx={{ fontSize: '0.85rem' }}>Nginx</MenuItem>
<MenuItem value="apache" sx={{ fontSize: '0.85rem' }}>Apache</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={6}>
<FormControl fullWidth variant="filled">
<InputLabel>{t('projects.php_version')}</InputLabel>
<FormControl fullWidth variant="filled" size="small">
<InputLabel sx={{ fontSize: '0.85rem' }}>{t('projects.php_version')}</InputLabel>
<Select
value={editForm.phpVersion}
onChange={(e) => setEditForm({ ...editForm, phpVersion: e.target.value })}
sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' } }}
sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' }, fontSize: '0.85rem' }}
>
{installedPhp.map(v => (
<MenuItem key={v.version} value={v.version}>PHP {v.version}</MenuItem>
<MenuItem key={v.version} value={v.version} sx={{ fontSize: '0.85rem' }}>PHP {v.version}</MenuItem>
))}
</Select>
</FormControl>
</Grid>
</Grid>
<FormControl fullWidth variant="filled">
<InputLabel>{t('projects.mariadb_version')}</InputLabel>
<FormControl fullWidth variant="filled" size="small">
<InputLabel sx={{ fontSize: '0.85rem' }}>{t('projects.mariadb_version')}</InputLabel>
<Select
value={editForm.mariaDbVersion}
onChange={(e) => setEditForm({ ...editForm, mariaDbVersion: e.target.value })}
sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' } }}
sx={{ borderRadius: 2, '& .MuiFilledInput-root': { bgcolor: 'rgba(0,0,0,0.2)' }, fontSize: '0.85rem' }}
>
{installedMariaDb.map(v => (
<MenuItem key={v.version} value={v.version}>MariaDB {v.version}</MenuItem>
<MenuItem key={v.version} value={v.version} sx={{ fontSize: '0.85rem' }}>MariaDB {v.version}</MenuItem>
))}
</Select>
</FormControl>
@@ -408,11 +414,11 @@ export default function ProjectDetails({
<Box sx={{ pt: 1, display: 'flex', justifyContent: 'flex-start' }}>
<Button
variant="outlined"
size="large"
startIcon={isSaving ? <CircularProgress size={20} color="inherit" /> : <SaveIcon />}
size="small"
startIcon={isSaving ? <CircularProgress size={16} color="inherit" /> : <SaveIcon sx={{ fontSize: '1rem !important' }} />}
onClick={handleProjectUpdate}
disabled={isSaving}
sx={{ px: 6, py: 1.5, borderRadius: 3, fontWeight: 800, fontSize: '1rem', color: 'primary.main', border: '1px solid rgba(33, 150, 243, 0.5)', '&:hover': { bgcolor: 'rgba(33, 150, 243, 0.05)', borderColor: 'primary.main' } }}
sx={{ px: 4, py: 1, borderRadius: 2, fontWeight: 700, fontSize: '0.75rem', color: 'primary.main', border: '1px solid rgba(33, 150, 243, 0.5)', '&:hover': { bgcolor: 'rgba(33, 150, 243, 0.05)', borderColor: 'primary.main' } }}
>
{t('common.save').toUpperCase()}
</Button>
@@ -424,8 +430,8 @@ export default function ProjectDetails({
{/* Status Monitor Side */}
<Grid item xs={12} md={5}>
<Paper sx={{ p: 3, borderRadius: 4, bgcolor: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.05)', height: '100%' }}>
<Typography variant="subtitle1" sx={{ mb: 4, fontWeight: 800, display: 'flex', alignItems: 'center', gap: 1.5, color: 'rgba(255,255,255,0.6)' }}>
<StatusIcon sx={{ color: 'secondary.main' }} /> {t('dashboard.status')}
<Typography variant="subtitle1" sx={{ mb: 4, fontWeight: 800, display: 'flex', alignItems: 'center', gap: 1.5, color: 'rgba(255,255,255,0.6)', fontSize: '0.9rem' }}>
<StatusIcon sx={{ color: 'secondary.main', fontSize: '1.25rem' }} /> {t('dashboard.status')}
</Typography>
<Box sx={{ mb: 4 }}>
@@ -439,10 +445,10 @@ export default function ProjectDetails({
<Divider sx={{ mb: 3, opacity: 0.1 }} />
<Box sx={{ p: 2.5, borderRadius: 3, bgcolor: 'rgba(57, 167, 255, 0.03)', border: '1px solid rgba(57, 167, 255, 0.1)' }}>
<Typography variant="caption" sx={{ color: 'primary.light', fontWeight: 800, display: 'flex', alignItems: 'center', gap: 1, mb: 1.5, textTransform: 'uppercase', letterSpacing: 1 }}>
<Typography variant="caption" sx={{ color: 'primary.light', fontWeight: 800, display: 'flex', alignItems: 'center', gap: 1, mb: 1.5, textTransform: 'uppercase', letterSpacing: 1, fontSize: '0.7rem' }}>
<CodeIcon fontSize="small" /> System Info
</Typography>
<Typography variant="body2" sx={{ opacity: 0.5, lineHeight: 1.6 }}>
<Typography variant="body2" sx={{ opacity: 0.5, lineHeight: 1.6, fontSize: '0.75rem' }}>
{t('projects.manual_edit_help', 'Bu sayfadaki değişiklikler kaydedildiğinde servis yapılandırmaları ve hosts dosyası otomatik olarak yenilenecektir.')}
</Typography>
</Box>
@@ -458,22 +464,22 @@ export default function ProjectDetails({
<Paper sx={{ p: 4, borderRadius: 4, bgcolor: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.05)' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Avatar sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)', color: 'secondary.main' }}>
<Avatar sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)', color: 'secondary.main', width: 48, height: 48 }}>
<DbIcon />
</Avatar>
<Box>
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 800, letterSpacing: 1 }}>{t('projects.mariadb_version')}</Typography>
<Typography variant="h6" sx={{ fontWeight: 700 }}>MariaDB {p.mariadbVersion}</Typography>
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 800, letterSpacing: 1, fontSize: '0.65rem' }}>{t('projects.mariadb_version')}</Typography>
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: '1rem' }}>MariaDB {p.mariaDbVersion}</Typography>
</Box>
</Box>
<Button
variant="outlined"
size="medium"
startIcon={<OpenIcon />}
size="small"
startIcon={<OpenIcon sx={{ fontSize: '1rem !important' }} />}
onClick={onManageDb}
sx={{ borderRadius: 2.5, px: 3, height: 44, fontWeight: 700 }}
sx={{ borderRadius: 1.5, px: 2, height: 32, fontWeight: 700, fontSize: '0.75rem' }}
>
{t('mariadb_manager.title')}
{t('mariadb_manager.title').toUpperCase()}
</Button>
</Box>
@@ -487,8 +493,8 @@ export default function ProjectDetails({
{ label: 'Password', value: '(no password)', italic: true }
].map((item, idx) => (
<Grid item xs={6} key={idx}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700, display: 'block', mb: 0.5 }}>{item.label.toUpperCase()}</Typography>
<Typography variant="body1" sx={{ fontWeight: 600, color: item.italic ? 'rgba(255,255,255,0.3)' : '#fff', fontStyle: item.italic ? 'italic' : 'normal' }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.3)', fontWeight: 700, display: 'block', mb: 0.5, fontSize: '0.65rem' }}>{item.label.toUpperCase()}</Typography>
<Typography variant="body1" sx={{ fontWeight: 600, color: item.italic ? 'rgba(255,255,255,0.3)' : '#fff', fontStyle: item.italic ? 'italic' : 'normal', fontSize: '0.85rem' }}>
{item.value}
</Typography>
</Grid>
@@ -501,19 +507,19 @@ export default function ProjectDetails({
{projectDetailTab === 2 && (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ p: 2.5, bgcolor: 'rgba(255,255,255,0.02)', borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Box sx={{ p: 2, bgcolor: 'rgba(255,255,255,0.02)', borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<ServerIcon color="primary" />
<Typography variant="subtitle2" sx={{ fontWeight: 800, textTransform: 'uppercase', letterSpacing: 1 }}>
<ServerIcon color="primary" sx={{ fontSize: '1.25rem' }} />
<Typography variant="subtitle2" sx={{ fontWeight: 800, textTransform: 'uppercase', letterSpacing: 1, fontSize: '0.75rem' }}>
{p.serverType} {t('projects.server_settings')}
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 1.5 }}>
<Button
size="small"
startIcon={<TuningIcon />}
startIcon={<TuningIcon sx={{ fontSize: '1rem !important' }} />}
onClick={loadConfig}
sx={{ color: 'rgba(255,255,255,0.5)', borderRadius: 2 }}
sx={{ color: 'rgba(255,255,255,0.5)', borderRadius: 1.5, fontSize: '0.75rem' }}
>
{t('common.refresh')}
</Button>
@@ -521,24 +527,24 @@ export default function ProjectDetails({
variant="outlined"
size="small"
color="primary"
startIcon={isConfigSaving ? <CircularProgress size={16} color="inherit" /> : <SaveIcon />}
startIcon={isConfigSaving ? <CircularProgress size={14} color="inherit" /> : <SaveIcon sx={{ fontSize: '1rem !important' }} />}
disabled={isConfigSaving || isConfigLoading}
onClick={handleSaveConfig}
sx={{ px: 4, borderRadius: 2, fontWeight: 800 }}
sx={{ px: 3, height: 32, borderRadius: 1.5, fontWeight: 800, fontSize: '0.75rem' }}
>
{t('common.save').toUpperCase()}
</Button>
</Box>
</Box>
<Alert severity="info" sx={{ borderRadius: 0, py: 1, bgcolor: 'rgba(57, 167, 255, 0.05)', color: 'primary.light', borderBottom: '1px solid rgba(255,255,255,0.05)', fontWeight: 600 }}>
<Alert severity="info" sx={{ borderRadius: 0, py: 0.5, bgcolor: 'rgba(57, 167, 255, 0.05)', color: 'primary.light', borderBottom: '1px solid rgba(255,255,255,0.05)', fontWeight: 600, '& .MuiAlert-message': { fontSize: '0.75rem' } }}>
{t('projects.manual_edit_warning')}
</Alert>
<Box sx={{ flexGrow: 1, position: 'relative', overflow: 'hidden' }}>
{isConfigLoading ? (
<Box sx={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', bgcolor: 'rgba(0,0,0,0.5)', zIndex: 10 }}>
<CircularProgress />
<CircularProgress size={32} />
</Box>
) : null}
<ProjectConfigEditor
@@ -24,6 +24,7 @@ interface ProjectManagerProps {
onOpenVSCode: (path: string) => void;
onAddHost: (hostname: string) => void;
onManageDb: () => void;
onRefreshProjects: () => void;
}
export default function ProjectManager({
@@ -47,7 +48,8 @@ export default function ProjectManager({
onOpenFolder,
onOpenVSCode,
onAddHost,
onManageDb
onManageDb,
onRefreshProjects
}: ProjectManagerProps) {
// Defensive check: find selected project
@@ -89,6 +91,7 @@ export default function ProjectManager({
onOpenVSCode={() => onOpenVSCode(p.path)}
onAddHost={onAddHost}
onManageDb={onManageDb}
onRefreshProjects={onRefreshProjects}
/>
</Box>
);
+2
View File
@@ -60,6 +60,8 @@
"quick_look": "Quick Look",
"start_all": "Start All",
"stop_all": "Stop All",
"starting_all": "Starting All Services",
"stopping_all": "Stopping All Services",
"all_started": "All services started.",
"all_stopped": "All services stopped.",
"overview": "Overview",
+2
View File
@@ -61,6 +61,8 @@
"quick_look": "Hızlı Bakış",
"start_all": "Tümünü Başlat",
"stop_all": "Tümünü Durdur",
"starting_all": "Tüm Servisler Başlatılıyor",
"stopping_all": "Tüm Servisler Durduruluyor",
"all_started": "Tüm servisler başlatıldı.",
"all_stopped": "Tüm servisler durduruldu.",
"overview": "Genel Bakış",