feat: implement dashboard sidebar and service management infrastructure

This commit is contained in:
Ümit Tunç
2026-04-03 06:51:16 +03:00
parent 2c2399a157
commit cdb36196e5
7 changed files with 2194 additions and 428 deletions
+20
View File
@@ -337,6 +337,26 @@ async function restartApache(): Promise<{ success: boolean; message: string }> {
}
export function registerIpcHandlers(): void {
console.log('Registering config:template IPC handlers...')
ipcMain.handle('config:template:get', async (_event, templateName: string) => {
try {
console.log('Fetching template:', templateName)
return { success: true, content: configService.getTemplate(templateName) }
} catch (error: any) {
console.error('Failed to get template:', error)
return { success: false, message: error.message }
}
})
ipcMain.handle('config:template:save', async (_event, { templateName, content }) => {
try {
configService.saveTemplate(templateName, content)
return { success: true, message: 'Şablon başarıyla kaydedildi.' }
} catch (error: any) {
return { success: false, message: error.message }
}
})
ipcMain.handle('mariadb:processlist', async (_event, versionId) => {
try {
return await mariaDbManagerService.getProcessList(versionId)
+20
View File
@@ -71,6 +71,26 @@ export class ConfigService {
return path.join(this.configDir, targetName)
}
getTemplate(templateName: string): string {
const templatePath = path.join(this.templateDir, templateName)
if (!fs.existsSync(templatePath)) {
throw new Error(`Template not found: ${templatePath}`)
}
return fs.readFileSync(templatePath, 'utf-8')
}
saveTemplate(templateName: string, content: string): void {
const templatePath = path.join(this.templateDir, templateName)
const backupPath = `${templatePath}.bak`
// Simple backup
if (fs.existsSync(templatePath)) {
fs.copyFileSync(templatePath, backupPath)
}
fs.writeFileSync(templatePath, content)
}
getSettings(): any {
try {
if (fs.existsSync(this.settingsPath)) {
+67 -428
View File
@@ -71,7 +71,6 @@ import {
Close as CloseIcon,
ContentCopy as ContentCopyIcon,
OpenInNew as OpenIcon,
Restore as ResetIcon,
Tune as TuningIcon,
History as HistoryIcon,
MenuBook as GuideIcon,
@@ -90,6 +89,7 @@ import ServerTuning from './components/ServerTuning'
import ServiceEventMonitor from './components/ServiceEventMonitor'
import UserGuide from './components/UserGuide'
import ProjectManager from './components/ProjectManager/ProjectManager'
import DashboardManager from './components/Dashboard/DashboardManager'
declare global {
interface Window {
@@ -135,7 +135,6 @@ function App(): JSX.Element {
const [projectSearch, setProjectSearch] = useState('')
const [cliConfig, setCliConfig] = useState<any>({ enabled: true, defaultPhp: '', defaultMariaDb: '', binDir: '' })
const [settingsTab, setSettingsTab] = useState(0)
const [dashboardTab, setDashboardTab] = useState(0)
const [projects, setProjects] = useState<any[]>([])
const [phpVersions, setPhpVersions] = useState<any[]>([])
const [mariaDbVersions, setMariaDbVersions] = useState<any[]>([])
@@ -190,6 +189,11 @@ function App(): JSX.Element {
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
const [projectDetailTab, setProjectDetailTab] = useState(0)
const handleSelectProject = (projectId: string) => {
setSelectedProjectId(projectId)
setActiveTab('projects')
}
const parseNginxError = (message: string) => {
const match = message.match(/in (.*):(\d+)/)
if (!match) return null
@@ -270,9 +274,10 @@ function App(): JSX.Element {
}
const fetchApacheVersions = async () => {
if (!window.api || !window.api.getApacheVersions) return
const v = await window.api.getApacheVersions()
setApacheVersions(v || [])
if (window.api && window.api.getApacheVersions) {
const list = await window.api.getApacheVersions()
setApacheVersions(list || [])
}
}
const fetchLogs = async (service: string) => {
@@ -385,6 +390,49 @@ function App(): JSX.Element {
}
}, [logs, logDialogOpen])
const handleOpenNginxConfig = async () => {
if (window.api && window.api.invoke) {
const config = await window.api.invoke('nginx:config:get')
setNginxConfig(config)
setIsNginxConfigOpen(true)
}
}
const handleSaveNginxConfig = async () => {
if (window.api && window.api.invoke) {
const result = await window.api.invoke('nginx:config:save', nginxConfig)
if (result.success) {
Swal.fire({ title: t('common.success'), text: t('server.nginx.config_saved'), icon: 'success', background: '#1e1e1e', color: '#fff' })
setIsNginxConfigOpen(false)
}
}
}
const handleOpenApacheConfig = async () => {
if (window.api && window.api.invoke) {
const config = await window.api.invoke('apache:config:get')
setApacheConfig(config)
setIsApacheConfigOpen(true)
}
}
const handleSaveApacheConfig = async () => {
if (window.api && window.api.invoke) {
const result = await window.api.invoke('apache:config:save', apacheConfig)
if (result.success) {
Swal.fire({ title: t('common.success'), text: t('server.apache.config_saved'), icon: 'success', background: '#1e1e1e', color: '#fff' })
setIsApacheConfigOpen(false)
}
}
}
const handleTestApacheConfig = async () => {
if (window.api && window.api.invoke) {
const result = await window.api.invoke('apache:config:test')
Swal.fire({ title: result.success ? t('common.success') : t('common.error'), text: result.message, icon: result.success ? 'success' : 'error', background: '#1e1e1e', color: '#fff' })
}
}
const handleSelectPath = async () => {
if (window.api && window.api.selectDirectory) {
const pathStr = await window.api.selectDirectory()
@@ -396,76 +444,6 @@ function App(): JSX.Element {
}
}
const handleToggleService = async (service: string) => {
if (!window.api) return
const isStarting = ((status[service] as any)?.status || status[service] || 'stopped') === 'stopped'
if (isStarting) {
setStatus(prev => ({ ...prev, [service]: { status: 'starting' } }))
}
const result = ((status[service] as any)?.status || status[service] || 'stopped') === 'running'
? await window.api.stopService(service)
: await window.api.startService(service)
// Refresh status after operation
fetchStatus()
if (!result.success) {
Swal.fire({
title: t('common.error'),
text: result.message,
icon: 'error',
confirmButtonText: t('common.ok'),
background: '#1e1e1e',
color: '#fff',
confirmButtonColor: '#153E5E',
customClass: {
popup: 'swal2-dark-popup'
}
})
} else {
setNotification({
open: true,
message: result.message,
severity: 'success'
})
}
}
const handleOpenLogs = async (service: string) => {
setSelectedService(service)
setLogDialogOpen(true)
await fetchLogs(service)
}
const handleResetData = async (service: string) => {
const confirmResult = await Swal.fire({
title: t('common.reset_confirm_title'),
text: t('common.reset_confirm_text', { service }),
icon: 'warning',
showCancelButton: true,
confirmButtonText: t('common.reset_btn'),
cancelButtonText: t('common.cancel'),
background: '#1e1e1e',
color: '#fff',
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6'
})
if (confirmResult.isConfirmed) {
const result = await window.api.invoke('services:reset-data', service)
Swal.fire({
title: result.success ? t('common.success') : t('common.error'),
text: result.message,
icon: result.success ? 'success' : 'error',
background: '#1e1e1e',
color: '#fff'
})
fetchStatus()
}
}
const handleSaveSettings = async () => {
if (window.api && window.api.saveSettings) {
const result = await window.api.saveSettings(settings)
@@ -532,74 +510,6 @@ function App(): JSX.Element {
return `${Math.round(mb)} MB`
}
const getStatusChip = (s: string | { status: string, cpu?: number, memory?: number }) => {
const state = typeof s === 'string' ? s : s.status
if (state === 'starting') {
return <Chip label={t('common.starting')} color="warning" size="small" sx={{ fontWeight: 'bold' }} icon={<CircularProgress size={14} color="inherit" />} />
}
if (state === 'stopping') {
return <Chip label={t('common.stopping')} color="warning" size="small" sx={{ fontWeight: 'bold' }} icon={<CircularProgress size={14} color="inherit" />} />
}
const color = state === 'running' ? 'success' : state === 'stopped' ? 'error' : 'warning'
return <Chip label={t(`common.${state}`).toUpperCase()} color={color} size="small" sx={{ fontWeight: 'bold' }} />
}
const ResourceMonitor = ({ cpu, memory }: { cpu?: number; memory?: number }) => {
if (cpu === undefined && memory === undefined) return null;
const ramMB = memory ? Math.round(memory / (1024 * 1024)) : 0;
const ramLimit = 1024; // 1GB scale for visual
const cpuValue = cpu || 0;
const ramPercent = Math.min((ramMB / ramLimit) * 100, 100);
return (
<Box sx={{ mt: 1.5, mb: 1 }}>
<Box sx={{ mb: 1.5 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.5)', fontSize: '0.7rem', fontWeight: 'bold' }}>CPU</Typography>
<Typography variant="caption" sx={{ color: 'primary.light', fontSize: '0.7rem', fontWeight: 'bold' }}>{cpuValue}%</Typography>
</Box>
<LinearProgress
variant="determinate"
value={cpuValue}
sx={{
height: 4,
borderRadius: 2,
bgcolor: 'rgba(255,255,255,0.05)',
'.MuiLinearProgress-bar': {
borderRadius: 2,
backgroundImage: `linear-gradient(90deg, #33d9b2 ${cpuValue}%, #ff5252 100%)`,
bgcolor: 'transparent'
}
}}
/>
</Box>
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.5)', fontSize: '0.7rem', fontWeight: 'bold' }}>RAM</Typography>
<Typography variant="caption" sx={{ color: 'secondary.light', fontSize: '0.7rem', fontWeight: 'bold' }}>{formatMemory(memory as number)}</Typography>
</Box>
<LinearProgress
variant="determinate"
value={ramPercent}
sx={{
height: 4,
borderRadius: 2,
bgcolor: 'rgba(255,255,255,0.05)',
'.MuiLinearProgress-bar': {
borderRadius: 2,
bgcolor: 'secondary.main'
}
}}
/>
</Box>
</Box>
);
};
const handleAddProject = async () => {
if (!window.api) return
let result;
@@ -638,77 +548,11 @@ function App(): JSX.Element {
}
}
const handleOpenNginxConfig = async () => {
const handleOpenProcessList = async (id: string) => {
setSelectedMariaDbId(id)
setIsProcessListOpen(true)
if (window.api && window.api.invoke) {
const config = await window.api.invoke('nginx:config:get')
setNginxConfig(config)
setIsNginxConfigOpen(true)
}
}
const handleSaveNginxConfig = async () => {
if (window.api && window.api.invoke) {
const result = await window.api.invoke('nginx:config:save', nginxConfig)
if (result.success) {
Swal.fire({
title: t('common.success'),
text: t('server.nginx.config_saved'),
icon: 'success',
background: '#1e1e1e',
color: '#fff',
confirmButtonColor: '#153E5E',
customClass: { popup: 'swal2-dark-popup' }
})
setIsNginxConfigOpen(false)
}
}
}
const handleOpenApacheConfig = async () => {
if (window.api && window.api.invoke) {
const config = await window.api.invoke('apache:config:get')
setApacheConfig(config)
setIsApacheConfigOpen(true)
}
}
const handleSaveApacheConfig = async () => {
if (window.api && window.api.invoke) {
const result = await window.api.invoke('apache:config:save', apacheConfig)
if (result.success) {
Swal.fire({
title: t('common.success'),
text: t('server.apache.config_saved'),
icon: 'success',
background: '#1e1e1e',
color: '#fff',
confirmButtonColor: '#153E5E',
customClass: { popup: 'swal2-dark-popup' }
})
setIsApacheConfigOpen(false)
}
}
}
const handleTestApacheConfig = async () => {
if (window.api && window.api.invoke) {
const result = await window.api.invoke('apache:config:test')
const formattedMessage = result.message
.replace(/failed/g, '<span style="color: #ff5252;">failed</span>')
.replace(/successful/g, '<span style="color: #33d9b2;">successful</span>')
Swal.fire({
title: result.success ? 'Başarılı' : 'Yapılandırma Hatası',
html: `
<div class="nginx-error-container">
${formattedMessage}
</div>
`,
icon: result.success ? 'success' : 'error',
background: '#1e1e1e',
color: '#fff',
width: '600px'
})
await fetchProcessList(id)
}
}
@@ -849,10 +693,6 @@ function App(): JSX.Element {
setIsPhpExtensionsOpen(true)
}
const handleOpenProcessList = async (id: string) => {
setSelectedMariaDbId(id)
setActiveTab('db_monitor')
}
const fetchProcessList = async (id: string) => {
if (!window.api) return
@@ -1530,217 +1370,16 @@ function App(): JSX.Element {
)}
{activeTab === 'dashboard' && (
<>
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
<Tabs value={dashboardTab} onChange={(_e, v) => setDashboardTab(v)} textColor="primary" indicatorColor="primary">
<Tab label={t('common.all')} />
<Tab label="Nginx" />
<Tab label="Apache" />
<Tab label="PHP" />
<Tab label="MariaDB" />
</Tabs>
</Box>
<Grid container spacing={3} sx={{ mb: 4 }}>
{/* Nginx Box */}
{(dashboardTab === 0 || dashboardTab === 1) && (
<Grid item xs={12} sm={6} md={4}>
<Paper elevation={4} sx={{
p: 3, borderRadius: 3,
bgcolor: 'rgba(255, 255, 255, 0.03)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255, 255, 255, 0.05)',
transition: 'transform 0.2s',
'&:hover': { transform: 'scale(1.02)' }
}}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<WebIcon color="primary" sx={{ fontSize: 40 }} />
{getStatusChip(status.nginx)}
</Box>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>Nginx</Typography>
<Typography variant="body2" color="text.secondary">Port: {settings.nginxPort}</Typography>
<ResourceMonitor cpu={status.nginx?.cpu} memory={status.nginx?.memory} />
<Box sx={{ mt: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', gap: 1 }}>
<IconButton onClick={() => handleOpenLogs('nginx')} title={t('diagnostics.logs')} size="small" sx={{ bgcolor: 'rgba(255,255,255,0.05)' }}>
<TerminalIcon fontSize="small" />
</IconButton>
<IconButton
onClick={handleOpenNginxConfig}
title={t('server.nginx.config')}
size="small"
sx={{ bgcolor: 'rgba(255,255,255,0.05)', '&:hover': { color: 'primary.main' } }}
>
<SettingsIcon fontSize="small" />
</IconButton>
<IconButton
onClick={() => {
window.api.invoke('services:reload', 'nginx').then((res: any) => {
Swal.fire({
title: res.success ? t('common.success') : t('common.error'),
text: res.message,
icon: res.success ? 'success' : 'error',
background: '#1e1e1e',
color: '#fff',
confirmButtonColor: '#153E5E',
customClass: { popup: 'swal2-dark-popup' }
})
})
}}
title={t('server.nginx.save_reload')}
size="small"
disabled={status.nginx?.status !== 'running'}
sx={{ bgcolor: 'rgba(255,255,255,0.05)', '&:hover': { color: 'primary.main' } }}
>
<RefreshIcon fontSize="small" />
</IconButton>
</Box>
<Switch
checked={status.nginx?.status === 'running' || status.nginx?.status === 'starting'}
onChange={() => handleToggleService('nginx')}
disabled={status.nginx?.status === 'starting'}
/>
</Box>
</Paper>
</Grid>
)}
{/* Apache Box */}
{(dashboardTab === 0 || dashboardTab === 2) && (
<Grid item xs={12} sm={6} md={4}>
<Paper elevation={4} sx={{
p: 3, borderRadius: 3,
bgcolor: 'rgba(255, 255, 255, 0.03)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255, 255, 255, 0.05)',
transition: 'transform 0.2s',
'&:hover': { transform: 'scale(1.02)' }
}}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<ServerIcon color="primary" sx={{ fontSize: 40 }} />
{getStatusChip(status.apache)}
</Box>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>Apache</Typography>
<Typography variant="body2" color="text.secondary">Port: {settings.apachePort}</Typography>
<ResourceMonitor cpu={status.apache?.cpu} memory={status.apache?.memory} />
<Box sx={{ mt: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', gap: 1 }}>
<IconButton onClick={() => handleOpenLogs('apache')} title={t('diagnostics.logs')} size="small" sx={{ bgcolor: 'rgba(255,255,255,0.05)' }}>
<TerminalIcon fontSize="small" />
</IconButton>
<IconButton
onClick={handleOpenApacheConfig}
title={t('server.apache.config')}
size="small"
sx={{ bgcolor: 'rgba(255,255,255,0.05)', '&:hover': { color: 'primary.main' } }}
>
<SettingsIcon fontSize="small" />
</IconButton>
<IconButton
onClick={() => {
window.api.invoke('services:reload', 'apache').then((res: any) => {
Swal.fire({
title: res.success ? t('common.success') : t('common.error'),
text: res.message,
icon: res.success ? 'success' : 'error',
background: '#1e1e1e',
color: '#fff',
confirmButtonColor: '#153E5E',
customClass: { popup: 'swal2-dark-popup' }
})
})
}}
title={t('server.apache.save_reload')}
size="small"
disabled={status.apache?.status !== 'running'}
sx={{ bgcolor: 'rgba(255,255,255,0.05)', '&:hover': { color: 'primary.main' } }}
>
<RefreshIcon fontSize="small" />
</IconButton>
</Box>
<Switch
checked={status.apache?.status === 'running' || status.apache?.status === 'starting'}
onChange={() => handleToggleService('apache')}
disabled={status.apache?.status === 'starting'}
/>
</Box>
</Paper>
</Grid>
)}
{/* PHP Boxes */}
{(dashboardTab === 0 || dashboardTab === 3) && installedPhpVersions.map(v => (
<Grid item xs={12} sm={6} md={4} key={v.id}>
<Paper elevation={4} sx={{
p: 3, borderRadius: 3,
bgcolor: 'rgba(255, 255, 255, 0.03)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255, 255, 255, 0.05)',
transition: 'transform 0.2s',
'&:hover': { transform: 'scale(1.02)' }
}}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<PhpIcon color="primary" sx={{ fontSize: 40 }} />
{getStatusChip(status[`php:${v.version}`] || { status: 'stopped' })}
</Box>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>PHP {v.version}</Typography>
<Typography variant="body2" color="text.secondary">Port: {v.port || '?'}</Typography>
<ResourceMonitor cpu={status[`php:${v.version}`]?.cpu} memory={status[`php:${v.version}`]?.memory} />
<Box sx={{ mt: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<IconButton onClick={() => handleOpenLogs(`php:${v.version}`)} size="small" sx={{ bgcolor: 'rgba(255,255,255,0.05)' }}>
<TerminalIcon fontSize="small" />
</IconButton>
<Switch
checked={status[`php:${v.version}`]?.status === 'running' || status[`php:${v.version}`]?.status === 'starting'}
onChange={() => handleToggleService(`php:${v.version}`)}
disabled={status[`php:${v.version}`]?.status === 'starting'}
/>
</Box>
</Paper>
</Grid>
))}
{/* MariaDB Boxes */}
{(dashboardTab === 0 || dashboardTab === 4) && mariaDbVersions.filter(v => v.status === 'installed').map(v => (
<Grid item xs={12} sm={6} md={4} key={v.id}>
<Paper elevation={4} sx={{
p: 3, borderRadius: 3,
bgcolor: 'rgba(255, 255, 255, 0.03)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255, 255, 255, 0.05)',
transition: 'transform 0.2s',
'&:hover': { transform: 'scale(1.02)' }
}}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<DbIcon color="primary" sx={{ fontSize: 40 }} />
{getStatusChip(status[`mariadb:${v.version}`] || { status: 'stopped' })}
</Box>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>MariaDB {v.version}</Typography>
<Typography variant="body2" color="text.secondary">Port: {settings.mariaDbPorts?.[v.id] || v.port}</Typography>
<ResourceMonitor cpu={status[`mariadb:${v.version}`]?.cpu} memory={status[`mariadb:${v.version}`]?.memory} />
<Box sx={{ mt: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', gap: 1 }}>
<IconButton onClick={() => handleOpenLogs(`mariadb:${v.version}`)} title={t('diagnostics.logs')} size="small" sx={{ bgcolor: 'rgba(255,255,255,0.05)' }}>
<TerminalIcon fontSize="small" />
</IconButton>
<IconButton onClick={() => handleOpenProcessList(v.id)} title={t('mariadb_process_monitor.title')} size="small" sx={{ bgcolor: 'rgba(255,255,255,0.05)', '&:hover': { color: 'primary.main' } }}>
<ToolsIcon fontSize="small" />
</IconButton>
<IconButton onClick={() => handleResetData(`mariadb:${v.version}`)} title={t('common.reset')} size="small" sx={{ bgcolor: 'rgba(255,255,255,0.05)', '&:hover': { color: 'error.main' } }}>
<ResetIcon fontSize="small" />
</IconButton>
</Box>
<Switch
checked={status[`mariadb:${v.version}`]?.status === 'running' || status[`mariadb:${v.version}`]?.status === 'starting'}
onChange={() => handleToggleService(`mariadb:${v.version}`)}
disabled={status[`mariadb:${v.version}`]?.status === 'starting'}
/>
</Box>
</Paper>
</Grid>
))}
</Grid>
</>
<DashboardManager
status={status}
settings={settings}
projects={projects}
phpVersions={phpVersions}
mariaDbVersions={mariaDbVersions}
t={t}
onFetchStatus={fetchStatus}
onSelectProject={handleSelectProject}
/>
)}
{activeTab === 'db_monitor' && (
@@ -0,0 +1,462 @@
import {
Box,
Typography,
Tabs,
Tab,
Paper,
Grid,
IconButton,
Button,
List,
ListItem,
ListItemText,
ListItemSecondaryAction,
Divider,
LinearProgress,
CircularProgress,
Stack
} from '@mui/material'
import {
Refresh as RefreshIcon,
PlayArrow as StartIcon,
Stop as StopIcon,
OpenInNew as OpenIcon,
Save as SaveIcon,
Folder as ProjectIcon,
Language as WebIcon,
Dns as ServerIcon,
Code as PhpIcon,
Storage as DbIcon
} from '@mui/icons-material'
import { useState, useEffect, useRef } from 'react'
import 'prismjs/themes/prism-tomorrow.css'
import 'prismjs/components/prism-nginx'
import 'prismjs/components/prism-apacheconf'
import 'prismjs/components/prism-ini'
import Editor from 'react-simple-code-editor'
import Prism from 'prismjs'
import Swal from 'sweetalert2'
interface DashboardDetailsProps {
serviceId: string | null;
status: any;
settings: any;
projects: any[];
t: any;
onToggleService: (id: string) => void;
onReloadService: (id: string) => void;
onSelectProject: (id: string) => void;
}
export default function DashboardDetails({
serviceId,
status,
settings,
projects,
t,
onToggleService,
onReloadService,
onSelectProject
}: DashboardDetailsProps) {
const [activeTab, setActiveTab] = useState(0);
const [configContent, setConfigContent] = useState('');
const [projectConfigContent, setProjectConfigContent] = useState('');
const [isLoadingConfig, setIsLoadingConfig] = useState(false);
const [logs, setLogs] = useState<string[]>([]);
const logEndRef = useRef<HTMLDivElement>(null);
const getServiceType = () => {
if (!serviceId) return null;
if (serviceId === 'nginx') return 'nginx';
if (serviceId === 'apache') return 'apache';
if (serviceId.startsWith('php:')) return 'php';
if (serviceId.startsWith('mariadb:')) return 'mariadb';
return null;
};
const serviceType = getServiceType();
const getTemplateName = (): string => {
if (serviceId === 'nginx') return 'nginx.conf.template';
if (serviceType === 'apache') return 'httpd.conf.template';
if (serviceType === 'php') return 'php.ini.template';
if (serviceType === 'mariadb') return 'mariadb.ini.template';
return '';
};
const getProjectTemplateNames = () => {
if (serviceId === 'nginx' || serviceType === 'php') return ['nginx_project_location.conf.template'];
if (serviceId === 'apache') return ['apache_project.conf.template'];
return [];
};
const fetchConfig = async (templateName: string, setter: (v: string) => void) => {
if (!templateName) return;
setIsLoadingConfig(true);
try {
const res = await window.api.invoke('config:template:get', templateName);
if (res && res.success) {
setter(res.content || '');
} else {
console.error('Failed to fetch config:', res?.message);
setter('');
}
} catch (error) {
console.error('Error fetching config:', error);
setter('');
} finally {
setIsLoadingConfig(false);
}
};
const saveConfig = async (templateName: string, content: string) => {
const res = await window.api.invoke('config:template:save', { templateName, content });
if (res.success) {
Swal.fire({
title: t('common.success'),
text: res.message,
icon: 'success',
background: '#1e1e1e',
color: '#fff'
});
} else {
Swal.fire({
title: t('common.error'),
text: res.message,
icon: 'error',
background: '#1e1e1e',
color: '#fff'
});
}
};
const fetchLogs = async () => {
if (!serviceId) return;
const res = await window.api.invoke('services:logs', serviceId);
setLogs(res || []);
};
const getHighlightLanguage = (templateName: string) => {
if (!templateName) return Prism.languages.clike;
if (templateName.includes('nginx')) return Prism.languages.nginx;
if (templateName.includes('httpd') || templateName.includes('apache')) return Prism.languages.apacheconf;
if (templateName.includes('.ini') || templateName.includes('.conf')) return Prism.languages.ini;
return Prism.languages.clike;
};
const getHighlightLabel = (templateName: string) => {
if (!templateName) return 'clike';
if (templateName.includes('nginx')) return 'nginx';
if (templateName.includes('httpd') || templateName.includes('apache')) return 'apacheconf';
if (templateName.includes('.ini') || templateName.includes('.conf')) return 'ini';
return 'clike';
};
useEffect(() => {
if (activeTab === 1) fetchConfig(getTemplateName(), setConfigContent);
if (activeTab === 2) {
const templates = getProjectTemplateNames();
if (templates.length > 0) fetchConfig(templates[0], setProjectConfigContent);
}
if (activeTab === 3) {
fetchLogs();
const interval = setInterval(fetchLogs, 3000);
return () => clearInterval(interval);
}
return undefined;
}, [activeTab, serviceId]);
useEffect(() => {
if (activeTab === 3 && logEndRef.current) {
logEndRef.current.scrollIntoView({ behavior: 'smooth' });
}
}, [logs, activeTab]);
if (!serviceId) {
return (
<Box sx={{ flexGrow: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', opacity: 0.5 }}>
<Typography variant="h6">{t('dashboard.select_service', 'Lütfen bir servis seçin')}</Typography>
</Box>
);
}
const currentStatus = status[serviceId] || { status: 'stopped' };
const isRunning = currentStatus.status === 'running';
const serviceProjects = projects.filter(p => {
if (serviceId === 'nginx') return !p.serverType || p.serverType === 'nginx';
if (serviceId === 'apache') return p.serverType === 'apache';
if (serviceType === 'php') return p.phpVersion === serviceId.split(':')[1];
if (serviceType === 'mariadb') return true; // MariaDB is shared (or filter if multi-instance)
return false;
});
const ResourceMonitor = ({ cpu, memory }: { cpu?: number; memory?: number }) => (
<Box sx={{ mt: 2 }}>
<Box sx={{ mb: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', fontWeight: 'bold' }}>CPU</Typography>
<Typography variant="caption" sx={{ color: 'primary.light', fontWeight: 'bold' }}>{cpu || 0}%</Typography>
</Box>
<LinearProgress variant="determinate" value={cpu || 0} sx={{ height: 6, borderRadius: 3, bgcolor: 'rgba(255,255,255,0.05)' }} />
</Box>
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', fontWeight: 'bold' }}>RAM</Typography>
<Typography variant="caption" sx={{ color: 'secondary.light', fontWeight: 'bold' }}>
{memory ? `${Math.round(memory / (1024 * 1024))} MB` : '0 MB'}
</Typography>
</Box>
<LinearProgress variant="determinate" value={Math.min(((memory || 0) / (1024 * 1024) / 1024) * 100, 100)} sx={{ height: 6, borderRadius: 3, bgcolor: 'rgba(255,255,255,0.05)', '.MuiLinearProgress-bar': { bgcolor: 'secondary.main' } }} />
</Box>
</Box>
);
return (
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
<Box sx={{ p: 3, borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', alignItems: 'center', gap: 2 }}>
<Paper sx={{ p: 1.5, bgcolor: 'rgba(255, 255, 255, 0.05)', borderRadius: 3 }}>
{serviceType === 'nginx' && <WebIcon color="primary" fontSize="large" />}
{serviceType === 'apache' && <ServerIcon color="primary" fontSize="large" />}
{serviceType === 'php' && <PhpIcon color="primary" fontSize="large" />}
{serviceType === 'mariadb' && <DbIcon color="primary" fontSize="large" />}
</Paper>
<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')}
</Typography>
</Box>
<Stack direction="row" spacing={1}>
{isRunning && (
<IconButton onClick={() => onReloadService(serviceId)} sx={{ bgcolor: 'rgba(57, 167, 255, 0.1)', color: 'primary.main' }}>
<RefreshIcon />
</IconButton>
)}
<Button
variant="contained"
color={isRunning ? 'error' : 'success'}
startIcon={isRunning ? <StopIcon /> : <StartIcon />}
onClick={() => onToggleService(serviceId)}
sx={{ borderRadius: 2 }}
>
{isRunning ? t('common.stop', 'Durdur') : t('common.start', 'Başlat')}
</Button>
</Stack>
</Box>
<Box sx={{ borderBottom: 1, borderColor: 'rgba(255,255,255,0.05)', px: 3 }}>
<Tabs value={activeTab} onChange={(_e, v) => setActiveTab(v)}>
<Tab label={t('common.overview', 'Genel Bakış')} />
<Tab label={t('dashboard.service_settings', 'Servis Ayarları')} />
{(serviceId === 'nginx' || serviceId === 'apache' || serviceType === 'php') && (
<Tab label={t('dashboard.project_settings', 'Proje Ayarları')} />
)}
<Tab label={t('diagnostics.logs', 'Hata Logları')} />
<Tab label={t('projects.title', 'Projeler')} />
</Tabs>
</Box>
<Box sx={{ flexGrow: 1, overflowY: 'auto', p: 3 }}>
{activeTab === 0 && (
<Grid container spacing={4}>
<Grid item xs={12} md={6}>
<Typography variant="subtitle2" sx={{ fontWeight: 800, mb: 2, color: 'primary.main' }}>SYSTEM STATS</Typography>
<Paper sx={{ p: 3, bgcolor: 'rgba(0,0,0,0.2)', 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="subtitle2" sx={{ fontWeight: 800, mb: 2, color: 'primary.main' }}>SERVICE INFO</Typography>
<Paper sx={{ p: 3, bgcolor: 'rgba(0,0,0,0.2)', borderRadius: 3, border: '1px solid rgba(255,255,255,0.05)' }}>
<Stack spacing={2}>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" sx={{ opacity: 0.5 }}>Port</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{currentStatus.port || (() => {
if (serviceId === 'nginx') return settings.nginxPort;
if (serviceId === 'apache') return settings.apachePort;
if (serviceType === 'php') return settings.phpPort;
if (serviceType === 'mariadb') {
const verId = serviceId.split(':')[1];
return (settings.mariaDbPorts && settings.mariaDbPorts[verId]) || settings.mariaDbPort;
}
return 'n/a';
})()}
</Typography>
</Box>
<Divider sx={{ borderColor: 'rgba(255,255,255,0.05)' }} />
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" sx={{ opacity: 0.5 }}>Status</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, color: isRunning ? 'success.main' : 'error.main' }}>
{currentStatus.status.toUpperCase()}
</Typography>
</Box>
</Stack>
</Paper>
</Grid>
</Grid>
)}
{activeTab === 1 && (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="overline" sx={{ fontWeight: 800, color: 'primary.main', letterSpacing: 2 }}>
{getTemplateName().toUpperCase()}
</Typography>
<Button
variant="contained"
size="small"
startIcon={<SaveIcon />}
onClick={() => saveConfig(getTemplateName(), configContent)}
sx={{ borderRadius: 2 }}
>
{t('common.save', 'KAYDET')}
</Button>
</Box>
<Paper sx={{
flexGrow: 1,
bgcolor: '#1e1e1e',
borderRadius: 3,
border: '1px solid rgba(255,255,255,0.05)',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column'
}}>
{isLoadingConfig ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flexGrow: 1 }}><CircularProgress /></Box>
) : (
<Box sx={{ flexGrow: 1, overflow: 'auto' }}>
<Editor
value={configContent}
onValueChange={setConfigContent}
highlight={code => Prism.highlight(code, getHighlightLanguage(getTemplateName()), getHighlightLabel(getTemplateName()))}
padding={20}
style={{
fontFamily: '"Fira Code", "Consolas", monospace',
fontSize: '14px',
minHeight: '100%',
outline: 'none'
}}
textareaClassName="editor-textarea"
/>
</Box>
)}
</Paper>
</Box>
)}
{activeTab === 2 && (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="overline" sx={{ fontWeight: 800, color: 'primary.main', letterSpacing: 2 }}>
{getProjectTemplateNames()[0]?.toUpperCase()}
</Typography>
<Button
variant="contained"
size="small"
startIcon={<SaveIcon />}
onClick={() => saveConfig(getProjectTemplateNames()[0], projectConfigContent)}
sx={{ borderRadius: 2 }}
>
{t('common.save', 'KAYDET')}
</Button>
</Box>
<Paper sx={{
flexGrow: 1,
bgcolor: '#1e1e1e',
borderRadius: 3,
border: '1px solid rgba(255,255,255,0.05)',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column'
}}>
{isLoadingConfig ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flexGrow: 1 }}><CircularProgress /></Box>
) : (
<Box sx={{ flexGrow: 1, overflow: 'auto' }}>
<Editor
value={projectConfigContent}
onValueChange={setProjectConfigContent}
highlight={code => Prism.highlight(code, getHighlightLanguage(getProjectTemplateNames()[0]), getHighlightLabel(getProjectTemplateNames()[0]))}
padding={20}
style={{
fontFamily: '"Fira Code", "Consolas", monospace',
fontSize: '14px',
minHeight: '100%',
outline: 'none'
}}
textareaClassName="editor-textarea"
/>
</Box>
)}
</Paper>
</Box>
)}
{activeTab === 3 && (
<Box sx={{ bgcolor: '#000', borderRadius: 2, p: 2, height: '60vh', overflowY: 'auto' }}>
{logs.map((log, i) => (
<Typography key={i} sx={{
fontFamily: 'monospace',
fontSize: '0.8rem',
color: log.toLowerCase().includes('error') ? '#f44336' : (log.toLowerCase().includes('warn') ? '#ff9800' : '#33d9b2'),
mb: 0.5
}}>
{log}
</Typography>
))}
<div ref={logEndRef} />
</Box>
)}
{activeTab === 4 && (
<Paper sx={{ bgcolor: 'rgba(0,0,0,0.2)', borderRadius: 3, border: '1px solid rgba(255,255,255,0.05)', overflow: 'hidden' }}>
<List disablePadding>
{serviceProjects.length === 0 ? (
<Box sx={{ p: 4, textAlign: 'center', opacity: 0.3 }}><Typography>{t('projects.empty')}</Typography></Box>
) : (
serviceProjects.map((p, idx) => (
<Box key={p.id}>
<ListItem>
<Paper sx={{ p: 1, mr: 2, bgcolor: 'rgba(255,255,255,0.03)' }}><ProjectIcon sx={{ opacity: 0.5 }} /></Paper>
<ListItemText
primary={p.name}
secondary={`${p.host} • PHP ${p.phpVersion}`}
primaryTypographyProps={{ fontWeight: 700 }}
/>
<ListItemSecondaryAction>
<IconButton onClick={() => onSelectProject(p.id)} color="primary">
<OpenIcon fontSize="small" />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
{idx < serviceProjects.length - 1 && <Divider sx={{ borderColor: 'rgba(255,255,255,0.05)' }} />}
</Box>
))
)}
</List>
</Paper>
)}
</Box>
<style>{`
.editor-textarea {
outline: none !important;
}
.token.comment { color: #6a9955; }
.token.directive { color: #569cd6; font-weight: bold; }
.token.variable { color: #9cdcfe; }
.token.string { color: #ce9178; }
.token.number { color: #b5cea8; }
.token.boolean { color: #569cd6; }
.token.operator { color: #d4d4d4; }
.token.punctuation { color: #d4d4d4; }
.token.keyword { color: #569cd6; }
.token.function { color: #dcdcaa; }
.token.class-name { color: #4ec9b0; }
.token.attribute-name { color: #9cdcfe; }
.token.attribute-value { color: #ce9178; }
`}</style>
</Box>
);
}
@@ -0,0 +1,89 @@
import { Box } from '@mui/material'
import DashboardSidebar from './DashboardSidebar'
import DashboardDetails from './DashboardDetails'
import { useState } from 'react'
import Swal from 'sweetalert2'
interface DashboardManagerProps {
status: any;
settings: any;
projects: any[];
phpVersions: any[];
mariaDbVersions: any[];
t: any;
onFetchStatus: () => void;
onSelectProject: (projectId: string) => void;
}
export default function DashboardManager({
status,
settings,
projects,
phpVersions,
mariaDbVersions,
t,
onFetchStatus,
onSelectProject
}: DashboardManagerProps) {
const [selectedServiceId, setSelectedServiceId] = useState<string | null>('nginx');
const handleToggleService = async (service: string) => {
if (!window.api) return;
const currentStatus = status[service]?.status || 'stopped';
const isRunning = currentStatus === 'running';
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'
});
}
onFetchStatus();
};
const handleReloadService = async (service: string) => {
if (!window.api) return;
const result = await window.api.invoke('services:reload', service);
Swal.fire({
title: result && result.success ? t('common.success') : t('common.error'),
text: result ? result.message : 'Error',
icon: result && result.success ? 'success' : 'error',
background: '#1e1e1e',
color: '#fff'
});
onFetchStatus();
};
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}
phpVersions={phpVersions}
mariaDbVersions={mariaDbVersions}
selectedServiceId={selectedServiceId}
setSelectedServiceId={setSelectedServiceId}
t={t}
/>
<DashboardDetails
serviceId={selectedServiceId}
status={status}
settings={settings}
projects={projects}
t={t}
onToggleService={handleToggleService}
onReloadService={handleReloadService}
onSelectProject={onSelectProject}
/>
</Box>
);
}
@@ -0,0 +1,140 @@
import {
Box,
Typography,
List,
ListItemButton,
Chip,
CircularProgress
} from '@mui/material'
import {
Language as WebIcon,
Dns as ServerIcon,
Code as PhpIcon,
Storage as DbIcon
} from '@mui/icons-material'
interface DashboardSidebarProps {
status: any;
phpVersions: any[];
mariaDbVersions: any[];
selectedServiceId: string | null;
setSelectedServiceId: (v: string | null) => void;
t: any;
}
export default function DashboardSidebar({
status,
phpVersions,
mariaDbVersions,
selectedServiceId,
setSelectedServiceId,
t
}: DashboardSidebarProps) {
const installedPhp = phpVersions.filter(v => v.status === 'installed');
const installedMariaDb = mariaDbVersions.filter(v => v.status === 'installed');
const getStatusInfo = (service: string) => {
const state = typeof status[service] === 'string' ? status[service] : status[service]?.status || 'stopped';
const color = state === 'running' ? 'success' : state === 'stopped' ? 'error' : 'warning';
return { state, color };
};
const ServiceItem = ({ id, label, icon: Icon, serviceKey }: { id: string, label: string, icon: any, serviceKey: string }) => {
const { state } = getStatusInfo(serviceKey);
const isRunning = state === 'running';
const isStarting = state === 'starting' || state === 'stopping';
return (
<ListItemButton
selected={selectedServiceId === id}
onClick={() => setSelectedServiceId(id)}
sx={{
borderRadius: 2,
mb: 0.5,
border: selectedServiceId === id ? '1px solid rgba(57, 167, 255, 0.3)' : '1px solid transparent',
'&.Mui-selected': { bgcolor: 'rgba(57, 167, 255, 0.08)' }
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%', gap: 1.5 }}>
<Icon sx={{ color: selectedServiceId === id ? 'primary.main' : 'rgba(255,255,255,0.4)', fontSize: '1.2rem' }} />
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
<Typography variant="body2" noWrap sx={{ fontWeight: 600 }}>
{label}
</Typography>
</Box>
<Box sx={{
width: 10,
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',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
{isStarting && <CircularProgress size={8} thickness={6} color="inherit" />}
</Box>
</Box>
</ListItemButton>
);
};
return (
<Box sx={{
width: 280,
borderRight: '1px solid rgba(255, 255, 255, 0.05)',
display: 'flex',
flexDirection: 'column',
bgcolor: 'rgba(255, 255, 255, 0.01)',
height: '100%',
overflowY: 'auto'
}}>
<Box sx={{ p: 2, borderBottom: '1px solid rgba(255, 255, 255, 0.05)' }}>
<Typography variant="h6" sx={{ fontWeight: 700, fontSize: '1.1rem' }}>
{t('dashboard.services', 'Servisler')}
</Typography>
</Box>
<Box sx={{ p: 1 }}>
<Typography variant="overline" sx={{ px: 1.5, color: 'rgba(255,255,255,0.3)', fontWeight: 800, letterSpacing: 1 }}>
WEB SERVERS
</Typography>
<List dense disablePadding sx={{ mb: 2 }}>
<ServiceItem id="nginx" label="Nginx" icon={WebIcon} serviceKey="nginx" />
<ServiceItem id="apache" label="Apache" icon={ServerIcon} serviceKey="apache" />
</List>
<Typography variant="overline" sx={{ px: 1.5, color: 'rgba(255,255,255,0.3)', fontWeight: 800, letterSpacing: 1 }}>
PHP VERSIONS
</Typography>
<List dense disablePadding sx={{ mb: 2 }}>
{installedPhp.map(v => (
<ServiceItem
key={v.id}
id={`php:${v.version}`}
label={`PHP ${v.version}`}
icon={PhpIcon}
serviceKey={`php:${v.version}`}
/>
))}
</List>
<Typography variant="overline" sx={{ px: 1.5, color: 'rgba(255,255,255,0.3)', fontWeight: 800, letterSpacing: 1 }}>
MARIADB VERSIONS
</Typography>
<List dense disablePadding>
{installedMariaDb.map(v => (
<ServiceItem
key={v.id}
id={`mariadb:${v.version}`}
label={`MariaDB ${v.version}`}
icon={DbIcon}
serviceKey={`mariadb:${v.version}`}
/>
))}
</List>
</Box>
</Box>
);
}