feat: implement dashboard UI components, IPC handlers, and service configuration templates

This commit is contained in:
Ümit Tunç
2026-04-03 07:06:39 +03:00
parent e7be5c1c19
commit ee4e3660f0
8 changed files with 436 additions and 21 deletions
+3
View File
@@ -258,6 +258,7 @@ async function rebuildApacheConfig(forceProjects: boolean = false): Promise<{ bi
if (!normalizedPath.endsWith('/')) normalizedPath += '/'
const phpDir = path.join(configService.getBasePath(), 'bin', `php-${p.phpVersion}`)
const phpIniDir = path.join(configService.getBasePath(), 'generated_configs', 'php', p.phpVersion)
const phpCgiBinary = findExecutable(phpDir, process.platform === 'win32' ? 'php-cgi.exe' : 'php-cgi') || ''
// Auto-suffix hostname if it's a simple slug
@@ -268,6 +269,7 @@ async function rebuildApacheConfig(forceProjects: boolean = false): Promise<{ bi
PATH: normalizedPath,
PHP_PORT: phpPort.toString(),
PHP_DIR: phpDir.replace(/\\/g, '/'),
PHP_INI_DIR: phpIniDir.replace(/\\/g, '/'),
PHP_CGI_PATH: phpCgiBinary.replace(/\\/g, '/'),
PHP_BIN_URL: `/php-bin-${p.host}`,
PORT: settings.apachePort || '8080'
@@ -279,6 +281,7 @@ async function rebuildApacheConfig(forceProjects: boolean = false): Promise<{ bi
PATH: normalizedPath,
PHP_PORT: phpPort.toString(),
PHP_DIR: phpDir.replace(/\\/g, '/'),
PHP_INI_DIR: phpIniDir.replace(/\\/g, '/'),
PHP_CGI_PATH: phpCgiBinary.replace(/\\/g, '/'),
PHP_BIN_URL: `/php-bin-alias-${p.host}`,
PORT: settings.apachePort || '8080'
@@ -202,6 +202,9 @@ export default function DashboardDetails({
return 'n/a';
};
const isRunning = currentStatus.status === 'running';
const isStarting = currentStatus.status === 'starting';
const isStopping = currentStatus.status === 'stopping';
const isTransitioning = isStarting || isStopping;
const serviceProjects = projects.filter(p => {
if (serviceId === 'nginx') return !p.serverType || p.serverType === 'nginx';
@@ -244,11 +247,14 @@ export default function DashboardDetails({
<Box sx={{ flexGrow: 1 }}>
<Typography variant="h5" sx={{ fontWeight: 800 }}>{serviceId.toUpperCase()}</Typography>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.4)' }}>
{isRunning ? t('common.running', 'Çalışıyor') : t('common.stopped', 'Durduruldu')}
{isStarting && <Box component="span" sx={{ color: 'warning.light' }}>{t('common.starting', 'Başlatılıyor...')}</Box>}
{isStopping && <Box component="span" sx={{ color: 'warning.light' }}>{t('common.stopping', 'Durduruluyor...')}</Box>}
{isRunning && <Box component="span" sx={{ color: 'success.light' }}>{t('common.running', 'Çalışıyor')}</Box>}
{!isRunning && !isStarting && !isStopping && <Box component="span" sx={{ opacity: 0.5 }}>{t('common.stopped', 'Durduruldu')}</Box>}
</Typography>
</Box>
<Stack direction="row" spacing={1}>
{isRunning && (
{isRunning && !isTransitioning && (
<IconButton onClick={() => onReloadService(serviceId)} sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)', color: 'primary.main' }}>
<RefreshIcon />
</IconButton>
@@ -256,11 +262,16 @@ export default function DashboardDetails({
<Button
variant="contained"
color={isRunning ? 'error' : 'success'}
startIcon={isRunning ? <StopIcon /> : <StartIcon />}
disabled={isTransitioning}
startIcon={
isTransitioning
? <CircularProgress size={20} color="inherit" />
: isRunning ? <StopIcon /> : <StartIcon />
}
onClick={() => onToggleService(serviceId)}
sx={{ borderRadius: 2 }}
sx={{ borderRadius: 2, minWidth: 100 }}
>
{isRunning ? t('common.stop', 'Durdur') : t('common.start', 'Başlat')}
{isStarting ? t('common.starting', '...') : (isStopping ? t('common.stopping', '...') : (isRunning ? t('common.stop', 'Durdur') : t('common.start', 'Başlat')))}
</Button>
</Stack>
</Box>
@@ -26,29 +26,51 @@ export default function DashboardManager({
onSelectProject
}: 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';
setIsToggling(service);
// Optimistically set transition state if not already there
setToggledStates(prev => ({ ...prev, [service]: isRunning ? 'stopping' : 'starting' }));
const result = isRunning
? await window.api.stopService(service)
: await window.api.startService(service);
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'
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();
}
onFetchStatus();
};
// 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;
const result = await window.api.invoke('services:reload', service);
@@ -67,7 +89,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={status}
status={mergedStatus}
phpVersions={phpVersions}
mariaDbVersions={mariaDbVersions}
selectedServiceId={selectedServiceId}
@@ -76,7 +98,7 @@ export default function DashboardManager({
/>
<DashboardDetails
serviceId={selectedServiceId}
status={status}
status={mergedStatus}
settings={settings}
projects={projects}
t={t}
@@ -72,10 +72,17 @@ export default function DashboardSidebar({
height: 10,
borderRadius: '50%',
bgcolor: isStarting ? 'warning.main' : (isRunning ? 'success.main' : 'error.main'),
boxShadow: isRunning ? '0 0 8px rgba(76, 175, 80, 0.5)' : 'none',
boxShadow: isRunning ? '0 0 8px rgba(76, 175, 80, 0.5)' : (isStarting ? '0 0 8px rgba(255, 177, 66, 0.5)' : 'none'),
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
justifyContent: 'center',
position: 'relative',
'@keyframes pulse': {
'0%': { transform: 'scale(1)', opacity: 1 },
'50%': { transform: 'scale(1.2)', opacity: 0.7 },
'100%': { transform: 'scale(1)', opacity: 1 }
},
animation: isStarting ? 'pulse 1.5s infinite ease-in-out' : 'none'
}}>
{isStarting && <CircularProgress size={8} thickness={6} color="inherit" />}
</Box>