feat: implement multi-service management dashboard with project, database, and PHP extension support
This commit is contained in:
@@ -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
@@ -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 AÇ
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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ış",
|
||||
|
||||
Reference in New Issue
Block a user